diff --git a/blocks/ORB/View.pm b/blocks/ORB/View.pm
new file mode 100644
index 0000000..2019f1b
--- /dev/null
+++ b/blocks/ORB/View.pm
@@ -0,0 +1,232 @@
+## @file
+# This file contains the implementation of the view page.
+#
+# @author Chris Page <chris@starforge.co.uk>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see http://www.gnu.org/licenses/.
+
+## @class
+package ORB::View;
+
+use strict;
+use parent qw(ORB); # This class extends the ORB block class
+use experimental qw(smartmatch);
+use Regexp::Common qw(URI);
+use v5.14;
+
+
+## @method private $ _generate_ingredients($ingreds)
+# Given an array of ingredients, convert them to a list of ingredients
+# to show in the recipe.
+#
+# @param ingreds A reference to an array of ingredient hashes.
+# @return A string containing the convered ingredient list.
+sub _generate_ingredients {
+ my $self = shift;
+ my $ingreds = shift;
+
+ my @result;
+ foreach my $ingred (@{$ingreds}) {
+ if($ingred -> {"separator"}) {
+ push(@result, $self -> {"template"} -> load_template("view/separator.tem",
+ {
+ "%(separator)s" => $ingred -> {"separator"}
+ }));
+ } else {
+ my $units = $ingred -> {"units"} eq "None" ? "" : $ingred -> {"units"};
+ my $quantity = $ingred -> {"quantity"} ? $ingred -> {"quantity"} : "";
+
+ push(@result, $self -> {"template"} -> load_template("view/ingredient.tem",
+ {
+ "%(quantity)s" => $quantity,
+ "%(units)s" => $units,
+ "%(prepmethod)s" => $ingred -> {"prepmethod"},
+ "%(ingredient)s" => $ingred -> {"ingredient"},
+ "%(notes)s" => $ingred -> {"notes"} ? "(".$ingred -> {"notes"}.")" : "",
+ }));
+ }
+ }
+
+ return join("\n", @result);
+}
+
+
+## @method private $ _generate_tags($tags)
+# Convert a list of tags into a string that can be shown in the recipe
+# page.
+#
+# @param tags A reference to a list of tag names.
+# @return A string containing the tag list.
+sub _generate_tags {
+ my $self = shift;
+ my $tags = shift;
+
+ my @result;
+ foreach my $tag (@{$tags}) {
+ push(@result, $self -> {"template"} -> load_template("view/tag.tem",
+ {
+ "%(name)s" => $tag -> {"name"},
+ "%(color)s" => $tag -> {"color"},
+ "%(bgcol)s" => $tag -> {"background"},
+ "%(faicon)s" => $tag -> {"fa-icon"}
+ }));
+ }
+
+ return join("", @result);
+}
+
+
+## @method private $ _convert_source($source)
+# Replace all URLs in the specified source string with clickable links.
+#
+# @param source The source string to replace URLs in.
+# @return The processed source string.
+sub _convert_source {
+ my $self = shift;
+ my $source = shift;
+
+ my $match = $RE{URI}{HTTP}{-scheme => qr(https?)};
+ $source =~ s|($match)|$1 |gi;
+
+ return $source;
+}
+
+
+## @method private $ _generate_view($rid)
+# Generate a page containing the recipe identified by the specified ID.
+#
+# @param rid The ID of the recipe to fetch the data for
+# @return An array containing the page title, content, extra header data, and
+# extra javascript content.
+sub _generate_view {
+ my $self = shift;
+ my $rid = shift;
+
+ # Try to fetch the data.
+ my $recipe = $self -> {"system"} -> {"recipe"} -> get_recipe($rid);
+
+ # Stop here if there's no recipe data available...
+ return $self -> _fatal_error("{L_VIEW_ERROR_NORECIPE}")
+ unless($recipe && $recipe -> {"id"});
+
+ # convert various bits to blocks of HTML
+ my $ingreds = $self -> _generate_ingredients($recipe -> {"ingredients"});
+ my $tags = $self -> _generate_tags($recipe -> {"tags"});
+ my $source = $self -> _convert_source($recipe -> {"source"});
+ my $title = $recipe -> {"name"};
+
+ my $state = $self -> check_permission('recipe.edit') ? "enabled" : "disabled";
+ my $controls = $self -> {"template"} -> load_template("view/controls-$state.tem",
+ {
+ "%(url-edit)s" => $self -> build_url(block => "edit",
+ pathinfo => [ $recipe -> {"id"} ]),
+ "%(url-delete)s" => $self -> build_url(block => "delete",
+ pathinfo => [ $recipe -> {"id"} ]),
+ });
+
+ # and build the page itself
+ my $body = $self -> {"template"} -> load_template("view/content.tem",
+ {
+ "%(name)s" => $title,
+ "%(source)s" => $source,
+ "%(yield)s" => $recipe -> {"yield"},
+ "%(timereq)s" => $recipe -> {"timereq"},
+ "%(timemins)s" => $self -> {"template"} -> humanise_seconds($recipe -> {"timemins"} * 60),
+ "%(temp)s" => $recipe -> {"temp"} ? $recipe -> {"temp"} : "",
+ "%(temptype)s" => $recipe -> {"temptype"} // "",
+ "%(type)s" => $recipe -> {"type"},
+ "%(status)s" => $recipe -> {"status"},
+ "%(tags)s" => $tags,
+ "%(ingredients)s" => $ingreds,
+ "%(method)s" => $recipe -> {"method"},
+ "%(notes)s" => $recipe -> {"notes"},
+ "%(controls)s" => $controls
+ });
+
+ return ($title,
+ $body,
+ $self -> {"template"} -> load_template("view/extrahead.tem"),
+ "");
+}
+
+
+# ============================================================================
+# UI handler/dispatcher functions
+
+## @method private @ _fatal_error($error)
+# Generate the tile and content for an error page.
+#
+# @param error A string containing the error message to display
+# @return The title of the error page and an error message to place in the page.
+sub _fatal_error {
+ my $self = shift;
+ my $error = shift;
+
+ return ("{L_VIEW_ERROR_FATAL}",
+ $self -> {"template"} -> load_template("error/page_error.tem",
+ { "%(message)s" => $error,
+ "%(url-logout)s" => $self -> build_url(block => "login", pathinfo => ["signout"])
+ })
+ );
+}
+
+
+## @method private $ _dispatch_ui()
+# Implements the core behaviour dispatcher for non-api functions. This will
+# inspect the state of the pathinfo and invoke the appropriate handler
+# function to generate content for the user.
+#
+# @return A string containing the page HTML.
+sub _dispatch_ui {
+ my $self = shift;
+
+ # We need to determine what the page title should be, and the content to shove in it...
+ my @pathinfo = $self -> {"cgi"} -> multi_param("pathinfo");
+ my ($title, $body, $extrahead, $extrajs) = $self -> _generate_view($pathinfo[0]);
+
+ # Done generating the page content, return the filled in page template
+ return $self -> generate_orb_page(title => $title,
+ content => $body,
+ extrahead => $extrahead,
+ extrajs => $extrajs,
+ active => substr($title, 0, 1),
+ doclink => 'summary');
+}
+
+
+# ============================================================================
+# Module interface functions
+
+## @method $ page_display()
+# Generate the page content for this module.
+sub page_display {
+ my $self = shift;
+
+ # Is this an API call, or a normal page operation?
+ my $apiop = $self -> is_api_operation();
+ if(defined($apiop)) {
+ # API call - dispatch to appropriate handler.
+ given($apiop) {
+ default {
+ return $self -> api_response($self -> api_errorhash('bad_op',
+ $self -> {"template"} -> replace_langvar("API_BAD_OP")))
+ }
+ }
+ } else {
+ return $self -> _dispatch_ui();
+ }
+}
+
+
+1;
\ No newline at end of file
diff --git a/lang/en/view.lang b/lang/en/view.lang
new file mode 100644
index 0000000..6d9500a
--- /dev/null
+++ b/lang/en/view.lang
@@ -0,0 +1,20 @@
+VIEW_NAME = Name
+VIEW_SOURCE = Source
+VIEW_YIELD = Yield
+VIEW_PREPINFO = Prep info
+VIEW_TIMEREQ = Time required
+VIEW_OVENTEMP = Oven preheat
+VIEW_TYPE = Type
+VIEW_STATUS = Status
+VIEW_TAGS = Tags
+
+VIEW_INGREDIENTS = Ingredients
+VIEW_METHOD = Method
+VIEW_NOTES = Notes
+
+VIEW_EDIT = Edit
+VIEW_CLONE = Clone
+VIEW_DELETE = Delete
+
+VIEW_ERROR_FATAL = View error
+VIEW_ERROR_NORECIPE = No matching recipe found
\ No newline at end of file
diff --git a/modules/ORB/System/Recipe.pm b/modules/ORB/System/Recipe.pm
index ba0c175..7b44cf3 100644
--- a/modules/ORB/System/Recipe.pm
+++ b/modules/ORB/System/Recipe.pm
@@ -324,7 +324,7 @@ sub get_recipe_list {
my @wherefrag = ();
# Get the status IDs for excluded states
- my $states = $slef -> _convert_states($exlstates);
+ my $states = $self -> _convert_states($exlstates);
# And add them to the query
if(scalar(@{$states})) {
@@ -508,8 +508,8 @@ sub find {
$args -> {"tagids"} = $self -> _hashlist_to_list($tags, "id");
# Find should always exclude deleted and edited recipes
- my $exclstates = $self -> _convert_state($self -> {"settings"} -> {"config"} -> {"Recipe:status:edited"} // "Edited",
- $self -> {"settings"} -> {"config"} -> {"Recipe:status:deleted"} // "Deleted");
+ my $exclstates = $self -> _convert_states($self -> {"settings"} -> {"config"} -> {"Recipe:status:edited"} // "Edited",
+ $self -> {"settings"} -> {"config"} -> {"Recipe:status:deleted"} // "Deleted");
# Fix up default matching modes
$args -> {"ingredmatch"} = "all" unless($args -> {"ingredmatch"} && $args -> {"ingredmatch"} eq "any");
@@ -750,11 +750,18 @@ sub _get_ingredients {
$self -> clear_error();
- my $ingh = $self -> {"dbh"} -> prepare("SELECT `ri`.*, `i`.`name`
+ my $ingh = $self -> {"dbh"} -> prepare("SELECT `ri`.*,
+ `i`.`name` AS `ingredient`,
+ `p`.`name` AS `prepmethod`,
+ `u`.`name` AS `units`
FROM `".$self -> {"settings"} -> {"database"} -> {"recipeing"}."` AS `ri`
- `".$self -> {"settings"} -> {"database"} -> {"ingredients"}."` AS `i`
- WHERE `i`.`id` = `ri`.`ingred_id`
- AND `ri`.`recipe_id` = ?
+ LEFT JOIN `".$self -> {"settings"} -> {"database"} -> {"ingredients"}."` AS `i`
+ ON `i`.`id` = `ri`.`ingred_id`
+ LEFT JOIN `".$self -> {"settings"} -> {"database"} -> {"prep"}."` AS `p`
+ ON `p`.`id` = `ri`.`prep_id`
+ LEFT JOIN `".$self -> {"settings"} -> {"database"} -> {"units"}."` AS `u`
+ ON `u`.`id` = `ri`.`unit_id`
+ WHERE `ri`.`recipe_id` = ?
ORDER BY `ri`.`position`");
$ingh -> execute($recipeid)
or return $self -> self_error("Ingredient lookup for '$recipeid' failed: ".$self -> {"dbh"} -> errstr());
diff --git a/templates/default/css/clear-input.css b/templates/default/css/clear-input.css
new file mode 100644
index 0000000..089f439
--- /dev/null
+++ b/templates/default/css/clear-input.css
@@ -0,0 +1,24 @@
+.clear-holder{
+ position:relative;
+ float:left;
+ width: 100%;
+}
+.clear-helper{
+ margin-top: 4px;
+ margin-right: 4px;
+ text-align: center;
+ position: absolute;
+ right: 4px;
+ height: 2rem;
+ width: 2rem;
+ cursor: pointer;
+ display:none;
+ background:#e0e0e0;
+ border-radius:2px;
+ line-height: 2rem;
+}
+
+/* button will be misplaced if input is not inline */
+.clear-holder > input {
+ display: inline-block;
+}
\ No newline at end of file
diff --git a/templates/default/css/foundation.mods.css b/templates/default/css/foundation.mods.css
index 6acc0d3..efbaf60 100755
--- a/templates/default/css/foundation.mods.css
+++ b/templates/default/css/foundation.mods.css
@@ -25,6 +25,7 @@ div.top-bar-title {
/* fix menu button pad */
#menubtn {
margin-right: 0.5rem;
+ margin-top: 0.5rem;
}
/* fix padding for menu text with image */
@@ -70,4 +71,4 @@ div.off-canvas .user {
.pagemenu > li.active > a {
background: #1779ba;
color: #fefefe !important;
-}
\ No newline at end of file
+}
diff --git a/templates/default/css/login.css b/templates/default/css/login.css
index cdcd95b..596ed62 100755
--- a/templates/default/css/login.css
+++ b/templates/default/css/login.css
@@ -2,3 +2,7 @@
div.contextlink {
font-size: 0.75rem;
}
+
+div.topspace {
+ margin-top: 0.5rem;
+}
diff --git a/templates/default/css/recipelist.css b/templates/default/css/recipelist.css
index f13f14b..ee23a15 100644
--- a/templates/default/css/recipelist.css
+++ b/templates/default/css/recipelist.css
@@ -1,5 +1,5 @@
li.recipe {
- padding: 3px;
+ padding: 5px 3px 3px 3px;
border-bottom: 1px solid #e7e7e7;
}
@@ -53,4 +53,8 @@ span.ljs-tag {
margin-right: .3rem;
border-radius: 0;
white-space: nowrap;
+}
+
+#recipelist div.input-group-button {
+ margin-bottom: 0px;
}
\ No newline at end of file
diff --git a/templates/default/css/view.css b/templates/default/css/view.css
new file mode 100644
index 0000000..eb9f355
--- /dev/null
+++ b/templates/default/css/view.css
@@ -0,0 +1,38 @@
+div.recipe {
+ margin: 0px 1.5rem;
+}
+
+div.recipe h4 {
+ border-bottom: 1px solid #888;
+}
+
+div.title {
+ font-weight: bold;
+}
+
+div.ingredients ul {
+ margin: 0px;
+ padding: 0px;
+}
+
+div.ingredients ul li {
+ list-style: none;
+}
+
+div.ingredients ul li.ingredient {
+ margin-left: 1rem;
+}
+
+div.ingredients ul li.separator {
+ font-weight: bold;
+ border-bottom: 1px solid #ccc;
+}
+
+.tag {
+ font-size: 0.7rem;
+ display: inline-block;
+ padding: .33333rem .5rem;
+ margin-right: .3rem;
+ border-radius: 0;
+ white-space: nowrap;
+}
\ No newline at end of file
diff --git a/templates/default/error/page_error.tem b/templates/default/error/page_error.tem
index 98499da..814d305 100755
--- a/templates/default/error/page_error.tem
+++ b/templates/default/error/page_error.tem
@@ -1,4 +1,4 @@
-
+
diff --git a/templates/default/js/ckeditor-config.js b/templates/default/js/ckeditor-config.js
new file mode 100755
index 0000000..2b503db
--- /dev/null
+++ b/templates/default/js/ckeditor-config.js
@@ -0,0 +1,25 @@
+CKEDITOR.editorConfig = function( config ) {
+ config.toolbar = [
+ { name: 'document', items: [ 'Source', 'Maximize', 'ShowBlocks' ] },
+ { name: 'clipboard', items: [ 'Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo' ] },
+ { name: 'editing', items: [ 'Find', 'Replace' ] },
+
+ { name: 'paragraph', items: [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock' ] },
+ { name: 'links', items: [ 'Link', 'Unlink', 'Anchor', 'Image', 'Table', 'HorizontalRule', 'Smiley', 'SpecialChar' ] },
+ '/',
+ { name: 'styles', items: [ 'Styles', 'Format', 'Font', 'FontSize' ] },
+ { name: 'basicstyles', items: [ 'Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'CopyFormatting', 'RemoveFormat' ] },
+ { name: 'colors', items: [ 'TextColor', 'BGColor' ] }
+ ];
+ config.font_names = 'Arial;Book Antiqua;Courier New; Tahoma;Times New Roman;Trebuchet MS;Verdana';
+};
+
+CKEDITOR.config.font_names = 'Arial/Arial, Helvetica, sans-serif;' +
+ 'Book Antiqua/Book Antiqua, cursive;' +
+ 'Courier New/Courier New, Courier, monospace;' +
+ 'Georgia/Georgia, serif;' +
+ 'Lucida Sans Unicode/Lucida Sans Unicode, Lucida Grande, sans-serif;' +
+ 'Tahoma/Tahoma, Geneva, sans-serif;' +
+ 'Times New Roman/Times New Roman, Times, serif;' +
+ 'Trebuchet MS/Trebuchet MS, Helvetica, sans-serif;' +
+ 'Verdana/Verdana, Geneva, sans-serif';
diff --git a/templates/default/js/clear-input.js b/templates/default/js/clear-input.js
new file mode 100644
index 0000000..06a09cd
--- /dev/null
+++ b/templates/default/js/clear-input.js
@@ -0,0 +1,21 @@
+/* Based on suggestions from Kroehre and Johannes in the following
+ * stackoverflow: http://stackoverflow.com/questions/5917776/clear-search-box-on-the-click-of-a-little-x-inside-of-it
+ */
+(function ($, undefined) {
+ $.fn.clearable = function () {
+ var $this = this;
+ $this.wrap('
');
+ var helper = $(' ');
+ $this.parent().append(helper);
+ $this.parent().on('keyup', function() {
+ if($this.val()) {
+ helper.css('display', 'inline-block');
+ } else helper.hide();
+ });
+ helper.click(function(){
+ $this.val("");
+ $this.trigger("keyup");
+ helper.hide();
+ });
+ };
+})(jQuery);
diff --git a/templates/default/js/recipe_list.js b/templates/default/js/recipe_list.js
index 4808066..7aa1656 100644
--- a/templates/default/js/recipe_list.js
+++ b/templates/default/js/recipe_list.js
@@ -7,4 +7,6 @@ $(function() {
var recipeList = new List('recipelist', options);
$('#listfilter').delaysearch(recipeList);
+ $('#listfilter').clearable();
+
});
\ No newline at end of file
diff --git a/templates/default/list/content.tem b/templates/default/list/content.tem
index e660586..e8dfba3 100644
--- a/templates/default/list/content.tem
+++ b/templates/default/list/content.tem
@@ -1,11 +1,4 @@
-%(pagemenu)s
-
-
-
-
+
%(controls)s
diff --git a/templates/default/page.tem b/templates/default/page.tem
index 910fb6b..8e11263 100755
--- a/templates/default/page.tem
+++ b/templates/default/page.tem
@@ -6,11 +6,11 @@
%(title)s
-
+
%(extrahead)s
@@ -22,13 +22,14 @@
%(userbar)s
-
-
+
+%(pagemenu)s
+
%(content)s
-
+
@@ -36,7 +37,9 @@
+
+
diff --git a/templates/default/summary/content.tem b/templates/default/summary/content.tem
index 0f7c568..63a6aa0 100644
--- a/templates/default/summary/content.tem
+++ b/templates/default/summary/content.tem
@@ -1,11 +1,10 @@
-%(pagemenu)s
-
+
{L_SUMMARY_ADDED}
- {L_SUMMARY_NAME}
{L_SUMMARY_TYPE}
+ {L_SUMMARY_NAME}
@@ -14,13 +13,13 @@
-
+
{L_SUMMARY_VIEWED}
- {L_SUMMARY_NAME}
{L_SUMMARY_TYPE}
+ {L_SUMMARY_NAME}
@@ -29,13 +28,13 @@
-
+
{L_SUMMARY_UPDATED}
- {L_SUMMARY_NAME}
{L_SUMMARY_TYPE}
+ {L_SUMMARY_NAME}
diff --git a/templates/default/summary/row.tem b/templates/default/summary/row.tem
index 8dd6f74..a783e8c 100644
--- a/templates/default/summary/row.tem
+++ b/templates/default/summary/row.tem
@@ -1,4 +1,4 @@
- %(name)s
%(type)s
+ %(name)s
diff --git a/templates/default/view/content.tem b/templates/default/view/content.tem
new file mode 100644
index 0000000..aabb1dd
--- /dev/null
+++ b/templates/default/view/content.tem
@@ -0,0 +1,52 @@
+
+
+
%(name)s
+
+
{L_VIEW_SOURCE}
+
%(source)s
+
+
+
{L_VIEW_YIELD}
+
%(yield)s
+
+
+
{L_VIEW_PREPINFO}
+
%(timereq)s
+
+
+
{L_VIEW_TIMEREQ}
+
%(timemins)s
+
+
+
{L_VIEW_OVENTEMP}
+
%(temp)s%(temptype)s
+
+
+
{L_VIEW_TYPE}
+
%(type)s
+
+
+
{L_VIEW_STATUS}
+
%(status)s
+
+
+
{L_VIEW_TAGS}
+
%(tags)s
+
+
+
+
{L_VIEW_INGREDIENTS}
+
+
+
+
{L_VIEW_METHOD}
+
%(method)s
+
+
+
{L_VIEW_NOTES}
+
%(notes)s
+
+%(controls)s
+
diff --git a/templates/default/view/controls-disabled.tem b/templates/default/view/controls-disabled.tem
new file mode 100644
index 0000000..86fef0f
--- /dev/null
+++ b/templates/default/view/controls-disabled.tem
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/templates/default/view/controls-enabled.tem b/templates/default/view/controls-enabled.tem
new file mode 100644
index 0000000..97f3f7c
--- /dev/null
+++ b/templates/default/view/controls-enabled.tem
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/templates/default/view/extrahead.tem b/templates/default/view/extrahead.tem
new file mode 100644
index 0000000..0f34614
--- /dev/null
+++ b/templates/default/view/extrahead.tem
@@ -0,0 +1 @@
+
diff --git a/templates/default/view/ingredient.tem b/templates/default/view/ingredient.tem
new file mode 100644
index 0000000..6bf0821
--- /dev/null
+++ b/templates/default/view/ingredient.tem
@@ -0,0 +1 @@
+%(quantity)s %(units)s %(prepmethod)s %(ingredient)s %(notes)s
\ No newline at end of file
diff --git a/templates/default/view/separator.tem b/templates/default/view/separator.tem
new file mode 100644
index 0000000..ef7057b
--- /dev/null
+++ b/templates/default/view/separator.tem
@@ -0,0 +1 @@
+%(separator)s
\ No newline at end of file
diff --git a/templates/default/view/tag.tem b/templates/default/view/tag.tem
new file mode 100644
index 0000000..68b7444
--- /dev/null
+++ b/templates/default/view/tag.tem
@@ -0,0 +1 @@
+ %(name)s
\ No newline at end of file