onedirve and google drive
This commit is contained in:
parent
a9bcec09b1
commit
901ec678a3
12 changed files with 373 additions and 29 deletions
|
|
@ -9,6 +9,7 @@ import '../services/app_state.dart';
|
|||
import '../services/ocr_service.dart';
|
||||
import '../services/vin_parser.dart';
|
||||
import '../widgets/image_source_sheet.dart';
|
||||
import 'vehicle_detail_screen.dart';
|
||||
|
||||
/// Add/edit form for a vehicle. Pass an existing [vehicle] to edit it, or
|
||||
/// omit it to create a new one.
|
||||
|
|
@ -28,6 +29,7 @@ class _AddEditVehicleScreenState extends State<AddEditVehicleScreen> {
|
|||
|
||||
bool _saving = false;
|
||||
bool _scanningVin = false;
|
||||
bool _checkingDuplicateVin = false;
|
||||
String? _error;
|
||||
|
||||
bool get _isEditing => widget.vehicle != null;
|
||||
|
|
@ -84,6 +86,7 @@ class _AddEditVehicleScreenState extends State<AddEditVehicleScreen> {
|
|||
|
||||
if (vin != null) {
|
||||
_vinController.text = vin;
|
||||
await _checkForExistingVin();
|
||||
} else if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
|
|
@ -93,6 +96,62 @@ class _AddEditVehicleScreenState extends State<AddEditVehicleScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Field-level counterpart to [_checkForExistingVin]'s dialog: catches
|
||||
/// "Required" as before, plus flags the field itself as invalid when the
|
||||
/// VIN belongs to another active vehicle — excluding this vehicle's own
|
||||
/// id while editing, same as [AppState.updateVehicle]'s check, so editing
|
||||
/// a vehicle without touching its VIN doesn't flag itself.
|
||||
String? _validateVin(String? value) {
|
||||
final vin = value?.trim() ?? '';
|
||||
if (vin.isEmpty) return 'Required';
|
||||
|
||||
final existing = context.read<AppState>().vehicleByVin(vin);
|
||||
if (existing != null && existing.id != widget.vehicle?.id) {
|
||||
return 'A vehicle with VIN "$vin" already exists.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Only relevant when adding a new vehicle — editing one already starts
|
||||
/// with its own VIN pre-filled, which would trivially "match itself".
|
||||
/// Guarded against overlapping calls so rapid edits (or the scan-fill
|
||||
/// path firing right after `onChanged`) can't stack multiple dialogs.
|
||||
Future<void> _checkForExistingVin() async {
|
||||
if (_isEditing || _checkingDuplicateVin) return;
|
||||
|
||||
final vin = _vinController.text.trim();
|
||||
if (vin.isEmpty) return;
|
||||
final existing = context.read<AppState>().vehicleByVin(vin);
|
||||
if (existing == null) return;
|
||||
|
||||
_checkingDuplicateVin = true;
|
||||
final openExisting = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Vehicle already exists'),
|
||||
content: Text('Vehicle with the VIN $vin already exists, would you like to open that vehicle?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.of(context).pop(false), child: const Text('No')),
|
||||
TextButton(onPressed: () => Navigator.of(context).pop(true), child: const Text('Yes')),
|
||||
],
|
||||
),
|
||||
);
|
||||
_checkingDuplicateVin = false;
|
||||
if (!mounted) return;
|
||||
|
||||
if (openExisting != true) {
|
||||
// Declining to open the existing vehicle leaves them here with a
|
||||
// still-duplicate VIN — surface that on the field itself rather than
|
||||
// only catching it later at Save.
|
||||
_formKey.currentState?.validate();
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..push(MaterialPageRoute(builder: (_) => VehicleDetailScreen(vehicleId: existing.id)));
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
|
|
@ -205,7 +264,8 @@ class _AddEditVehicleScreenState extends State<AddEditVehicleScreen> {
|
|||
),
|
||||
),
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null,
|
||||
validator: _validateVin,
|
||||
onChanged: _isEditing ? null : (_) => _checkForExistingVin(),
|
||||
),
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ enum _BrowseRoot { myFiles, sharedWithMe }
|
|||
/// Lets the user navigate whichever cloud storage provider is connected —
|
||||
/// their own files, and (if the provider supports it) files others have
|
||||
/// shared with them — and pick a parent location. Confirming looks for (or
|
||||
/// creates) the `MO-Fuel-Tax-Back` folder under that location, so two
|
||||
/// people pointing at the same shared parent converge on the same app
|
||||
/// folder, regardless of which provider each of them is using.
|
||||
/// creates) the `Show Me The Fuel Refund` folder under that location, so
|
||||
/// two people pointing at the same shared parent converge on the same app
|
||||
/// folder, regardless of which provider each of them is using. Picking a
|
||||
/// folder that's already named that directly (e.g. a shared folder set up
|
||||
/// by someone else) uses it as-is instead of nesting another one inside —
|
||||
/// see `CloudSyncService.selectAppFolder`.
|
||||
class CloudFolderBrowserScreen extends StatefulWidget {
|
||||
const CloudFolderBrowserScreen({super.key});
|
||||
|
||||
|
|
@ -54,6 +57,12 @@ class _CloudFolderBrowserScreenState extends State<CloudFolderBrowserScreen> {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// The currently browsed-into folder's own name, so [_useThisFolder] can
|
||||
/// tell whether the user picked a folder already named
|
||||
/// `Show Me The Fuel Refund` — null at a virtual root ("My Files"),
|
||||
/// which is never itself named that.
|
||||
String? get _currentFolderName => _pathStack.isNotEmpty ? _pathStack.last.name : null;
|
||||
|
||||
String get _breadcrumbPath =>
|
||||
([_rootLabel] + _pathStack.map((f) => f.name).toList()).join(' / ');
|
||||
|
||||
|
|
@ -121,6 +130,7 @@ class _CloudFolderBrowserScreenState extends State<CloudFolderBrowserScreen> {
|
|||
await context.read<AppState>().chooseCloudFolder(
|
||||
parentId: parentId,
|
||||
breadcrumbPath: _breadcrumbPath,
|
||||
currentFolderName: _currentFolderName,
|
||||
);
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
} catch (e) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue