diff --git a/.gitignore b/.gitignore index d18387f6..045f7ae9 100755 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,13 @@ cookbook.tar.gz .idea/ + +# Built js package +js/* + +### NPM ### + +# Node modules +node_modules +# Package lock +package-lock.json diff --git a/appinfo/info.xml b/appinfo/info.xml index 6623931e..74162d45 100755 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ Cookbook An integrated cookbook using schema.org JSON files as recipes - 0.6.5 + 0.7.0 agpl Jeppe Zapp Cookbook diff --git a/appinfo/routes.php b/appinfo/routes.php index a3327b91..783af79c 100755 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -25,6 +25,7 @@ return [ ['name' => 'recipe#image', 'url' => '/recipes/{id}/image', 'verb' => 'GET', 'requirements' => ['id' => '\d+']], ['name' => 'config#reindex', 'url' => '/reindex', 'verb' => 'POST'], ['name' => 'config#config', 'url' => '/config', 'verb' => 'POST'], + /* API routes */ ], 'resources' => [ 'recipe' => ['url' => '/api/recipes'] diff --git a/css/style.css b/css/style.css index 91c4f917..a4e26e6b 100755 --- a/css/style.css +++ b/css/style.css @@ -1,179 +1,26 @@ -/** - * Helpers - */ - -.icon-arrow-up { - background-image: var(--icon-triangle-n-000); -} - -.icon-arrow-down { - background-image: var(--icon-triangle-s-000); -} - -.input-group { - display: flex; - width: 100%; -} - - .input-group > input { - width: 100%; - margin: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - .input-group > .input-group-addon { - display: flex; - } - - .input-group > .input-group-addon > button { - padding: 0; - margin: 0; - min-width: 34px; - min-height: 34px; - } - - .input-group > .input-group-addon > button { - border-radius: 0; - border-left-color: transparent; - border-right-color: transparent; - } - - .input-group > .input-group-addon > button:last-child { - border-top-right-radius: var(--border-radius); - border-bottom-right-radius: var(--border-radius); - border-right-width: 1px; - } - -.textarea-group { - -} - .textarea-group-addon { - float: right; - display: flex; - position: relative; - top: 1px; - z-index: 1; - } - - .textarea-group-addon::after { - display: table; - content: ''; - clear: both; - } - - .textarea-group-addon button { - min-width: 34px; - min-height: 34px; - border-radius: 0; - margin: 0; - } - - .textarea-group-addon button:first-child { - border-top-left-radius: var(--border-radius); - } - - .textarea-group-addon button:last-child { - border-top-right-radius: var(--border-radius); - } - /** * Main */ -#app { + #app { width: 100%; } -/** - * Navigation - */ -#app-navigation {} - #app-navigation .app-navigation-create { - padding: 10px; - } - - #app-navigation .app-navigation-create .button { - display: inline-block; - width: 100%; - padding: 10px; - padding-left: 34px; - background-position: 10px center; - text-align: left; - margin: 0; - } - - #app-navigation li { - background-position: 10px center; - text-align: left; - margin: 0; - } - - #app-navigation .app-navigation-new { - display: flex !important; - } - #app-navigation .app-navigation-new:not(:first-child) { - padding-top: 0; - } - - #app-navigation #create-recipe > button { - height: 40px !important; - } - - #app-navigation .app-navigation-new input { - border-radius: var(--border-radius) 0 0 var(--border-radius); - line-height: 40px; - height: 40px; - margin: 0; - flex-grow: 1; - border-right-width: 0; - flex-basis: 0; - } - - #app-navigation .app-navigation-new button { - border-radius: 0 var(--border-radius) var(--border-radius) 0; - padding: 0 !important; - width: 40px !important; - height: 40px !important; - background-position: 50% 50%; - } - - /* For some reason, some images would disappear below 30px width if they were displayed inline */ - #app-navigation > ul > li > a:first-child img { - display: block; - float: left; - margin-top: 12px; - } - -/** - * Settings - */ -#app-settings {} - #app-settings .button { - padding: 6px 12px; - padding-left: 12px; - padding-left: 34px; - margin: 0 0 1em 0; - border-radius: var(--border-radius); - background-position: left 9px center; - z-index: 2; - } - - #app-settings input[type="text"], - #app-settings input[type="number"], - #app-settings .button { - width: 100%; - display: block; - } - -/** - * Content - */ -#app-content { - min-width: calc(100% - 300px); +.app-navigation-new button { + min-height: 44px !important; + background-image: var(--icon-add-000) !important; + background-repeat: no-repeat !important; } - -#app-content-wrapper { - flex-wrap: wrap; +.app-navigation-entry *:not(.app-navigation-entry-icon) { + background: initial !important; +} +.app-navigation-entry.recipe { + /* Let's not waste space in front of the recipe if we're only using the icon to show loading */ + padding-left: 0 !important; +} +.app-navigation-entry:hover, +.app-navigation-entry.router-link-exact-active { + opacity: 1 !important; + box-shadow: inset 4px 0 var(--color-primary) !important; } .home { padding: 1rem; @@ -437,122 +284,14 @@ width: 100%; }} - #app-content-wrapper form fieldset > input[name="image"] { - width: calc(100% - 14em); - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-right: 0; - } - @media(max-width:1199px) { #app-content-wrapper form fieldset > input[name="image"] { - width: calc(100% - 3em); - }} + .app-navigation-entry:not(:hover) li.recipe { + box-shadow: inset 4px 0 rgba(255, 255, 255, 0.5); + } + .app-navigation-entry:hover li.recipe { + box-shadow: inset 4px 0 rgba(255, 255, 255, 1); + } - #app-content-wrapper form fieldset > input[name="image"] + button { - border-top-right-radius: var(--border-radius); - border-bottom-right-radius: var(--border-radius); - border-top-left-radius: 0; - border-bottom-left-radius: 0; - width: 3em; - margin: 0; - } - - #app-content-wrapper form fieldset > input[name="image"] + button > * { - pointer-events: none; - } - - #app-content-wrapper form fieldset > label { - display: inline-block; - width: 10em; - line-height: 18px; - font-weight: bold; - float: left; - } - @media(max-width:1199px) { #app-content-wrapper form fieldset > label { - display: block; - float: none; - }} - - #app-content-wrapper form fieldset ul label input[type="checkbox"] { - margin-left: 1em; - vertical-align: middle; - cursor: pointer; - } - - #app-content-wrapper form fieldset > ul { - margin-top: 2rem; - padding-left: 1em; - } - - #app-content-wrapper form fieldset > ul + button { - width: 36px; - text-align: center; - padding: 0; - float: right; - } - - #app-content-wrapper form fieldset > ul > li { - margin: 0 0 1em 0; - } - - #app-content-wrapper form fieldset > ul > li > input { - width: 100%; - margin: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - - #app-content-wrapper form fieldset > ul > li > button { - position: absolute; - right: 10px; - margin-top: 8px; - margin-right: 9px; - z-index: 10; - border: 0; - background-color: transparent; - } - - #app-content-wrapper form fieldset > ul > li > textarea { - min-height: 10em; - resize: vertical; - width: 100%; - margin: 0; - border-top-right-radius: 0; - } - - #app-content-wrapper form fieldset > ul > li .step-number { - padding: 6px; - float: left; - } - - #app-content-wrapper form button[type="submit"] { - margin-left: auto; - display: block; - } - -/** - * Print - */ @media print { - #header, - #app-navigation, - #controls, - .recipe-toolbar, - #app-content header:not(.printable), - .times button { - display: none !important; - } - - #content, - #app-content-wrapper { - display: block !important; - padding: 0 !important; - overflow: visible !important; - } - - #app-content { - margin-left: 0 !important; - } - a:link:after, a:visited:after { content:" [" attr(href) "] "; diff --git a/js/script.js b/js/script.js deleted file mode 100755 index bd84d0a4..00000000 --- a/js/script.js +++ /dev/null @@ -1,747 +0,0 @@ -(function (OC, window, $, undefined) { -'use strict'; - -var appName = 'cookbook'; - -$(document).ready(function () { - -/** - * The API helper - */ -var Cookbook = function (baseUrl) { - this._baseUrl = baseUrl; -}; - -Cookbook.prototype = { - /** - * Reindexes all recipes - */ - reindex: function () { - var deferred = $.Deferred(); - var self = this; - $.ajax({ - url: this._baseUrl + '/reindex', - method: 'POST' - }).done(function () { - deferred.resolve(); - }).fail(function (jqXHR, textStatus, errorThrown) { - deferred.reject(new Error(jqXHR.responseText)); - }); - return deferred.promise(); - }, - - /** - * Updates a recipe with form data - */ - update: function(form) { - var action = form.getAttribute('action'); - var url = action === '#' ? location.hash.substr(1) : action; - var data = $(form).serialize(); - var deferred = $.Deferred(); - - $.ajax({ - url: this._baseUrl + '/' + url, - method: form.getAttribute('method'), - data: data - }).done(function (response) { - deferred.resolve(response); - }).fail(function (jqXHR, textStatus, errorThrown) { - deferred.reject(new Error(jqXHR.responseText)); - }); - - return deferred.promise(); - }, - - /** - * Loads a recipe by id - * - * @param {String} id - */ - load: function (id) { - location.hash = id; - }, - - /** - * Gets the loaded recipe id - * - * @return {String} Id - */ - getActiveId: function () { - return parseInt(location.hash.replace( /[^0-9]/g, '')) || null; - }, - - /** - * Imports a recipe from a URL - * - * @param {String} url - */ - import: function (url) { - var deferred = $.Deferred(); - var self = this; - - $('#import-recipe .icon-download').hide(); - $('#import-recipe .icon-loading').show(); - - $.ajax({ - url: this._baseUrl + '/import', - method: 'POST', - data: 'url=' + url - }).done(function (recipe) { - $('#import-recipe .icon-download').show(); - $('#import-recipe .icon-loading').hide(); - - location.hash = 'recipes/' + recipe.id; - deferred.resolve(); - }).fail(function (jqXHR, textStatus, errorThrown) { - $('#import-recipe .icon-download').show(); - $('#import-recipe .icon-loading').hide(); - - deferred.reject(new Error(jqXHR.responseText)); - }); - return deferred.promise(); - }, - - /** - * Gets all recipes - * - * @return {Array} Recipes - */ - getAll: function () { - return this._recipes; - }, - - /** - * Loads all recipes for display - */ - loadAll: function () { - var deferred = $.Deferred(); - var self = this; - $.get(this._baseUrl + '/recipes').done(function (recipes) { - self._recipes = recipes; - deferred.resolve(); - }).fail(function (jqXHR, textStatus, errorThrown) { - deferred.reject(new Error(jqXHR.responseText)); - }); - - return deferred.promise(); - }, - - /** - * Sets the config update interval - * - * @param {Number} interval - */ - setUpdateInterval: function(interval) { - var self = this; - - $.ajax({ - url: self._baseUrl + '/config', - method: 'POST', - data: { 'update_interval': interval } - }).fail(function(e) { - alert(t(appName, 'Could not set recipe update interval to {interval}', {interval: interval})); - }); - }, - - /** - * Sets the config to print recipe image - * - * @param {Boolean} interval - */ - setPrintImage: function(printImage) { - var self = this; - - $.ajax({ - url: self._baseUrl + '/config', - method: 'POST', - data: { 'print_image': printImage ? 1 : 0 } - }).fail(function(e) { - alert(t(appName, 'Could not set preference for image printing')); - }); - }, - - /** - * Sets the recipe base directory using a callback - * - * @param {Function} cb - */ - setFolder: function(cb) { - var self = this; - - OC.dialogs.filepicker( - t(appName, 'Path to your recipe collection'), - function (path) { - $.ajax({ - url: self._baseUrl + '/config', - method: 'POST', - data: { 'folder': path }, - }).done(function () { - self.loadAll() - .then(function() { - self._activeRecipe = null; - location.hash = ''; - - cb(path); - }); - }).fail(function(e) { - alert(t(appName, 'Could not set recipe folder to {path}', {path: path})); - cb(null); - }); - }, - false, - 'httpd/unix-directory', - true - ); - }, - - /** - * Shows a notification to the user - * - * @param {String} title - * @param {Object} options - */ - notify: function notify(title, options) { - if(!('Notification' in window)) { - return; - } else if(Notification.permission === "granted") { - var notification = new Notification(title, options); - } else if(Notification.permission !== 'denied') { - Notification.requestPermission(function(permission) { - if(!('permission' in Notification)) { - Notification.permission = permission; - } - if(permission === "granted") { - var notification = new Notification(title, options); - } else { - alert(title); - } - }); - } - } -}; - -/** - * The content view - */ -var Content = function (cookbook) { - var self = this; - - /** - * Render - */ - self.render = function () { - var route = location.hash.substr(1); - - if(route.length === 0) { - route = 'home'; - } - $.ajax({ - url: cookbook._baseUrl + '/' + route, - method: 'GET', - }) - .done(function (html) { - $('#app-content-wrapper').html(html); - - // Common - $('#print-recipe').click(self.onPrintRecipe); - $('#delete-recipe').click(self.onDeleteRecipe); - - // Editor - $('#app-content-wrapper form').off('submit'); - $('#app-content-wrapper form').submit(self.onUpdateRecipe); - - $('#pick-image').off('click'); - $('#pick-image').click(self.onPickImage); - - $('#app-content-wrapper form ul + button.add-list-item').off('click'); - $('#app-content-wrapper form ul + button.add-list-item').click(self.onAddListItem); - - $('#app-content-wrapper form ul li input[type="text"]').off('keypress'); - $('#app-content-wrapper form ul li input[type="text"]').on('keypress', self.onListInputKeyDown); - - $('#app-settings [title]').tooltip('destroy'); - $('#app-settings [title]').tooltip(); - - self.updateListItems(); - - // View - $('header img').click(self.onImageClick); - $('main .instruction').click(self.onInstructionClick); - $('.time button').click(self.onTimerToggle); - - nav.highlightActive(); - }) - .fail(function(e) { - $('#app-content-wrapper').load(cookbook._baseUrl + '/error'); - - if(e && e instanceof Error) { throw e; } - }); - }; - - /** - * Event: Pick image - */ - self.onPickImage = function(e) { - e.preventDefault(); - - OC.dialogs.filepicker( - t(appName, 'Path to your Recipe Image'), - function (path) { - $('input[name="image"]').val(path); - }, - false, - ['image/jpeg', 'image/png'], - true, - OC.dialogs.FILEPICKER_TYPE_CHOOSE - ); - } - - /** - * Event: Delete recipe - */ - self.onDeleteRecipe = function(e) { - if(!confirm(t(appName, 'Are you sure you want to delete this recipe?'))) { return; } - - var id = e.currentTarget.dataset.id; - - $.ajax({ - url: cookbook._baseUrl + '/api/recipes/' + id, - method: 'DELETE', - }) - .done(function(html) { - if(cookbook.getActiveId() == id) { - location.hash = ''; - } - - self.render(); - nav.render(); - }) - .fail(function(e) { - alert(t(appName, 'Failed to delete recipe')); - - if(e && e instanceof Error) { throw e; } - }); - }; - - /** - * Event: Print recipe - */ - self.onPrintRecipe = function(e) { - window.print(); - }; - - /** - * Event: click on a recipe instruction - */ - self.onInstructionClick = function(e) { - $(e.target).toggleClass('done'); - } - - /** - * Event: click the recipe's image - */ - self.onImageClick = function(e) { - $(e.target).parent().toggleClass('collapsed'); - } - - /** - * Event: Toggle timer - */ - self.onTimerToggle = function(e) { - if($(e.target).hasClass('icon-play')) { - var hours = parseInt($(e.target).data('hours')); - var minutes = parseInt($(e.target).data('minutes')); - var seconds = 0; - - if( - (self.hours === undefined || self.hours === hours) && - (self.minutes === undefined || self.minutes === minutes) - ) { - self.hours = hours; - self.minutes = minutes; - self.seconds = 0; - } - - self.timer = window.setInterval(function() { - self.seconds--; - - if(self.seconds <= 0) { - self.seconds = 59; - self.minutes--; - } - - if(self.minutes <= 0) { - self.minutes = 59; - self.hours--; - } - - var text = ''; - - if(self.hours < 10) { text += '0'; } - text += self.hours + ':'; - - if(self.minutes < 10) { text += '0'; } - text += self.minutes + ':'; - - if(self.seconds < 10) { text += '0'; } - text += self.seconds; - - $(e.target).closest('.time').find('p').text(text); - - if(self.hours < 0 || self.minutes < 0) { - self.onTimerEnd(e.target); - } - }, 1000); - } else { - window.clearInterval(self.timer); - } - $(e.target).toggleClass('icon-play icon-pause'); - } - - /** - * Event: Timer ended - */ - self.onTimerEnd = function(button) { - window.clearInterval(self.timer); - $(button).removeClass('icon-pause').addClass('icon-play'); - cookbook.notify(t(appName, 'Cooking time is up!')); - } - - /** - * Updates all lists items with click events - */ - self.updateListItems = function(e) { - $('#app-content-wrapper form .remove-list-item') - .off('click') - .click(self.onDeleteListItem); - - $('#app-content-wrapper form .move-list-item-up') - .off('click') - .click(self.onMoveListItemUp) - .prop('disabled', false); - - $('#app-content-wrapper form li:first-of-type .move-list-item-up').prop('disabled', true); - - $('#app-content-wrapper form .move-list-item-down') - .off('click') - .click(self.onMoveListItemDown) - .prop('disabled', false); - - $('#app-content-wrapper form li:last-of-type .move-list-item-down').prop('disabled', true); - - $('#app-content-wrapper form ul li input[type="text"]') - .off('keypress') - .on('keypress', self.onListInputKeyDown); - - console.log('order'); - $('#app-content-wrapper form ul').each(function() { - var stepNumber = 1; - $(this).find('.step-number').each(function() { - $(this).text(stepNumber++ + '.'); - }); - }); - } - - /** - * Event: Click delete list element - */ - self.onDeleteListItem = function(e) { - e.preventDefault(); - - var button = e.currentTarget; - var tools = button.parentElement; - var listItem = tools.parentElement; - var list = listItem.parentElement; - - list.removeChild(listItem); - - self.updateListItems(); - }; - - /** - * Event: Keydown on a list itme input - */ - self.onListInputKeyDown = function(e) { - if(e.keyCode === 13 || e.keyCode === 10) { - e.preventDefault(); - - var $li = $(e.currentTarget).parents('li'); - var $ul = $li.parents('ul'); - - if($li.index() >= $ul.children('li').length) { - self.onAddListItem(e); - - } else { - $ul.children('li').eq($li.index()).find('input').focus(); - - } - } - }; - - /** - * Event: Click add list item - */ - self.onAddListItem = function(e) { - e.preventDefault(); - - var $ul = $(e.currentTarget).closest('fieldset').children('ul'); - var template = $ul.find('template').html(); - var $item = $(template); - - $ul.append($item); - - $item.find('input').focus(); - - self.updateListItems(); - }; - - /** - * Event: Click move list item up - */ - self.onMoveListItemUp = function(e) { - e.preventDefault(); - - var button = e.currentTarget; - var tools = button.parentElement; - var listItem = tools.parentElement; - - if(!listItem.previousElementSibling) { - return; - } - - $(listItem).insertBefore($(listItem.previousElementSibling)); - - self.updateListItems(); - }; - - /** - * Event: Click move list item down - */ - self.onMoveListItemDown = function(e) { - e.preventDefault(); - - var button = e.currentTarget; - var tools = button.parentElement; - var listItem = tools.parentElement; - - if(!listItem.nextElementSibling) { - return; - } - - $(listItem).insertAfter($(listItem.nextElementSibling)); - - self.updateListItems(); - }; - - /** - * Event: Update recipe - */ - self.onUpdateRecipe = function(e) { - e.preventDefault(); - - cookbook.update(e.currentTarget) - .then(function(id) { - location.hash = '/recipes/' + id; - self.render(); - nav.render(); - }) - .fail(function(e) { - alert(t(appName, 'Could not update recipe') + (e instanceof Error ? ': ' + e.message : '')); - - if(e && e instanceof Error) { throw e; } - }); - return false; - }; -}; - -/** - * The navigation view - */ -var Nav = function (cookbook) { - var self = this; - - /** - * Event: Change recipe folder - */ - self.onChangeRecipeFolder = function(e) { - cookbook.setFolder(function(path) { - e.currentTarget.value = path; - - self.render(); - }); - }; - - /** - * Event: Change recipe update interval - */ - self.onChangeRecipeUpdateInterval = function(e) { - cookbook.setUpdateInterval(e.currentTarget.value); - }; - - /** - * Event: Change recipe update interval - */ - self.onChangePrintImage = function(e) { - cookbook.setPrintImage(e.currentTarget.checked); - }; - - /** - * Event: Import new recipe - */ - self.onImportRecipe = function(e) { - e.preventDefault(); - - var url = e.currentTarget.url.value; - - cookbook.import(url) - .done(function() { - self.render(); - }) - .fail(function(e) { - alert(t(appName, 'Could not add recipe') + (e instanceof Error ? ': ' + e.message : '')); - - if(e && e instanceof Error) { throw e; } - }); - }; - - /** - * Event: Pick a category - */ - self.onCategorizeRecipes = function(e) { - e.preventDefault(); - - self.render(); - }; - - /** - * Event: Submit new search query - */ - self.onFindRecipes = function(query) { - if(query) { - location.hash = '#search/' + encodeURIComponent(query); - } else { - location.hash = '#'; - } - - self.render(); - }; - - /** - * Event: Reindex database - */ - self.onReindexRecipes = function(e) { - cookbook.reindex() - .done(function () { - self.render(); - }) - .fail(function(e) { - alert(t(appName, 'Could not rebuild recipe index.') + (e instanceof Error ? ': ' + e.message : '')); - - if(e && e instanceof Error) { throw e; } - }); - }; - - /** - * Event: Clear recipe search - */ - self.onClearRecipeSearch = function() { - location.hash = '#'; - } - - /** - * Get the current input keywords - * - * @return {String} Keywords - */ - self.getKeywords = function() { - return [self.query].join(','); - } - - /** - * Highlight the active item - */ - self.highlightActive = function() { - $('#app-navigation #categories a').each(function() { - $(this).toggleClass('active', $(this).attr('href').substr(1) === location.hash.substr(1)); - }); - } - - /** - * Render the view - */ - self.render = function () { - $.ajax({ - url: cookbook._baseUrl + '/categories', - method: 'GET', - }) - .done(function(json) { - json = json || []; - - var html = '
  • ' + t(appName, 'All recipes') + '
  • '; - - html += json.map(function(category) { - var entry = '
  • '; - entry += ''; - entry += '' + category.recipe_count + ''; - entry += category.name === '*' ? t(appName, 'No category') : category.name; - entry += '
  • '; - return entry; - }).join("\n"); - - $('#app-navigation #categories').html(html); - - self.highlightActive(); - }) - .fail(function(e) { - alert(t(appName, 'Failed to fetch categories')); - - if(e && e instanceof Error) { throw e; } - }); - - // Add a new recipe - $('#import-recipe').off('submit'); - $('#import-recipe').submit(self.onImportRecipe); - - // Change cache update interval - $('#recipe-update-interval').off('change'); - $('#recipe-update-interval').change(self.onChangeRecipeUpdateInterval); - - // Change print image setting - $('#recipe-print-image').off('change'); - $('#recipe-print-image').change(self.onChangePrintImage); - - // Change recipe folder - $('#recipe-folder').off('change'); - $('#recipe-folder').change(self.onChangeRecipeFolder); - - // Categorise recipes - $('#categorize-recipes select').off('change'); - $('#categorize-recipes select').on('change', self.onCategorizeRecipes); - - // Clear recipe search - $('#clear-recipe-search').off('click'); - $('#clear-recipe-search').click(self.onClearRecipeSearch); - - // Reindex recipes - $('#reindex-recipes').off('click'); - $('#reindex-recipes').click(self.onReindexRecipes); - - }; - - this.search = new OCA.Search(self.onFindRecipes, self.onClearRecipeSearch); -} - -var cookbook = new Cookbook(OC.generateUrl('/apps/cookbook')); -var nav = new Nav(cookbook); -var content = new Content(cookbook); - -// Render the views -nav.render(); -content.render(); - -// Render content view on hash change -window.addEventListener('hashchange', content.render); - -}); - -})(OC, window, jQuery); diff --git a/lib/Controller/MainController.php b/lib/Controller/MainController.php index 86dac6f2..e59b13ca 100755 --- a/lib/Controller/MainController.php +++ b/lib/Controller/MainController.php @@ -46,7 +46,7 @@ class MainController extends Controller return new TemplateResponse($this->appName, 'index', $view_data); // templates/index.php } - + /** * @NoAdminRequired * @NoCSRFRequired @@ -75,7 +75,7 @@ class MainController extends Controller { try { $recipes = $this->service->getAllRecipesInSearchIndex(); - + foreach ($recipes as $i => $recipe) { $recipes[$i]['image_url'] = $this->urlGenerator->linkToRoute( 'cookbook.recipe.image', @@ -86,7 +86,7 @@ class MainController extends Controller ] ); } - + $response = new TemplateResponse($this->appName, 'content/search', ['recipes' => $recipes]); $response->renderAs('blank'); @@ -107,7 +107,7 @@ class MainController extends Controller return $response; } - + /** * @NoAdminRequired * @NoCSRFRequired @@ -117,7 +117,7 @@ class MainController extends Controller $query = urldecode($query); try { $recipes = $this->service->findRecipesInSearchIndex($query); - + foreach ($recipes as $i => $recipe) { $recipes[$i]['image_url'] = $this->urlGenerator->linkToRoute( 'cookbook.recipe.image', @@ -128,7 +128,7 @@ class MainController extends Controller ] ); } - + $response = new TemplateResponse($this->appName, 'content/search', ['query' => $query, 'recipes' => $recipes]); $response->renderAs('blank'); @@ -137,7 +137,7 @@ class MainController extends Controller return new DataResponse($e->getMessage(), 500); } } - + /** * @NoAdminRequired * @NoCSRFRequired @@ -145,12 +145,10 @@ class MainController extends Controller public function category($category) { $category = urldecode($category); - try { $recipes = $this->service->getRecipesByCategory($category); - foreach ($recipes as $i => $recipe) { - $recipes[$i]['image_url'] = $this->urlGenerator->linkToRoute( + $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( 'cookbook.recipe.image', [ 'id' => $recipe['recipe_id'], @@ -159,11 +157,8 @@ class MainController extends Controller ] ); } - - $response = new TemplateResponse($this->appName, 'content/search', ['recipes' => $recipes]); - $response->renderAs('blank'); - - return $response; + + return new DataResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); } catch (\Exception $e) { return new DataResponse($e->getMessage(), 500); } @@ -187,7 +182,7 @@ class MainController extends Controller ); $recipe['id'] = $id; $recipe['print_image'] = $this->service->getPrintImage(); - $response = new TemplateResponse($this->appName, 'content/recipe', $recipe); + $response = new TemplateResponse($this->appName, 'content/recipe_vue', $recipe); $response->renderAs('blank'); return $response; @@ -204,7 +199,7 @@ class MainController extends Controller { try { $recipe = []; - + $response = new TemplateResponse($this->appName, 'content/edit', $recipe); $response->renderAs('blank'); @@ -213,7 +208,7 @@ class MainController extends Controller return new DataResponse($e->getMessage(), 500); } } - + /** * @NoAdminRequired * @NoCSRFRequired @@ -243,7 +238,7 @@ class MainController extends Controller try { $recipe_data = $_POST; $file = $this->service->addRecipe($recipe_data); - + return new DataResponse($file->getParent()->getId()); } catch (\Exception $e) { return new DataResponse($e->getMessage(), 500); @@ -266,7 +261,7 @@ class MainController extends Controller $recipe['id'] = $id; } - + $response = new TemplateResponse($this->appName, 'content/edit', $recipe); $response->renderAs('blank'); diff --git a/lib/Controller/RecipeController.php b/lib/Controller/RecipeController.php index 40fd4e0d..997a52be 100755 --- a/lib/Controller/RecipeController.php +++ b/lib/Controller/RecipeController.php @@ -46,7 +46,7 @@ class RecipeController extends Controller $recipes = $this->service->findRecipesInSearchIndex(isset($_GET['keywords']) ? $_GET['keywords'] : ''); } foreach ($recipes as $i => $recipe) { - $recipes[$i]['image_url'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $recipe['recipe_id'], 'size' => 'thumb']); + $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $recipe['recipe_id'], 'size' => 'thumb']); } return new DataResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); } @@ -64,6 +64,7 @@ class RecipeController extends Controller if (null === $json) { return new DataResponse($id, Http::STATUS_NOT_FOUND, ['Content-Type' => 'application/json']); } + $json['imageUrl'] = $this->urlGenerator->linkToRoute('cookbook.recipe.image', ['id' => $json['id'], 'size' => 'full']); return new DataResponse($json, Http::STATUS_OK, ['Content-Type' => 'application/json']); } @@ -134,10 +135,10 @@ class RecipeController extends Controller $file = $this->service->getRecipeImageFileByFolderId($id, $size); return new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/jpeg', 'Cache-Control' => 'public, max-age=604800']); - + } catch (\Exception $e) { $file = file_get_contents(dirname(__FILE__) . '/../../img/recipe-' . $size . '.jpg'); - + return new DataDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'image/jpeg']); } } diff --git a/lib/Service/RecipeService.php b/lib/Service/RecipeService.php index 2989411d..24954801 100755 --- a/lib/Service/RecipeService.php +++ b/lib/Service/RecipeService.php @@ -7,6 +7,7 @@ use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\Image; use OCP\IConfig; +use OCP\IL10N; use OCP\Files\IRootFolder; use OCP\Files\FileInfo; use OCP\Files\File; @@ -26,13 +27,15 @@ class RecipeService private $user_id; private $db; private $config; + private $il10n; - public function __construct(?string $UserId, IRootFolder $root, RecipeDb $db, IConfig $config) + public function __construct(?string $UserId, IRootFolder $root, RecipeDb $db, IConfig $config, IL10N $il10n) { $this->user_id = $UserId; $this->root = $root; $this->db = $db; $this->config = $config; + $this->il10n = $il10n; } /** @@ -113,8 +116,17 @@ class RecipeService */ public function checkRecipe(array $json): array { - if (!$json) { throw new Exception('Recipe array was null'); } - if (empty($json['name'])) { throw new Exception('Field "name" is required'); } + if (!$json) { + throw new Exception('Recipe array was null'); + } + + if (empty($json['name'])) { + throw new Exception('Field "name" is required'); + } + + if (strpos(empty($json['name']), '/') !== false) { + throw new Exception('Illegal characters in recipe name'); + } // Make sure the schema.org fields are present $json['@context'] = 'http://schema.org'; @@ -164,7 +176,16 @@ class RecipeService } else { $json['image'] = ''; } - + + // The image is a URL without a scheme, fix it + if (strpos($json['image'], '//') === 0) { + if(isset($json['url']) && strpos($json['url'], 'https') === 0) { + $json['image'] = 'https:' . $json['image']; + } else { + $json['image'] = 'http:' . $json['image']; + } + } + // Clean up the image URL string $json['image'] = stripslashes($json['image']); @@ -337,6 +358,7 @@ class RecipeService $json['url'] = ""; } + // Parse duration fields $durations = ['prepTime', 'cookTime', 'totalTime']; $duration_patterns = [ '/P.*T(\d+H)?(\d+M)?/', // ISO 8601 @@ -369,6 +391,11 @@ class RecipeService } } + while($duration_minutes >= 60) { + $duration_minutes -= 60; + $duration_hours++; + } + $json[$duration] = 'PT' . $duration_hours . 'H' . $duration_minutes . 'M'; } @@ -419,6 +446,11 @@ class RecipeService } } + // Check if json is an array for some reason + if($json && isset($json[0])) { + $json = $json[0]; + } + if (!$json || !isset($json['@type']) || $json['@type'] !== 'Recipe') { continue; } @@ -463,7 +495,18 @@ class RecipeService if(!isset($json[$prop]) || !is_array($json[$prop])) { $json[$prop] = []; } - $src = $prop_element->getAttribute('src'); + if(!empty($prop_element->getAttribute('src'))) { + $src = $prop_element->getAttribute('src'); + + } else if( + null !== $prop_element->getAttributeNode('content') && + !empty($prop_element->getAttributeNode('content')->value) + ) { + $src = $prop_element->getAttributeNode('content')->value; + + } else { + break; + } array_push($json[$prop], $src); break; @@ -474,7 +517,10 @@ class RecipeService if(!isset($json[$prop]) || !is_array($json[$prop])) { $json[$prop] = []; } - if(null !== $prop_element->getAttributeNode('content')) { + if( + null !== $prop_element->getAttributeNode('content') && + !empty($prop_element->getAttributeNode('content')->value) + ) { array_push($json[$prop], $prop_element->getAttributeNode('content')->value); } else { array_push($json[$prop], $prop_element->nodeValue); @@ -500,7 +546,7 @@ class RecipeService } } } - + // Make one final desparate attempt at getting the instructions if (!isset($json['recipeInstructions']) || !$json['recipeInstructions'] || sizeof($json['recipeInstructions']) < 1) { $json['recipeInstructions'] = []; @@ -612,6 +658,16 @@ class RecipeService } } + + // The image field was empty, remove images in the recipe folder + } else { + if($recipe_folder->nodeExists('full.jpg')) { + $recipe_folder->get('full.jpg')->delete(); + } + + if($recipe_folder->nodeExists('thumb.jpg')) { + $recipe_folder->get('thumb.jpg')->delete(); + } } // If image data was fetched, write it to disk @@ -640,6 +696,14 @@ class RecipeService $thumb_image_file->putContent($thumb_image->data()); } + // Write .nomedia file to avoid gallery indexing + if(!$recipe_folder->nodeExists('.nomedia')) { + $recipe_folder->newFile('.nomedia'); + } + + // Make sure the directory has been marked as changed + $recipe_folder->touch(); + return $recipe_file; } @@ -867,7 +931,7 @@ class RecipeService $path = $this->config->getUserValue($this->user_id, 'cookbook', 'folder'); if (!$path) { - $path = '/Recipes'; + $path = '/' . $this->il10n->t('Recipes'); } return $path; diff --git a/package.json b/package.json new file mode 100644 index 00000000..b43937c8 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "nextcloud-cookbook", + "version": "0.7.0", + "description": "", + "main": "src/main.js", + "scripts": { + "build": "node node_modules/webpack/bin/webpack.js --progress --hide-modules --config webpack.build.js", + "dev": "node node_modules/webpack/bin/webpack.js --progress --watch --config webpack.devel.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/mrzapp/nextcloud-cookbook.git" + }, + "author": "", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/mrzapp/nextcloud-cookbook/issues" + }, + "homepage": "https://github.com/mrzapp/nextcloud-cookbook#readme", + "dependencies": { + "@nextcloud/event-bus": "^1.1.4", + "@nextcloud/vue": "^1.5.0", + "vue": "^2.6.11", + "vue-i18n": "^8.17.1", + "vue-router": "^3.1.6", + "vuex": "^3.1.3" + }, + "devDependencies": { + "@babel/core": "^7.9.0", + "babel-loader": "^8.1.0", + "css-loader": "^3.5.2", + "file-loader": "^6.0.0", + "url-loader": "^4.1.0", + "vue-loader": "^15.9.1", + "vue-style-loader": "^4.1.2", + "vue-template-compiler": "^2.6.11", + "vue-template-loader": "^1.1.0", + "webpack": "^4.42.1", + "webpack-cli": "^3.3.11", + "webpack-merge": "^4.2.2" + } +} diff --git a/src/components/AppControls.vue b/src/components/AppControls.vue new file mode 100644 index 00000000..5e55a99f --- /dev/null +++ b/src/components/AppControls.vue @@ -0,0 +1,242 @@ + + + + + diff --git a/src/components/AppIndex.vue b/src/components/AppIndex.vue new file mode 100644 index 00000000..8a5a0a04 --- /dev/null +++ b/src/components/AppIndex.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/src/components/AppMain.vue b/src/components/AppMain.vue new file mode 100644 index 00000000..cd4cdd6f --- /dev/null +++ b/src/components/AppMain.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/components/AppNavi.vue b/src/components/AppNavi.vue new file mode 100644 index 00000000..d9c6e43e --- /dev/null +++ b/src/components/AppNavi.vue @@ -0,0 +1,356 @@ + + + + + diff --git a/src/components/EditImageField.vue b/src/components/EditImageField.vue new file mode 100644 index 00000000..a6ed23cb --- /dev/null +++ b/src/components/EditImageField.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/src/components/EditInputField.vue b/src/components/EditInputField.vue new file mode 100644 index 00000000..c012ccda --- /dev/null +++ b/src/components/EditInputField.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/components/EditInputGroup.vue b/src/components/EditInputGroup.vue new file mode 100644 index 00000000..13def7fb --- /dev/null +++ b/src/components/EditInputGroup.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/src/components/EditTimeField.vue b/src/components/EditTimeField.vue new file mode 100644 index 00000000..4307e936 --- /dev/null +++ b/src/components/EditTimeField.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/src/components/NotFound.vue b/src/components/NotFound.vue new file mode 100644 index 00000000..d536fcdc --- /dev/null +++ b/src/components/NotFound.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/src/components/RecipeEdit.vue b/src/components/RecipeEdit.vue new file mode 100644 index 00000000..d790b3f4 --- /dev/null +++ b/src/components/RecipeEdit.vue @@ -0,0 +1,282 @@ + + + + + diff --git a/src/components/RecipeImages.vue b/src/components/RecipeImages.vue new file mode 100644 index 00000000..8063d129 --- /dev/null +++ b/src/components/RecipeImages.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/src/components/RecipeIngredient.vue b/src/components/RecipeIngredient.vue new file mode 100644 index 00000000..21498674 --- /dev/null +++ b/src/components/RecipeIngredient.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/src/components/RecipeInstruction.vue b/src/components/RecipeInstruction.vue new file mode 100644 index 00000000..d19a858d --- /dev/null +++ b/src/components/RecipeInstruction.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/src/components/RecipeTimer.vue b/src/components/RecipeTimer.vue new file mode 100644 index 00000000..80950d38 --- /dev/null +++ b/src/components/RecipeTimer.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/components/RecipeTool.vue b/src/components/RecipeTool.vue new file mode 100644 index 00000000..e68ea24c --- /dev/null +++ b/src/components/RecipeTool.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/src/components/RecipeView.vue b/src/components/RecipeView.vue new file mode 100644 index 00000000..4eefe1d5 --- /dev/null +++ b/src/components/RecipeView.vue @@ -0,0 +1,242 @@ + + + + + diff --git a/src/components/SearchResults.vue b/src/components/SearchResults.vue new file mode 100644 index 00000000..d0656ef4 --- /dev/null +++ b/src/components/SearchResults.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..38bdd7d6 --- /dev/null +++ b/src/main.js @@ -0,0 +1,189 @@ +/** + * Nextcloud Cookbook app + * Vue frontend entry file + * --------------------------- + * @license AGPL3 or later +*/ + +// TODO: Agree on a markdown parser +// TODO: Remove dependency on jQuery + +import Vue from 'vue' +import router from './router' +import store from './store' + +//import AppNavi from './components/AppNavi' +import AppMain from './components/AppMain' + +(function (OC, window, $, undefined) { + 'use strict' + + // Fetch Nextcloud nonce identifier for dynamic script loading + __webpack_nonce__ = btoa(OC.requestToken) + + window.baseUrl = OC.generateUrl('apps/cookbook') + + // Check if two routes point to the same component but have different content + window.shouldReloadContent = function(url1, url2) { + if (url1 === url2) { + return false // Obviously should not if both routes are the same + } + + let comps1 = url1.split('/') + let comps2 = url2.split('/') + + if (comps1.length < 2 || comps2.length < 2) { + return false // Just a failsafe, this should never happen + } + + // The route structure is as follows: + // - /{item}/:id View + // - /{item}/:id/edit Edit + // - /{item}/create Create + // If the items are different, then the router automatically handles + // component loading: do not manually reload + if (comps1[1] !== comps2[1]) { + return false + } + + // If one of the routes is edit and the other is not + if (comps1.length !== comps2.length) { + // Only reload if changing from edit to create + if (comps1.pop() === 'create' || comps2.pop() === 'create') { + return true + } + + return false + + } else if (comps1.pop() === 'create') { + // But, if we are moving from create to view, do not reload + // the create component + return false + + } + + // Only options left are that both of the routes are edit or view, + // but not identical, or that we're moving from view to create + // -> reload view + return true + } + + // Check if the two urls point to the same item instance + window.isSameItemInstance = function(url1, url2) { + if (url1 === url2) { + return true // Obviously true if the routes are the same + } + let comps1 = url1.split('/') + let comps2 = url2.split('/') + if (comps1.length < 2 || comps2.length < 2) { + return false // Just a failsafe, this should never happen + } + // If the items are different, then the item instance cannot be + // the same either + if (comps1[1] !== comps2[1]) { + return false + } + if (comps1.length < 3 || comps2.length < 3) { + // ID is the third url component, so can't be the same instance if + // either of the urls have less than three components + return false + } + if (comps1[2] !== comps2[2]) { + // Different IDs, not same instance + return false + } + return true + } + + // A simple function to sanitize HTML tags + window.escapeHTML = function(text) { + return text.replace(/[\"&'\/<>]/g, function (a) { + return { + '&': '&', + '"': '"', + "'": ''', + '<': '<', + '>': '>' + }[a] + }) + } + + // Fix the decimal separator for languages that use a comma instead of dot + window.fixDecimalSeparator = function(value, io) { + // value is the string value of the number to process + // io is either 'i' as in input or 'o' as in output + if (!value) { + return '' + } + if (io === 'i') { + // Check if it's an American number where a comma precedes a dot + // e.g. 12,500.25 + if (value.indexOf('.') > value.indexOf(',')) { + return value.replace(',', '') + } else { + return value.replace(',', '.') + } + } else if (io === 'o') { + return value.toString().replace('.', ',') + } + } + + // This will replace the PHP function nl2br in Vue components + window.nl2br = function(text) { + return text.replace(/\n/g, '
    ') + } + + // A simple function that converts a MySQL datetime into a timestamp. + window.getTimestamp = function(date) { + if (date) { + return new Date(date) + } else { + return null + } + } + + // Push a new URL to the router, essentially navigating to that page. + window.goTo = function(url) { + router.push(url) + } + + // Notify the user if notifications are allowed + window.notify = function notify(title, options) { + if (!('Notification' in window)) { + return + } else if (Notification.permission === "granted") { + var notification = new Notification(title, options) + } else if (Notification.permission !== 'denied') { + Notification.requestPermission(function(permission) { + if (!('permission' in Notification)) { + Notification.permission = permission + } + if (permission === "granted") { + var notification = new Notification(title, options) + } else { + alert(title) + } + }) + } + } + + // Also make the injections available in Vue components + Vue.prototype.$window = window + Vue.prototype.OC = OC + + // Make translations easier by automatically providing the app name + let tx = function(text) { + return window.t('cookbook', text) + } + + Vue.prototype.t = tx + + // Start the app once document is done loading + $(document).ready(function () { + const App = Vue.extend(AppMain) + new App({ + store, + router, + }).$mount("#app") + }) +})(OC, window, jQuery) diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 00000000..800295bc --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,54 @@ +/** + * Nextcloud Cookbook app + * Vue router module + * ---------------------- + * @license AGPL3 or later + */ +import Vue from 'vue' +import VueRouter from 'vue-router' + +import Index from '../components/AppIndex' +import NotFound from '../components/NotFound' +import RecipeView from '../components/RecipeView' +import RecipeEdit from '../components/RecipeEdit' +import Search from '../components/SearchResults' + +Vue.use(VueRouter) + +// The router will try to match routers in a descending order. +// Routes that share the same root, must be listed from the +// most descriptive to the least descriptive, e.g. +// /section/component/subcomponent/edit/:id +// /section/component/subcomponent/new +// /section/component/subcomponent/:id +// /section/component/:id +// /section/:id +const routes = [ + // Search routes + { path: '/category/:value', name: 'search-category', component: Search, props: { query: 'cat' } }, + { path: '/name/:value', name: 'search-name', component: Search, props: { query: 'name' } }, + { path: '/tag/:value', name: 'search-tag', component: Search, props: { query: 'tag' } }, + + // Recipe routes + // Vue router has a strange way of determining when it renders a component again and when not. + // In essence, when two routes point to the same component, it usually will not be re-rendered + // automatically. If the contents change (e.g. between /recipe/xxx and /recipe/yyy) this must + // be checked for and the component re-rendered manually. In order to avoid the need to write + // separate checks for different item types, all items MUST follow this route convention: + // - View: /{item}/:id + // - Edit: /{item}/:id/edit + // - Create: /{item}/create + { path: '/recipe/create', name: 'recipe-create', component: RecipeEdit }, + { path: '/recipe/:id/edit', name: 'recipe-edit', component: RecipeEdit }, + { path: '/recipe/:id', name: 'recipe-view', component: RecipeView }, + + // Index is the last defined route + { path: '/', name:'index', component: Index }, + + // Anything not matched goes to NotFound + { path: '*', name:'not-found', component: NotFound }, +]; + +export default new VueRouter({ + routes +}) diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 00000000..247c8339 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,82 @@ +/** + * Nextcloud Cookbook app + * Vuex store module + * ---------------------- + * @license AGPL3 or later + */ +import Vue from 'vue' +import Vuex from 'vuex' + +Vue.use(Vuex) + +// We are using the vuex store linking changes within the components to updates in the navigation panel. +export default new Vuex.Store({ + // Vuex store handles value changes through actions and mutations. + // From the App, you trigger an action, that changes the store + // state through a set mutation. You can process the data within + // the mutation if you want. + state: { + user: null, + // Page is for keeping track of the page the user is on and + // setting the appropriate navigation entry active. + page: null, + // We'll save the recipe here, since the data is used by + // several independent components + recipe: null, + // Loading and saving states to determine which loader icons to show. + // State of -1 is reserved for recipe and edit views to be set when the + // User loads the app at one of these locations and has to wait for an + // asynchronous recipe loading. + loadingRecipe: 0, + // This is used if when a recipe is reloaded in edit or view + reloadingRecipe: 0, + // A recipe save is in progress + savingRecipe: false, + }, + + mutations: { + setLoadingRecipe(s, { r }) { + s.loadingRecipe = r + }, + setPage(s, { p }) { + s.page = p + }, + setRecipe(s, { r }) { + s.recipe = r + // Setting recipe also means that loading/reloading the recipe has finished + s.loadingRecipe = 0 + s.reloadingRecipe = 0 + }, + setReloadingRecipe(s, { r }) { + s.reloadingRecipe = r + }, + setSavingRecipe(s, { b }) { + s.savingRecipe = b + }, + setUser(s, { u }) { + s.user = u + } + }, + + actions: { + setLoadingRecipe(c, { recipe }) { + c.commit('setLoadingRecipe', { r: parseInt(recipe) }) + }, + setPage(c, { page }) { + c.commit('setPage', { p: page }) + }, + setRecipe(c, { recipe }) { + c.commit('setRecipe', { r: recipe }) + }, + setReloadingRecipe(c, { recipe }) { + c.commit('setReloadingRecipe', { r: parseInt(recipe) }) + }, + setSavingRecipe(c, { saving }) { + c.commit('setSavingRecipe', { b: saving }) + }, + setUser(c, { user }) { + c.commit('setUser', { u: user }) + }, + } + +}) diff --git a/templates/content/edit.php b/templates/content/edit.php deleted file mode 100755 index 3454d74f..00000000 --- a/templates/content/edit.php +++ /dev/null @@ -1,179 +0,0 @@ -
    -
    - -
    - -
    - - -
    - -
    -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - - : - -
    - -
    - - - : - -
    - -
    - - - : - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - -
      - - - $tool) { ?> -
    • - -
      - - - -
      -
    • - - -
    - -
    - -
    - -
      - - - $ingredient) { ?> -
    • - -
      - - - -
      -
    • - - -
    - -
    - -
    - -
      - - - $step) { ?> -
    • -
      .
      -
      - - - -
      - -
    • - - -
    - -
    -
    -
    diff --git a/templates/content/recipe.php b/templates/content/recipe.php deleted file mode 100755 index 5462ee96..00000000 --- a/templates/content/recipe.php +++ /dev/null @@ -1,125 +0,0 @@ -
    - -
    - - - t('Edit recipe')); ?> - -
    -
    - -
    -
    - -
    -
    - - -
    - -
    - - -
    -

    - -
    -

    - - -

    t('Source')); ?>:

    - - -

    t('Servings')); ?>:

    - -
    - format('%I'); - $prep_hours = $prep_interval->format('%h'); - if ($prep_hours > 0 || $prep_mins > 0) { - ?> -
    -

    t('Preparation time')); ?>

    -

    -
    - - - format('%I'); - $cook_hours = $cook_interval->format('%h'); - if ($cook_hours > 0 || $cook_mins > 0) { - ?> -
    - -

    t('Cooking time')); ?>

    -

    -
    - - - format('%I'); - $total_hours = $total_interval->format('%h'); - if ($total_hours > 0 || $total_mins > 0) { - ?> -
    -

    t('Total time')); ?>

    -

    -
    - -
    -
    -
    - -
    - - - -
    -

    t('Instructions')); ?>

    -
      - -
    1. - -
    -
    - -
    diff --git a/templates/content/search.php b/templates/content/search.php deleted file mode 100644 index 11dfe7da..00000000 --- a/templates/content/search.php +++ /dev/null @@ -1,33 +0,0 @@ -
    -

    - - t('Search')); ?> - - t('Tag')); ?> - - t('Category')); ?> - - t('All recipes')); ?> - -

    - -

    - t('No results')); ?> -

    - - - -
    diff --git a/templates/index.php b/templates/index.php index 9098ecdc..74bba7c4 100755 --- a/templates/index.php +++ b/templates/index.php @@ -1,20 +1,7 @@
    -
    - inc('navigation/index')); ?> - inc('settings/index')); ?> -
    - -
    -
    -
    - -
    -
    -
    - diff --git a/templates/navigation/error.php b/templates/navigation/error.php deleted file mode 100644 index 772f9970..00000000 --- a/templates/navigation/error.php +++ /dev/null @@ -1,6 +0,0 @@ -
    -

    t('Error')); ?>

    -
    - t('This page doesn\'t exist.')); ?> -
    -
    diff --git a/templates/navigation/index.php b/templates/navigation/index.php deleted file mode 100755 index 7b2690ed..00000000 --- a/templates/navigation/index.php +++ /dev/null @@ -1,15 +0,0 @@ -
    - t('Create recipe')); ?> -
    - -
    - - -
    - - diff --git a/templates/navigation/recipes.php b/templates/navigation/recipes.php deleted file mode 100755 index c163f6f6..00000000 --- a/templates/navigation/recipes.php +++ /dev/null @@ -1,8 +0,0 @@ - -
  • - - - - -
  • - diff --git a/templates/settings/index.php b/templates/settings/index.php deleted file mode 100755 index 60d24cf4..00000000 --- a/templates/settings/index.php +++ /dev/null @@ -1,35 +0,0 @@ -
    -
    - -
    -
    -
    -
      -
    • - -
    • -
    • - - -
    • -
    • - -
      - -
      - -
      -
      -
    • -
    • - > - -
    • -
    -
    -
    -
    diff --git a/webpack.build.js b/webpack.build.js new file mode 100644 index 00000000..fe27aa89 --- /dev/null +++ b/webpack.build.js @@ -0,0 +1,7 @@ +const merge = require('webpack-merge') +const common = require('./webpack.config.js') + +module.exports = merge(common, { + mode: 'production', + devtool: 'source-map', +}) diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..822545f5 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,65 @@ +/** + * Nextcloud Cookbook app + * Main Webpack configuration file. + * Different configurations for development and build runs + * are located in the appropriate files. + */ +const path = require('path') +const { VueLoaderPlugin } = require('vue-loader') + +module.exports = { + + entry:{ + vue: path.join(__dirname, 'src', 'main.js'), + }, + output: { + path: path.resolve(__dirname, './js'), + publicPath: '/js/', + filename: '[name].js', + chunkFilename: '[name].js?v=[contenthash]', + }, + module: { + rules: [ + { + test: /\.css$/, + use: ['vue-style-loader', 'css-loader'], + }, + { + test: /\.html$/, + loader: 'vue-template-loader', + }, + { + test: /\.vue$/, + loader: 'vue-loader', + }, + { + test: /\.js$/, + loader: 'babel-loader', + exclude: /node_modules/, + }, + { + test: /\.(png|jpg|gif)$/, + loader: 'file-loader', + options: { + name: '[name].[ext]?[hash]' + }, + }, + { + test: /\.(eot|woff|woff2|ttf|svg)$/, + loaders: 'file-loader', + options: { + name: '[path][name].[ext]?[hash]' + }, + }, + ], + }, + plugins: [new VueLoaderPlugin()], + resolve: { + extensions: ['*', '.js', '.vue', '.json'], + modules: [ + path.resolve(__dirname, './node_modules') + ], + symlinks: false, + }, + +} diff --git a/webpack.devel.js b/webpack.devel.js new file mode 100644 index 00000000..df8512a4 --- /dev/null +++ b/webpack.devel.js @@ -0,0 +1,7 @@ +const merge = require('webpack-merge') +const common = require('./webpack.config.js') + +module.exports = merge(common, { + mode: 'development', + devtool: 'inline-cheap-source-map', +})