add two views
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
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),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user