This commit is contained in:
2025-10-23 08:19:18 +02:00
parent c084347fbd
commit 47cbdf3b7c
18 changed files with 1984 additions and 21 deletions
+34 -1
View File
@@ -1,9 +1,12 @@
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';
import 'features/auth/application/splash_screen.dart';
import 'features/igel/presentation/igel_list_screen.dart';
import 'features/igel/presentation/igel_detail_screen.dart'; // <-- WICHTIG
import 'features/igel/presentation/igel_gallery_screen.dart';
GoRouter buildRouter() => GoRouter(
initialLocation: '/splash',
@@ -11,6 +14,36 @@ GoRouter buildRouter() => GoRouter(
GoRoute(path: '/splash', builder: (_, __) => const SplashScreen()),
GoRoute(path: '/login', builder: (_, __) => const LoginScreen()),
GoRoute(path: '/register', builder: (_, __) => const RegisterScreen()),
// Liste
GoRoute(path: '/igel', builder: (_, __) => const IgelListScreen()),
// Detail: /igel/:id (z. B. /igel/3)
GoRoute(
path: '/igel/:id',
builder: (ctx, st) {
final idStr = st.pathParameters['id'] ?? '';
final id = int.tryParse(idStr);
if (id == null) {
return const Scaffold(
body: Center(child: Text('Fehlerhafte ID')),
);
}
return IgelDetailScreen(igelId: id);
},
),
GoRoute(
path: '/igel/:id/gallery',
builder: (ctx, st) {
final id = int.tryParse(st.pathParameters['id'] ?? '');
final initial =
int.tryParse(st.uri.queryParameters['index'] ?? '0') ?? 0;
if (id == null) {
return const Scaffold(
body: Center(child: Text('Fehlerhafte ID')));
}
return IgelGalleryScreen(igelId: id, initialIndex: initial);
},
),
],
);
@@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../auth/data/token_storage.dart';
import '../../../main.dart';
import 'package:go_router/go_router.dart';
@@ -0,0 +1,68 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../shared/api_client.dart';
class IgelImage {
final int id;
final String url;
final String? thumbUrl; // ✅ Thumbnail-Feld ergänzt
final String? originalName;
final String? mime;
final int? sizeBytes;
IgelImage({
required this.id,
required this.url,
this.thumbUrl,
this.originalName,
this.mime,
this.sizeBytes,
});
factory IgelImage.fromMap(Map<String, dynamic> m) => IgelImage(
id: (m['id'] as num).toInt(),
url: (m['url'] ?? '') as String,
thumbUrl:
(m['thumb_url'] ?? m['thumbUrl']) as String?, // ✅ Feld gemappt
originalName: m['original_name'] as String?,
mime: m['mime'] as String?,
sizeBytes: (m['size_bytes'] as num?)?.toInt(),
);
}
class IgelImagesRepository {
IgelImagesRepository(this.api, {required this.getAccessToken});
final ApiClient api;
final Future<String?> Function() getAccessToken;
Future<List<IgelImage>> list(int igelId) async {
final res = await api.get<dynamic>('/igel/$igelId/images');
final list = (res as List).cast<Map<String, dynamic>>();
return list.map(IgelImage.fromMap).toList();
}
Future<List<IgelImage>> upload(
int igelId, List<http.MultipartFile> files) async {
final uri = Uri.parse(
'${api.baseUrl}/igel/$igelId/images'); // api.baseUrl endet auf ?r=
final req = http.MultipartRequest('POST', uri);
final token = await getAccessToken();
if (token != null) req.headers['Authorization'] = 'Bearer $token';
// keine Content-Type-Header setzen -> MultipartRequest macht das korrekt
for (final f in files) {
req.files.add(f); // Feldname egal: server sammelt alle
}
final streamRes = await req.send();
final body = await streamRes.stream.bytesToString();
if (streamRes.statusCode < 200 || streamRes.statusCode >= 300) {
throw ApiException(streamRes.statusCode, body);
}
final decoded = jsonDecode(body);
final uploaded = (decoded['uploaded'] as List).cast<Map<String, dynamic>>();
return uploaded.map(IgelImage.fromMap).toList();
}
Future<void> delete(int imageId) async {
await api.delete<dynamic>('/igel/images/$imageId');
}
}
@@ -35,4 +35,9 @@ class IgelRepository {
Future<void> delete(int id) async {
await api.delete<dynamic>('/igel/$id');
}
Future<Igel> get(int id) async {
final map = await api.get<Map<String, dynamic>>('/igel/$id');
return Igel.fromMap(map);
}
}
+58 -14
View File
@@ -1,22 +1,66 @@
class Igel {
final int id;
final String name;
final String? note;
final String? gender; // männlich | weiblich | unbekannt
final String? gender; // 'männlich' | 'weiblich' | 'unbekannt'
final String? feature; // Merkmal
final String? note; // Information
final DateTime? createdAt;
final DateTime? updatedAt;
Igel(
{required this.id,
required this.name,
this.note,
this.gender,
this.feature});
Igel({
required this.id,
required this.name,
this.gender,
this.feature,
this.note,
this.createdAt,
this.updatedAt,
});
factory Igel.fromMap(Map<String, dynamic> m) => Igel(
id: (m['id'] as num).toInt(),
name: (m['name'] ?? '') as String,
note: m['note'] as String?,
gender: m['gender'] as String?,
feature: m['feature'] as String?,
// ---------- Factory fromMap ----------
factory Igel.fromMap(Map<String, dynamic> map) => Igel(
id: (map['id'] as num).toInt(),
name: map['name'] as String,
gender: map['gender'] as String?,
feature: map['feature'] as String?,
note: map['note'] as String?,
createdAt: map['created_at'] != null
? DateTime.parse(map['created_at'])
: null,
updatedAt: map['updated_at'] != null
? DateTime.parse(map['updated_at'])
: null,
);
// ---------- toMap (optional für PUT/POST) ----------
Map<String, dynamic> toMap() => {
'id': id,
'name': name,
if (gender != null) 'gender': gender,
if (feature != null) 'feature': feature,
if (note != null) 'note': note,
if (createdAt != null) 'created_at': createdAt!.toIso8601String(),
if (updatedAt != null) 'updated_at': updatedAt!.toIso8601String(),
};
// ---------- copyWith ----------
Igel copyWith({
int? id,
String? name,
String? gender,
String? feature,
String? note,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Igel(
id: id ?? this.id,
name: name ?? this.name,
gender: gender ?? this.gender,
feature: feature ?? this.feature,
note: note ?? this.note,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,397 @@
// lib/features/igel/presentation/igel_gallery_screen.dart
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import '../../../main.dart';
import '../../igel/data/igel_images_repository.dart';
class IgelGalleryScreen extends ConsumerStatefulWidget {
const IgelGalleryScreen(
{super.key, required this.igelId, this.initialIndex = 0});
final int igelId;
final int initialIndex;
@override
ConsumerState<IgelGalleryScreen> createState() => _IgelGalleryState();
}
class _IgelGalleryState extends ConsumerState<IgelGalleryScreen> {
late final IgelImagesRepository repo;
List<IgelImage> images = [];
bool busy = true;
String? err;
late PageController pageC;
int currentIndex = 0;
// Zoom-Handling
final TransformationController _tc = TransformationController();
bool _isZoomed = false;
TapDownDetails? _doubleTapDetails;
// UI Overlays (Titel/Buttons) ein-/ausblenden
bool _chromeVisible = true;
@override
void initState() {
super.initState();
repo = ref.read(igelImagesRepoProvider);
pageC = PageController(initialPage: widget.initialIndex);
currentIndex = widget.initialIndex;
// Nur auf Mobile/Desktop echte Immersion
if (!kIsWeb) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
SystemChrome.setPreferredOrientations(const [
DeviceOrientation.portraitUp,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
_tc.addListener(_onMatrixChanged);
_load();
}
@override
void dispose() {
_tc.removeListener(_onMatrixChanged);
_tc.dispose();
pageC.dispose();
if (!kIsWeb) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
}
super.dispose();
}
void _onMatrixChanged() {
final m = _tc.value;
final zoomedNow = !_matrixIsIdentity(m);
if (zoomedNow != _isZoomed && mounted) {
setState(() => _isZoomed = zoomedNow);
}
}
bool _matrixIsIdentity(Matrix4 m, {double eps = 1e-3}) {
// Tolerante Prüfung auf Identität
final s = m.storage;
bool eq(double a, double b) => (a - b).abs() <= eps;
return eq(s[0], 1) &&
eq(s[5], 1) &&
eq(s[10], 1) &&
eq(s[15], 1) &&
eq(s[1], 0) &&
eq(s[2], 0) &&
eq(s[3], 0) &&
eq(s[4], 0) &&
eq(s[6], 0) &&
eq(s[7], 0) &&
eq(s[8], 0) &&
eq(s[9], 0) &&
eq(s[11], 0) &&
eq(s[12], 0) &&
eq(s[13], 0) &&
eq(s[14], 0);
}
Future<void> _load() async {
setState(() {
busy = true;
err = null;
});
try {
images = await repo.list(widget.igelId);
// initialIndex clampen
final newIndex =
images.isEmpty ? 0 : math.min(widget.initialIndex, images.length - 1);
if (newIndex != currentIndex) {
currentIndex = newIndex;
pageC = PageController(initialPage: currentIndex);
}
if (mounted && images.isNotEmpty) {
await _prefetchAround(currentIndex);
}
} catch (e) {
err = e.toString();
} finally {
if (mounted) setState(() => busy = false);
}
}
Future<void> _prefetchAround(int index) async {
if (!mounted || images.isEmpty) return;
Future<void> pre(String url) async {
try {
await precacheImage(NetworkImage(url), context);
} catch (_) {}
}
await pre(images[index].url);
if (index - 1 >= 0) await pre(images[index - 1].url);
if (index + 1 < images.length) await pre(images[index + 1].url);
}
void _onPageChanged(int i) async {
setState(() {
currentIndex = i;
_tc.value = Matrix4.identity();
_isZoomed = false;
});
await _prefetchAround(i);
}
void _onDoubleTap() {
// Toggle Zoom (2x) um Tap-Position
if (!_isZoomed && _tc.value == Matrix4.identity()) {
final pos = _doubleTapDetails?.localPosition ?? const Offset(0, 0);
const scale = 2.0;
final z = Matrix4.identity()
..translate(-pos.dx * (scale - 1), -pos.dy * (scale - 1))
..scale(scale);
_tc.value = z;
} else {
_tc.value = Matrix4.identity();
}
}
Future<void> _deleteCurrent() async {
if (images.isEmpty) return;
final img = images[currentIndex];
final ok = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Bild löschen?'),
content: const Text('Dieses Bild wirklich löschen?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Abbrechen')),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Löschen')),
],
),
) ??
false;
if (!ok) return;
try {
await repo.delete(img.id);
final oldIndex = currentIndex;
await _load();
if (images.isEmpty) {
if (mounted) context.pop();
return;
}
final nextIndex = oldIndex.clamp(0, images.length - 1);
pageC.jumpToPage(nextIndex);
_onPageChanged(nextIndex);
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Bild gelöscht')));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Löschen fehlgeschlagen: $e')));
}
}
}
Future<void> _downloadCurrent() async {
if (images.isEmpty) return;
final url = images[currentIndex].url;
if (kIsWeb) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Im Web per Rechtsklick/Neuer Tab speichern.')),
);
return;
}
try {
final res = await http.get(Uri.parse(url));
if (res.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Bild heruntergeladen (im Speicher).')),
);
} else {
throw Exception('HTTP ${res.statusCode}');
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Download fehlgeschlagen: $e')),
);
}
}
void _toggleChrome() => setState(() => _chromeVisible = !_chromeVisible);
@override
Widget build(BuildContext context) {
final total = images.length;
final canSwipe = !_isZoomed; // Wischen nur, wenn nicht gezoomt
return Scaffold(
backgroundColor: Colors.black,
// keine AppBar -> echtes Fullscreen. Overlays bauen wir selbst.
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _toggleChrome, // Overlay ein-/ausblenden
child: SafeArea(
// SafeArea, damit Notch nicht überlagert; wir blenden UI überlagert ein.
child: Stack(
children: [
// Seiten
if (busy)
const Center(child: CircularProgressIndicator())
else if (err != null)
Center(
child: Padding(
padding: const EdgeInsets.all(24),
child:
Text(err!, style: const TextStyle(color: Colors.white)),
),
)
else if (total == 0)
const Center(
child: Text('Keine Bilder',
style: TextStyle(color: Colors.white70)))
else
PageView.builder(
controller: pageC,
onPageChanged: _onPageChanged,
physics: canSwipe
? const PageScrollPhysics()
: const NeverScrollableScrollPhysics(),
itemCount: total,
itemBuilder: (_, i) {
final img = images[i];
final full = img.url;
return Listener(
// Maus/Touch-Events nicht "schlucken", wenn nicht gezoomt, damit PageView scrollt
behavior: HitTestBehavior.deferToChild,
child: Center(
child: GestureDetector(
onTapDown: (d) => _doubleTapDetails = d,
onDoubleTap: _onDoubleTap,
child: InteractiveViewer(
transformationController: _tc,
minScale: 1,
maxScale: 4,
panEnabled: _isZoomed, // Pan nur im Zoom
scaleEnabled: true,
clipBehavior: Clip.none,
child: Image.network(
full,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const Icon(
Icons.broken_image,
color: Colors.white70,
size: 64),
),
),
),
),
);
},
),
// Top-Bar (Back, Index, Löschen) ein-/ausblendbar
AnimatedPositioned(
duration: const Duration(milliseconds: 180),
top: _chromeVisible ? 0 : -80,
left: 0,
right: 0,
child: Container(
color: const Color(0x66000000),
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
child: Row(
children: [
IconButton(
color: Colors.white,
icon: const Icon(Icons.arrow_back),
onPressed: () {
final r = GoRouter.of(context);
if (r.canPop()) {
context.pop();
} else {
context.go('/igel/${widget.igelId}');
}
},
),
const SizedBox(width: 6),
Expanded(
child: Text(
total == 0
? 'Galerie'
: '${currentIndex + 1} / $total',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600),
),
),
IconButton(
tooltip: 'Löschen',
color: Colors.white,
onPressed: total == 0 ? null : _deleteCurrent,
icon: const Icon(Icons.delete_outline),
),
],
),
),
),
// Bottom-Bar (Dateiname + Download) ein-/ausblendbar
AnimatedPositioned(
duration: const Duration(milliseconds: 180),
bottom: _chromeVisible ? 0 : -72,
left: 0,
right: 0,
child: SafeArea(
top: false,
child: Container(
color: const Color(0x66000000),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Expanded(
child: Text(
total == 0
? ''
: (images[currentIndex].originalName ?? ''),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white70),
),
),
const SizedBox(width: 8),
IconButton(
tooltip: 'Download',
color: Colors.white,
onPressed: total == 0 ? null : _downloadCurrent,
icon: const Icon(Icons.download),
),
],
),
),
),
),
],
),
),
),
);
}
}
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../main.dart';
import '../../igel/data/igel_repository.dart';
import '../domain/igel.dart';
@@ -9,25 +10,30 @@ import '../domain/igel.dart';
class IgelListScreen extends ConsumerStatefulWidget {
const IgelListScreen({super.key});
@override
ConsumerState<IgelListScreen> createState() => _State();
ConsumerState<IgelListScreen> createState() => _IgelListState();
}
class _State extends ConsumerState<IgelListScreen> {
class _IgelListState extends ConsumerState<IgelListScreen> {
late final IgelRepository repo;
List<Igel> items = [];
bool busy = true;
String? err;
// Suche
final searchC = TextEditingController();
String query = '';
Timer? _debounce;
// Inline-Form
bool showForm = false;
final nameC = TextEditingController();
final noteC = TextEditingController();
final noteC =
TextEditingController(); // Notiz bleibt im Edit/Erstellen, wird aber NICHT in der Liste gezeigt
final featureC = TextEditingController();
String? gender;
String? gender; // 'männlich' | 'weiblich' | 'unbekannt' | null (keine Angabe)
// Undo
Igel? _lastDeleted;
@override
@@ -97,7 +103,7 @@ class _State extends ConsumerState<IgelListScreen> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(msg),
action: onAction != null && actionLabel != null
action: (onAction != null && actionLabel != null)
? SnackBarAction(label: actionLabel, onPressed: onAction)
: null,
),
@@ -146,6 +152,7 @@ class _State extends ConsumerState<IgelListScreen> {
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
// Suche
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
@@ -159,6 +166,7 @@ class _State extends ConsumerState<IgelListScreen> {
),
),
),
// Neu-Button
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(
@@ -176,6 +184,7 @@ class _State extends ConsumerState<IgelListScreen> {
),
),
),
// Inline-Form
if (showForm)
SliverToBoxAdapter(
child: Padding(
@@ -266,6 +275,7 @@ class _State extends ConsumerState<IgelListScreen> {
),
),
),
// Liste
SliverList.separated(
itemCount: visible.length,
separatorBuilder: (_, __) => const Divider(height: 1),
@@ -274,6 +284,8 @@ class _State extends ConsumerState<IgelListScreen> {
return _IgelTile(
key: ValueKey('igel-${x.id}'),
igel: x,
onTap: () =>
context.go('/igel/${x.id}'), // Detailseite
onEditAll: (result) async {
await repo.update(
x.id,
@@ -329,11 +341,13 @@ class _IgelTile extends StatelessWidget {
const _IgelTile({
super.key,
required this.igel,
required this.onTap,
required this.onEditAll,
required this.onDelete,
});
final Igel igel;
final VoidCallback onTap;
final Future<void> Function(_IgelEditResult updated) onEditAll;
final Future<void> Function() onDelete;
@@ -375,6 +389,7 @@ class _IgelTile extends StatelessWidget {
?.copyWith(fontWeight: FontWeight.w600);
return ListTile(
onTap: onTap,
leading: CircleAvatar(
child: Text(igel.name.isNotEmpty ? igel.name[0].toUpperCase() : '?')),
title: Row(
@@ -437,6 +452,8 @@ class _IgelTile extends StatelessWidget {
}
}
// ----- Edit-Dialog: Name, Geschlecht (optional), Merkmal, Notiz --------------
class _IgelEditResult {
final String name;
final String? gender;
@@ -0,0 +1,91 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../main.dart'; // kApiBase
import '../../auth/data/token_storage.dart'; // tokenStorageProvider
import '../domain/messwert.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MesswerteRepository {
MesswerteRepository(this._tokens);
final TokenStorage _tokens;
Future<List<Messwert>> list(int igelId) async {
final t = await _tokens.access;
final r = await http.get(
Uri.parse('$kApiBase/igel/$igelId/messwerte'),
headers: {'Authorization': 'Bearer $t'},
);
if (r.statusCode != 200) {
throw Exception('API ${r.statusCode}: ${r.body}');
}
final List data = jsonDecode(r.body) as List;
return data
.map((e) => Messwert.fromMap(e as Map<String, dynamic>))
.toList();
}
Future<void> create({
required int igelId,
required DateTime datum,
required int gewicht,
String? behandlung,
String? bemerkung,
}) async {
final t = await _tokens.access;
final r = await http.post(
Uri.parse('$kApiBase/igel/$igelId/messwerte'),
headers: {
'Authorization': 'Bearer $t',
'Content-Type': 'application/json',
},
body: jsonEncode({
'datum': datum.toUtc().toIso8601String(),
'gewicht': gewicht,
'behandlung': behandlung,
'bemerkung': bemerkung,
}),
);
if (r.statusCode != 201) {
throw Exception('API ${r.statusCode}: ${r.body}');
}
}
Future<void> update(int id, Messwert m) async {
final t = await _tokens.access;
final r = await http.put(
Uri.parse('$kApiBase/messwerte/$id'),
headers: {
'Authorization': 'Bearer $t',
'Content-Type': 'application/json',
},
body: jsonEncode({
// Wichtig: KEIN 'id' im Body mitschicken der steht bereits in der URL!
'datum': m.datum.toUtc().toIso8601String(),
'gewicht': m.gewicht,
'behandlung': m.behandlung,
'bemerkung': m.bemerkung,
}),
);
if (r.statusCode != 200) {
throw Exception('API ${r.statusCode}: ${r.body}');
}
}
Future<void> delete(int id) async {
final t = await _tokens.access;
final r = await http.delete(
Uri.parse('$kApiBase/messwerte/$id'),
headers: {'Authorization': 'Bearer $t'},
);
if (r.statusCode != 200) {
throw Exception('API ${r.statusCode}: ${r.body}');
}
}
}
// Riverpod Provider
final messwerteRepoProvider = Provider<MesswerteRepository>((ref) {
final tokens = ref.read(tokenStorageProvider);
return MesswerteRepository(tokens);
});
@@ -0,0 +1,47 @@
class Messwert {
final int id;
final DateTime datum;
final int gewicht;
final String? behandlung;
final String? bemerkung;
Messwert({
required this.id,
required this.datum,
required this.gewicht,
this.behandlung,
this.bemerkung,
});
factory Messwert.fromMap(Map<String, dynamic> m) => Messwert(
id: (m['id'] as num).toInt(),
datum: DateTime.parse(m['datum'] as String).toLocal(),
gewicht: (m['gewicht'] as num).toInt(),
behandlung: m['behandlung'] as String?,
bemerkung: m['bemerkung'] as String?,
);
Map<String, dynamic> toMap() => {
'id': id,
'datum': datum.toIso8601String(),
'gewicht': gewicht,
'behandlung': behandlung,
'bemerkung': bemerkung,
};
Messwert copyWith({
int? id,
DateTime? datum,
int? gewicht,
String? behandlung,
String? bemerkung,
}) {
return Messwert(
id: id ?? this.id,
datum: datum ?? this.datum,
gewicht: gewicht ?? this.gewicht,
behandlung: behandlung ?? this.behandlung,
bemerkung: bemerkung ?? this.bemerkung,
);
}
}
+7
View File
@@ -4,6 +4,7 @@ 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/igel/data/igel_images_repository.dart';
const kApiBase = String.fromEnvironment(
'API_BASE',
@@ -20,6 +21,12 @@ final apiClientProvider = Provider<ApiClient>((ref) => ApiClient(
));
final igelRepoProvider = Provider<IgelRepository>(
(ref) => IgelRepository(ref.read(apiClientProvider)));
final igelImagesRepoProvider =
Provider<IgelImagesRepository>((ref) => IgelImagesRepository(
ref.read(apiClientProvider),
getAccessToken: () =>
ref.read(tokenStorageProvider).getValidAccessToken(),
));
void main() {
runApp(const ProviderScope(child: IgelApp()));
@@ -6,9 +6,13 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_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) 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);
+1
View File
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_secure_storage_linux
)
@@ -5,10 +5,12 @@
import FlutterMacOS
import Foundation
import file_selector_macos
import flutter_secure_storage_macos
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
}
+128
View File
@@ -150,6 +150,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
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:
@@ -182,6 +190,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
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:
@@ -195,6 +235,22 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
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:
@@ -312,6 +368,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
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"
io:
dependency: transitive
description:
@@ -344,6 +464,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.9.0"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
logging:
dependency: transitive
description:
+2
View File
@@ -10,10 +10,12 @@ dependencies:
http: ^1.2.2
flutter_secure_storage: ^9.2.2
json_annotation: ^4.9.0
image_picker: ^1.0.7
dev_dependencies:
build_runner: ^2.4.11
json_serializable: ^6.9.0
flutter_lints: ^3.0.2
flutter:
uses-material-design: true
@@ -6,9 +6,12 @@
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
}
+1
View File
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_secure_storage_windows
)