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 -
- +
diff --git a/templates/default/list/recipe.tem b/templates/default/list/recipe.tem index b8a42f3..14821f9 100644 --- a/templates/default/list/recipe.tem +++ b/templates/default/list/recipe.tem @@ -7,7 +7,7 @@
  •  %(time)s
  • %(temp)s -
    :%(tags)s
    +
    %(tags)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}

    error
    - + @@ -14,13 +13,13 @@
    {L_SUMMARY_NAME} {L_SUMMARY_TYPE}{L_SUMMARY_NAME}
    -
    +

    {L_SUMMARY_VIEWED}

    - + @@ -29,13 +28,13 @@
    {L_SUMMARY_NAME} {L_SUMMARY_TYPE}{L_SUMMARY_NAME}
    -
    +

    {L_SUMMARY_UPDATED}

    - + 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 @@ - + 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}

    +
      + %(ingredients)s +
    +
    +
    +

    {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
    {L_SUMMARY_NAME} {L_SUMMARY_TYPE}{L_SUMMARY_NAME}
    %(name)s %(type)s%(name)s