Skip to content

Commit

Permalink
Use NostrEvent directly
Browse files Browse the repository at this point in the history
  • Loading branch information
chebizarro committed Feb 11, 2025
1 parent d3af56d commit 96780bf
Show file tree
Hide file tree
Showing 17 changed files with 989 additions and 33 deletions.
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "disabled",
"java.configuration.updateBuildConfiguration": "interactive"
}
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# let_him_cook
# Let Him Cook (mobile)

A new Flutter project.

Expand Down Expand Up @@ -27,4 +27,3 @@ the `lib/src/localization` directory.

To support additional languages, please visit the tutorial on
[Internationalizing Flutter apps](https://flutter.dev/to/internationalization).
# let_him_cook_mobile
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {
}

android {
namespace = "com.example.let_him_cook"
namespace = "org.nsf.let_him_cook"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion

Expand All @@ -21,7 +21,7 @@ android {

defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.let_him_cook"
applicationId = "org.nsf.let_him_cook"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
Expand Down
5 changes: 4 additions & 1 deletion lib/src/features/recipes/recipe.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ extension RecipeEvent on NostrEvent {
String? get summary => _getTagValue('summary');

List<String> get ingredients => _getTags('ingredient');
List<String> get directions => content!.split('\n');
List<String> get directions => content!
.replaceAll('\n\n','\n')
.replaceAll(RegExp(r'^\d+\.\s*', multiLine: true), '')
.split('\n');
List<String> get categories => _getTags('t');

String? _getTagValue(String key) {
Expand Down
23 changes: 15 additions & 8 deletions lib/src/features/recipes/recipe_detail_view.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'package:dart_nostr/dart_nostr.dart';
import 'package:flutter/material.dart';
import 'package:let_him_cook/src/features/recipes/recipe.dart';
import 'package:let_him_cook/src/features/recipes/recipe_edit_screen.dart';

class RecipeDetailView extends StatelessWidget {
final Recipe recipe;
final NostrEvent recipe;

const RecipeDetailView({super.key, required this.recipe});

Expand Down Expand Up @@ -38,7 +40,12 @@ class RecipeDetailView extends StatelessWidget {
),
TextButton(
onPressed: () {
// TODO: Navigate to edit screen
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RecipeEditScreen(recipe: recipe),
),
);
},
child: const Text(
'EDIT',
Expand Down Expand Up @@ -90,7 +97,7 @@ class RecipeDetailView extends StatelessWidget {
AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
recipe.imageUrl,
recipe.image,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Center(child: Icon(Icons.broken_image));
Expand Down Expand Up @@ -140,19 +147,19 @@ class RecipeDetailView extends StatelessWidget {
const SizedBox(height: 8),
Row(
children: [
Row(
const Row(
children: [
const Icon(Icons.thumb_up, size: 16),
const SizedBox(width: 4),
Text('${recipe.likes}'),
Icon(Icons.thumb_up, size: 16),
SizedBox(width: 4),
Text('0'),
],
),
const SizedBox(width: 16),
Row(
children: [
Icon(Icons.bolt, size: 16, color: Colors.yellow[700]),
const SizedBox(width: 4),
Text('${recipe.zaps}'),
const Text('0'),
],
),
],
Expand Down
109 changes: 109 additions & 0 deletions lib/src/features/recipes/recipe_edit_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import 'package:dart_nostr/dart_nostr.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:let_him_cook/src/features/recipes/recipe.dart';

class RecipeEditState {
final String title;
final List<String> tags;
final String summary;
final String prepTime;
final String cookTime;
final String servings;
final List<MapEntry<String, String>> ingredients;
final List<String> directions;
final List<String> images;
final Map<String, String> relatedRecipes;
final bool isPublishing;

RecipeEditState({
this.title = '',
this.tags = const [],
this.summary = '',
this.prepTime = '',
this.cookTime = '',
this.servings = '',
this.ingredients = const [],
this.directions = const [],
this.images = const [],
this.relatedRecipes = const {},
this.isPublishing = false,
});

RecipeEditState copyWith({
String? title,
List<String>? tags,
String? summary,
String? prepTime,
String? cookTime,
String? servings,
List<MapEntry<String, String>>? ingredients,
List<String>? directions,
List<String>? images,
Map<String, String>? relatedRecipes,
bool? isPublishing,
}) {
return RecipeEditState(
title: title ?? this.title,
tags: tags ?? this.tags,
summary: summary ?? this.summary,
prepTime: prepTime ?? this.prepTime,
cookTime: cookTime ?? this.cookTime,
servings: servings ?? this.servings,
ingredients: ingredients ?? this.ingredients,
directions: directions ?? this.directions,
images: images ?? this.images,
relatedRecipes: relatedRecipes ?? this.relatedRecipes,
isPublishing: isPublishing ?? this.isPublishing,
);
}
}

class RecipeEditNotifier extends StateNotifier<RecipeEditState> {
RecipeEditNotifier(NostrEvent? recipe) : super(RecipeEditState()) {
if (recipe != null) {
state = state.copyWith(
title: recipe.title,
tags: recipe.categories,
summary: recipe.summary,
prepTime: recipe.prepTime,
cookTime: recipe.cookTime,
servings: recipe.servings,
ingredients:
recipe.ingredients.map((e) => MapEntry(e, e)).toList(),
directions: recipe.directions,
images: [recipe.image],
);
}
}

void updateTitle(String val) => state = state.copyWith(title: val);
void updateSummary(String val) => state = state.copyWith(summary: val);
void updatePrepTime(String val) => state = state.copyWith(prepTime: val);
void updateCookTime(String val) => state = state.copyWith(cookTime: val);
void updateServings(String val) => state = state.copyWith(servings: val);
void updateIngredients(List<MapEntry<String, String>> val) =>
state = state.copyWith(ingredients: val);
void updateDirections(String val) => state = state.copyWith(directions: [val]);
void addImage(String url) =>
state = state.copyWith(images: [...state.images, url]);
void updateRelatedRecipes(Map<String, String> newMap) {
state = state.copyWith(relatedRecipes: newMap);
}

Future<void> publishRecipe() async {
state = state.copyWith(isPublishing: true);
//await NostrService.publishRecipe(state);
state = state.copyWith(isPublishing: false);
}

removeTag(int index) {}

removeImage(int index) {}

addTag(String newTag) {}
}

final recipeEditProvider =
StateNotifierProvider.family<RecipeEditNotifier, RecipeEditState, NostrEvent?>(
(ref, recipe) => RecipeEditNotifier(recipe),
);
174 changes: 174 additions & 0 deletions lib/src/features/recipes/recipe_edit_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import 'package:dart_nostr/dart_nostr.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:let_him_cook/src/features/recipes/recipe_edit_provider.dart';
import 'package:let_him_cook/src/features/recipes/widgets/images_combo_box.dart';
import 'package:let_him_cook/src/features/recipes/widgets/markdown_editor.dart';
import 'package:let_him_cook/src/features/recipes/widgets/recipe_combo_box.dart';
import 'package:let_him_cook/src/features/recipes/widgets/tags_combo_box.dart';
import 'package:let_him_cook/src/features/recipes/widgets/tuple_combo_box.dart';

class RecipeEditScreen extends ConsumerWidget {
final NostrEvent? recipe; // If null, create new recipe

const RecipeEditScreen({super.key, this.recipe});

@override
Widget build(BuildContext context, WidgetRef ref) {
final editState = ref.watch(recipeEditProvider(recipe));

return Scaffold(
appBar: AppBar(
title: Text(recipe == null ? 'Create Recipe' : 'Edit Recipe'),
actions: [
TextButton(
onPressed: editState.isPublishing
? null
: () {
ref
.read(recipeEditProvider(recipe).notifier)
.publishRecipe();
},
child: Text(
recipe == null ? 'Publish' : 'Update',
style: TextStyle(
color: editState.isPublishing ? Colors.grey : Colors.white),
),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTextField(
label: 'Title*',
hint: 'Unique recipe title',
value: editState.title,
onChanged: (val) => ref
.read(recipeEditProvider(recipe).notifier)
.updateTitle(val),
),
const SizedBox(height: 16),
TagsComboBox(
selectedTags: editState.tags,
onTagSelected: (newTag) =>
ref.read(recipeEditProvider(recipe).notifier).addTag(newTag),
onTagRemoved: (index) => ref
.read(recipeEditProvider(recipe).notifier)
.removeTag(index),
),
const SizedBox(height: 16),
_buildTextField(
label: 'Brief Summary',
hint: 'A short description of the dish',
value: editState.summary,
maxLines: 3,
onChanged: (val) => ref
.read(recipeEditProvider(recipe).notifier)
.updateSummary(val),
),
const SizedBox(height: 16),
RecipeComboBox(
selectedRecipes: editState.relatedRecipes,
onChanged: (updatedMap) {
ref
.read(recipeEditProvider(recipe).notifier)
.updateRelatedRecipes(updatedMap);
},
placeholder: 'Select a recipe...',
),
const SizedBox(height: 16),
_buildTextField(
label: 'Prep Time',
hint: 'e.g., 20 min',
value: editState.prepTime,
onChanged: (val) => ref
.read(recipeEditProvider(recipe).notifier)
.updatePrepTime(val),
),
_buildTextField(
label: 'Cook Time',
hint: 'e.g., 1 hour',
value: editState.cookTime,
onChanged: (val) => ref
.read(recipeEditProvider(recipe).notifier)
.updateCookTime(val),
),
_buildTextField(
label: 'Servings',
hint: 'e.g., 4 persons',
value: editState.servings,
onChanged: (val) => ref
.read(recipeEditProvider(recipe).notifier)
.updateServings(val),
),
const SizedBox(height: 16),
TupleComboBox(
selectedItems: editState.ingredients,
amountPlaceholder: 'Quantity (e.g., 1 cup)',
itemPlaceholder: 'Ingredient (e.g., flour)',
onChanged: (ingredients) => ref
.read(recipeEditProvider(recipe).notifier)
.updateIngredients(ingredients),
),
const SizedBox(height: 16),
MarkdownEditor(
content: editState.directions.toString(),
onChanged: (val) => ref
.read(recipeEditProvider(recipe).notifier)
.updateDirections(val),
),
const SizedBox(height: 16),
ImagesComboBox(
images: editState.images,
onImageAdded: (url) =>
ref.read(recipeEditProvider(recipe).notifier).addImage(url),
onImageRemoved: (index) => ref
.read(recipeEditProvider(recipe).notifier)
.removeImage(index),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: editState.isPublishing
? null
: () {
ref
.read(recipeEditProvider(recipe).notifier)
.publishRecipe();
},
child: editState.isPublishing
? const CircularProgressIndicator()
: Text(recipe == null ? 'Publish' : 'Update'),
),
],
),
),
);
}

Widget _buildTextField({
required String label,
required String hint,
required String value,
int maxLines = 1,
required Function(String) onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
TextField(
decoration: InputDecoration(
hintText: hint,
border: const OutlineInputBorder(),
),
maxLines: maxLines,
onChanged: onChanged,
),
],
);
}
}
Loading

0 comments on commit 96780bf

Please sign in to comment.