feat: enhance UI, branding assets, onboarding flow, and screen layouts

This commit is contained in:
R-Bharathraj
2026-09-18 15:23:30 +05:30
parent cafffd27e0
commit 5122eebc11
68 changed files with 1661 additions and 470 deletions

View File

@@ -1,12 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../core/theme.dart';
import '../core/permissions.dart';
import '../models/compliance.dart';
import '../state/compliance_controller.dart';
import '../state/session_controller.dart';
import '../widgets/common.dart';
IconData _sectionIcon(ComplianceSection s) => switch (s) {
@@ -299,8 +303,12 @@ class ComplianceDetailScreen extends ConsumerWidget {
final ComplianceSection section;
final String itemId;
/// Picks a real file: the camera for a new photo, the gallery for an
/// existing one. Camera access is requested first, because image_picker
/// surfaces a refusal as a plain null and the worker would be left tapping a
/// button that silently does nothing.
Future<void> _upload(BuildContext context, WidgetRef ref) async {
final choice = await showModalBottomSheet<String>(
final source = await showModalBottomSheet<ImageSource>(
context: context,
showDragHandle: true,
builder: (context) => SafeArea(
@@ -311,29 +319,67 @@ class ComplianceDetailScreen extends ConsumerWidget {
leading: const Icon(LucideIcons.camera),
title: const Text('Take a photo'),
subtitle: const Text('It has to be the real item, worn by you'),
onTap: () => Navigator.pop(context, 'camera'),
onTap: () => Navigator.pop(context, ImageSource.camera),
),
ListTile(
leading: const Icon(LucideIcons.image),
title: const Text('Choose from library'),
onTap: () => Navigator.pop(context, 'library'),
),
ListTile(
leading: const Icon(LucideIcons.file),
title: const Text('Pick a PDF'),
onTap: () => Navigator.pop(context, 'file'),
onTap: () => Navigator.pop(context, ImageSource.gallery),
),
],
),
),
);
if (choice == null || !context.mounted) return;
if (source == null || !context.mounted) return;
final ext = choice == 'file' ? 'pdf' : 'jpg';
final slug = itemId.toLowerCase();
if (source == ImageSource.camera) {
final granted = await ref
.read(sessionProvider.notifier)
.request(AppPermission.camera);
if (!context.mounted) return;
if (!granted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Camera access is off'),
action: SnackBarAction(
label: 'Settings',
onPressed: ref.read(permissionServiceProvider).openSettings,
),
),
);
return;
}
}
final XFile? picked;
try {
picked = await ImagePicker().pickImage(
source: source,
// Documents only need to be legible, and a full-resolution phone
// photo is a slow upload on a venue's wifi.
maxWidth: 2000,
imageQuality: 85,
);
} on PlatformException catch (error) {
if (!context.mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Could not open: ${error.code}')));
return;
}
// Null means the picker was dismissed, which is not an error.
if (picked == null || !context.mounted) return;
final size = await picked.length();
if (!context.mounted) return;
ref
.read(complianceProvider.notifier)
.uploadFile(section, itemId, 'krow-$slug.$ext');
.uploadFile(
section,
itemId,
picked.name,
meta: '${(size / 1024 / 1024).toStringAsFixed(1)} MB · just now',
);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Uploaded - sent for review')));