* Task/Ticket desktop dialogs — widened from 520 → 600dp, and tasks now uses SizedBox(width: 600) instead of ConstrainedBox(maxWidth:) which was the root cause of the "still small" issue (max-only never forced expansion)
* Pass Slip + Leave — both dialogs gained an isSheet flag; callers now branch on AppBreakpoints.tablet: bottom sheet on mobile, dialog on desktop
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'package:flutter/foundation.dart' show debugPrint, kIsWeb;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
@@ -13,6 +16,61 @@ import 'connectivity_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
|
||||
const _kFaceCacheEnrolledAtPrefix = 'face_cache_enrolled_at:';
|
||||
|
||||
/// Reads cached enrolled-face bytes from app documents (mobile/desktop only).
|
||||
Future<Uint8List?> _readCachedEnrolledFace(String userId) async {
|
||||
if (kIsWeb) return null;
|
||||
try {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final file = File('${dir.path}/face_enrollment/$userId.jpg');
|
||||
if (await file.exists()) {
|
||||
return await file.readAsBytes();
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Writes enrolled-face bytes to disk so face matching can run offline.
|
||||
/// Best-effort — never throws.
|
||||
Future<void> _writeCachedEnrolledFace(
|
||||
String userId,
|
||||
Uint8List bytes,
|
||||
String? enrolledAt,
|
||||
) async {
|
||||
if (kIsWeb) return;
|
||||
try {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final cacheDir = Directory('${dir.path}/face_enrollment');
|
||||
if (!await cacheDir.exists()) {
|
||||
await cacheDir.create(recursive: true);
|
||||
}
|
||||
final file = File('${cacheDir.path}/$userId.jpg');
|
||||
await file.writeAsBytes(bytes);
|
||||
if (enrolledAt != null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('$_kFaceCacheEnrolledAtPrefix$userId', enrolledAt);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// Pre-warms the enrolled-face cache once per session if it is missing or
|
||||
/// out-of-date relative to the server's `face_enrolled_at` timestamp.
|
||||
/// No-op on web. Safe to call from a profile listener.
|
||||
Future<void> prewarmEnrolledFaceCache({
|
||||
required ProfileController controller,
|
||||
required String userId,
|
||||
required String? enrolledAt,
|
||||
}) async {
|
||||
if (kIsWeb || enrolledAt == null) return;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cachedAt = prefs.getString('$_kFaceCacheEnrolledAtPrefix$userId');
|
||||
if (cachedAt == enrolledAt) return; // already up-to-date
|
||||
await controller.downloadFacePhoto(userId, enrolledAt: enrolledAt);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offline-pending state
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -217,13 +275,26 @@ class ProfileController {
|
||||
|
||||
/// Download the face enrollment photo bytes for the given user.
|
||||
/// Uses Supabase authenticated storage API (works with private buckets).
|
||||
Future<Uint8List?> downloadFacePhoto(String userId) async {
|
||||
///
|
||||
/// On a successful online download, bytes are also written to a local cache
|
||||
/// (`${appDocs}/face_enrollment/<userId>.jpg`) so subsequent offline
|
||||
/// verification can read them. When the storage download fails (offline,
|
||||
/// auth refresh, etc.) we fall back to the cached file. Pass [enrolledAt]
|
||||
/// (the server's `face_enrolled_at` ISO timestamp) so the prewarm helper
|
||||
/// can detect re-enrollments and refresh the cache.
|
||||
Future<Uint8List?> downloadFacePhoto(
|
||||
String userId, {
|
||||
String? enrolledAt,
|
||||
}) async {
|
||||
try {
|
||||
return await _client.storage
|
||||
final bytes = await _client.storage
|
||||
.from('face-enrollment')
|
||||
.download('$userId/face.jpg');
|
||||
// Best-effort cache write so face matching can run offline next time.
|
||||
unawaited(_writeCachedEnrolledFace(userId, bytes, enrolledAt));
|
||||
return bytes;
|
||||
} catch (_) {
|
||||
return null;
|
||||
return await _readCachedEnrolledFace(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user