Adds the Flutter inventory app source, cleaned up for a shared repo: generic package name, no hardcoded ERP endpoints (moved to gitignored local config), no dead auth code, no debug logging of session data.
559 lines
16 KiB
Dart
559 lines
16 KiB
Dart
import 'dart:typed_data';
|
|
import 'dart:ui' show ImageFilter;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:inventory_app/constants.dart';
|
|
|
|
// ── Gradient background ───────────────────────────────────────────────────────
|
|
|
|
class FancyBackground extends StatelessWidget {
|
|
const FancyBackground({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final dark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors:
|
|
dark
|
|
? [
|
|
const Color(0xFF0A0A0A),
|
|
const Color(0xFF121212),
|
|
const Color(0xFF1E1E1E),
|
|
]
|
|
: [
|
|
const Color(0xFFEEF2FF),
|
|
const Color(0xFFE0F2FE),
|
|
const Color(0xFFE6FFFA),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Positioned(
|
|
top: -60,
|
|
right: -40,
|
|
child: _Blob(
|
|
color:
|
|
dark
|
|
? Colors.blue.withOpacity(0.1)
|
|
: Colors.blue.withOpacity(0.18),
|
|
size: 170,
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: -40,
|
|
left: -40,
|
|
child: _Blob(
|
|
color:
|
|
dark
|
|
? Colors.cyanAccent.withOpacity(0.08)
|
|
: Colors.cyan.withOpacity(0.16),
|
|
size: 150,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Blob extends StatelessWidget {
|
|
final Color color;
|
|
final double size;
|
|
|
|
const _Blob({required this.color, required this.size});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: size,
|
|
height: size,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: color,
|
|
boxShadow: [BoxShadow(color: color, blurRadius: 60, spreadRadius: 30)],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Glass card ────────────────────────────────────────────────────────────────
|
|
|
|
class GlassCard extends StatelessWidget {
|
|
final Widget child;
|
|
final EdgeInsets? padding;
|
|
|
|
const GlassCard({super.key, required this.child, this.padding});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final dark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
gradient: LinearGradient(
|
|
colors:
|
|
dark
|
|
? [
|
|
Colors.white.withOpacity(0.06),
|
|
Colors.white.withOpacity(0.03),
|
|
]
|
|
: [
|
|
Colors.white.withOpacity(0.42),
|
|
Colors.white.withOpacity(0.18),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
border: Border.all(
|
|
color:
|
|
dark
|
|
? Colors.white.withOpacity(0.08)
|
|
: Colors.white.withOpacity(0.5),
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color:
|
|
dark
|
|
? Colors.black.withOpacity(0.4)
|
|
: Colors.blueGrey.withOpacity(0.15),
|
|
blurRadius: 18,
|
|
offset: const Offset(0, 10),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: BackdropFilter(
|
|
filter: ImageFilter.blur(sigmaX: 14, sigmaY: 14),
|
|
child: Padding(
|
|
padding: padding ?? const EdgeInsets.all(16),
|
|
child: child,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── App bar for sub-pages (with back button) ──────────────────────────────────
|
|
|
|
class BackAppBar extends StatelessWidget {
|
|
final String title;
|
|
final IconData? icon;
|
|
final List<Widget>? actions;
|
|
|
|
const BackAppBar({super.key, required this.title, this.icon, this.actions});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 10),
|
|
child: GlassCard(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
|
child: Row(
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.arrow_back_rounded),
|
|
onPressed: () => Get.back(),
|
|
color: isDark ? Colors.white70 : Colors.black87,
|
|
),
|
|
if (icon != null) Icon(icon!, color: Colors.blue),
|
|
if (icon != null) const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w800,
|
|
color: isDark ? Colors.white : Colors.blue.shade700,
|
|
),
|
|
),
|
|
),
|
|
if (actions != null) ...actions!,
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Glass bottom sheet ────────────────────────────────────────────────────────
|
|
|
|
class GlassBottomSheet extends StatelessWidget {
|
|
final Widget child;
|
|
|
|
const GlassBottomSheet({super.key, required this.child});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: GlassCard(child: child),
|
|
);
|
|
}
|
|
}
|
|
|
|
class SheetHandle extends StatelessWidget {
|
|
final Color color;
|
|
|
|
const SheetHandle(this.color, {super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Container(
|
|
width: 50,
|
|
height: 6,
|
|
margin: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(.4),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Text field ────────────────────────────────────────────────────────────────
|
|
|
|
class AppTextField extends StatelessWidget {
|
|
final TextEditingController controller;
|
|
final String label;
|
|
final bool enabled;
|
|
final IconData? icon;
|
|
final TextInputType? type;
|
|
|
|
const AppTextField({
|
|
super.key,
|
|
required this.controller,
|
|
required this.label,
|
|
this.enabled = true,
|
|
this.icon,
|
|
this.type,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final dark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return TextField(
|
|
controller: controller,
|
|
enabled: enabled,
|
|
keyboardType: type,
|
|
style: kTextFormFieldStyle(
|
|
color: dark ? Colors.white.withOpacity(.9) : Colors.black87,
|
|
),
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
prefixIcon:
|
|
icon != null
|
|
? Icon(icon, color: dark ? Colors.white70 : Colors.black54)
|
|
: null,
|
|
filled: true,
|
|
fillColor: dark ? Colors.white12 : Colors.white.withOpacity(.8),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class FancyTextField extends StatelessWidget {
|
|
final TextEditingController controller;
|
|
final String label;
|
|
final String hint;
|
|
final IconData icon;
|
|
final int maxLines;
|
|
final String? Function(String?)? validator;
|
|
|
|
const FancyTextField({
|
|
super.key,
|
|
required this.controller,
|
|
required this.label,
|
|
required this.hint,
|
|
required this.icon,
|
|
this.maxLines = 1,
|
|
this.validator,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final border = OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
borderSide: BorderSide.none,
|
|
);
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return TextFormField(
|
|
controller: controller,
|
|
maxLines: maxLines,
|
|
validator: validator,
|
|
style: TextStyle(
|
|
color: isDark ? Colors.white.withOpacity(0.9) : Colors.black87,
|
|
),
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
hintText: hint,
|
|
prefixIcon: Icon(icon, color: isDark ? Colors.white70 : Colors.black54),
|
|
filled: true,
|
|
fillColor:
|
|
isDark
|
|
? Colors.white.withOpacity(0.08)
|
|
: Colors.white.withOpacity(0.75),
|
|
enabledBorder: border,
|
|
focusedBorder: border,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Dropdown ──────────────────────────────────────────────────────────────────
|
|
|
|
class AppDropdown<T> extends StatelessWidget {
|
|
final T? value;
|
|
final List<T> items;
|
|
final String hint;
|
|
final String Function(T) labelBuilder;
|
|
final ValueChanged<T?> onChanged;
|
|
|
|
const AppDropdown({
|
|
super.key,
|
|
required this.value,
|
|
required this.items,
|
|
required this.hint,
|
|
required this.labelBuilder,
|
|
required this.onChanged,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final dark = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return DropdownButtonFormField<T>(
|
|
value: value,
|
|
isExpanded: true,
|
|
items:
|
|
items
|
|
.map(
|
|
(e) => DropdownMenuItem(
|
|
value: e,
|
|
child: Text(
|
|
labelBuilder(e),
|
|
overflow: TextOverflow.ellipsis,
|
|
style: kTextFormFieldStyle(
|
|
color: dark ? Colors.white : Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
.toList(),
|
|
onChanged: onChanged,
|
|
decoration: InputDecoration(
|
|
labelText: hint,
|
|
filled: true,
|
|
fillColor: dark ? Colors.white12 : Colors.white.withOpacity(.8),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Image loader ──────────────────────────────────────────────────────────────
|
|
|
|
class AppImage extends StatelessWidget {
|
|
final Future<Uint8List?> future;
|
|
|
|
const AppImage({super.key, required this.future});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FutureBuilder<Uint8List?>(
|
|
future: future,
|
|
builder: (_, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const GlassCard(
|
|
child: Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(12),
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (!snapshot.hasData) return const SizedBox.shrink();
|
|
|
|
return GlassCard(
|
|
padding: EdgeInsets.zero,
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: Image.memory(
|
|
snapshot.data!,
|
|
height: 200,
|
|
width: double.infinity,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Section header ────────────────────────────────────────────────────────────
|
|
|
|
class SectionHeader extends StatelessWidget {
|
|
final IconData icon;
|
|
final String title;
|
|
final Color accent;
|
|
|
|
const SectionHeader({
|
|
super.key,
|
|
required this.icon,
|
|
required this.title,
|
|
required this.accent,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: accent.withOpacity(.15),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(icon, color: accent),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
title,
|
|
style: kTextFormFieldStyle(
|
|
fontSize: 18.0,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.blue.shade700,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class SubLabel extends StatelessWidget {
|
|
final String text;
|
|
final MaterialColor color;
|
|
|
|
const SubLabel(this.text, this.color, {super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Text(
|
|
text,
|
|
style: kTextFormFieldStyle(
|
|
fontSize: 14.0,
|
|
fontWeight: FontWeight.w600,
|
|
color: color.shade700,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Radio tile ────────────────────────────────────────────────────────────────
|
|
|
|
class TileRadio extends StatelessWidget {
|
|
final String value;
|
|
final String? groupValue;
|
|
final String label;
|
|
final IconData icon;
|
|
final ValueChanged<String?> onChanged;
|
|
|
|
const TileRadio({
|
|
super.key,
|
|
required this.value,
|
|
required this.groupValue,
|
|
required this.label,
|
|
required this.icon,
|
|
required this.onChanged,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final selected = value == groupValue;
|
|
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
margin: const EdgeInsets.symmetric(vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color:
|
|
selected
|
|
? Colors.white.withOpacity(.85)
|
|
: Colors.white.withOpacity(.6),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(
|
|
color:
|
|
selected
|
|
? Colors.blue.withOpacity(.6)
|
|
: Colors.white.withOpacity(.4),
|
|
),
|
|
),
|
|
child: ListTile(
|
|
leading: Icon(icon, color: selected ? Colors.blue : Colors.black54),
|
|
title: Text(label, style: kTextFormFieldStyle()),
|
|
trailing: Radio<String>(
|
|
value: value,
|
|
groupValue: groupValue,
|
|
onChanged: onChanged,
|
|
),
|
|
onTap: () => onChanged(value),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── FAB ───────────────────────────────────────────────────────────────────────
|
|
|
|
class FancyFAB extends StatelessWidget {
|
|
final Color primary;
|
|
final VoidCallback onTap;
|
|
|
|
const FancyFAB({super.key, required this.primary, required this.onTap});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(colors: [primary, primary.withOpacity(.8)]),
|
|
borderRadius: BorderRadius.circular(28),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: primary.withOpacity(.35),
|
|
blurRadius: 24,
|
|
offset: const Offset(0, 10),
|
|
),
|
|
],
|
|
),
|
|
child: FloatingActionButton.extended(
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
onPressed: onTap,
|
|
icon: const Icon(Icons.document_scanner, color: Colors.white),
|
|
label: const Text("Scanner", style: TextStyle(color: Colors.white)),
|
|
),
|
|
);
|
|
}
|
|
}
|