Add full app source
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.
This commit is contained in:
558
lib/widgets/glass_widgets.dart
Normal file
558
lib/widgets/glass_widgets.dart
Normal file
@@ -0,0 +1,558 @@
|
||||
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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user