mirror of
https://gitlab.com/Zer0interrupt/recipe-manager.git
synced 2026-11-01 23:24:11 -05:00
210 lines
6.4 KiB
Dart
210 lines
6.4 KiB
Dart
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<String> images;
|
||
final List<String> ingredients;
|
||
final List<String> 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<String>? images,
|
||
List<String>? ingredients,
|
||
List<String>? instructions,
|
||
this.sourceUrl,
|
||
this.shareToken,
|
||
this.createdAt,
|
||
this.updatedAt,
|
||
}) : images = images ?? [],
|
||
ingredients = ingredients ?? [],
|
||
instructions = instructions ?? [];
|
||
|
||
factory Recipe.fromJson(Map<String, dynamic> json) {
|
||
List<String> 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<String, dynamic> 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<String>? images,
|
||
List<String>? ingredients,
|
||
List<String>? 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 '';
|
||
}
|
||
}
|