class Recipe { final String? id; final String title; final String? description; final String? servings; final String? prepTime; final String? cookTime; final String? totalTime; final String? image; final List images; final List ingredients; final List instructions; final String? sourceUrl; final String? shareToken; final String? createdAt; final String? updatedAt; Recipe({ this.id, required this.title, this.description, this.servings, this.prepTime, this.cookTime, this.totalTime, this.image, List? images, List? ingredients, List? instructions, this.sourceUrl, this.shareToken, this.createdAt, this.updatedAt, }) : images = images ?? [], ingredients = ingredients ?? [], instructions = instructions ?? []; factory Recipe.fromJson(Map json) { List parseStringList(dynamic val) { if (val is List) { return val.map((e) => e.toString().trim()).where((s) => s.isNotEmpty).toList(); } return []; } return Recipe( id: json['id']?.toString(), title: json['title']?.toString() ?? 'Untitled Recipe', description: json['description']?.toString(), servings: json['servings']?.toString(), prepTime: json['prepTime']?.toString(), cookTime: json['cookTime']?.toString(), totalTime: json['totalTime']?.toString(), image: json['image']?.toString(), images: parseStringList(json['images']), ingredients: parseStringList(json['ingredients']), instructions: parseStringList(json['instructions']), sourceUrl: json['sourceUrl']?.toString(), shareToken: json['shareToken']?.toString(), createdAt: json['createdAt']?.toString(), updatedAt: json['updatedAt']?.toString(), ); } Map toJson() { return { if (id != null) 'id': id, 'title': title, 'description': description ?? '', 'servings': servings ?? '', 'prepTime': prepTime ?? '', 'cookTime': cookTime ?? '', 'totalTime': totalTime ?? '', 'image': image ?? '', 'images': images, 'ingredients': ingredients, 'instructions': instructions, 'sourceUrl': sourceUrl ?? '', }; } String get calculatedTotalTime => RecipeTimeUtils.calculateTotalTime( prepTime: prepTime, cookTime: cookTime, totalTime: totalTime, ); Recipe copyWith({ String? id, String? title, String? description, String? servings, String? prepTime, String? cookTime, String? totalTime, String? image, List? images, List? ingredients, List? instructions, String? sourceUrl, String? shareToken, String? createdAt, String? updatedAt, }) { return Recipe( id: id ?? this.id, title: title ?? this.title, description: description ?? this.description, servings: servings ?? this.servings, prepTime: prepTime ?? this.prepTime, cookTime: cookTime ?? this.cookTime, totalTime: totalTime ?? this.totalTime, image: image ?? this.image, images: images ?? this.images, ingredients: ingredients ?? this.ingredients, instructions: instructions ?? this.instructions, sourceUrl: sourceUrl ?? this.sourceUrl, shareToken: shareToken ?? this.shareToken, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, ); } } class RecipeTimeUtils { static int parseDurationToMinutes(String? str) { if (str == null) return 0; final s = str.trim(); if (s.isEmpty) return 0; // ISO 8601 Durations (e.g. PT1H30M, PT600S, PT45M, P0DT1H) if (s.startsWith('P') || s.startsWith('p')) { final isoRegex = RegExp(r'P(?:(\d+)D)?T?(?:(\d+(?:\.\d+)?)H)?(?:(\d+)M)?(?:(\d+)S)?', caseSensitive: false); final match = isoRegex.firstMatch(s); if (match != null) { final days = int.tryParse(match.group(1) ?? '0') ?? 0; final hours = double.tryParse(match.group(2) ?? '0') ?? 0.0; final mins = int.tryParse(match.group(3) ?? '0') ?? 0; final secs = int.tryParse(match.group(4) ?? '0') ?? 0; final totalSecs = (days * 86400) + (hours * 3600).round() + (mins * 60) + secs; if (totalSecs > 0) return (totalSecs / 60).round(); } } int totalMinutes = 0; bool matched = false; // Hours: e.g. 1 hr, 1.5 hours, 2 hrs final hrRegex = RegExp(r'(\d+(?:\.\d+)?)\s*(?:hours?|hrs?|h\b)', caseSensitive: false); final hrMatch = hrRegex.firstMatch(s); if (hrMatch != null) { final h = double.tryParse(hrMatch.group(1) ?? '0') ?? 0.0; totalMinutes += (h * 60).round(); matched = true; } // Minutes: e.g. 15 mins, 10-14 mins, 45m final minRegex = RegExp(r'(?:(\d+)\s*[-–to]+\s*)?(\d+)\s*(?:minutes?|mins?|m\b)', caseSensitive: false); final minMatch = minRegex.firstMatch(s); if (minMatch != null) { final m = int.tryParse(minMatch.group(2) ?? '0') ?? 0; totalMinutes += m; matched = true; } if (!matched && RegExp(r'^\d+$').hasMatch(s)) { return int.tryParse(s) ?? 0; } return totalMinutes; } static String formatMinutes(int minutes) { if (minutes <= 0) return ''; final hrs = minutes ~/ 60; final mins = minutes % 60; if (hrs > 0 && mins > 0) { return '$hrs hr${hrs > 1 ? 's' : ''} $mins min${mins > 1 ? 's' : ''}'; } if (hrs > 0) { return '$hrs hr${hrs > 1 ? 's' : ''}'; } return '$mins min${mins > 1 ? 's' : ''}'; } static String calculateTotalTime({String? prepTime, String? cookTime, String? totalTime}) { if (totalTime != null && totalTime.trim().isNotEmpty) { final t = totalTime.trim(); if (t.startsWith('P') || t.startsWith('p')) { final m = parseDurationToMinutes(t); if (m > 0) return formatMinutes(m); } return t; } final prepM = parseDurationToMinutes(prepTime); final cookM = parseDurationToMinutes(cookTime); if (prepM > 0 || cookM > 0) { return formatMinutes(prepM + cookM); } if (cookTime != null && cookTime.trim().isNotEmpty) { final cM = parseDurationToMinutes(cookTime); return cM > 0 ? formatMinutes(cM) : cookTime.trim(); } if (prepTime != null && prepTime.trim().isNotEmpty) { final pM = parseDurationToMinutes(prepTime); return pM > 0 ? formatMinutes(pM) : prepTime.trim(); } return ''; } }