Files
filament-tracker/lib/pages/detail_view.dart
2026-03-03 09:56:06 +01:00

658 lines
23 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/detail_controller.dart';
import '../models/filament_model.dart';
class DetailPage extends GetView<DetailController> {
static const String namedRoute = '/detail-page';
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: const Color(0xFF0D0F14),
body: PopScope(
// Beim System-Zurück das aktuelle Filament zurückgeben
canPop: false,
onPopInvokedWithResult: (_, _) =>
Get.back(result: controller.filament.value),
child: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final f = controller.filament.value;
return Stack(
children: [
// ── Hintergrund-Gradient ──────────────────────────────
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0D0F14),
Color(0xFF141824),
Color(0xFF1A2035),
Color(0xFF0F1520),
],
stops: [0.0, 0.35, 0.65, 1.0],
),
),
),
// ── Farbiger Glow oben ────────────────────────────────
Positioned(
top: -80,
left: -60,
child: Container(
width: 280,
height: 280,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _parseColor(f.color).withValues(alpha: 0.12),
),
),
),
// ── Content ──────────────────────────────────────────
SafeArea(
child: CustomScrollView(
slivers: [
// ── App Bar ───────────────────────────────────
SliverAppBar(
backgroundColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
elevation: 0,
pinned: true,
leading: IconButton(
icon: const Icon(
Icons.arrow_back_ios_new_rounded,
color: Colors.white,
),
// Aktuelles Filament beim Zurücknavigieren übergeben
onPressed: () =>
Get.back(result: controller.filament.value),
),
title: Obx(
() => Text(
controller.filament.value.name,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
actions: [
IconButton(
icon: const Icon(
Icons.edit_outlined,
color: Colors.white70,
),
onPressed: controller.openEditDialog,
tooltip: 'Bearbeiten',
),
IconButton(
icon: const Icon(
Icons.delete_outline_rounded,
color: Color(0xFFFF6B6B),
),
onPressed: controller.deleteFilament,
tooltip: 'Löschen',
),
const SizedBox(width: 8),
],
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
sliver: SliverList(
delegate: SliverChildListDelegate([
// ── Hero-Header ──────────────────────────
_HeroHeader(filament: f),
const SizedBox(height: 20),
// ── Gewicht-Karte ────────────────────────
_WeightCard(controller: controller),
const SizedBox(height: 16),
// ── Druck-Parameter ──────────────────────
_PrintParamsCard(filament: f),
const SizedBox(height: 16),
// ── Kauf-Info ────────────────────────────
_PurchaseCard(filament: f),
const SizedBox(height: 16),
// ── Notizen ──────────────────────────────
if (f.notes != null && f.notes!.isNotEmpty)
_NotesCard(notes: f.notes!),
]),
),
),
],
),
),
],
);
}),
), // PopScope
);
}
Color _parseColor(String colorStr) {
final known = {
'rot': Colors.red,
'blau': Colors.blue,
'grün': Colors.green,
'grau': Colors.grey,
'schwarz': Colors.black,
'weiß': Colors.white,
'gelb': Colors.yellow,
'orange': Colors.orange,
'lila': Colors.purple,
'pink': Colors.pink,
'braun': Colors.brown,
};
final lower = colorStr.toLowerCase();
if (known.containsKey(lower)) return known[lower]!;
final hex = colorStr.replaceAll('#', '');
if (hex.length == 6) {
final v = int.tryParse('FF$hex', radix: 16);
if (v != null) return Color(v);
}
return const Color(0xFF7B9FFF);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Hero Header
// ─────────────────────────────────────────────────────────────────────────────
class _HeroHeader extends StatelessWidget {
final FilamentModel filament;
const _HeroHeader({required this.filament});
Color get _color {
final known = {
'rot': Colors.red,
'blau': Colors.blue,
'grün': Colors.green,
'grau': Colors.grey,
'schwarz': Colors.black,
'weiß': Colors.white,
'gelb': Colors.yellow,
'orange': Colors.orange,
'lila': Colors.purple,
'pink': Colors.pink,
'braun': Colors.brown,
};
final lower = filament.color.toLowerCase();
if (known.containsKey(lower)) return known[lower]!;
final hex = filament.color.replaceAll('#', '');
if (hex.length == 6) {
final v = int.tryParse('FF$hex', radix: 16);
if (v != null) return Color(v);
}
return const Color(0xFF7B9FFF);
}
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Container(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white.withValues(alpha: 0.15)),
),
padding: const EdgeInsets.all(20),
child: Row(
children: [
// Großer Farb-Kreis
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _color,
boxShadow: [
BoxShadow(
color: _color.withValues(alpha: 0.45),
blurRadius: 20,
spreadRadius: 2,
),
],
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 2,
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
filament.name,
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Row(
children: [
_Badge(
label: filament.type,
color: const Color(0xFF7B9FFF),
),
const SizedBox(width: 8),
_Badge(label: filament.color, color: _color),
],
),
const SizedBox(height: 4),
Text(
filament.manufacturer,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 13,
),
),
],
),
),
],
),
),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Gewicht-Karte mit interaktivem Slider
// ─────────────────────────────────────────────────────────────────────────────
class _WeightCard extends StatefulWidget {
final DetailController controller;
const _WeightCard({required this.controller});
@override
State<_WeightCard> createState() => _WeightCardState();
}
class _WeightCardState extends State<_WeightCard> {
late double _sliderValue;
@override
void initState() {
super.initState();
_sliderValue = widget.controller.filament.value.weightUsed;
}
double get _remaining =>
(widget.controller.filament.value.weight - _sliderValue).clamp(
0,
widget.controller.filament.value.weight,
);
Color get _progressColor {
final p = widget.controller.filament.value.weight > 0
? _remaining / widget.controller.filament.value.weight
: 0;
if (p > 0.5) return const Color(0xFF4FFFB0);
if (p > 0.2) return const Color(0xFFFFD166);
return const Color(0xFFFF6B6B);
}
@override
Widget build(BuildContext context) {
final f = widget.controller.filament.value;
final maxWeight = f.weight;
return _GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _CardTitle(icon: Icons.scale_outlined, label: 'Füllstand'),
const SizedBox(height: 16),
// Kreisanzeige + Zahlen
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 120,
height: 120,
child: Stack(
alignment: Alignment.center,
children: [
SizedBox.expand(
child: CircularProgressIndicator(
value: maxWeight > 0 ? _remaining / maxWeight : 0,
strokeWidth: 10,
backgroundColor: Colors.white.withValues(alpha: 0.1),
valueColor: AlwaysStoppedAnimation(_progressColor),
strokeCap: StrokeCap.round,
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_remaining.toStringAsFixed(0),
style: TextStyle(
color: _progressColor,
fontSize: 26,
fontWeight: FontWeight.bold,
),
),
Text(
'von ${maxWeight.toStringAsFixed(0)} g',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.45),
fontSize: 11,
),
),
],
),
],
),
),
],
),
const SizedBox(height: 20),
// Slider für verbrauchtes Gewicht
Text(
'Verbraucht: ${_sliderValue.toStringAsFixed(0)} g',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 13,
),
),
const SizedBox(height: 6),
SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: _progressColor,
inactiveTrackColor: Colors.white.withValues(alpha: 0.12),
thumbColor: _progressColor,
overlayColor: _progressColor.withValues(alpha: 0.2),
trackHeight: 6,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10),
),
child: Slider(
value: _sliderValue.clamp(0, maxWeight),
min: 0,
max: maxWeight > 0 ? maxWeight : 1,
divisions: maxWeight > 0 ? maxWeight.toInt() : 1,
onChanged: (v) => setState(() => _sliderValue = v),
onChangeEnd: (v) => widget.controller.updateWeightUsed(v),
),
),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Druck-Parameter
// ─────────────────────────────────────────────────────────────────────────────
class _PrintParamsCard extends StatelessWidget {
final FilamentModel filament;
const _PrintParamsCard({required this.filament});
@override
Widget build(BuildContext context) {
return _GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _CardTitle(
icon: Icons.settings_outlined,
label: 'Druckparameter',
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _ParamTile(
icon: Icons.thermostat_outlined,
label: 'Düsentemperatur',
value: '${filament.printingTemp} °C',
color: const Color(0xFFFF9F43),
),
),
const SizedBox(width: 12),
Expanded(
child: _ParamTile(
icon: Icons.bed_outlined,
label: 'Betttemperatur',
value: '${filament.bedTemp} °C',
color: const Color(0xFF7B9FFF),
),
),
],
),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Kauf-Info
// ─────────────────────────────────────────────────────────────────────────────
class _PurchaseCard extends StatelessWidget {
final FilamentModel filament;
const _PurchaseCard({required this.filament});
@override
Widget build(BuildContext context) {
return _GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _CardTitle(
icon: Icons.receipt_long_outlined,
label: 'Kaufinfo',
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _ParamTile(
icon: Icons.euro_outlined,
label: 'Preis',
value: '${filament.price.toStringAsFixed(2)}',
color: const Color(0xFF4FFFB0),
),
),
const SizedBox(width: 12),
Expanded(
child: _ParamTile(
icon: Icons.calendar_today_outlined,
label: 'Kaufdatum',
value: filament.purchaseDate.isNotEmpty
? filament.purchaseDate
: '',
color: const Color(0xFFFFD166),
),
),
],
),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Notizen
// ─────────────────────────────────────────────────────────────────────────────
class _NotesCard extends StatelessWidget {
final String notes;
const _NotesCard({required this.notes});
@override
Widget build(BuildContext context) {
return _GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _CardTitle(icon: Icons.notes_outlined, label: 'Notizen'),
const SizedBox(height: 12),
Text(
notes,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.75),
fontSize: 14,
height: 1.5,
),
),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Shared Helper Widgets
// ─────────────────────────────────────────────────────────────────────────────
class _GlassCard extends StatelessWidget {
final Widget child;
const _GlassCard({required this.child});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.white.withValues(alpha: 0.12)),
),
padding: const EdgeInsets.all(20),
child: child,
),
),
);
}
}
class _CardTitle extends StatelessWidget {
final IconData icon;
final String label;
const _CardTitle({required this.icon, required this.label});
@override
Widget build(BuildContext context) {
return Row(
children: [
Icon(icon, color: Colors.white54, size: 18),
const SizedBox(width: 8),
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.55),
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 1.1,
),
),
],
);
}
}
class _ParamTile extends StatelessWidget {
final IconData icon;
final String label;
final String value;
final Color color;
const _ParamTile({
required this.icon,
required this.label,
required this.value,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: color.withValues(alpha: 0.25)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
color: color,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.45),
fontSize: 11,
),
),
],
),
);
}
}
class _Badge extends StatelessWidget {
final String label;
final Color color;
const _Badge({required this.label, required this.color});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withValues(alpha: 0.4)),
),
child: Text(
label,
style: TextStyle(
color: color,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
);
}
}