241 lines
6.4 KiB
Dart
241 lines
6.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/app_theme.dart';
|
|
|
|
class EntryPanel extends StatelessWidget {
|
|
const EntryPanel({
|
|
super.key,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.child,
|
|
});
|
|
final String title;
|
|
final String subtitle;
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.inputSurface,
|
|
borderRadius: BorderRadius.circular(24),
|
|
border: Border.all(color: const Color(0xFFCEE6DF)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(fontSize: 25, fontWeight: FontWeight.w700),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
subtitle,
|
|
style: const TextStyle(color: AppTheme.muted, height: 1.5),
|
|
),
|
|
const SizedBox(height: 24),
|
|
child,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
class EntryMessage extends StatelessWidget {
|
|
const EntryMessage({
|
|
super.key,
|
|
required this.message,
|
|
required this.hasError,
|
|
});
|
|
final String? message;
|
|
final bool hasError;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => message == null
|
|
? const SizedBox.shrink()
|
|
: Padding(
|
|
padding: const EdgeInsets.only(top: 16),
|
|
child: Semantics(
|
|
liveRegion: true,
|
|
child: Text(
|
|
message!,
|
|
style: TextStyle(
|
|
color: hasError
|
|
? Theme.of(context).colorScheme.error
|
|
: AppTheme.teal,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
class EntryNumberField extends StatelessWidget {
|
|
const EntryNumberField({
|
|
super.key,
|
|
required this.controller,
|
|
required this.label,
|
|
required this.unit,
|
|
required this.validator,
|
|
});
|
|
final TextEditingController controller;
|
|
final String label;
|
|
final String unit;
|
|
final FormFieldValidator<String> validator;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => TextFormField(
|
|
controller: controller,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
decoration: InputDecoration(labelText: label, suffixText: unit),
|
|
validator: validator,
|
|
);
|
|
}
|
|
|
|
class EntryFieldGrid extends StatelessWidget {
|
|
const EntryFieldGrid({super.key, required this.children});
|
|
final List<Widget> children;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final columns = constraints.maxWidth >= 600 ? 2 : 1;
|
|
final width = (constraints.maxWidth - (columns - 1) * 16) / columns;
|
|
return Wrap(
|
|
spacing: 16,
|
|
runSpacing: 16,
|
|
children: children
|
|
.map((child) => SizedBox(width: width, child: child))
|
|
.toList(),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Lazily builds saved entries below the input form.
|
|
class SavedEntries extends StatelessWidget {
|
|
const SavedEntries({
|
|
super.key,
|
|
required this.title,
|
|
required this.count,
|
|
required this.emptyText,
|
|
required this.itemBuilder,
|
|
});
|
|
final String title;
|
|
final int count;
|
|
final String emptyText;
|
|
final IndexedWidgetBuilder itemBuilder;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => DecoratedSliver(
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.inventorySurface,
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
sliver: SliverPadding(
|
|
padding: const EdgeInsets.all(24),
|
|
sliver: SliverMainAxisGroup(
|
|
slivers: [
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(bottom: 20),
|
|
child: Text(
|
|
'$title ($count)',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 23,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (count == 0)
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
|
child: Text(
|
|
emptyText,
|
|
style: const TextStyle(color: Color(0xFFB9CCD9), height: 1.5),
|
|
),
|
|
),
|
|
)
|
|
else
|
|
SliverList.separated(
|
|
itemCount: count,
|
|
itemBuilder: itemBuilder,
|
|
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
class SavedEntryCard extends StatelessWidget {
|
|
const SavedEntryCard({
|
|
super.key,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.details,
|
|
this.notes = '',
|
|
});
|
|
final String title;
|
|
final String subtitle;
|
|
final List<String> details;
|
|
final String notes;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Card(
|
|
margin: EdgeInsets.zero,
|
|
elevation: 0,
|
|
color: Colors.white,
|
|
surfaceTintColor: Colors.transparent,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 18),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
subtitle,
|
|
style: const TextStyle(color: AppTheme.muted, height: 1.5),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: details
|
|
.map(
|
|
(detail) => Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10,
|
|
vertical: 8,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.inputSurface,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
detail,
|
|
style: const TextStyle(color: AppTheme.teal),
|
|
),
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
if (notes.isNotEmpty) ...[
|
|
const SizedBox(height: 14),
|
|
Text(
|
|
notes,
|
|
style: const TextStyle(color: AppTheme.muted, height: 1.5),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|