merge with master, fixes
continuous-integration/drone/push Build is passing Details

master
maxime.BATISTA@etu.uca.fr 2 years ago
commit 5829ba56bb

@ -3,7 +3,12 @@ type: docker
name: CI Workflow
steps:
- name: Build
- name: Test
image: hub.codefirst.iut.uca.fr/marc.chevaldonne/codefirst-dotnet7-maui:latest
commands:
- dotnet test --framework net7.0 'Tests\\\\Tests.csproj'
- name: Deploy doc
image: hub.codefirst.iut.uca.fr/maxime.batista/codefirst-docdeployer:latest
commands:
- /entrypoint.sh -t doxygen -l . -d sourcecode_documentation

@ -0,0 +1,7 @@
[*.cs]
# CS0618: Le type ou le membre est obsolète
dotnet_diagnostic.CS0618.severity = silent
# CS1998: Async method lacks 'await' operators and will run synchronously
dotnet_diagnostic.CS1998.severity = none

@ -1,21 +1,23 @@
namespace ShoopNCook;
using Models;
using Endpoint;
using LocalEndpoint;
using Services;
using LocalServices;
// Classe principale de l'application qui implémente l'interface ConnectionObserver et IApp
public partial class App : Application, ConnectionObserver, IApp
{
// Initialisation de l'interface IEndpoint avec une nouvelle instance de LocalEndpoint
private IEndpoint Endpoint = new LocalEndpoint();
public IUserNotifier Notifier => new ConsoleUserNotifier();
public App()
{
InitializeComponent();
ForceLogin(); //start in login shell
ForceLogin(); // Commencer l'application avec l'écran de connexion
}
// Méthode appelée lorsque l'utilisateur se connecte avec succès
public void OnAccountConnected(Account account)
{
Shell shell = new MainAppShell(account, Endpoint, this);
@ -23,9 +25,10 @@ public partial class App : Application, ConnectionObserver, IApp
MainPage = shell;
}
// Méthode pour forcer l'utilisateur à se connecter
public void ForceLogin()
{
Shell shell = new ConnectAppShell(this, Endpoint.AuthService, Notifier);
Shell shell = new ConnectAppShell(this, Endpoint.AuthService);
shell.GoToAsync("//Splash");
MainPage = shell;
}

@ -1,16 +1,23 @@
namespace ShoopNCook;
using Microsoft.Maui.Controls;
using Models;
using Endpoint;
using Services;
using ShoopNCook.Controllers;
using ShoopNCook.Pages;
// Shell pour la phase de connexion de l'application
public partial class ConnectAppShell : Shell
{
public ConnectAppShell(ConnectionObserver observer, IAuthService accounts, IUserNotifier notifier)
// Constructeur qui prend un observateur de connexion et un service d'authentification en argument
public ConnectAppShell(ConnectionObserver observer, IAuthService accounts)
{
ConnectionController controller = new ConnectionController(observer, accounts, notifier);
// Création d'un nouveau contrôleur de connexion
ConnectionController controller = new ConnectionController(observer, accounts);
InitializeComponent();
// Initialisation des pages de connexion et d'inscription avec le contrôleur de connexion
LoginPage.ContentTemplate = new DataTemplate(() => new LoginPage(controller));
RegisterPage.ContentTemplate = new DataTemplate(() => new RegisterPage(controller));
}

@ -2,6 +2,8 @@
namespace ShoopNCook
{
// Interface définissant un observateur de connexion.
// Tout objet implémentant cette interface doit définir la méthode OnAccountConnected().
public interface ConnectionObserver
{
public void OnAccountConnected(Account account);

@ -1,31 +0,0 @@
namespace ShoopNCook
{
/// <summary>
/// A notice reporter implementation that prints in console the applications's user notices.
/// </summary>
public class ConsoleUserNotifier :
IUserNotifier
{
public void Success(string message)
{
Console.WriteLine("<User Notice> Success: " + message);
}
public void Error(string message)
{
Console.WriteLine("<User Notice> Error: " + message);
}
public void Notice(string message)
{
Console.WriteLine("<User Notice> Notice: " + message);
}
public void Warn(string message)
{
Console.WriteLine("<User Notice> Warn: " + message);
}
}
}

@ -1,4 +1,4 @@
using Endpoint;
using Services;
using Models;
namespace ShoopNCook.Controllers
@ -7,19 +7,27 @@ namespace ShoopNCook.Controllers
{
private readonly ConnectionObserver observer;
private readonly IAuthService accounts;
private readonly IUserNotifier notifier;
public ConnectionController(ConnectionObserver observer, IAuthService accounts, IUserNotifier notifier) {
public ConnectionController(ConnectionObserver observer, IAuthService accounts) {
this.observer = observer;
this.accounts = accounts;
this.notifier = notifier;
}
public void Login(string email, string password)
{
if (email == null)
{
UserNotifier.Notice("Please provide an email address");
return;
}
if (password == null)
{
UserNotifier.Notice("Please provide your password");
return;
}
Account? acc = accounts.Login(email, password);
if (acc == null)
{
notifier.Error("Email or password invalid.");
UserNotifier.Error("Email or password invalid.");
return;
}
observer.OnAccountConnected(acc);
@ -27,10 +35,25 @@ namespace ShoopNCook.Controllers
public void Register(string username, string email, string password)
{
if (email == null)
{
UserNotifier.Notice("Please provide an email address");
return;
}
if (password == null)
{
UserNotifier.Notice("Please provide your password");
return;
}
if (username == null)
{
UserNotifier.Notice("Please provide an username");
return;
}
Account? acc = accounts.Register(username, email, password);
if (acc == null)
{
notifier.Error("Invalid credentials.");
UserNotifier.Error("Invalid credentials.");
return;
}
observer.OnAccountConnected(acc);

@ -1,4 +1,4 @@
using Endpoint;
using Services;
using Models;
using ShoopNCook.Pages;
@ -20,18 +20,18 @@ namespace ShoopNCook.Controllers
public void Logout()
{
app.Notifier.Notice("You have been loged out.");
UserNotifier.Notice("You have been loged out.");
app.ForceLogin();
}
public void GoToMyRecipesPage()
public async void GoToMyRecipesPage()
{
Shell.Current.Navigation.PushAsync(new MyRecipesPage(account, endpoint.RecipesService, app.Notifier));
await Shell.Current.Navigation.PushAsync(new MyRecipesPage(account, endpoint.RecipesService));
}
public void GoToProfilePage()
public async void GoToProfilePage()
{
Shell.Current.Navigation.PushAsync(new ProfilePage(account));
await Shell.Current.Navigation.PushAsync(new ProfilePage(account));
}
}
}

@ -0,0 +1,429 @@
#---------------------------------------------------------------------------
# Project related configuration options
#---------------------------------------------------------------------------
DOXYFILE_ENCODING = UTF-8
PROJECT_NAME = "ShopNCook"
PROJECT_NUMBER = 1.0.0
PROJECT_BRIEF = "A Cook application"
PROJECT_LOGO = Resources/appicon.svg
OUTPUT_DIRECTORY = /docs/doxygen
CREATE_SUBDIRS = NO
ALLOW_UNICODE_NAMES = NO
OUTPUT_LANGUAGE = English
BRIEF_MEMBER_DESC = YES
REPEAT_BRIEF = YES
ABBREVIATE_BRIEF = "The $name class" \
"The $name widget" \
"The $name file" \
is \
provides \
specifies \
contains \
represents \
a \
an \
the
ALWAYS_DETAILED_SEC = NO
INLINE_INHERITED_MEMB = NO
FULL_PATH_NAMES = YES
STRIP_FROM_PATH =
STRIP_FROM_INC_PATH =
SHORT_NAMES = NO
JAVADOC_AUTOBRIEF = NO
JAVADOC_BANNER = NO
QT_AUTOBRIEF = NO
MULTILINE_CPP_IS_BRIEF = NO
PYTHON_DOCSTRING = YES
INHERIT_DOCS = YES
SEPARATE_MEMBER_PAGES = NO
TAB_SIZE = 4
ALIASES =
OPTIMIZE_OUTPUT_FOR_C = NO
# Well... the one for Java looks so similar to the one for C#...
OPTIMIZE_OUTPUT_JAVA = YES
OPTIMIZE_FOR_FORTRAN = NO
OPTIMIZE_OUTPUT_VHDL = NO
OPTIMIZE_OUTPUT_SLICE = NO
EXTENSION_MAPPING =
MARKDOWN_SUPPORT = YES
TOC_INCLUDE_HEADINGS = 5
AUTOLINK_SUPPORT = YES
BUILTIN_STL_SUPPORT = NO
CPP_CLI_SUPPORT = NO
SIP_SUPPORT = NO
IDL_PROPERTY_SUPPORT = YES
DISTRIBUTE_GROUP_DOC = NO
GROUP_NESTED_COMPOUNDS = NO
SUBGROUPING = YES
INLINE_GROUPED_CLASSES = NO
INLINE_SIMPLE_STRUCTS = NO
TYPEDEF_HIDES_STRUCT = NO
LOOKUP_CACHE_SIZE = 0
NUM_PROC_THREADS = 1
#---------------------------------------------------------------------------
# Build related configuration options
#---------------------------------------------------------------------------
EXTRACT_ALL = YES
# I do not like other members to see my private members... but you can set it to YES if you prefer.
EXTRACT_PRIVATE = NO
EXTRACT_PRIV_VIRTUAL = NO
EXTRACT_PACKAGE = NO
EXTRACT_STATIC = YES
EXTRACT_LOCAL_CLASSES = YES
EXTRACT_LOCAL_METHODS = NO
EXTRACT_ANON_NSPACES = NO
RESOLVE_UNNAMED_PARAMS = YES
HIDE_UNDOC_MEMBERS = NO
HIDE_UNDOC_CLASSES = NO
HIDE_FRIEND_COMPOUNDS = NO
HIDE_IN_BODY_DOCS = NO
INTERNAL_DOCS = NO
CASE_SENSE_NAMES = NO
HIDE_SCOPE_NAMES = NO
HIDE_COMPOUND_REFERENCE= NO
SHOW_HEADERFILE = YES
SHOW_INCLUDE_FILES = YES
SHOW_GROUPED_MEMB_INC = NO
FORCE_LOCAL_INCLUDES = NO
INLINE_INFO = YES
SORT_MEMBER_DOCS = NO
SORT_BRIEF_DOCS = NO
SORT_MEMBERS_CTORS_1ST = NO
SORT_GROUP_NAMES = NO
SORT_BY_SCOPE_NAME = NO
STRICT_PROTO_MATCHING = NO
GENERATE_TODOLIST = YES
GENERATE_TESTLIST = YES
GENERATE_BUGLIST = YES
GENERATE_DEPRECATEDLIST= YES
ENABLED_SECTIONS =
MAX_INITIALIZER_LINES = 30
SHOW_USED_FILES = YES
SHOW_FILES = YES
SHOW_NAMESPACES = YES
FILE_VERSION_FILTER =
LAYOUT_FILE =
CITE_BIB_FILES =
#---------------------------------------------------------------------------
# Configuration options related to warning and progress messages
#---------------------------------------------------------------------------
QUIET = NO
WARNINGS = YES
WARN_IF_UNDOCUMENTED = YES
WARN_IF_DOC_ERROR = YES
WARN_IF_INCOMPLETE_DOC = YES
WARN_NO_PARAMDOC = NO
WARN_AS_ERROR = NO
WARN_FORMAT = "$file:$line: $text"
WARN_LOGFILE =
#---------------------------------------------------------------------------
# Configuration options related to the input files
#---------------------------------------------------------------------------
INPUT = .
INPUT_ENCODING = UTF-8
FILE_PATTERNS = *.c \
*.cc \
*.cxx \
*.cpp \
*.c++ \
*.java \
*.ii \
*.ixx \
*.ipp \
*.i++ \
*.inl \
*.idl \
*.ddl \
*.odl \
*.h \
*.hh \
*.hxx \
*.hpp \
*.h++ \
*.l \
*.cs \
*.d \
*.php \
*.php4 \
*.php5 \
*.phtml \
*.inc \
*.m \
*.markdown \
*.md \
*.mm \
*.dox \
*.py \
*.pyw \
*.f90 \
*.f95 \
*.f03 \
*.f08 \
*.f18 \
*.f \
*.for \
*.vhd \
*.vhdl \
*.ucf \
*.qsf \
*.ice
RECURSIVE = YES
EXCLUDE =
EXCLUDE_SYMLINKS = NO
EXCLUDE_PATTERNS = */Tests/*
EXCLUDE_PATTERNS += */bin/*
EXCLUDE_PATTERNS += */obj/*
EXCLUDE_PATTERNS += documentation/*
EXCLUDE_SYMBOLS =
EXAMPLE_PATH =
EXAMPLE_PATTERNS = *
EXAMPLE_RECURSIVE = NO
IMAGE_PATH =
INPUT_FILTER =
FILTER_PATTERNS =
FILTER_SOURCE_FILES = NO
FILTER_SOURCE_PATTERNS =
USE_MDFILE_AS_MAINPAGE =
#---------------------------------------------------------------------------
# Configuration options related to source browsing
#---------------------------------------------------------------------------
SOURCE_BROWSER = NO
INLINE_SOURCES = NO
STRIP_CODE_COMMENTS = YES
REFERENCED_BY_RELATION = NO
REFERENCES_RELATION = NO
REFERENCES_LINK_SOURCE = YES
SOURCE_TOOLTIPS = YES
USE_HTAGS = NO
VERBATIM_HEADERS = YES
CLANG_ASSISTED_PARSING = NO
CLANG_ADD_INC_PATHS = YES
CLANG_OPTIONS =
CLANG_DATABASE_PATH =
#---------------------------------------------------------------------------
# Configuration options related to the alphabetical class index
#---------------------------------------------------------------------------
ALPHABETICAL_INDEX = YES
IGNORE_PREFIX =
#---------------------------------------------------------------------------
# Configuration options related to the HTML output
#---------------------------------------------------------------------------
GENERATE_HTML = YES
HTML_OUTPUT = html
HTML_FILE_EXTENSION = .html
HTML_HEADER =
HTML_STYLESHEET =
HTML_EXTRA_STYLESHEET =
HTML_EXTRA_FILES = images/CodeFirst.png images/clubinfo.png
HTML_COLORSTYLE_HUE = 215
HTML_COLORSTYLE_SAT = 45
HTML_COLORSTYLE_GAMMA = 240
HTML_TIMESTAMP = NO
HTML_DYNAMIC_MENUS = YES
HTML_DYNAMIC_SECTIONS = NO
HTML_INDEX_NUM_ENTRIES = 100
GENERATE_DOCSET = NO
DOCSET_FEEDNAME = "Doxygen generated docs"
DOCSET_FEEDURL =
DOCSET_BUNDLE_ID = org.doxygen.Project
DOCSET_PUBLISHER_ID = org.doxygen.Publisher
DOCSET_PUBLISHER_NAME = Publisher
GENERATE_HTMLHELP = NO
CHM_FILE =
HHC_LOCATION =
GENERATE_CHI = NO
CHM_INDEX_ENCODING =
BINARY_TOC = NO
TOC_EXPAND = NO
GENERATE_QHP = NO
QCH_FILE =
QHP_NAMESPACE = org.doxygen.Project
QHP_VIRTUAL_FOLDER = doc
QHP_CUST_FILTER_NAME =
QHP_CUST_FILTER_ATTRS =
QHP_SECT_FILTER_ATTRS =
QHG_LOCATION =
GENERATE_ECLIPSEHELP = NO
ECLIPSE_DOC_ID = org.doxygen.Project
DISABLE_INDEX = NO
GENERATE_TREEVIEW = NO
FULL_SIDEBAR = NO
ENUM_VALUES_PER_LINE = 4
TREEVIEW_WIDTH = 250
EXT_LINKS_IN_WINDOW = NO
OBFUSCATE_EMAILS = YES
HTML_FORMULA_FORMAT = png
FORMULA_FONTSIZE = 10
FORMULA_TRANSPARENT = YES
FORMULA_MACROFILE =
USE_MATHJAX = NO
MATHJAX_VERSION = MathJax_2
MATHJAX_FORMAT = HTML-CSS
MATHJAX_RELPATH =
MATHJAX_EXTENSIONS =
MATHJAX_CODEFILE =
SEARCHENGINE = YES
SERVER_BASED_SEARCH = NO
EXTERNAL_SEARCH = NO
SEARCHENGINE_URL =
SEARCHDATA_FILE = searchdata.xml
EXTERNAL_SEARCH_ID =
EXTRA_SEARCH_MAPPINGS =
#---------------------------------------------------------------------------
# Configuration options related to the LaTeX output
#---------------------------------------------------------------------------
GENERATE_LATEX = NO
LATEX_OUTPUT = latex
LATEX_CMD_NAME =
MAKEINDEX_CMD_NAME = makeindex
LATEX_MAKEINDEX_CMD = makeindex
COMPACT_LATEX = NO
PAPER_TYPE = a4
EXTRA_PACKAGES =
LATEX_HEADER =
LATEX_FOOTER =
LATEX_EXTRA_STYLESHEET =
LATEX_EXTRA_FILES =
PDF_HYPERLINKS = YES
USE_PDFLATEX = YES
LATEX_BATCHMODE = NO
LATEX_HIDE_INDICES = NO
LATEX_BIB_STYLE = plain
LATEX_TIMESTAMP = NO
LATEX_EMOJI_DIRECTORY =
#---------------------------------------------------------------------------
# Configuration options related to the RTF output
#---------------------------------------------------------------------------
GENERATE_RTF = NO
RTF_OUTPUT = rtf
COMPACT_RTF = NO
RTF_HYPERLINKS = NO
RTF_STYLESHEET_FILE =
RTF_EXTENSIONS_FILE =
#---------------------------------------------------------------------------
# Configuration options related to the man page output
#---------------------------------------------------------------------------
GENERATE_MAN = NO
MAN_OUTPUT = man
MAN_EXTENSION = .3
MAN_SUBDIR =
MAN_LINKS = NO
#---------------------------------------------------------------------------
# Configuration options related to the XML output
#---------------------------------------------------------------------------
GENERATE_XML = NO
XML_OUTPUT = xml
XML_PROGRAMLISTING = YES
XML_NS_MEMB_FILE_SCOPE = NO
#---------------------------------------------------------------------------
# Configuration options related to the DOCBOOK output
#---------------------------------------------------------------------------
GENERATE_DOCBOOK = NO
DOCBOOK_OUTPUT = docbook
#---------------------------------------------------------------------------
# Configuration options for the AutoGen Definitions output
#---------------------------------------------------------------------------
GENERATE_AUTOGEN_DEF = NO
#---------------------------------------------------------------------------
# Configuration options related to Sqlite3 output
#---------------------------------------------------------------------------
#---------------------------------------------------------------------------
# Configuration options related to the Perl module output
#---------------------------------------------------------------------------
GENERATE_PERLMOD = NO
PERLMOD_LATEX = NO
PERLMOD_PRETTY = YES
PERLMOD_MAKEVAR_PREFIX =
#---------------------------------------------------------------------------
# Configuration options related to the preprocessor
#---------------------------------------------------------------------------
ENABLE_PREPROCESSING = YES
MACRO_EXPANSION = NO
EXPAND_ONLY_PREDEF = NO
SEARCH_INCLUDES = YES
INCLUDE_PATH =
INCLUDE_FILE_PATTERNS =
PREDEFINED =
EXPAND_AS_DEFINED =
SKIP_FUNCTION_MACROS = YES
#---------------------------------------------------------------------------
# Configuration options related to external references
#---------------------------------------------------------------------------
TAGFILES =
GENERATE_TAGFILE =
ALLEXTERNALS = NO
EXTERNAL_GROUPS = YES
EXTERNAL_PAGES = YES
#---------------------------------------------------------------------------
# Configuration options related to the dot tool
#---------------------------------------------------------------------------
DIA_PATH =
HIDE_UNDOC_RELATIONS = YES
HAVE_DOT = NO
DOT_NUM_THREADS = 0
DOT_FONTNAME = Helvetica
DOT_FONTSIZE = 10
DOT_FONTPATH =
CLASS_GRAPH = YES
COLLABORATION_GRAPH = YES
GROUP_GRAPHS = YES
UML_LOOK = NO
UML_LIMIT_NUM_FIELDS = 10
DOT_UML_DETAILS = NO
DOT_WRAP_THRESHOLD = 17
TEMPLATE_RELATIONS = NO
INCLUDE_GRAPH = YES
INCLUDED_BY_GRAPH = YES
CALL_GRAPH = NO
CALLER_GRAPH = NO
GRAPHICAL_HIERARCHY = YES
DIRECTORY_GRAPH = YES
DIR_GRAPH_MAX_DEPTH = 1
DOT_IMAGE_FORMAT = png
INTERACTIVE_SVG = NO
DOT_PATH =
DOTFILE_DIRS =
MSCFILE_DIRS =
DIAFILE_DIRS =
PLANTUML_JAR_PATH =
PLANTUML_CFG_FILE =
PLANTUML_INCLUDE_PATH =
DOT_GRAPH_MAX_NODES = 50
MAX_DOT_GRAPH_DEPTH = 0
DOT_TRANSPARENT = NO
DOT_MULTI_TARGETS = NO
GENERATE_LEGEND = YES
DOT_CLEANUP = YES

@ -7,10 +7,10 @@ using System.Threading.Tasks;
namespace ShoopNCook
{
// Interface définissant une application.
// Tout objet implémentant cette interface doit définir la méthode ForceLogin().
public interface IApp
{
public IUserNotifier Notifier { get; }
public void ForceLogin();
}
}

@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ShoopNCook
{
public interface IUserNotifier
{
public void Success(string message);
public void Notice(string message);
public void Error(string message);
public void Warn(string message);
}
}

@ -1,19 +1,19 @@
using LocalEndpoint;
using LocalEndpoint.Data;
using Services;
using LocalServices.Data;
using Models;
using System.Collections.Immutable;
namespace Endpoint
namespace Services
{
internal class AccountOwnedRecipes : IAccountOwnedRecipes
public class AccountOwnedRecipes : IAccountOwnedRecipesService
{
public Account Account { get; init; }
private readonly Dictionary<Guid, Recipe> ownedRecipes = new Dictionary<Guid, Recipe>();
private readonly Database db;
private readonly IDatabase db;
public AccountOwnedRecipes(Account account, Database db)
public AccountOwnedRecipes(Account account, IDatabase db)
{
Account = account;
this.db = db;
@ -48,7 +48,5 @@ namespace Endpoint
{
return ownedRecipes.Values.ToImmutableList().ConvertAll(r => r.Info);
}
}
}

@ -1,15 +1,15 @@
using Endpoint;
using LocalEndpoint.Data;
using Services;
using LocalServices.Data;
using Models;
using System.Collections.Immutable;
namespace LocalEndpoint
namespace LocalServices
{
internal class AccountRecipesPreferences : IAccountRecipesPreferences
public class AccountRecipesPreferences : IAccountRecipesPreferencesService
{
private readonly Database db;
public AccountRecipesPreferences(Account account, Database db)
private readonly IDatabase db;
public AccountRecipesPreferences(Account account, IDatabase db)
{
Account = account;
this.db = db;
@ -56,7 +56,7 @@ namespace LocalEndpoint
public RecipeRate GetRate(RecipeInfo info)
{
RecipeRate rate = null;
RecipeRate? rate = null;
var ratings = db.ListRatesOf(Account.User.Id);
if (!ratings.TryGetValue(info.Id, out rate))
@ -102,7 +102,6 @@ namespace LocalEndpoint
public void AddToFavorites(RecipeInfo info)
{
Guid userId = Account.User.Id;
var ratings = db.ListRatesOf(userId);
RecipeRate rate = GetRate(info);
db.InsertRate(userId, info.Id, new RecipeRate(true, rate.Rate));
@ -111,7 +110,6 @@ namespace LocalEndpoint
public void RemoveFromFavorites(RecipeInfo info)
{
Guid userId = Account.User.Id;
var ratings = db.ListRatesOf(userId);
RecipeRate rate = GetRate(info);
db.InsertRate(userId, info.Id, new RecipeRate(false, rate.Rate));
@ -120,10 +118,9 @@ namespace LocalEndpoint
public void SetReviewScore(RecipeInfo info, uint score)
{
Guid userId = Account.User.Id;
var ratings = db.ListRatesOf(userId);
RecipeRate rate = GetRate(info);
db.InsertRate(userId, info.Id, new RecipeRate(rate.IsFavorite, score));
db.InsertRate(userId, info.Id, new RecipeRate(rate.IsFavorite, Math.Min(score, 5)));
}
}
}

@ -1,6 +1,6 @@
using Endpoint;
using Services;
namespace LocalEndpoint
namespace LocalServices
{
internal record AccountServices(IAccountOwnedRecipes Recipes, IAccountRecipesPreferences Preferences);
internal record AccountServices(IAccountOwnedRecipesService Recipes, IAccountRecipesPreferencesService Preferences);
}

@ -1,21 +1,15 @@
using Models;
using Endpoint;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LocalEndpoint.Data;
using System.Security.Cryptography;
using Services;
using LocalServices.Data;
namespace LocalEndpoint
namespace LocalServices
{
internal class AuthService : IAuthService
public class AuthService : IAuthService
{
private readonly Database db;
private readonly IDatabase db;
public AuthService(Database db)
public AuthService(IDatabase db)
{
this.db = db;
}

@ -1,8 +1,8 @@
using Models;
namespace LocalEndpoint
namespace LocalServices
{
internal class Constants
public class Constants
{
public static readonly Uri DEFAULT_ACCOUNT_IMAGE = new Uri("https://www.pngkey.com/png/full/115-1150152_default-profile-picture-avatar-png-green.png");
}

@ -1,12 +1,6 @@
using Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization;
namespace LocalEndpoint.Data
namespace LocalServices.Data
{
[DataContract]
internal record AccountData(

@ -1,25 +1,18 @@
using Models;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.Serialization;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using System.Text.Json;
namespace LocalEndpoint.Data
namespace LocalServices.Data
{
/// <summary>
/// Database implementation with catastrophic performances.
/// This database implementation persists data in xml and will save all the data in their files on each mutable requests.
/// This database implementation persists data in json and will save all the data in their files for each mutable requests.
/// </summary>
internal class CatastrophicPerformancesDatabase : Database
public class CatastrophicPerformancesDatabase : IDatabase
{
private static readonly DataContractSerializer RECIPES_SERIALIZER = new DataContractSerializer(typeof(Dictionary<Guid, RecipeData>));
private static readonly DataContractSerializer USERS_SERIALIZER = new DataContractSerializer(typeof(Dictionary<Guid, UserData>));
private static readonly DataContractSerializer ACCOUNTS_SERIALIZER = new DataContractSerializer(typeof(Dictionary<string, AccountData>));
private static readonly string RECIPES_FILENAME = "recipes_data.xml";
private static readonly string USERS_FILENAME = "users_data.xml";
private static readonly string ACCOUNTS_FILENAME = "accounts_data.xml";
@ -37,9 +30,9 @@ namespace LocalEndpoint.Data
if (!Directory.Exists(folderPath))
Directory.CreateDirectory(folderPath);
usersData = Load<Guid, UserData>(USERS_FILENAME, USERS_SERIALIZER);
recipesData = Load<Guid, RecipeData>(RECIPES_FILENAME, RECIPES_SERIALIZER);
accountsData = Load<string, AccountData>(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER);
usersData = Load<Guid, UserData>(USERS_FILENAME);
recipesData = Load<Guid, RecipeData>(RECIPES_FILENAME);
accountsData = Load<string, AccountData>(ACCOUNTS_FILENAME);
}
public bool IsEmpty()
@ -59,12 +52,15 @@ namespace LocalEndpoint.Data
public void InsertAccount(Account account, string passwordHash)
{
accountsData[account.Email] = new AccountData(account.User.Id, account.Email, passwordHash);
Save(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER, accountsData);
Save(ACCOUNTS_FILENAME, accountsData);
InsertUser(account.User);
}
public Recipe GetRecipe(Guid id)
public Recipe? GetRecipe(Guid id)
{
return ConvertRecipeDataToRecipe(recipesData[id]);
if (recipesData.TryGetValue(id, out RecipeData? data))
return ConvertRecipeDataToRecipe(data);
return null;
}
public RecipeRate GetRecipeRate(Guid user, Guid recipe)
@ -75,38 +71,38 @@ namespace LocalEndpoint.Data
public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount)
{
usersData[userId].RecipesList[recipeId] = persAmount;
Save(USERS_FILENAME, USERS_SERIALIZER, usersData);
Save(USERS_FILENAME, usersData);
}
public void RemoveFromUserList(Guid userId, Guid recipeId)
{
usersData[userId].RecipesList.Remove(recipeId);
Save(USERS_FILENAME, USERS_SERIALIZER, usersData);
Save(USERS_FILENAME, usersData);
}
public void InsertRecipe(Recipe recipe)
{
recipesData[recipe.Info.Id] = new RecipeData(recipe.Info, recipe.Owner.Id, recipe.Ingredients, recipe.Steps);
Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData);
Save(RECIPES_FILENAME, recipesData);
}
public void InsertUser(User user)
{
usersData[user.Id] = new UserData(user, new Dictionary<Guid, RecipeRate>(), new Dictionary<Guid, uint>());
Save(USERS_FILENAME, USERS_SERIALIZER, usersData);
Save(USERS_FILENAME, usersData);
}
public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate)
{
usersData[userId].Rates[recipeId] = rate;
Save(USERS_FILENAME, USERS_SERIALIZER, usersData);
Save(USERS_FILENAME, usersData);
}
public void RemoveRecipe(Guid id)
{
recipesData.Remove(id);
Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData);
Save(RECIPES_FILENAME, recipesData);
}
public ImmutableList<Recipe> ListAllRecipes()
@ -131,7 +127,7 @@ namespace LocalEndpoint.Data
return new Recipe(rd.Info, owner, rd.Ingredients, rd.Steps);
}
private Dictionary<K, V> Load<K, V>(string fileName, DataContractSerializer deserializer)
private Dictionary<K, V> Load<K, V>(string fileName)
{
var file = dbPath + "/" + fileName;
var fileInfo = new FileInfo(file);
@ -141,20 +137,49 @@ namespace LocalEndpoint.Data
if (fileInfo.Length == 0)
return new Dictionary<K, V>(); //file is empty thus there is nothing to deserialize
Console.WriteLine(File.ReadAllText(file));
using (var stream = File.OpenRead(file))
return deserializer.ReadObject(stream) as Dictionary<K, V> ?? throw new Exception("object read from " + file + " is not a dictionnary");
string text = File.ReadAllText(file);
return JsonSerializer.Deserialize<Dictionary<K, V>>(text);
}
private async void Save<K, T>(string fileName, Dictionary<K, T> dict)
{
string json = JsonSerializer.Serialize(dict);
using (var stream = WaitForFile(dbPath + "/" + fileName, FileMode.Open, FileAccess.Write, FileShare.Write))
{
var bytes = Encoding.ASCII.GetBytes(json);
await stream.WriteAsync(bytes, 0, bytes.Length);
}
}
private void Save<K, T>(string fileName, DataContractSerializer serializer, Dictionary<K, T> dict)
// This is a workaround function to wait for a file to be released before opening it.
// This was to fix the Save method that used to throw sometimes as the file were oftenly being scanned by the androids' antivirus.
// Simply wait until the file is released and return it. This function will never return until
private static FileStream WaitForFile(string fullPath, FileMode mode, FileAccess access, FileShare share)
{
for (int attempt = 0 ; attempt < 40; attempt++)
{
using (var stream = File.OpenWrite(dbPath + "/" + fileName))
FileStream? fs = null;
try
{
serializer.WriteObject(stream, dict);
stream.Flush();
fs = new FileStream(fullPath, mode, access, share);
return fs;
}
catch (FileNotFoundException e)
{
throw e;
}
catch (IOException e)
{
Debug.WriteLine(e.Message + " in thread " + Thread.CurrentThread.Name + " " + Thread.CurrentThread.ManagedThreadId);
if (fs != null)
fs.Dispose();
Thread.Sleep(200);
}
}
throw new TimeoutException("Could not access file '" + fullPath + "', maximum attempts reached.");
}
}
}

@ -1,36 +0,0 @@

using Models;
using System.Collections.Immutable;
namespace LocalEndpoint.Data
{
// The database interface defines all the different kinds of requests the LocalEndpoint needs to store and retrieve data.
public interface Database
{
public Recipe GetRecipe(Guid id);
public RecipeRate GetRecipeRate(Guid user, Guid recipe);
public Account? GetAccount(string email, string passwordHash);
public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount);
public void RemoveFromUserList(Guid userId, Guid recipeId);
public void InsertAccount(Account account, string passwordHash);
public void InsertRecipe(Recipe recipe);
public void InsertUser(User user);
public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate);
public void RemoveRecipe(Guid id);
public ImmutableList<Recipe> ListAllRecipes();
public ImmutableDictionary<Guid, RecipeRate> ListRatesOf(Guid user);
public ImmutableDictionary<Guid, uint> GetRecipeListOf(Guid user);
}
}

@ -0,0 +1,103 @@

using Models;
using System.Collections.Immutable;
namespace LocalServices.Data
{
/// <summary>
/// The database interface defines all the different kinds of requests the LocalEndpoint needs to store and retrieve data.
/// </summary>
public interface IDatabase
{
/// <summary>
/// Get a Recipe from its identifier
/// </summary>
/// <param name="id"></param>
/// <returns>The recipe if the identifier is registered in the database</returns>
public Recipe? GetRecipe(Guid id);
/// <summary>
/// Get the rate of a user for a given recipe
/// </summary>
/// <param name="user">The user identifier</param>
/// <param name="recipe">The recipe identifier</param>
/// <returns>Returns a rate</returns>
public RecipeRate GetRecipeRate(Guid user, Guid recipe);
/// <summary>
/// Gets an account from an email and a password hash
/// </summary>
/// <param name="email"></param>
/// <param name="passwordHash"></param>
/// <returns>some account if the email and the hash is found in the database</returns>
public Account? GetAccount(string email, string passwordHash);
/// <summary>
/// Insert a recipe in user's weekly list
/// </summary>
/// <param name="userId"></param>
/// <param name="recipeId"></param>
/// <param name="persAmount"></param>
public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount);
/// <summary>
/// Remove a recipe from user's weekly list
/// </summary>
/// <param name="userId"></param>
/// <param name="recipeId"></param>
public void RemoveFromUserList(Guid userId, Guid recipeId);
/// <summary>
/// Inserts an account in the database, with the given passwordhash
/// </summary>
/// <param name="account"></param>
/// <param name="passwordHash"></param>
public void InsertAccount(Account account, string passwordHash);
/// <summary>
/// Inserts a recipe
/// </summary>
/// <param name="recipe"></param>
public void InsertRecipe(Recipe recipe);
/// <summary>
/// Inserts a user
/// </summary>
/// <param name="user"></param>
public void InsertUser(User user);
/// <summary>
/// Inserts a user rate over the given recipe identifier
/// </summary>
/// <param name="userId"></param>
/// <param name="recipeId"></param>
/// <param name="rate"></param>
public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate);
/// <summary>
/// Removes a recipe
/// </summary>
/// <param name="id"></param>
public void RemoveRecipe(Guid id);
/// <summary>
/// Lists all recipes in the database
/// </summary>
/// <returns></returns>
public ImmutableList<Recipe> ListAllRecipes();
/// <summary>
/// List the ratings of an user
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public ImmutableDictionary<Guid, RecipeRate> ListRatesOf(Guid user);
/// <summary>
/// Get the weekly list of given user
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public ImmutableDictionary<Guid, uint> GetRecipeListOf(Guid user);
}
}

@ -2,7 +2,7 @@
using System.Collections.Immutable;
using System.Runtime.Serialization;
namespace LocalEndpoint.Data
namespace LocalServices.Data
{
[DataContract]
internal record RecipeData(

@ -0,0 +1,72 @@
using LocalServices.Data;
using Models;
using System.Collections.Immutable;
namespace LocalServices.Data
{
/// <summary>
/// stub implementation for IDatabase
/// </summary>
internal class StubDatabase : IDatabase
{
private readonly User User = new User(new Uri("https://images.pexels.com/photos/37546/woman-portrait-face-studio-37546.jpeg?cs=srgb&dl=beauty-face-headshot-37546.jpg&fm=jpg"), "David", Guid.NewGuid());
public Account? GetAccount(string email, string passwordHash)
{
return new Account(User, "david@gmail.com");
}
public Recipe? GetRecipe(Guid id)
{
return new Recipe(new RecipeInfo("Foo", 4, 5, new Uri("https://th.bing.com/th/id/R.e57009c4044bef3e86f8a7b18a7cf36a?rik=K3dSu3KoTgeRSw&pid=ImgRaw&r=0"), 4.5F, id), User, new List<Ingredient> { new Ingredient("Chocolate", 4, "g") }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Eat Chocolate", "Eat the chocolate") }.ToImmutableList());
}
public ImmutableDictionary<Guid, uint> GetRecipeListOf(Guid user)
{
return new Dictionary<Guid, uint>().ToImmutableDictionary();
}
public RecipeRate GetRecipeRate(Guid user, Guid recipe)
{
return new RecipeRate(true, 4);
}
public void InsertAccount(Account account, string passwordHash)
{
}
public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount)
{
}
public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate)
{
}
public void InsertRecipe(Recipe recipe)
{
}
public void InsertUser(User user)
{
}
public ImmutableList<Recipe> ListAllRecipes()
{
return new List<Recipe>().ToImmutableList();
}
public ImmutableDictionary<Guid, RecipeRate> ListRatesOf(Guid user)
{
return new Dictionary<Guid, RecipeRate>().ToImmutableDictionary();
}
public void RemoveFromUserList(Guid userId, Guid recipeId)
{
}
public void RemoveRecipe(Guid id)
{
}
}
}

@ -3,7 +3,7 @@
using Models;
using System.Runtime.Serialization;
namespace LocalEndpoint.Data
namespace LocalServices.Data
{
[DataContract]
internal record UserData(

@ -1,14 +1,14 @@
using Endpoint;
using LocalEndpoint.Data;
using Services;
using LocalServices.Data;
using Models;
using System.Collections.Immutable;
namespace LocalEndpoint
namespace LocalServices
{
/// <summary>
/// The local endpoint is an implementation of the Endpoint API definition.
///
/// The local endpoint is an implementation of the Services API.
/// This class is the _entry point_ class of the implementation
/// </summary>
public class LocalEndpoint : IEndpoint
{
@ -20,6 +20,7 @@ namespace LocalEndpoint
{
var db = new CatastrophicPerformancesDatabase(Environment.GetFolderPath(Environment.SpecialFolder.Personal));
if (db.IsEmpty())
PrepareDatabase(db);
@ -31,7 +32,12 @@ namespace LocalEndpoint
public IRecipesService RecipesService => recipesService;
private static void PrepareDatabase(Database db)
/// <summary>
/// Inserts sample data in the local database
/// </summary>
/// <param name="db"></param>
private static void PrepareDatabase(IDatabase db)
{
User USER1 = new User(new Uri("https://i.ibb.co/L6t6bGR/DALL-E-2023-05-10-20-27-31-cook-looking-at-the-camera-with-a-chef-s-hat-laughing-in-an-exaggerated-w.png"), "The Funny Chief", MakeGuid(1));
User USER2 = new User(Constants.DEFAULT_ACCOUNT_IMAGE, "Yanis", MakeGuid(2));
@ -45,14 +51,19 @@ namespace LocalEndpoint
db.InsertAccount(new Account(USER2, "yanis@google.com"), "123456");
db.InsertAccount(new Account(USER3, "leo@google.com"), "123456");
db.InsertRecipe(new Recipe(new RecipeInfo("Chicken Salad", 500, 20, new Uri("https://healthyfitnessmeals.com/wp-content/uploads/2021/04/Southwest-chicken-salad-7-500x500.jpg"), 4, Guid.NewGuid()), USER1, new List<Ingredient> { new Ingredient("Ingredient 1", 6) }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList()));
db.InsertRecipe(new Recipe(new RecipeInfo("Chocolate Cake", 2500, 10, new Uri("https://bakewithshivesh.com/wp-content/uploads/2022/08/IMG_0248-scaled.jpg"), 3, Guid.NewGuid()), USER2, new List<Ingredient> { new Ingredient("Ingredient 1", 6) }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList()));
db.InsertRecipe(new Recipe(new RecipeInfo("Salmon", 20, 10, new Uri("https://www.wholesomeyum.com/wp-content/uploads/2021/06/wholesomeyum-Pan-Seared-Salmon-Recipe-13.jpg"), 4, Guid.NewGuid()), USER1, new List<Ingredient> { new Ingredient("Ingredient 1", 6) }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList()));
db.InsertRecipe(new Recipe(new RecipeInfo("Fish", 50, 30, new Uri("https://www.ciaanet.org/wp-content/uploads/2022/07/Atlantic-and-Pacific-whole-salmon-1024x683.jpg"), 4.5F, Guid.NewGuid()), USER3, new List<Ingredient> { new Ingredient("Ingredient 1", 6) }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList()));
db.InsertRecipe(new Recipe(new RecipeInfo("Space Cake", 800, 5, new Uri("https://static.youmiam.com/images/recipe/1500x1000/space-cake-22706?placeholder=web_recipe&sig=f14a7a86da837c6b8cc678cde424d6d5902f99ec&v3"), 5, Guid.NewGuid()), USER3, new List<Ingredient> { new Ingredient("Ingredient 1", 6) }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList()));
db.InsertRecipe(new Recipe(new RecipeInfo("Cupcake", 500, 12, new Uri("https://www.mycake.fr/wp-content/uploads/2015/12/rs_cupcake_4x3.jpg"), 4.2F, Guid.NewGuid()), USER1, new List<Ingredient> { new Ingredient("Chocolate", 4) }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Eat Chocolate", "Eat the chocolate") }.ToImmutableList()));
db.InsertRecipe(new Recipe(new RecipeInfo("Chicken Salad", 500, 20, new Uri("https://healthyfitnessmeals.com/wp-content/uploads/2021/04/Southwest-chicken-salad-7-500x500.jpg"), 4, Guid.NewGuid()), USER1, new List<Ingredient> { new Ingredient("Ingredient 1", 6, "g") }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList()));
db.InsertRecipe(new Recipe(new RecipeInfo("Chocolate Cake", 2500, 10, new Uri("https://bakewithshivesh.com/wp-content/uploads/2022/08/IMG_0248-scaled.jpg"), 3, Guid.NewGuid()), USER2, new List<Ingredient> { new Ingredient("Ingredient 1", 6, "g") }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList()));
db.InsertRecipe(new Recipe(new RecipeInfo("Salmon", 20, 10, new Uri("https://www.wholesomeyum.com/wp-content/uploads/2021/06/wholesomeyum-Pan-Seared-Salmon-Recipe-13.jpg"), 4, Guid.NewGuid()), USER1, new List<Ingredient> { new Ingredient("Ingredient 1", 6, "g") }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList()));
db.InsertRecipe(new Recipe(new RecipeInfo("Fish", 50, 30, new Uri("https://www.ciaanet.org/wp-content/uploads/2022/07/Atlantic-and-Pacific-whole-salmon-1024x683.jpg"), 4.5F, Guid.NewGuid()), USER3, new List<Ingredient> { new Ingredient("Ingredient 1", 6, "g") }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList()));
db.InsertRecipe(new Recipe(new RecipeInfo("Space Cake", 800, 5, new Uri("https://static.youmiam.com/images/recipe/1500x1000/space-cake-22706?placeholder=web_recipe&sig=f14a7a86da837c6b8cc678cde424d6d5902f99ec&v3"), 5, Guid.NewGuid()), USER3, new List<Ingredient> { new Ingredient("Ingredient 1", 6, "g") }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Step 1", "Bake the eggs") }.ToImmutableList()));
db.InsertRecipe(new Recipe(new RecipeInfo("Cupcake", 500, 12, new Uri("https://www.mycake.fr/wp-content/uploads/2015/12/rs_cupcake_4x3.jpg"), 4.2F, Guid.NewGuid()), USER1, new List<Ingredient> { new Ingredient("Chocolate", 4, "g") }.ToImmutableList(), new List<PreparationStep> { new PreparationStep("Eat Chocolate", "Eat the chocolate") }.ToImmutableList()));
}
/// <summary>
/// helper function to Create a Guid from a given seed
/// </summary>
/// <param name="seed">the seed to use for the generation</param>
/// <returns></returns>
private static Guid MakeGuid(int seed)
{
var r = new Random(seed);

@ -1,17 +1,17 @@
using Endpoint;
using LocalEndpoint.Data;
using Services;
using LocalServices.Data;
using Models;
using System.Collections.Immutable;
namespace LocalEndpoint
namespace LocalServices
{
internal class RecipesService : IRecipesService
public class RecipesService : IRecipesService
{
private readonly Database db;
private readonly IDatabase db;
private readonly Dictionary<Account, AccountServices> accountsData = new Dictionary<Account, AccountServices>();
public RecipesService(Database db)
public RecipesService(IDatabase db)
{
this.db = db;
}
@ -21,17 +21,17 @@ namespace LocalEndpoint
return db.ListAllRecipes().Take(4).ToImmutableList().ConvertAll(v => v.Info);
}
public Recipe GetRecipe(RecipeInfo info)
public Recipe? GetRecipe(RecipeInfo info)
{
return db.GetRecipe(info.Id);
}
public IAccountOwnedRecipes GetRecipesOf(Account account)
public IAccountOwnedRecipesService GetRecipesOf(Account account)
{
return GetOrInitData(account).Recipes;
}
public IAccountRecipesPreferences GetPreferencesOf(Account account)
public IAccountRecipesPreferencesService GetPreferencesOf(Account account)
{
return GetOrInitData(account).Preferences;
}
@ -51,5 +51,11 @@ namespace LocalEndpoint
return data;
}
public ImmutableList<RecipeInfo> SearchRecipes(string prompt)
{
return db.ListAllRecipes()
.ConvertAll(r => r.Info)
.FindAll(i => i.Name.ToLower().Contains(prompt.ToLower()));
}
}
}

@ -3,15 +3,20 @@ using Microsoft.Maui.Controls;
using Models;
using ShoopNCook.Controllers;
using ShoopNCook.Pages;
using Endpoint;
using Services;
// Shell principale de l'application après connexion de l'utilisateur
public partial class MainAppShell : Shell
{
// Constructeur qui prend en argument un compte, un endpoint et une application
public MainAppShell(Account account, IEndpoint endpoint, IApp app)
{
InitializeComponent();
HomeTab.ContentTemplate = new DataTemplate(() => new HomePage(account, app.Notifier, endpoint));
FavoritesTab.ContentTemplate = new DataTemplate(() => new FavoritesPage(account, app.Notifier, endpoint.RecipesService));
MyListTab.ContentTemplate = new DataTemplate(() => new MyListPage(account, app.Notifier, endpoint.RecipesService));
// Initialisation de chaque onglet avec sa page respective
HomeTab.ContentTemplate = new DataTemplate(() => new HomePage(account, endpoint));
FavoritesTab.ContentTemplate = new DataTemplate(() => new FavoritesPage(account, endpoint.RecipesService));
MyListTab.ContentTemplate = new DataTemplate(() => new MyListPage(account, endpoint.RecipesService));
MoreTab.ContentTemplate = new DataTemplate(() => new MorePage(account, new MorePageController(account, endpoint, app)));
}
}

@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using CommunityToolkit.Maui;
using Microsoft.Extensions.Logging;
namespace ShoopNCook;
@ -9,6 +10,7 @@ public static class MauiProgram
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");

@ -1,4 +1,9 @@
namespace Models
{
/// <summary>
/// Contains the informations of an account.
/// </summary>
/// <param name="User">The account's public information</param>
/// <param name="Email">The account's email address</param>
public record Account(User User, string Email);
}

@ -3,7 +3,11 @@
namespace Models
{
/// <summary>
/// An ingredient
/// </summary>
/// <param name="Name">The ingredient's name</param>
/// <param name="Amount">The ingredient's amount (in kilograms or liters)</param>
[DataContract]
public record Ingredient([property: DataMember] string Name, [property: DataMember] float Amount);
public record Ingredient([property: DataMember] string Name, [property: DataMember] float Amount, [property: DataMember] string Unit);
}

@ -2,6 +2,11 @@
namespace Models
{
/// <summary>
/// A step of preparation
/// </summary>
/// <param name="Name">The step's name</param>
/// <param name="Description">The step's instructions / description</param>
[DataContract]
public record PreparationStep([property: DataMember] string Name, [property: DataMember] string Description);
}

@ -3,6 +3,13 @@ using System.Runtime.Serialization;
namespace Models
{
/// <summary>
/// A Recipe
/// </summary>
/// <param name="Info">The essential information of the recipe</param>
/// <param name="Owner">The creator of the recipe</param>
/// <param name="Ingredients">The needed ingredients of the recipe</param>
/// <param name="Steps">The preparation steps</param>
[DataContract]
public record Recipe(
[property: DataMember] RecipeInfo Info,
@ -10,5 +17,4 @@ namespace Models
[property: DataMember] ImmutableList<Ingredient> Ingredients,
[property: DataMember] ImmutableList<PreparationStep> Steps
);
}

@ -7,6 +7,9 @@ using System.Threading.Tasks;
namespace Models
{
/// <summary>
/// A Simple builder to create a recipe
/// </summary>
public class RecipeBuilder
{
private readonly string name;

@ -2,12 +2,21 @@
namespace Models
{
/// <summary>
/// The essential information about a recipe
/// </summary>
/// <param name="Name">The recipe's name</param>
/// <param name="CalPerPers">The energy input</param>
/// <param name="CookTimeMins">Estimated time of preparation in minutes</param>
/// <param name="Image">An illustrative image of the recipe</param>
/// <param name="AverageNote">The average rate of the recipe</param>
/// <param name="Id">An unique identifier</param>
[DataContract]
public record RecipeInfo(
[property: DataMember] string Name,
[property: DataMember] uint CalPerPers,
[property: DataMember] uint CookTimeMins,
[property: DataMember] Uri? Image,
[property: DataMember] Uri Image,
[property: DataMember] float AverageNote,
[property: DataMember] Guid Id
);

@ -3,6 +3,11 @@ using System.Runtime.Serialization;
namespace Models
{
/// <summary>
/// The rate of a recipe, usually, the instances are bound with an account.
/// </summary>
/// <param name="IsFavorite"></param>
/// <param name="Rate">a rate between 0 and 5</param>
[DataContract]
public record RecipeRate(
[property: DataMember] bool IsFavorite = false,

@ -2,13 +2,25 @@
namespace Models
{
/// <summary>
/// Publics informations of a user
/// </summary>
[DataContract]
public class User
{
/// <summary>
/// The profile picture
/// </summary>
[DataMember]
public Uri ProfilePicture { get; init; }
/// <summary>
/// The username
/// </summary>
[DataMember]
public string Name { get; init; }
/// <summary>
/// An unique identifier
/// </summary>
[DataMember]
public Guid Id { get; init; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

@ -1,23 +0,0 @@
using Models;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LocalEndpoint
{
public interface IAccountOwnedRecipes
{
public Account Account { get; }
public bool UploadRecipe(Recipe recipe);
public bool RemoveRecipe(RecipeInfo info);
public ImmutableList<RecipeInfo> GetAccountRecipes();
}
}

@ -0,0 +1,38 @@
using Models;
using System.Collections.Immutable;
namespace Services
{
/// <summary>
/// This service handles the recipes created by an account
/// </summary>
public interface IAccountOwnedRecipesService
{
/// <summary>
/// This service's bound account
/// </summary>
public Account Account { get; }
/// <summary>
/// Upload a new recipe, ensuring the recipe's owner matches the service's bound account user.
/// </summary>
/// <param name="recipe">The recipe to upload</param>
/// <returns>true if the recipe could be uploaded, false instead</returns>
public bool UploadRecipe(Recipe recipe);
/// <summary>
/// Removes a recipe
/// </summary>
/// <param name="info">The informations about the recipe to remove</param>
/// <returns>true if the recipe could be removed, false instead</returns>
public bool RemoveRecipe(RecipeInfo info);
/// <summary>
/// The living recipes created by this account.
/// If the user removes a recipe (using <see cref="RemoveRecipe(RecipeInfo)"/>) it'll no longer apear in the
/// next invocations of this recipe
/// </summary>
/// <returns>the list of all the living recipes of the account</returns>
public ImmutableList<RecipeInfo> GetAccountRecipes();
}
}

@ -1,29 +0,0 @@
using Models;
using System.Collections.Immutable;
namespace Endpoint
{
public interface IAccountRecipesPreferences
{
public Account Account { get; }
public void AddToFavorites(RecipeInfo info);
public void RemoveFromFavorites(RecipeInfo info);
public void SetReviewScore(RecipeInfo info, uint score);
public bool AddToWeeklyList(RecipeInfo info, uint persAmount);
public bool RemoveFromWeeklyList(RecipeInfo info);
public bool IsInWeeklyList(RecipeInfo info);
public RecipeRate GetRate(RecipeInfo info);
public ImmutableList<RecipeInfo> GetFavorites();
public ImmutableList<RecipeInfo> GetRecommendedRecipes();
public ImmutableList<(RecipeInfo, uint)> GetWeeklyList();
}
}

@ -0,0 +1,68 @@
using Models;
using System.Collections.Immutable;
namespace Services
{
/// <summary>
/// This service handles the preferences of a bound account
/// </summary>
public interface IAccountRecipesPreferencesService
{
/// <summary>
/// The bound account
/// </summary>
public Account Account { get; }
/// <summary>
/// Adds a recipe in the favorites of the bound account
/// </summary>
/// <param name="info">The information about the recipe to add in favorites</param>
public void AddToFavorites(RecipeInfo info);
/// <summary>
/// Removes a recipe from the favorites of the bound account
/// </summary>
/// <param name="info">The information about the recipe to remove from favorites</param>
public void RemoveFromFavorites(RecipeInfo info);
/// <summary>
/// Sets a score for the specified recipe
/// </summary>
/// <param name="info">The information about the targeted recipe</param>
/// <param name="score">The score to set</param>
public void SetReviewScore(RecipeInfo info, uint score);
/// <summary>
/// Adds a recipe to the weekly list, specifying the amount of persons that must be fed for the week
/// </summary>
/// <param name="info">The information about the targeted recipe</param>
/// <param name="persAmount">The amount of guests that needs to be fed by the recipe for the week</param>
/// <returns></returns>
public bool AddToWeeklyList(RecipeInfo info, uint persAmount);
/// <summary>
/// Retrieves the rate of the targeted recipe
/// The rate contains the user's score and whether if the recipe is in the favorites list.
/// <see cref="RecipeInfo"/>
/// </summary>
/// <param name="info">The information about the targeted recipe</param>
/// <returns></returns>
public RecipeRate GetRate(RecipeInfo info);
/// <summary>
/// The favorites recipes of the account
/// </summary>
/// <returns>A list containing all the recipe info that are marked as favorite by the bound account</returns>
public ImmutableList<RecipeInfo> GetFavorites();
/// <summary>
/// The recommended recipes for the user based on his preferences.
/// </summary>
/// <returns>A list of the recommended recipes based on the preferences of the bound account</returns>
public ImmutableList<RecipeInfo> GetRecommendedRecipes();
/// <summary>
/// The weekly list of the bound account
/// </summary>
/// <returns>The weekly list of the bound account, containing tuples that binds a recipe to the number of guests to feed for the week</returns>
public ImmutableList<(RecipeInfo, uint)> GetWeeklyList();
}
}

@ -1,10 +1,33 @@
using Models;
namespace Endpoint
namespace Services
{
/// <summary>
/// Service for account authentification
/// Passwords are being passed in this service clearly as the hash function used for passwords is implementation specific
/// </summary>
public interface IAuthService
{
/// <summary>
/// Tries to login to an account using its mail address and password.
/// </summary>
/// <param name="email"> The mail address which acts as an identifier for the targeted account </param>
/// <param name="password"> The (clear) password used to login.</param>
/// <returns>
/// Returns an instance of Account representing the account that got logged in.
/// If the login credentials are invalid to log in the targeted acccount, this method returns null.
/// </returns>
public Account? Login(string email, string password);
/// <summary>
/// Tries to register to a new account, defining its mail address, username and password.
/// </summary>
/// <param name="email"> The mail address which acts as an identifier for the targeted account </param>
/// <param name="username"> The username of the account </param>
/// <param name="password"> The (clear) password used to login on next connections attempt.</param>
/// <returns>
/// Returns an instance of Account representing the account that got newly registered.
/// If the register credentials are invalid, or if the account already exists, this method returns null.
/// </returns>
public Account? Register(string email, string username, string password);
}
}

@ -1,7 +1,10 @@

namespace Endpoint
namespace Services
{
/// <summary>
/// The endpoint is the central element of the 'Services' assembly.
/// </summary>
public interface IEndpoint
{
public IAuthService AuthService { get; }

@ -1,17 +1,47 @@
using LocalEndpoint;
using Models;
using Models;
using System.Collections.Immutable;
namespace Endpoint
namespace Services
{
/// <summary>
/// The services that is in charge of handling the application's recipes.
/// </summary>
public interface IRecipesService
{
/// <summary>
///
/// </summary>
/// <returns>A list containg the popular recipes of the week</returns>
public ImmutableList<RecipeInfo> PopularRecipes();
public Recipe GetRecipe(RecipeInfo info);
/// <summary>
/// performs a search over all the recipes
/// </summary>
/// <returns>A list containg the recipes that matches the prompt</returns>
public ImmutableList<RecipeInfo> SearchRecipes(string prompt);
public IAccountOwnedRecipes GetRecipesOf(Account account);
public IAccountRecipesPreferences GetPreferencesOf(Account account);
/// <summary>
/// Retrieves a recipe from given RecipeInfo
/// </summary>
/// <param name="info">the informations about the recipe that we want to retrieve</param>
/// <returns>some recipe if the recipe was found, null else</returns>
public Recipe? GetRecipe(RecipeInfo info);
/// <summary>
/// Gets the service that is in charge of handling the account's owned recipes.
/// The account's owned recipes are the recipes that the account created.
/// </summary>
/// <param name="account">The account logged in</param>
/// <returns>The service that handles the given account's recipes</returns>
public IAccountOwnedRecipesService GetRecipesOf(Account account);
/// <summary>
/// Gets the service that handles all the preferences of the given account
/// </summary>
/// <param name="account">The account logged in</param>
/// <returns>
/// The service that handles the given account's preferences
/// </returns>
public IAccountRecipesPreferencesService GetPreferencesOf(Account account);
}

@ -6,16 +6,17 @@
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net7.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<RootNamespace>ShoopNCook</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Display name -->
<ApplicationTitle>ShoopNCook</ApplicationTitle>
<ApplicationTitle>ShopNCook</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.shoopncook</ApplicationId>
<ApplicationId>com.companyname.shopncook</ApplicationId>
<ApplicationIdGuid>bf17e1fe-a722-42f6-a24d-3327d351c924</ApplicationIdGuid>
<!-- Versions -->
@ -48,26 +49,31 @@
<ItemGroup>
<AndroidResource Remove="ShopNCookTests\**" />
<AndroidResource Remove="Tests\**" />
<Compile Remove="Foo\**" />
<Compile Remove="Services\**" />
<Compile Remove="LocalServices\**" />
<Compile Remove="Models\**" />
<Compile Remove="ShopNCookTests\**" />
<Compile Remove="Tests\**" />
<EmbeddedResource Remove="Foo\**" />
<EmbeddedResource Remove="Services\**" />
<EmbeddedResource Remove="LocalServices\**" />
<EmbeddedResource Remove="Models\**" />
<EmbeddedResource Remove="ShopNCookTests\**" />
<EmbeddedResource Remove="Tests\**" />
<MauiCss Remove="Foo\**" />
<MauiCss Remove="Services\**" />
<MauiCss Remove="LocalServices\**" />
<MauiCss Remove="Models\**" />
<MauiCss Remove="ShopNCookTests\**" />
<MauiCss Remove="Tests\**" />
<MauiXaml Remove="Foo\**" />
<MauiXaml Remove="Services\**" />
<MauiXaml Remove="LocalServices\**" />
<MauiXaml Remove="Models\**" />
<MauiXaml Remove="ShopNCookTests\**" />
<MauiXaml Remove="Tests\**" />
<None Remove="Foo\**" />
<None Remove="Services\**" />
<None Remove="LocalServices\**" />
<None Remove="Models\**" />
@ -113,6 +119,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Maui" Version="5.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="xunit" Version="2.4.2" />
</ItemGroup>

@ -10,7 +10,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "Models\Models.csp
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LocalServices", "LocalServices\LocalServices.csproj", "{57732316-93B9-4DA0-A212-F8892D3D968B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services", "Services\Services.csproj", "{C976BDD8-710D-4162-8A42-973B634491F9}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Services", "Services\Services.csproj", "{C976BDD8-710D-4162-8A42-973B634491F9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6DEA92EF-71CD-4A21-9CC0-67F228E1155D}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

@ -0,0 +1,26 @@
using LocalServices.Data;
using Models;
using Services;
using System.Collections.Immutable;
namespace Tests
{
public class AccountOwnedRecipesTest
{
private static readonly User SAMPLE_USER = new User(new Uri("https://www.referenseo.com/wp-content/uploads/2019/03/image-attractive-960x540.jpg"), "user", Guid.NewGuid());
private static readonly Account SAMPLE_ACC = new Account(SAMPLE_USER, "mail");
private static readonly Recipe SAMPLE_RECIPE = new RecipeBuilder("foo", SAMPLE_USER).Build();
[Fact]
public void UploadRemove()
{
var db = new Mock<IDatabase>();
db.Setup(x => x.ListAllRecipes()).Returns(() => new List<Recipe>().ToImmutableList());
var owned = new AccountOwnedRecipes(SAMPLE_ACC, db.Object);
owned.UploadRecipe(SAMPLE_RECIPE);
Assert.Contains(SAMPLE_RECIPE.Info, owned.GetAccountRecipes());
owned.RemoveRecipe(SAMPLE_RECIPE.Info);
Assert.DoesNotContain(SAMPLE_RECIPE.Info, owned.GetAccountRecipes());
}
}
}

@ -0,0 +1,46 @@
using LocalServices;
using LocalServices.Data;
using Models;
using System.Collections.Immutable;
namespace Tests
{
public class AccountRecipesPreferencesTests
{
private static readonly User SAMPLE_USER = new User(new Uri("https://www.referenseo.com/wp-content/uploads/2019/03/image-attractive-960x540.jpg"), "user", Guid.NewGuid());
private static readonly Account SAMPLE_ACC = new Account(SAMPLE_USER, "mail");
private static readonly Recipe SAMPLE_RECIPE = new RecipeBuilder("foo", SAMPLE_USER).Build();
[Fact]
public void Review()
{
var fav_inserted = false;
var rate_inserted = false;
var dict = new Dictionary<Guid, RecipeRate>();
var db = new Mock<IDatabase>();
db.Setup(x => x.ListRatesOf(SAMPLE_USER.Id)).Returns(() => dict.ToImmutableDictionary());
db.Setup(x => x.InsertRate(SAMPLE_USER.Id, SAMPLE_RECIPE.Info.Id, new RecipeRate(true, 0))).Callback(() => fav_inserted = true);
db.Setup(x => x.InsertRate(SAMPLE_USER.Id, SAMPLE_RECIPE.Info.Id, new RecipeRate(false, 3))).Callback(() => rate_inserted = true);
var pref = new AccountRecipesPreferences(SAMPLE_ACC, db.Object);
pref.AddToFavorites(SAMPLE_RECIPE.Info);
pref.SetReviewScore(SAMPLE_RECIPE.Info, 3);
Assert.True(fav_inserted);
Assert.True(rate_inserted);
}
public void AddWeeklyList()
{
var inserted = false;
var dict = new Dictionary<Guid, uint> { };
dict.Add(SAMPLE_RECIPE.Info.Id, 88);
var db = new Mock<IDatabase>();
db.Setup(x => x.GetRecipeListOf(SAMPLE_USER.Id)).Returns(() => dict);
db.Setup(x => x.InsertInUserList(SAMPLE_USER.Id, SAMPLE_RECIPE.Info.Id, 88)).Callback(() => inserted = true);
var pref = new AccountRecipesPreferences(SAMPLE_ACC, db.Object);
pref.AddToWeeklyList(SAMPLE_RECIPE.Info, 88);
Assert.True(inserted);
Assert.True(pref.GetWeeklyList().Contains((SAMPLE_RECIPE.Info, 88)));
}
}
}

@ -0,0 +1,35 @@

using Models;
using LocalServices.Data;
using Moq;
using LocalServices;
using Services;
namespace Tests
{
public class AuthServiceTests
{
private static readonly User SAMPLE_USER = new User(new Uri("https://www.referenseo.com/wp-content/uploads/2019/03/image-attractive-960x540.jpg"), "user", Guid.NewGuid());
private static readonly Account SAMPLE_ACC = new Account(SAMPLE_USER, "mail");
[Fact]
public void TestLogin()
{
var database = new Mock<IDatabase>();
database.Setup(x => x.GetAccount("mail", "1234")).Returns(SAMPLE_ACC);
var service = new AuthService(database.Object);
var acc = service.Login("mail", "1234");
Assert.Equal(acc, SAMPLE_ACC);
}
[Fact]
public void TestRegister()
{
var database = new Mock<IDatabase>();
database.Setup(x => x.GetAccount("mail", "1234")).Returns(SAMPLE_ACC);
var service = new AuthService(database.Object);
var acc = service.Register("mail", "foo", "1234");
Assert.Equal(acc, new Account(new User(Constants.DEFAULT_ACCOUNT_IMAGE, "foo", acc.User.Id), "mail"));
}
}
}

@ -0,0 +1,25 @@
using LocalServices.Data;
using Models;
using LocalServices;
using Services;
using System.Collections.Immutable;
namespace Tests
{
public class RecipeServicesTests
{
private static readonly User SAMPLE_USER = new User(new Uri("https://www.referenseo.com/wp-content/uploads/2019/03/image-attractive-960x540.jpg"), "user", Guid.NewGuid());
private static readonly Account SAMPLE_ACC = new Account(SAMPLE_USER, "mail");
private static readonly Recipe SAMPLE_RECIPE = new RecipeBuilder("foo", SAMPLE_USER).Build();
[Fact]
public void GetRecipe()
{
var database = new Mock<IDatabase>();
database.Setup(x => x.GetRecipe(SAMPLE_RECIPE.Info.Id)).Returns(SAMPLE_RECIPE);
var service = new RecipesService(database.Object);
Assert.Equal(service.GetRecipe(SAMPLE_RECIPE.Info), SAMPLE_RECIPE);
}
}
}

@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -22,4 +23,10 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LocalServices\LocalServices.csproj" />
<ProjectReference Include="..\Models\Models.csproj" />
<ProjectReference Include="..\Services\Services.csproj" />
</ItemGroup>
</Project>

@ -1,13 +0,0 @@
namespace Tests
{
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}
}

@ -1 +1,2 @@
global using Xunit;
global using Moq;

@ -0,0 +1,42 @@
using CommunityToolkit.Maui.Alerts;
using CommunityToolkit.Maui.Core;
namespace ShoopNCook
{
internal class UserNotifier
{
private static async Task Show(string message, string messageType)
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
// Vous pouvez configurer la durée et la taille de police ici.
ToastDuration duration = ToastDuration.Short;
double fontSize = 14;
var toast = Toast.Make(message, duration, fontSize);
await toast.Show(cancellationTokenSource.Token);
}
public static void Error(string message)
{
Show(message, "Error");
}
public static void Warn(string message)
{
Show(message, "Warning");
}
public static void Notice(string message)
{
Show(message, "Notice");
}
public static void Success(string message)
{
Show(message, "Success");
}
}
}

@ -41,3 +41,4 @@ public partial class CounterView : ContentView
Count -= 1;
}
}

@ -1,19 +1,22 @@
namespace ShoopNCook.Views;
using Microsoft.Maui.Graphics;
// Classe représentant un bouton avec une tête (une image en préfixe)
public partial class HeadedButton : ContentView
{
// Texte du bouton
public string Text
{
set => BtnLabel.Text = value;
}
// Couleur de l'image en préfixe
public string HeadColor
{
set => PrefixBorder.BackgroundColor = Color.FromArgb(value);
}
// Source de l'image en préfixe
public string HeadSource
{
set => PrefixImage.Source = ImageSource.FromFile(value);

@ -22,7 +22,7 @@
Style="{StaticResource UserInput}"
Placeholder="Ingredient Name"
HeightRequest="40"
x:Name="NameEntry"/>
Text="{Binding NameText, Mode=OneWayToSource}"/>
</Border>
<Border
Grid.Column="1"
@ -35,7 +35,7 @@
Placeholder="Quantity"
HeightRequest="40"
Keyboard="Numeric"
x:Name="QuantityEntry"/>
Text="{Binding QuantityText, Mode=OneWayToSource}"/>
</Border>
<Border
Grid.Column="2"

@ -2,23 +2,31 @@ using Models;
namespace ShoopNCook.Views;
// Classe représentant une entrée d'ingrédient
public partial class IngredientEntry : ContentView
{
public string QuantityText { get; set; }
public string NameText { get; set; }
public IngredientEntry()
{
BindingContext = this;
InitializeComponent();
}
public Ingredient MakeValue()
// Renvoie une nouvelle instance de Ingredient à partir des informations entrées par l'utilisateur
public Ingredient? MakeValue()
{
float quantity;
if (!float.TryParse(QuantityEntry.Text, out quantity))
// Tente de convertir la quantité en float, sinon, attribue une valeur par défaut de 0
if (!float.TryParse(QuantityText, out quantity) || quantity < 0)
{
quantity = 0;
// TODO handle quantity text malformation by raising exception
UserNotifier.Error("La quantité doit être un nombre positif");
return null;
}
return new Ingredient(NameEntry.Text, quantity);
return new Ingredient(NameText, quantity, UnitPicker.SelectedItem as string);
}
}

@ -2,9 +2,11 @@ using Models;
namespace ShoopNCook.Views;
// Classe représentant une vue d'ingrédient
public partial class IngredientView : ContentView
{
public static readonly BindableProperty NameProperty =
// Propriétés liées pour le nom, la quantité et l'unité de l'ingrédient
private readonly BindableProperty NameProperty =
BindableProperty.Create(nameof(Name), typeof(string), typeof(IngredientView), default(string));
public static readonly BindableProperty QuantityProperty =
@ -35,9 +37,10 @@ public partial class IngredientView : ContentView
{
InitializeComponent();
// Initialisation des valeurs de l'ingrédient
Name = ingredient.Name;
Quantity = ingredient.Amount;
//TODO Unit implementation in IngredientView.xaml.cs
Unit = "TODO: Unit implementation in IngredientView.xaml.cs";
Unit = ingredient.Unit;
}
}

@ -5,11 +5,12 @@ namespace ShoopNCook.Views
public partial class RecipeView : ContentView
{
private readonly Action callback;
public RecipeInfo Info { get; private init; }
public RecipeView(RecipeInfo info, Action callback)
{
this.callback = callback;
Info = info;
BindingContext = info;
InitializeComponent();
Note = info.AverageNote;
@ -41,4 +42,3 @@ namespace ShoopNCook.Views
}
}
}

@ -37,11 +37,9 @@
<!-- Email entry -->
<Grid
Grid.Row="1"
RowDefinitions="*, Auto, Auto"
>
RowDefinitions="*, Auto, Auto">
<Label
Grid.Row="0"
FontSize="15"
TextColor="{StaticResource TextColorSecondary}"

@ -8,14 +8,12 @@ public partial class CreateRecipePage : ContentPage
private User owner;
private Action<Recipe> onRecipeCreated;
private IUserNotifier notifier;
public CreateRecipePage(User owner, IUserNotifier notifier, Action<Recipe> onRecipeCreated)
public CreateRecipePage(User owner, Action<Recipe> onRecipeCreated)
{
InitializeComponent();
this.owner = owner;
this.onRecipeCreated = onRecipeCreated;
this.notifier = notifier;
}
private void OnAddIngredientTapped(object sender, TappedEventArgs e)
@ -27,9 +25,9 @@ public partial class CreateRecipePage : ContentPage
{
StepList.Children.Add(new StepEntry((uint) StepList.Children.Count() + 1));
}
private void OnBackButtonClicked(object sender, EventArgs e)
private async void OnBackButtonClicked(object sender, EventArgs e)
{
Navigation.PopAsync();
await Navigation.PopAsync();
}
private void OnUploadRecipeClicked(object sender, EventArgs e)
@ -53,7 +51,7 @@ public partial class CreateRecipePage : ContentPage
if (hadErrors)
{
notifier.Error("You need to fix input errors before upload.");
UserNotifier.Error("You need to fix input errors before upload.");
return;
}
@ -64,7 +62,11 @@ public partial class CreateRecipePage : ContentPage
;
foreach (IngredientEntry entry in IngredientList.Children)
builder.AddIngredient(entry.MakeValue());
{
var ingredient = entry.MakeValue();
if (ingredient == null) return;
builder.AddIngredient(ingredient);
}
foreach (StepEntry entry in StepList.Children)
builder.AddStep(entry.MakeStep());

@ -1,8 +1,7 @@
using Models;
namespace ShoopNCook.Pages;
using Endpoint;
using Services;
using Models;
using ShoopNCook.Views;
@ -10,14 +9,12 @@ public partial class FavoritesPage : ContentPage
{
private readonly Account account;
private readonly IUserNotifier notifier;
private IRecipesService service;
public FavoritesPage(Account account, IUserNotifier notifier, IRecipesService service)
public FavoritesPage(Account account, IRecipesService service)
{
InitializeComponent();
this.account = account;
this.notifier = notifier;
this.service = service;
UpdateFavorites();
@ -25,15 +22,14 @@ public partial class FavoritesPage : ContentPage
private void UpdateFavorites()
{
IAccountRecipesPreferences preferences = service.GetPreferencesOf(account);
IAccountRecipesPreferencesService preferences = service.GetPreferencesOf(account);
RecipeViewLayout.Children.Clear();
preferences.GetFavorites().ForEach(info =>
{
RecipeViewLayout.Children.Add(new RecipeView(info, () =>
RecipeViewLayout.Children.Add(new RecipeView(info, async () =>
{
Recipe recipe = service.GetRecipe(info);
Shell.Current.Navigation.PushAsync(new RecipePage(recipe, notifier, preferences, 1));
await Shell.Current.Navigation.PushAsync(new RecipePage(recipe, preferences, 1));
}));
});
}

@ -20,7 +20,7 @@
BackgroundColor="{StaticResource BackgroundSecondary}"
StrokeShape="RoundRectangle 1500">
<ImageButton
x:Name="ProfilePictureImage"
Source="{Binding ProfilePicture}"
WidthRequest="65"
HeightRequest="65"/>
</Border>
@ -40,7 +40,8 @@
<Entry
Style="{StaticResource UserInput}"
Grid.Column="0"
Placeholder="Search here..."/>
Placeholder="Search here..."
x:Name="SearchPrompt"/>
</Border>
<ImageButton

@ -2,40 +2,47 @@
namespace ShoopNCook.Pages;
using Models;
using ShoopNCook.Views;
using Endpoint;
using Services;
public partial class HomePage : ContentPage
{
public HomePage(Account account, IUserNotifier notifier, IEndpoint endpoint)
private readonly IRecipesService recipes;
private readonly IAccountRecipesPreferencesService preferences;
public HomePage(Account account, IEndpoint endpoint)
{
InitializeComponent();
BindingContext = account;
IRecipesService service = endpoint.RecipesService;
IAccountRecipesPreferences preferences = service.GetPreferencesOf(account);
BindingContext = account.User;
recipes = endpoint.RecipesService;
preferences = recipes.GetPreferencesOf(account);
void PushRecipe(Layout layout, RecipeInfo info)
{
layout.Children.Add(new RecipeView(info, () =>
{
Recipe recipe = service.GetRecipe(info);
Shell.Current.Navigation.PushAsync(new RecipePage(recipe, notifier, preferences, 1));
Recipe recipe = recipes.GetRecipe(info);
if (recipe != null)
Shell.Current.Navigation.PushAsync(new RecipePage(recipe, preferences, 1));
else
{
UserNotifier.Error("Could not find recipe");
}
}));
}
service.PopularRecipes().ForEach(recipe => PushRecipe(PopularsList, recipe));
recipes.PopularRecipes().ForEach(recipe => PushRecipe(PopularsList, recipe));
preferences.GetRecommendedRecipes().ForEach(recipe => PushRecipe(RecommendedList, recipe));
ProfilePictureImage.Source = ImageSource.FromUri(account.User.ProfilePicture);
}
private void OnSyncButtonClicked(object sender, EventArgs e)
private async void OnSyncButtonClicked(object sender, EventArgs e)
{
Shell.Current.Navigation.PushAsync(new SearchPage());
string prompt = SearchPrompt.Text;
if (string.IsNullOrEmpty(prompt))
return;
var searchPage = new SearchPage(recipes, preferences);
await Shell.Current.Navigation.PushAsync(searchPage);
searchPage.MakeSearch(SearchPrompt.Text);
}
}

@ -1,4 +1,4 @@
using Endpoint;
using Services;
using ShoopNCook.Controllers;
namespace ShoopNCook.Pages;
@ -11,7 +11,7 @@ public partial class LoginPage : ContentPage
InitializeComponent();
this.controller = controller;
}
private async void OnLoginButtonClicked(object sender, EventArgs e)
private void OnLoginButtonClicked(object sender, EventArgs e)
{
string email = EmailEntry.Text;
string password = PasswordEntry.Text;

@ -1,5 +1,5 @@
using Endpoint;
using LocalEndpoint;
using Services;
using Services;
using Models;
using ShoopNCook.Views;
@ -8,16 +8,14 @@ namespace ShoopNCook.Pages;
public partial class MyListPage : ContentPage
{
private readonly IAccountRecipesPreferences preferences;
private readonly IUserNotifier notifier;
private readonly IAccountRecipesPreferencesService preferences;
private readonly IRecipesService service;
public MyListPage(Account account, IUserNotifier notifier, IRecipesService service)
public MyListPage(Account account, IRecipesService service)
{
InitializeComponent();
this.preferences = service.GetPreferencesOf(account);
this.notifier = notifier;
this.service = service;
UpdateMyList();
@ -32,12 +30,12 @@ public partial class MyListPage : ContentPage
RecipesLayout.Children.Add(new StoredRecipeView(info, tuple.Item2, amount =>
{
Recipe recipe = service.GetRecipe(info);
Shell.Current.Navigation.PushAsync(new RecipePage(recipe, notifier, preferences, amount));
Shell.Current.Navigation.PushAsync(new RecipePage(recipe, preferences, amount));
}));
});
}
private void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e)
private async void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e)
{
UpdateMyList();
}

@ -1,5 +1,5 @@
using Endpoint;
using LocalEndpoint;
using Services;
using Services;
using Models;
using ShoopNCook.Views;
@ -8,18 +8,15 @@ namespace ShoopNCook.Pages;
public partial class MyRecipesPage : ContentPage
{
private IUserNotifier notifier;
private IRecipesService service;
private Account account;
public MyRecipesPage(
Account account,
IRecipesService service,
IUserNotifier notifier)
IRecipesService service)
{
InitializeComponent();
this.notifier = notifier;
this.service = service;
this.account = account;
@ -34,8 +31,8 @@ public partial class MyRecipesPage : ContentPage
RecipesLayout.Children.Add(new OwnedRecipeView(info, () =>
{
Recipe recipe = service.GetRecipe(info);
IAccountRecipesPreferences preferences = service.GetPreferencesOf(account);
Shell.Current.Navigation.PushAsync(new RecipePage(recipe, notifier, preferences, 1));
IAccountRecipesPreferencesService preferences = service.GetPreferencesOf(account);
Shell.Current.Navigation.PushAsync(new RecipePage(recipe, preferences, 1));
},
() => RemoveRecipe(info)
));
@ -43,11 +40,11 @@ public partial class MyRecipesPage : ContentPage
private void RemoveRecipe(RecipeInfo info)
{
IAccountOwnedRecipes recipes = service.GetRecipesOf(account);
IAccountOwnedRecipesService recipes = service.GetRecipesOf(account);
if (!recipes.RemoveRecipe(info))
{
notifier.Error("Could not remove recipe");
UserNotifier.Error("Could not remove recipe");
return;
}
foreach (OwnedRecipeView view in RecipesLayout.Children)
@ -58,25 +55,25 @@ public partial class MyRecipesPage : ContentPage
break;
}
}
notifier.Success("Recipe successfully removed");
UserNotifier.Success("Recipe successfully removed");
}
private void OnBackButtonClicked(object sender, EventArgs e)
private async void OnBackButtonClicked(object sender, EventArgs e)
{
Navigation.PopAsync();
await Navigation.PopAsync();
}
private void OnAddRecipeButtonClicked(object sender, EventArgs e)
private async void OnAddRecipeButtonClicked(object sender, EventArgs e)
{
IAccountOwnedRecipes recipes = service.GetRecipesOf(account);
IAccountOwnedRecipesService recipes = service.GetRecipesOf(account);
var page = new CreateRecipePage(account.User, notifier, recipe =>
var page = new CreateRecipePage(account.User, recipe =>
{
if (!recipes.UploadRecipe(recipe))
{
notifier.Error("Could not upload recipe.");
UserNotifier.Error("Could not upload recipe.");
return;
}
notifier.Success("Recipe Successfuly uploaded !");
UserNotifier.Success("Recipe Successfuly uploaded !");
AddRecipeView(recipe.Info);
Shell.Current.Navigation.PopAsync(); //go back to current recipe page.
});

@ -154,6 +154,7 @@
Grid.Column="2"
Style="{StaticResource UserButton}"
BackgroundColor="Gray"
Text="Add To Weekly List"
x:Name="MyListStateButton"
Clicked="OnFooterButtonClicked"/>

@ -1,26 +1,23 @@
using ShoopNCook.Views;
using System.Windows.Input;
using Models;
using LocalEndpoint;
using Endpoint;
using Services;
namespace ShoopNCook.Pages;
public partial class RecipePage : ContentPage
{
private readonly IAccountRecipesPreferences preferences;
private readonly IUserNotifier notifier;
private readonly IAccountRecipesPreferencesService preferences;
private uint note;
private bool isFavorite;
private bool isInMyList;
public Recipe Recipe { get; init; }
public ICommand StarCommand => new Command<string>(count => SetNote(uint.Parse(count)));
public RecipePage(Recipe recipe, IUserNotifier notifier, IAccountRecipesPreferences preferences, uint amount)
public RecipePage(Recipe recipe, IAccountRecipesPreferencesService preferences, uint amount)
{
Recipe = recipe;
@ -29,7 +26,6 @@ public partial class RecipePage : ContentPage
InitializeComponent();
this.preferences = preferences;
this.notifier = notifier;
RecipeRate rate = preferences.GetRate(recipe.Info);
@ -38,7 +34,6 @@ public partial class RecipePage : ContentPage
SetFavorite(isFavorite);
SetNote(note);
SetIsInMyListState(preferences.IsInWeeklyList(recipe.Info));
Counter.Count = amount;
@ -49,7 +44,8 @@ public partial class RecipePage : ContentPage
var styles = Application.Current.Resources.MergedDictionaries.ElementAt(1);
int count = 0;
foreach (PreparationStep step in recipe.Steps) {
foreach (PreparationStep step in recipe.Steps)
{
//TODO display name of PreparationSteps.
Label label = new Label();
label.Style = (Style)styles["Small"];
@ -58,19 +54,6 @@ public partial class RecipePage : ContentPage
}
}
private void SetIsInMyListState(bool isInMyList)
{
this.isInMyList = isInMyList;
if (isInMyList)
{
MyListStateButton.Text = "Remove From My List";
}
else
{
MyListStateButton.Text = "Add To My List";
}
}
private void SetNote(uint note)
{
this.note = note;
@ -99,40 +82,20 @@ public partial class RecipePage : ContentPage
private void OnSubmitReviewClicked(object o, EventArgs e)
{
preferences.SetReviewScore(Recipe.Info, note);
notifier.Success("Your review has been successfuly submited");
UserNotifier.Success("Your review has been successfuly submited");
}
private void OnFooterButtonClicked(object o, EventArgs e)
{
SetIsInMyListState(!isInMyList);
if (isInMyList)
RemoveFromMyList();
else
AddToMyList();
}
private void RemoveFromMyList()
{
if (!preferences.RemoveFromWeeklyList(Recipe.Info))
{
notifier.Notice("This recipe does not figures in your personnal list");
}
else
{
notifier.Success("Recipe added to your weekly list.");
}
}
private void AddToMyList()
{
if (!preferences.AddToWeeklyList(Recipe.Info, Counter.Count))
{
notifier.Notice("You already added this recipe to you weekly list!");
}
UserNotifier.Notice("You already added this recipe to you weekly list!");
else
{
notifier.Success("Recipe added to your weekly list.");
}
UserNotifier.Success("Recipe added to your weekly list.");
}
private void SetFavorite(bool isFavorite)
@ -147,5 +110,4 @@ public partial class RecipePage : ContentPage
{
Navigation.PopAsync();
}
}

@ -14,7 +14,7 @@ public partial class RegisterPage : ContentPage
{
await Shell.Current.GoToAsync("//Login");
}
private void RegisterTapped(object sender, EventArgs e)
private async void RegisterTapped(object sender, EventArgs e)
{
string email = EmailEntry.Text;
string password = PasswordEntry.Text;

@ -49,18 +49,28 @@
Style="{StaticResource SecondaryBorderShadow}">
<Entry
Style="{StaticResource UserInput}"
Placeholder="Cake, Lasagna, Vegetarian..."/>
Placeholder="Cake, Lasagna, Vegetarian..."
x:Name="SearchPrompt"/>
</Border>
<Border
Style="{StaticResource SecondaryBorderShadow}"
Grid.Column="1"
BackgroundColor="{StaticResource ActionButton}"
Stroke="{StaticResource ActionButton}">
<ImageButton
Source="search_options.svg"
<Label
BackgroundColor="Transparent"
Text="GO"
FontFamily="PoppinsMedium"
TextColor="White"
VerticalTextAlignment="Center"
HorizontalTextAlignment="Center"
HeightRequest="40"
WidthRequest="40"/>
WidthRequest="40">
<Label.GestureRecognizers>
<TapGestureRecognizer
Tapped="OnSearchClicked"/>
</Label.GestureRecognizers>
</Label>
</Border>
</Grid>
@ -69,34 +79,10 @@
<!-- Selection result count -->
<Label
Grid.Row="2"
Text="%Nb_Recipes% Recipes Found"
Text="{Binding FoundRecipes.Count, StringFormat='{0} Recipes found'}"
TextColor="{StaticResource TextColorSecondary}"
FontSize="20"/>
<!-- Sort selection -->
<Grid
Grid.Row="3"
ColumnSpacing="10"
ColumnDefinitions="*, *">
<Button
Grid.Column="0"
Text="Most Relevent"
Style="{StaticResource UserButton}"
TextColor="{StaticResource White}"
BackgroundColor="{StaticResource Selected}">
</Button>
<Button
Grid.Column="1"
Text="Most Recent"
Style="{StaticResource UserButton}"
TextColor="{StaticResource TextColorSecondary}"
BackgroundColor="{StaticResource BackgroundSecondary}">
</Button>
</Grid>
<!-- Search result items -->
<ScrollView
Grid.Row="4">
@ -106,9 +92,22 @@
AlignContent="Start"
Direction="Row"
Wrap="Wrap"
x:Name="ResultSearchView">
BindableLayout.ItemsSource="{Binding FoundRecipes}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<ContentView Content="{Binding}" />
</DataTemplate>
</BindableLayout.ItemTemplate>
<BindableLayout.EmptyViewTemplate>
<DataTemplate>
<Label
Style="{StaticResource h1}"
Text="No recipes found"/>
</DataTemplate>
</BindableLayout.EmptyViewTemplate>
</FlexLayout>
</ScrollView>
</Grid>

@ -1,4 +1,3 @@
using Microsoft.Maui.Storage;
using Models;
using Services;
using ShoopNCook.Views;
@ -10,7 +9,6 @@ public partial class SearchPage : ContentPage
private readonly IRecipesService recipesService;
private readonly IAccountRecipesPreferencesService preferences;
public ObservableCollection<RecipeView> FoundRecipes { get; private init; } = new ObservableCollection<RecipeView>();
public SearchPage(IRecipesService recipes, IAccountRecipesPreferencesService preferences)
@ -27,6 +25,7 @@ public partial class SearchPage : ContentPage
{
return;
}
SearchPrompt.Text = prompt;
FoundRecipes.Clear();
foreach (RecipeInfo info in recipesService.SearchRecipes(prompt))
{
@ -46,15 +45,8 @@ public partial class SearchPage : ContentPage
await Navigation.PopAsync();
}
private void OnSortByRateClicked(object sender, EventArgs e)
{
FoundRecipes.OrderBy(view => view.Info)
}
private void OnSearchClicked(object sender, EventArgs e)
{
MakeSearch(SearchPrompt.Text);
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Loading…
Cancel
Save