From ddf8e34ddefaf0527cd18c04686754f0c8ce3ee4 Mon Sep 17 00:00:00 2001 From: korelstar Date: Sun, 16 Oct 2016 18:37:48 +0200 Subject: [PATCH 1/7] New feature: set note as favorite (star/unstar) --- appinfo/routes.php | 1 + controller/notescontroller.php | 16 ++++++++- css/notes.css | 10 +++++- db/note.php | 15 +++++++-- js/app/controllers/notescontroller.js | 8 +++++ js/app/services/notesmodel.js | 5 +-- js/public/app.min.js | 2 +- js/public/app.min.js.map | 2 +- service/notesservice.php | 47 ++++++++++++++++++++++++--- templates/main.php | 8 ++++- 10 files changed, 101 insertions(+), 13 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 9f1cdb81..89ab33db 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -18,6 +18,7 @@ ['name' => 'notes#get', 'url' => '/notes/{id}', 'verb' => 'GET'], ['name' => 'notes#create', 'url' => '/notes', 'verb' => 'POST'], ['name' => 'notes#update', 'url' => '/notes/{id}', 'verb' => 'PUT'], + ['name' => 'notes#favorite', 'url' => '/notes/{id}/favorite', 'verb' => 'PUT'], ['name' => 'notes#destroy', 'url' => '/notes/{id}', 'verb' => 'DELETE'], // api diff --git a/controller/notescontroller.php b/controller/notescontroller.php index b148c428..020e2d33 100644 --- a/controller/notescontroller.php +++ b/controller/notescontroller.php @@ -105,6 +105,20 @@ public function update($id, $content) { } + /** + * @NoAdminRequired + * + * @param int $id + * @param boolean $favorite + * @return DataResponse + */ + public function favorite($id, $favorite) { + return $this->respond(function () use ($id, $favorite) { + return $this->notesService->favorite($id, $favorite, $this->userId); + }); + } + + /** * @NoAdminRequired * @@ -118,4 +132,4 @@ public function destroy($id) { }); } -} \ No newline at end of file +} diff --git a/css/notes.css b/css/notes.css index bc4c0213..f549c2c9 100644 --- a/css/notes.css +++ b/css/notes.css @@ -43,9 +43,17 @@ } #app-navigation .active > .utils .icon-delete { - display: block; + display: inline-block; opacity: .3; } +#app-navigation .active > .utils .icon-star { + display: inline-block; + opacity: .3; +} +#app-navigation .utils .icon-starred { + display: inline-block; + opacity: 1 !important; +} .tooltip { text-shadow: none; diff --git a/db/note.php b/db/note.php index 84a84d3b..3fe34d4f 100644 --- a/db/note.php +++ b/db/note.php @@ -24,6 +24,8 @@ * @method void setTitle(string $value) * @method string getContent() * @method void setContent(string $value) + * @method boolean getFavorite() + * @method void setFavorite(boolean $value) * @package OCA\Notes\Db */ class Note extends Entity { @@ -31,9 +33,11 @@ class Note extends Entity { public $modified; public $title; public $content; + public $favorite = false; public function __construct() { $this->addType('modified', 'integer'); + $this->addType('favorite', 'boolean'); } /** @@ -46,9 +50,16 @@ public static function fromFile(File $file){ $note->setContent($file->getContent()); $note->setModified($file->getMTime()); $note->setTitle(pathinfo($file->getName(),PATHINFO_FILENAME)); // remove extension + $fileInfo = $file->getFileInfo(); + if($fileInfo->offsetExists('tags')) { + $tags = $fileInfo->offsetGet('tags'); + if(in_array(\OC\Tags::TAG_FAVORITE, $tags)) { + $note->setFavorite(true); + //unset($tags[array_search(\OC\Tags::TAG_FAVORITE, $tags)]); + } + } $note->resetUpdatedFields(); return $note; } - -} \ No newline at end of file +} diff --git a/js/app/controllers/notescontroller.js b/js/app/controllers/notescontroller.js index ff5d7070..d180e04b 100644 --- a/js/app/controllers/notescontroller.js +++ b/js/app/controllers/notescontroller.js @@ -35,4 +35,12 @@ app.controller('NotesController', function($routeParams, $scope, $location, }); }; + $scope.toggleFavorite = function (noteId) { + var note = NotesModel.get(noteId); + note.customPUT({favorite: !note.favorite}, + 'favorite', {}, {}).then(function (favorite) { + note.favorite = favorite ? true : false; + }); + }; + }); diff --git a/js/app/services/notesmodel.js b/js/app/services/notesmodel.js index 8877adff..4e074060 100644 --- a/js/app/services/notesmodel.js +++ b/js/app/services/notesmodel.js @@ -35,6 +35,7 @@ app.factory('NotesModel', function () { note.title = updated.title; note.modified = updated.modified; note.content = updated.content; + note.favorite = updated.favorite; } else { this.notes.push(updated); this.notesIds[updated.id] = updated; @@ -49,8 +50,8 @@ app.factory('NotesModel', function () { break; } } - } + }, }; return new NotesModel(); -}); \ No newline at end of file +}); diff --git a/js/public/app.min.js b/js/public/app.min.js index da94794a..c62615cd 100644 --- a/js/public/app.min.js +++ b/js/public/app.min.js @@ -1,2 +1,2 @@ -!function(e,n,o,i,r){"use strict";var s=e.module("Notes",["restangular","ngRoute"]).config(["$provide","$routeProvider","RestangularProvider","$httpProvider","$windowProvider",function(t,e,n,i,r){i.defaults.headers.common.requesttoken=o,t.value("Constants",{saveInterval:5e3}),e.when("/notes/:noteId",{templateUrl:"note.html",controller:"NoteController",resolve:{note:["$route","$q","is","Restangular",function(t,e,n,o){var i=e.defer(),r=t.current.params.noteId;return n.loading=!0,o.one("notes",r).get().then(function(t){n.loading=!1,i.resolve(t)},function(){n.loading=!1,i.reject()}),i.promise}]}}).otherwise({redirectTo:"/"});var s=OC.generateUrl("/apps/notes");n.setBaseUrl(s)}]).run(["$rootScope","$location","NotesModel",function(t,e,o){n('link[rel="shortcut icon"]').attr("href",OC.filePath("notes","img","favicon.png")),t.$on("$routeChangeError",function(){var t=o.getAll();if(t.length>0){var n=t.sort(function(t,e){return t.modified>e.modified?1:t.modified0){var n=t.sort(function(t,e){return t.modified>e.modified?1:t.modified\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/* jshint unused: false */\nvar app = angular.module('Notes', ['restangular', 'ngRoute']).\nconfig(function($provide, $routeProvider, RestangularProvider, $httpProvider,\n $windowProvider) {\n 'use strict';\n\n // Always send the CSRF token by default\n $httpProvider.defaults.headers.common.requesttoken = requestToken;\n\n // you have to use $provide inside the config method to provide a globally\n // shared and injectable object\n $provide.value('Constants', {\n saveInterval: 5*1000 // miliseconds\n });\n\n // define your routes that that load templates into the ng-view\n $routeProvider.when('/notes/:noteId', {\n templateUrl: 'note.html',\n controller: 'NoteController',\n resolve: {\n // $routeParams does not work inside resolve so use $route\n // note is the name of the argument that will be injected into the\n // controller\n /* @ngInject */\n note: function ($route, $q, is, Restangular) {\n\n var deferred = $q.defer();\n var noteId = $route.current.params.noteId;\n is.loading = true;\n\n Restangular.one('notes', noteId).get().then(function (note) {\n is.loading = false;\n deferred.resolve(note);\n }, function () {\n is.loading = false;\n deferred.reject();\n });\n\n return deferred.promise;\n }\n }\n }).otherwise({\n redirectTo: '/'\n });\n\n var baseUrl = OC.generateUrl('/apps/notes');\n RestangularProvider.setBaseUrl(baseUrl);\n\n\n\n}).run(function ($rootScope, $location, NotesModel) {\n 'use strict';\n\n $('link[rel=\"shortcut icon\"]').attr(\n\t\t 'href',\n\t\t OC.filePath('notes', 'img', 'favicon.png')\n );\n\n // handle route errors\n $rootScope.$on('$routeChangeError', function () {\n var notes = NotesModel.getAll();\n\n // route change error should redirect to the latest note if possible\n if (notes.length > 0) {\n var sorted = notes.sort(function (a, b) {\n if(a.modified > b.modified) {\n return 1;\n } else if(a.modified < b.modified) {\n return -1;\n } else {\n return 0;\n }\n });\n\n var note = notes[sorted.length-1];\n $location.path('/notes/' + note.id);\n } else {\n $location.path('/');\n }\n });\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('AppController', function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote) {\n if(lastViewedNote !== 0) {\n $location.path('/notes/' + lastViewedNote);\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('NoteController', function($routeParams, $scope, NotesModel,\n SaveQueue, note) {\n 'use strict';\n\n NotesModel.updateIfExists(note);\n\n $scope.note = NotesModel.get($routeParams.noteId);\n\n $scope.isSaving = function () {\n return SaveQueue.isSaving();\n };\n\n $scope.updateTitle = function () {\n $scope.note.title = $scope.note.content.split('\\n')[0] ||\n t('notes', 'New note');\n };\n\n $scope.save = function() {\n var note = $scope.note;\n SaveQueue.add(note);\n };\n\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', function($routeParams, $scope, $location,\n Restangular, NotesModel) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notes = NotesModel.getAll();\n\n var notesResource = Restangular.all('notes');\n\n // initial request for getting all notes\n notesResource.getList().then(function (notes) {\n NotesModel.addAll(notes);\n });\n\n $scope.create = function () {\n notesResource.post().then(function (note) {\n NotesModel.add(note);\n $location.path('/notes/' + note.id);\n });\n };\n\n $scope.delete = function (noteId) {\n var note = NotesModel.get(noteId);\n note.remove().then(function () {\n NotesModel.remove(noteId);\n $scope.$emit('$routeChangeError');\n });\n };\n\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesAutofocus', function () {\n 'use strict';\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.focus();\n }\n };\n});\n","/*global mdEdit*/\napp.directive('editor', ['$timeout', function ($timeout) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\t\t\tvar editor = mdEdit(element[0], {change: function(value) {\n\t\t\t\t$timeout(function(){\n\t\t\t\t\tscope.$apply(function() {\n\t\t\t\t\t\tscope.note.content = value;\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}});\n\t\t\teditor.setValue(scope.note.content);\n\t\t\telement.on('click', '.link', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = $(this).find('.link-params-inner').text();\n\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n}]);\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesIsSaving', function ($window) {\n 'use strict';\n return {\n restrict: 'A',\n scope: {\n 'notesIsSaving': '='\n },\n link: function (scope) {\n $window.onbeforeunload = function () {\n if (scope.notesIsSaving) {\n return t('notes', 'Note is currently saving. Leaving ' +\n 'the page will delete all changes!');\n } else {\n return null;\n }\n };\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/**\n * Like ng-change only that it does not fire when you type faster than\n * 300 ms\n */\napp.directive('notesTimeoutChange', function ($timeout) {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element, attributes) {\n var interval = 300; // 300 miliseconds timeout after typing\n var timeout;\n\n $(element).bind('input propertychange paste', function () {\n $timeout.cancel(timeout);\n\n timeout = $timeout(function () {\n scope.$apply(attributes.notesTimeoutChange);\n }, interval);\n });\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip();\n }\n };\n});\n","/**\n * removes whitespaces and leading #\n */\napp.filter('noteTitle', function () {\n\t'use strict';\n\treturn function (value) {\n \tvalue = value.split('\\n')[0] || 'newNote';\n\t\treturn value.trim().replace(/^#+/g, '');\n\t};\n});\n","app.filter('wordCount', function () {\n\t'use strict';\n\treturn function (value) {\n\t\tif (value && (typeof value === 'string')) {\n\t\t\tvar wordCount = value.split(/\\s+/).filter(\n\t\t\t\t// only count words containing\n\t\t\t\t// at least one alphanumeric character\n\t\t\t\tfunction(value) {\n\t\t\t\t\treturn value.search(/[A-Za-z0-9]/) !== -1;\n\t\t\t\t}\n\t\t\t).length;\n\t\t\treturn window.n('notes', '%n word', '%n words', wordCount);\n\t\t} else {\n\t\t\treturn 0;\n\t\t}\n\t};\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('is', function () {\n 'use strict';\n\n return {\n loading: false\n };\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// take care of fileconflicts by appending a number\napp.factory('NotesModel', function () {\n 'use strict';\n\n var NotesModel = function () {\n this.notes = [];\n this.notesIds = {};\n };\n\n NotesModel.prototype = {\n addAll: function (notes) {\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('SaveQueue', function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n _flush: function () {\n // if there are no changes dont execute the requests\n var keys = Object.keys(this._queue);\n if(keys.length === 0 || this._flushLock) {\n return;\n } else {\n this._flushLock = true;\n }\n\n var self = this;\n var requests = [];\n\n // iterate over updated objects and run an update request for\n // each one of them\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/* jshint unused: false */\nvar app = angular.module('Notes', ['restangular', 'ngRoute']).\nconfig(function($provide, $routeProvider, RestangularProvider, $httpProvider,\n $windowProvider) {\n 'use strict';\n\n // Always send the CSRF token by default\n $httpProvider.defaults.headers.common.requesttoken = requestToken;\n\n // you have to use $provide inside the config method to provide a globally\n // shared and injectable object\n $provide.value('Constants', {\n saveInterval: 5*1000 // miliseconds\n });\n\n // define your routes that that load templates into the ng-view\n $routeProvider.when('/notes/:noteId', {\n templateUrl: 'note.html',\n controller: 'NoteController',\n resolve: {\n // $routeParams does not work inside resolve so use $route\n // note is the name of the argument that will be injected into the\n // controller\n /* @ngInject */\n note: function ($route, $q, is, Restangular) {\n\n var deferred = $q.defer();\n var noteId = $route.current.params.noteId;\n is.loading = true;\n\n Restangular.one('notes', noteId).get().then(function (note) {\n is.loading = false;\n deferred.resolve(note);\n }, function () {\n is.loading = false;\n deferred.reject();\n });\n\n return deferred.promise;\n }\n }\n }).otherwise({\n redirectTo: '/'\n });\n\n var baseUrl = OC.generateUrl('/apps/notes');\n RestangularProvider.setBaseUrl(baseUrl);\n\n\n\n}).run(function ($rootScope, $location, NotesModel) {\n 'use strict';\n\n $('link[rel=\"shortcut icon\"]').attr(\n\t\t 'href',\n\t\t OC.filePath('notes', 'img', 'favicon.png')\n );\n\n // handle route errors\n $rootScope.$on('$routeChangeError', function () {\n var notes = NotesModel.getAll();\n\n // route change error should redirect to the latest note if possible\n if (notes.length > 0) {\n var sorted = notes.sort(function (a, b) {\n if(a.modified > b.modified) {\n return 1;\n } else if(a.modified < b.modified) {\n return -1;\n } else {\n return 0;\n }\n });\n\n var note = notes[sorted.length-1];\n $location.path('/notes/' + note.id);\n } else {\n $location.path('/');\n }\n });\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('AppController', function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote) {\n if(lastViewedNote !== 0) {\n $location.path('/notes/' + lastViewedNote);\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('NoteController', function($routeParams, $scope, NotesModel,\n SaveQueue, note) {\n 'use strict';\n\n NotesModel.updateIfExists(note);\n\n $scope.note = NotesModel.get($routeParams.noteId);\n\n $scope.isSaving = function () {\n return SaveQueue.isSaving();\n };\n\n $scope.updateTitle = function () {\n $scope.note.title = $scope.note.content.split('\\n')[0] ||\n t('notes', 'New note');\n };\n\n $scope.save = function() {\n var note = $scope.note;\n SaveQueue.add(note);\n };\n\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', function($routeParams, $scope, $location,\n Restangular, NotesModel) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notes = NotesModel.getAll();\n\n var notesResource = Restangular.all('notes');\n\n // initial request for getting all notes\n notesResource.getList().then(function (notes) {\n NotesModel.addAll(notes);\n });\n\n $scope.create = function () {\n notesResource.post().then(function (note) {\n NotesModel.add(note);\n $location.path('/notes/' + note.id);\n });\n };\n\n $scope.delete = function (noteId) {\n var note = NotesModel.get(noteId);\n note.remove().then(function () {\n NotesModel.remove(noteId);\n $scope.$emit('$routeChangeError');\n });\n };\n\n $scope.toggleFavorite = function (noteId) {\n var note = NotesModel.get(noteId);\n note.customPUT({favorite: !note.favorite},\n 'favorite', {}, {}).then(function (favorite) {\n note.favorite = favorite ? true : false;\n });\n };\n\n});\n","/**\n * removes whitespaces and leading #\n */\napp.filter('noteTitle', function () {\n\t'use strict';\n\treturn function (value) {\n \tvalue = value.split('\\n')[0] || 'newNote';\n\t\treturn value.trim().replace(/^#+/g, '');\n\t};\n});\n","app.filter('wordCount', function () {\n\t'use strict';\n\treturn function (value) {\n\t\tif (value && (typeof value === 'string')) {\n\t\t\tvar wordCount = value.split(/\\s+/).filter(\n\t\t\t\t// only count words containing\n\t\t\t\t// at least one alphanumeric character\n\t\t\t\tfunction(value) {\n\t\t\t\t\treturn value.search(/[A-Za-z0-9]/) !== -1;\n\t\t\t\t}\n\t\t\t).length;\n\t\t\treturn window.n('notes', '%n word', '%n words', wordCount);\n\t\t} else {\n\t\t\treturn 0;\n\t\t}\n\t};\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesAutofocus', function () {\n 'use strict';\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.focus();\n }\n };\n});\n","/*global mdEdit*/\napp.directive('editor', ['$timeout', function ($timeout) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\t\t\tvar editor = mdEdit(element[0], {change: function(value) {\n\t\t\t\t$timeout(function(){\n\t\t\t\t\tscope.$apply(function() {\n\t\t\t\t\t\tscope.note.content = value;\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}});\n\t\t\teditor.setValue(scope.note.content);\n\t\t\telement.on('click', '.link', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = $(this).find('.link-params-inner').text();\n\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n}]);\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesIsSaving', function ($window) {\n 'use strict';\n return {\n restrict: 'A',\n scope: {\n 'notesIsSaving': '='\n },\n link: function (scope) {\n $window.onbeforeunload = function () {\n if (scope.notesIsSaving) {\n return t('notes', 'Note is currently saving. Leaving ' +\n 'the page will delete all changes!');\n } else {\n return null;\n }\n };\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/**\n * Like ng-change only that it does not fire when you type faster than\n * 300 ms\n */\napp.directive('notesTimeoutChange', function ($timeout) {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element, attributes) {\n var interval = 300; // 300 miliseconds timeout after typing\n var timeout;\n\n $(element).bind('input propertychange paste', function () {\n $timeout.cancel(timeout);\n\n timeout = $timeout(function () {\n scope.$apply(attributes.notesTimeoutChange);\n }, interval);\n });\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip();\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('is', function () {\n 'use strict';\n\n return {\n loading: false\n };\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// take care of fileconflicts by appending a number\napp.factory('NotesModel', function () {\n 'use strict';\n\n var NotesModel = function () {\n this.notes = [];\n this.notesIds = {};\n };\n\n NotesModel.prototype = {\n addAll: function (notes) {\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('SaveQueue', function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n _flush: function () {\n // if there are no changes dont execute the requests\n var keys = Object.keys(this._queue);\n if(keys.length === 0 || this._flushLock) {\n return;\n } else {\n this._flushLock = true;\n }\n\n var self = this;\n var requests = [];\n\n // iterate over updated objects and run an update request for\n // each one of them\n for(var i=0; igetFolderForUser($userId); $files = $folder->getDirectoryListing(); - $notes = []; - + $filesById = []; foreach($files as $file) { if($this->isNote($file)) { - $notes[] = Note::fromFile($file); + $filesById[$file->getId()] = $file; } } + $tagger = \OC::$server->getTagManager()->load('files'); + $tags = $tagger->getTagsForObjects(array_keys($filesById)); + if ($tags) { + foreach ($tags as $fileId => $fileTags) { + $filesById[$fileId]->getFileInfo()->offsetSet('tags', $fileTags); + } + } + + $notes = []; + foreach($filesById as $file) { + $notes[] = Note::fromFile($file); + } return $notes; } @@ -135,6 +146,30 @@ public function update ($id, $content, $userId){ } + /** + * Set or unset a note as favorite. + * @param int $id the id of the note used to update + * @param boolean $favorite whether the note should be a favorite or not + * @throws NoteDoesNotExistException if note does not exist + * @return boolean the new favorite state of the note + */ + public function favorite ($id, $favorite, $userId){ + $folder = $this->getFolderForUser($userId); + $file = $this->getFileById($folder, $id); + if(!$this->isNote($file)) { + throw new NoteDoesNotExistException(); + } + $tagger = \OC::$server->getTagManager()->load('files'); + if($favorite) + $tagger->addToFavorites($id); + else + $tagger->removeFromFavorites($id); + + $tags = $tagger->getTagsForObjects([$id]); + return in_array(\OC\Tags::TAG_FAVORITE, $tags[$id]); + } + + /** * Deletes a note * @param int $id the id of the note which should be deleted @@ -161,7 +196,11 @@ private function getFileById ($folder, $id) { if(count($file) <= 0 || !$this->isNote($file[0])) { throw new NoteDoesNotExistException(); } - + $tagger = \OC::$server->getTagManager()->load('files'); + $tags = $tagger->getTagsForObjects([$id]); + if ($tags) { + $file[0]->getFileInfo()->offsetSet('tags', $tags[$id]); + } return $file[0]; } diff --git a/templates/main.php b/templates/main.php index d5ebcf8c..7d8c0400 100644 --- a/templates/main.php +++ b/templates/main.php @@ -42,7 +42,7 @@ + t('New note')); ?> -
  • {{ note.title | noteTitle }} @@ -53,6 +53,12 @@ notes-tooltip data-placement="bottom" ng-click="delete(note.id)"> +
  • From c55a59b1e72433b53425d0090362ffb9b2b92570 Mon Sep 17 00:00:00 2001 From: korelstar Date: Sun, 16 Oct 2016 20:55:47 +0200 Subject: [PATCH 2/7] fix use of hidden classes --- db/note.php | 12 ++++-------- js/public/app.min.js | 2 +- js/public/app.min.js.map | 2 +- service/notesservice.php | 23 +++++++++-------------- 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/db/note.php b/db/note.php index 3fe34d4f..8d48d605 100644 --- a/db/note.php +++ b/db/note.php @@ -44,19 +44,15 @@ public function __construct() { * @param File $file * @return static */ - public static function fromFile(File $file){ + public static function fromFile(File $file, array $tags=[]){ $note = new static(); $note->setId($file->getId()); $note->setContent($file->getContent()); $note->setModified($file->getMTime()); $note->setTitle(pathinfo($file->getName(),PATHINFO_FILENAME)); // remove extension - $fileInfo = $file->getFileInfo(); - if($fileInfo->offsetExists('tags')) { - $tags = $fileInfo->offsetGet('tags'); - if(in_array(\OC\Tags::TAG_FAVORITE, $tags)) { - $note->setFavorite(true); - //unset($tags[array_search(\OC\Tags::TAG_FAVORITE, $tags)]); - } + if(is_array($tags) && in_array(\OC\Tags::TAG_FAVORITE, $tags)) { + $note->setFavorite(true); + //unset($tags[array_search(\OC\Tags::TAG_FAVORITE, $tags)]); } $note->resetUpdatedFields(); return $note; diff --git a/js/public/app.min.js b/js/public/app.min.js index c62615cd..7483ff5d 100644 --- a/js/public/app.min.js +++ b/js/public/app.min.js @@ -1,2 +1,2 @@ -!function(e,n,o,i,r){"use strict";var s=e.module("Notes",["restangular","ngRoute"]).config(["$provide","$routeProvider","RestangularProvider","$httpProvider","$windowProvider",function(t,e,n,i,r){i.defaults.headers.common.requesttoken=o,t.value("Constants",{saveInterval:5e3}),e.when("/notes/:noteId",{templateUrl:"note.html",controller:"NoteController",resolve:{note:["$route","$q","is","Restangular",function(t,e,n,o){var i=e.defer(),r=t.current.params.noteId;return n.loading=!0,o.one("notes",r).get().then(function(t){n.loading=!1,i.resolve(t)},function(){n.loading=!1,i.reject()}),i.promise}]}}).otherwise({redirectTo:"/"});var s=OC.generateUrl("/apps/notes");n.setBaseUrl(s)}]).run(["$rootScope","$location","NotesModel",function(t,e,o){n('link[rel="shortcut icon"]').attr("href",OC.filePath("notes","img","favicon.png")),t.$on("$routeChangeError",function(){var t=o.getAll();if(t.length>0){var n=t.sort(function(t,e){return t.modified>e.modified?1:t.modified0){var n=t.sort(function(t,e){return t.modified>e.modified?1:t.modified\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/* jshint unused: false */\nvar app = angular.module('Notes', ['restangular', 'ngRoute']).\nconfig(function($provide, $routeProvider, RestangularProvider, $httpProvider,\n $windowProvider) {\n 'use strict';\n\n // Always send the CSRF token by default\n $httpProvider.defaults.headers.common.requesttoken = requestToken;\n\n // you have to use $provide inside the config method to provide a globally\n // shared and injectable object\n $provide.value('Constants', {\n saveInterval: 5*1000 // miliseconds\n });\n\n // define your routes that that load templates into the ng-view\n $routeProvider.when('/notes/:noteId', {\n templateUrl: 'note.html',\n controller: 'NoteController',\n resolve: {\n // $routeParams does not work inside resolve so use $route\n // note is the name of the argument that will be injected into the\n // controller\n /* @ngInject */\n note: function ($route, $q, is, Restangular) {\n\n var deferred = $q.defer();\n var noteId = $route.current.params.noteId;\n is.loading = true;\n\n Restangular.one('notes', noteId).get().then(function (note) {\n is.loading = false;\n deferred.resolve(note);\n }, function () {\n is.loading = false;\n deferred.reject();\n });\n\n return deferred.promise;\n }\n }\n }).otherwise({\n redirectTo: '/'\n });\n\n var baseUrl = OC.generateUrl('/apps/notes');\n RestangularProvider.setBaseUrl(baseUrl);\n\n\n\n}).run(function ($rootScope, $location, NotesModel) {\n 'use strict';\n\n $('link[rel=\"shortcut icon\"]').attr(\n\t\t 'href',\n\t\t OC.filePath('notes', 'img', 'favicon.png')\n );\n\n // handle route errors\n $rootScope.$on('$routeChangeError', function () {\n var notes = NotesModel.getAll();\n\n // route change error should redirect to the latest note if possible\n if (notes.length > 0) {\n var sorted = notes.sort(function (a, b) {\n if(a.modified > b.modified) {\n return 1;\n } else if(a.modified < b.modified) {\n return -1;\n } else {\n return 0;\n }\n });\n\n var note = notes[sorted.length-1];\n $location.path('/notes/' + note.id);\n } else {\n $location.path('/');\n }\n });\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('AppController', function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote) {\n if(lastViewedNote !== 0) {\n $location.path('/notes/' + lastViewedNote);\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('NoteController', function($routeParams, $scope, NotesModel,\n SaveQueue, note) {\n 'use strict';\n\n NotesModel.updateIfExists(note);\n\n $scope.note = NotesModel.get($routeParams.noteId);\n\n $scope.isSaving = function () {\n return SaveQueue.isSaving();\n };\n\n $scope.updateTitle = function () {\n $scope.note.title = $scope.note.content.split('\\n')[0] ||\n t('notes', 'New note');\n };\n\n $scope.save = function() {\n var note = $scope.note;\n SaveQueue.add(note);\n };\n\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', function($routeParams, $scope, $location,\n Restangular, NotesModel) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notes = NotesModel.getAll();\n\n var notesResource = Restangular.all('notes');\n\n // initial request for getting all notes\n notesResource.getList().then(function (notes) {\n NotesModel.addAll(notes);\n });\n\n $scope.create = function () {\n notesResource.post().then(function (note) {\n NotesModel.add(note);\n $location.path('/notes/' + note.id);\n });\n };\n\n $scope.delete = function (noteId) {\n var note = NotesModel.get(noteId);\n note.remove().then(function () {\n NotesModel.remove(noteId);\n $scope.$emit('$routeChangeError');\n });\n };\n\n $scope.toggleFavorite = function (noteId) {\n var note = NotesModel.get(noteId);\n note.customPUT({favorite: !note.favorite},\n 'favorite', {}, {}).then(function (favorite) {\n note.favorite = favorite ? true : false;\n });\n };\n\n});\n","/**\n * removes whitespaces and leading #\n */\napp.filter('noteTitle', function () {\n\t'use strict';\n\treturn function (value) {\n \tvalue = value.split('\\n')[0] || 'newNote';\n\t\treturn value.trim().replace(/^#+/g, '');\n\t};\n});\n","app.filter('wordCount', function () {\n\t'use strict';\n\treturn function (value) {\n\t\tif (value && (typeof value === 'string')) {\n\t\t\tvar wordCount = value.split(/\\s+/).filter(\n\t\t\t\t// only count words containing\n\t\t\t\t// at least one alphanumeric character\n\t\t\t\tfunction(value) {\n\t\t\t\t\treturn value.search(/[A-Za-z0-9]/) !== -1;\n\t\t\t\t}\n\t\t\t).length;\n\t\t\treturn window.n('notes', '%n word', '%n words', wordCount);\n\t\t} else {\n\t\t\treturn 0;\n\t\t}\n\t};\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesAutofocus', function () {\n 'use strict';\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.focus();\n }\n };\n});\n","/*global mdEdit*/\napp.directive('editor', ['$timeout', function ($timeout) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\t\t\tvar editor = mdEdit(element[0], {change: function(value) {\n\t\t\t\t$timeout(function(){\n\t\t\t\t\tscope.$apply(function() {\n\t\t\t\t\t\tscope.note.content = value;\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}});\n\t\t\teditor.setValue(scope.note.content);\n\t\t\telement.on('click', '.link', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = $(this).find('.link-params-inner').text();\n\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n}]);\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesIsSaving', function ($window) {\n 'use strict';\n return {\n restrict: 'A',\n scope: {\n 'notesIsSaving': '='\n },\n link: function (scope) {\n $window.onbeforeunload = function () {\n if (scope.notesIsSaving) {\n return t('notes', 'Note is currently saving. Leaving ' +\n 'the page will delete all changes!');\n } else {\n return null;\n }\n };\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/**\n * Like ng-change only that it does not fire when you type faster than\n * 300 ms\n */\napp.directive('notesTimeoutChange', function ($timeout) {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element, attributes) {\n var interval = 300; // 300 miliseconds timeout after typing\n var timeout;\n\n $(element).bind('input propertychange paste', function () {\n $timeout.cancel(timeout);\n\n timeout = $timeout(function () {\n scope.$apply(attributes.notesTimeoutChange);\n }, interval);\n });\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip();\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('is', function () {\n 'use strict';\n\n return {\n loading: false\n };\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// take care of fileconflicts by appending a number\napp.factory('NotesModel', function () {\n 'use strict';\n\n var NotesModel = function () {\n this.notes = [];\n this.notesIds = {};\n };\n\n NotesModel.prototype = {\n addAll: function (notes) {\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('SaveQueue', function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n _flush: function () {\n // if there are no changes dont execute the requests\n var keys = Object.keys(this._queue);\n if(keys.length === 0 || this._flushLock) {\n return;\n } else {\n this._flushLock = true;\n }\n\n var self = this;\n var requests = [];\n\n // iterate over updated objects and run an update request for\n // each one of them\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/* jshint unused: false */\nvar app = angular.module('Notes', ['restangular', 'ngRoute']).\nconfig(function($provide, $routeProvider, RestangularProvider, $httpProvider,\n $windowProvider) {\n 'use strict';\n\n // Always send the CSRF token by default\n $httpProvider.defaults.headers.common.requesttoken = requestToken;\n\n // you have to use $provide inside the config method to provide a globally\n // shared and injectable object\n $provide.value('Constants', {\n saveInterval: 5*1000 // miliseconds\n });\n\n // define your routes that that load templates into the ng-view\n $routeProvider.when('/notes/:noteId', {\n templateUrl: 'note.html',\n controller: 'NoteController',\n resolve: {\n // $routeParams does not work inside resolve so use $route\n // note is the name of the argument that will be injected into the\n // controller\n /* @ngInject */\n note: function ($route, $q, is, Restangular) {\n\n var deferred = $q.defer();\n var noteId = $route.current.params.noteId;\n is.loading = true;\n\n Restangular.one('notes', noteId).get().then(function (note) {\n is.loading = false;\n deferred.resolve(note);\n }, function () {\n is.loading = false;\n deferred.reject();\n });\n\n return deferred.promise;\n }\n }\n }).otherwise({\n redirectTo: '/'\n });\n\n var baseUrl = OC.generateUrl('/apps/notes');\n RestangularProvider.setBaseUrl(baseUrl);\n\n\n\n}).run(function ($rootScope, $location, NotesModel) {\n 'use strict';\n\n $('link[rel=\"shortcut icon\"]').attr(\n\t\t 'href',\n\t\t OC.filePath('notes', 'img', 'favicon.png')\n );\n\n // handle route errors\n $rootScope.$on('$routeChangeError', function () {\n var notes = NotesModel.getAll();\n\n // route change error should redirect to the latest note if possible\n if (notes.length > 0) {\n var sorted = notes.sort(function (a, b) {\n if(a.modified > b.modified) {\n return 1;\n } else if(a.modified < b.modified) {\n return -1;\n } else {\n return 0;\n }\n });\n\n var note = notes[sorted.length-1];\n $location.path('/notes/' + note.id);\n } else {\n $location.path('/');\n }\n });\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('AppController', function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote) {\n if(lastViewedNote !== 0) {\n $location.path('/notes/' + lastViewedNote);\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('NoteController', function($routeParams, $scope, NotesModel,\n SaveQueue, note) {\n 'use strict';\n\n NotesModel.updateIfExists(note);\n\n $scope.note = NotesModel.get($routeParams.noteId);\n\n $scope.isSaving = function () {\n return SaveQueue.isSaving();\n };\n\n $scope.updateTitle = function () {\n $scope.note.title = $scope.note.content.split('\\n')[0] ||\n t('notes', 'New note');\n };\n\n $scope.save = function() {\n var note = $scope.note;\n SaveQueue.add(note);\n };\n\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', function($routeParams, $scope, $location,\n Restangular, NotesModel) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notes = NotesModel.getAll();\n\n var notesResource = Restangular.all('notes');\n\n // initial request for getting all notes\n notesResource.getList().then(function (notes) {\n NotesModel.addAll(notes);\n });\n\n $scope.create = function () {\n notesResource.post().then(function (note) {\n NotesModel.add(note);\n $location.path('/notes/' + note.id);\n });\n };\n\n $scope.delete = function (noteId) {\n var note = NotesModel.get(noteId);\n note.remove().then(function () {\n NotesModel.remove(noteId);\n $scope.$emit('$routeChangeError');\n });\n };\n\n $scope.toggleFavorite = function (noteId) {\n var note = NotesModel.get(noteId);\n note.customPUT({favorite: !note.favorite},\n 'favorite', {}, {}).then(function (favorite) {\n note.favorite = favorite ? true : false;\n });\n };\n\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesAutofocus', function () {\n 'use strict';\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.focus();\n }\n };\n});\n","/*global mdEdit*/\napp.directive('editor', ['$timeout', function ($timeout) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\t\t\tvar editor = mdEdit(element[0], {change: function(value) {\n\t\t\t\t$timeout(function(){\n\t\t\t\t\tscope.$apply(function() {\n\t\t\t\t\t\tscope.note.content = value;\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}});\n\t\t\teditor.setValue(scope.note.content);\n\t\t\telement.on('click', '.link', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = $(this).find('.link-params-inner').text();\n\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n}]);\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesIsSaving', function ($window) {\n 'use strict';\n return {\n restrict: 'A',\n scope: {\n 'notesIsSaving': '='\n },\n link: function (scope) {\n $window.onbeforeunload = function () {\n if (scope.notesIsSaving) {\n return t('notes', 'Note is currently saving. Leaving ' +\n 'the page will delete all changes!');\n } else {\n return null;\n }\n };\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/**\n * Like ng-change only that it does not fire when you type faster than\n * 300 ms\n */\napp.directive('notesTimeoutChange', function ($timeout) {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element, attributes) {\n var interval = 300; // 300 miliseconds timeout after typing\n var timeout;\n\n $(element).bind('input propertychange paste', function () {\n $timeout.cancel(timeout);\n\n timeout = $timeout(function () {\n scope.$apply(attributes.notesTimeoutChange);\n }, interval);\n });\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip();\n }\n };\n});\n","/**\n * removes whitespaces and leading #\n */\napp.filter('noteTitle', function () {\n\t'use strict';\n\treturn function (value) {\n \tvalue = value.split('\\n')[0] || 'newNote';\n\t\treturn value.trim().replace(/^#+/g, '');\n\t};\n});\n","app.filter('wordCount', function () {\n\t'use strict';\n\treturn function (value) {\n\t\tif (value && (typeof value === 'string')) {\n\t\t\tvar wordCount = value.split(/\\s+/).filter(\n\t\t\t\t// only count words containing\n\t\t\t\t// at least one alphanumeric character\n\t\t\t\tfunction(value) {\n\t\t\t\t\treturn value.search(/[A-Za-z0-9]/) !== -1;\n\t\t\t\t}\n\t\t\t).length;\n\t\t\treturn window.n('notes', '%n word', '%n words', wordCount);\n\t\t} else {\n\t\t\treturn 0;\n\t\t}\n\t};\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('is', function () {\n 'use strict';\n\n return {\n loading: false\n };\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// take care of fileconflicts by appending a number\napp.factory('NotesModel', function () {\n 'use strict';\n\n var NotesModel = function () {\n this.notes = [];\n this.notesIds = {};\n };\n\n NotesModel.prototype = {\n addAll: function (notes) {\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('SaveQueue', function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n _flush: function () {\n // if there are no changes dont execute the requests\n var keys = Object.keys(this._queue);\n if(keys.length === 0 || this._flushLock) {\n return;\n } else {\n this._flushLock = true;\n }\n\n var self = this;\n var requests = [];\n\n // iterate over updated objects and run an update request for\n // each one of them\n for(var i=0; igetTagManager()->load('files'); $tags = $tagger->getTagsForObjects(array_keys($filesById)); - if ($tags) { - foreach ($tags as $fileId => $fileTags) { - $filesById[$fileId]->getFileInfo()->offsetSet('tags', $fileTags); - } - } $notes = []; - foreach($filesById as $file) { - $notes[] = Note::fromFile($file); + foreach($filesById as $id=>$file) { + $notes[] = Note::fromFile($file, array_key_exists($id, $tags) ? $tags[$id] : []); } return $notes; @@ -76,9 +71,14 @@ public function getAll ($userId){ */ public function get ($id, $userId) { $folder = $this->getFolderForUser($userId); - return Note::fromFile($this->getFileById($folder, $id)); + return Note::fromFile($this->getFileById($folder, $id), $this->getTags($id)); } + private function getTags ($id) { + $tagger = \OC::$server->getTagManager()->load('files'); + $tags = $tagger->getTagsForObjects([$id]); + return $tags[$id]; + } /** * Creates a note and returns the empty note @@ -142,7 +142,7 @@ public function update ($id, $content, $userId){ $file->putContent($content); - return Note::fromFile($file); + return Note::fromFile($file, $this->getTags($id)); } @@ -196,11 +196,6 @@ private function getFileById ($folder, $id) { if(count($file) <= 0 || !$this->isNote($file[0])) { throw new NoteDoesNotExistException(); } - $tagger = \OC::$server->getTagManager()->load('files'); - $tags = $tagger->getTagsForObjects([$id]); - if ($tags) { - $file[0]->getFileInfo()->offsetSet('tags', $tags[$id]); - } return $file[0]; } From 190331714cc97efc96cf93a36d6bd9bc696b5fa8 Mon Sep 17 00:00:00 2001 From: korelstar Date: Sun, 16 Oct 2016 21:00:05 +0200 Subject: [PATCH 3/7] minor optimizations requested by Henni --- css/notes.css | 5 +---- js/app/services/notesmodel.js | 2 +- js/public/app.min.js.map | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/css/notes.css b/css/notes.css index f549c2c9..0ff92022 100644 --- a/css/notes.css +++ b/css/notes.css @@ -42,10 +42,7 @@ display: none; } -#app-navigation .active > .utils .icon-delete { - display: inline-block; - opacity: .3; -} +#app-navigation .active > .utils .icon-delete, #app-navigation .active > .utils .icon-star { display: inline-block; opacity: .3; diff --git a/js/app/services/notesmodel.js b/js/app/services/notesmodel.js index 4e074060..3505bb57 100644 --- a/js/app/services/notesmodel.js +++ b/js/app/services/notesmodel.js @@ -50,7 +50,7 @@ app.factory('NotesModel', function () { break; } } - }, + } }; return new NotesModel(); diff --git a/js/public/app.min.js.map b/js/public/app.min.js.map index ed2e01b7..a159bd91 100644 --- a/js/public/app.min.js.map +++ b/js/public/app.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["app.js","controllers/appcontroller.js","controllers/notecontroller.js","controllers/notescontroller.js","directives/autofocus.js","directives/editor.js","directives/issaving.js","directives/timeoutchange.js","directives/tooltip.js","filters/noteTitle.js","filters/wordCount.js","services/is.js","services/notesmodel.js","services/savequeue.js"],"names":["angular","$","requestToken","mdEdit","undefined","app","module","config","$provide","$routeProvider","RestangularProvider","$httpProvider","$windowProvider","defaults","headers","common","requesttoken","value","saveInterval","when","templateUrl","controller","resolve","note","$route","$q","is","Restangular","deferred","defer","noteId","current","params","loading","one","get","then","reject","promise","otherwise","redirectTo","baseUrl","OC","generateUrl","setBaseUrl","run","$rootScope","$location","NotesModel","attr","filePath","$on","notes","getAll","length","sorted","sort","a","b","modified","path","id","$scope","init","lastViewedNote","$routeParams","SaveQueue","updateIfExists","isSaving","updateTitle","title","content","split","t","save","add","route","notesResource","all","getList","addAll","create","post","remove","$emit","toggleFavorite","customPUT","favorite","directive","restrict","link","scope","element","focus","$timeout","editor","change","$apply","setValue","on","event","ctrlKey","url","this","find","text","window","open","$window","notesIsSaving","onbeforeunload","attributes","timeout","interval","bind","cancel","notesTimeoutChange","tooltip","filter","trim","replace","wordCount","search","n","factory","notesIds","prototype","i","updated","isDefined","push","splice","_queue","_flushLock","_flush","keys","Object","self","requests","put","_noteUpdateRequest","response","jQuery","oc_requesttoken"],"mappings":"CAAA,SAAAA,EAAAC,EAAAC,EAAAC,EAAAC,GAAA,YAQA,IAAAC,GAAAL,EAAAM,OAAA,SAAA,cAAA,YACAC,QAAA,WAAA,iBAAA,sBAAA,gBAAA,kBAAA,SAAAC,EAAAC,EAAAC,EAAAC,EACAC,GAIAD,EAAAE,SAAAC,QAAAC,OAAAC,aAAAd,EAIAM,EAAAS,MAAA,aACAC,aAAA,MAIAT,EAAAU,KAAA,kBACAC,YAAA,YACAC,WAAA,iBACAC,SAKAC,MAAA,SAAA,KAAA,KAAA,cAAA,SAAAC,EAAAC,EAAAC,EAAAC,GAEA,GAAAC,GAAAH,EAAAI,QACAC,EAAAN,EAAAO,QAAAC,OAAAF,MAWA,OAVAJ,GAAAO,SAAA,EAEAN,EAAAO,IAAA,QAAAJ,GAAAK,MAAAC,KAAA,SAAAb,GACAG,EAAAO,SAAA,EACAL,EAAAN,QAAAC,IACA,WACAG,EAAAO,SAAA,EACAL,EAAAS,WAGAT,EAAAU,aAGAC,WACAC,WAAA,KAGA,IAAAC,GAAAC,GAAAC,YAAA,cACAjC,GAAAkC,WAAAH,MAIAI,KAAA,aAAA,YAAA,aAAA,SAAAC,EAAAC,EAAAC,GAGA/C,EAAA,6BAAAgD,KACA,OACAP,GAAAQ,SAAA,QAAA,MAAA,gBAIAJ,EAAAK,IAAA,oBAAA,WACA,GAAAC,GAAAJ,EAAAK,QAGA,IAAAD,EAAAE,OAAA,EAAA,CACA,GAAAC,GAAAH,EAAAI,KAAA,SAAAC,EAAAC,GACA,MAAAD,GAAAE,SAAAD,EAAAC,SACA,EACAF,EAAAE,SAAAD,EAAAC,YAGA,IAIApC,EAAA6B,EAAAG,EAAAD,OAAA,EACAP,GAAAa,KAAA,UAAArC,EAAAsC,QAEAd,GAAAa,KAAA,SC7EAvD,GAAAgB,WAAA,iBAAA,SAAA,YAAA,KAAA,SAAAyC,EAAAf,EAAArB,GAGAoC,EAAApC,GAAAA,EAEAoC,EAAAC,KAAA,SAAAC,GACA,IAAAA,GACAjB,EAAAa,KAAA,UAAAI,OCPA3D,EAAAgB,WAAA,kBAAA,eAAA,SAAA,aAAA,YAAA,OAAA,SAAA4C,EAAAH,EAAAd,EACAkB,EAAA3C,GAGAyB,EAAAmB,eAAA5C,GAEAuC,EAAAvC,KAAAyB,EAAAb,IAAA8B,EAAAnC,QAEAgC,EAAAM,SAAA,WACA,MAAAF,GAAAE,YAGAN,EAAAO,YAAA,WACAP,EAAAvC,KAAA+C,MAAAR,EAAAvC,KAAAgD,QAAAC,MAAA,MAAA,IACAC,EAAA,QAAA,aAGAX,EAAAY,KAAA,WACA,GAAAnD,GAAAuC,EAAAvC,IACA2C,GAAAS,IAAApD,OClBAlB,EAAAgB,WAAA,mBAAA,eAAA,SAAA,YAAA,cAAA,aAAA,SAAA4C,EAAAH,EAAAf,EACApB,EAAAqB,GAGAc,EAAAc,MAAAX,EACAH,EAAAV,MAAAJ,EAAAK,QAEA,IAAAwB,GAAAlD,EAAAmD,IAAA,QAGAD,GAAAE,UAAA3C,KAAA,SAAAgB,GACAJ,EAAAgC,OAAA5B,KAGAU,EAAAmB,OAAA,WACAJ,EAAAK,OAAA9C,KAAA,SAAAb,GACAyB,EAAA2B,IAAApD,GACAwB,EAAAa,KAAA,UAAArC,EAAAsC,OAIAC,EAAAA,UAAA,SAAAhC,GACA,GAAAP,GAAAyB,EAAAb,IAAAL,EACAP,GAAA4D,SAAA/C,KAAA,WACAY,EAAAmC,OAAArD,GACAgC,EAAAsB,MAAA,wBAIAtB,EAAAuB,eAAA,SAAAvD,GACA,GAAAP,GAAAyB,EAAAb,IAAAL,EACAP,GAAA+D,WAAAC,UAAAhE,EAAAgE,UACA,kBAAAnD,KAAA,SAAAmD,GACAhE,EAAAgE,WAAAA,QClCAlF,EAAAmF,UAAA,iBAAA,WAEA,OACAC,SAAA,IACAC,KAAA,SAAAC,EAAAC,GACAA,EAAAC,YCXAxF,EAAAmF,UAAA,UAAA,WAAA,SAAAM,GAEA,OACAL,SAAA,IACAC,KAAA,SAAAC,EAAAC,GACA,GAAAG,GAAA5F,EAAAyF,EAAA,IAAAI,OAAA,SAAA/E,GACA6E,EAAA,WACAH,EAAAM,OAAA,WACAN,EAAApE,KAAAgD,QAAAtD,EACA0E,EAAAtB,oBAIA0B,GAAAG,SAAAP,EAAApE,KAAAgD,SACAqB,EAAAO,GAAA,QAAA,QAAA,SAAAC,GACA,GAAAA,EAAAC,QAAA,CACA,GAAAC,GAAArG,EAAAsG,MAAAC,KAAA,sBAAAC,MACAC,QAAAC,KAAAL,EAAA,kBCXAjG,EAAAmF,UAAA,iBAAA,UAAA,SAAAoB,GAEA,OACAnB,SAAA,IACAE,OACAkB,cAAA,KAEAnB,KAAA,SAAAC,GACAiB,EAAAE,eAAA,WACA,MAAAnB,GAAAkB,cACApC,EAAA,QAAA,uEAGA,WCTApE,EAAAmF,UAAA,sBAAA,WAAA,SAAAM,GAGA,OACAL,SAAA,IACAC,KAAA,SAAAC,EAAAC,EAAAmB,GACA,GACAC,GADAC,EAAA,GAGAhH,GAAA2F,GAAAsB,KAAA,6BAAA,WACApB,EAAAqB,OAAAH,GAEAA,EAAAlB,EAAA,WACAH,EAAAM,OAAAc,EAAAK,qBACAH,UClBA5G,EAAAmF,UAAA,eAAA,WAGA,OACAC,SAAA,IACAC,KAAA,SAAAC,EAAAC,GACAA,EAAAyB,cCVAhH,EAAAiH,OAAA,YAAA,WAEA,MAAA,UAAArG,GAEA,MADAA,GAAAA,EAAAuD,MAAA,MAAA,IAAA,UACAvD,EAAAsG,OAAAC,QAAA,OAAA,OCPAnH,EAAAiH,OAAA,YAAA,WAEA,MAAA,UAAArG,GACA,GAAAA,GAAA,gBAAAA,GAAA,CACA,GAAAwG,GAAAxG,EAAAuD,MAAA,OAAA8C,OAGA,SAAArG,GACA,MAAAA,GAAAyG,OAAA,sBAEApE,MACA,OAAAoD,QAAAiB,EAAA,QAAA,UAAA,WAAAF,GAEA,MAAA,MCNApH,EAAAuH,QAAA,KAAA,WAGA,OACA3F,SAAA,KCHA5B,EAAAuH,QAAA,aAAA,WAGA,GAAA5E,GAAA,WACAuD,KAAAnD,SACAmD,KAAAsB,YA0CA,OAvCA7E,GAAA8E,WACA9C,OAAA,SAAA5B,GACA,IAAA,GAAA2E,GAAA,EAAAA,EAAA3E,EAAAE,OAAAyE,GAAA,EACAxB,KAAA5B,IAAAvB,EAAA2E,KAGApD,IAAA,SAAApD,GACAgF,KAAApC,eAAA5C,IAEA8B,OAAA,WACA,MAAAkD,MAAAnD,OAEAjB,IAAA,SAAA0B,GACA,MAAA0C,MAAAsB,SAAAhE,IAEAM,eAAA,SAAA6D,GACA,GAAAzG,GAAAgF,KAAAsB,SAAAG,EAAAnE,GACA7D,GAAAiI,UAAA1G,IACAA,EAAA+C,MAAA0D,EAAA1D,MACA/C,EAAAoC,SAAAqE,EAAArE,SACApC,EAAAgD,QAAAyD,EAAAzD,QACAhD,EAAAgE,SAAAyC,EAAAzC,WAEAgB,KAAAnD,MAAA8E,KAAAF,GACAzB,KAAAsB,SAAAG,EAAAnE,IAAAmE,IAGA7C,OAAA,SAAAtB,GACA,IAAA,GAAAkE,GAAA,EAAAA,EAAAxB,KAAAnD,MAAAE,OAAAyE,GAAA,EAAA,CACA,GAAAxG,GAAAgF,KAAAnD,MAAA2E,EACA,IAAAxG,EAAAsC,KAAAA,EAAA,CACA0C,KAAAnD,MAAA+E,OAAAJ,EAAA,SACAxB,MAAAsB,SAAAhE,EACA,WAMA,GAAAb,KChDA3C,EAAAuH,QAAA,aAAA,KAAA,SAAAnG,GAGA,GAAAyC,GAAA,WACAqC,KAAA6B,UACA7B,KAAA8B,YAAA,EAgDA,OA7CAnE,GAAA4D,WACAnD,IAAA,SAAApD,GACAgF,KAAA6B,OAAA7G,EAAAsC,IAAAtC,EACAgF,KAAA+B,UAEAA,OAAA,WAEA,GAAAC,GAAAC,OAAAD,KAAAhC,KAAA6B,OACA,IAAA,IAAAG,EAAAjF,SAAAiD,KAAA8B,WAAA,CAGA9B,KAAA8B,YAAA,CAQA,KAAA,GALAI,GAAAlC,KACAmC,KAIAX,EAAA,EAAAA,EAAAQ,EAAAjF,OAAAyE,GAAA,EAAA,CACA,GAAAxG,GAAAgF,KAAA6B,OAAAG,EAAAR,GAGAW,GAAAR,KAAA3G,EAAAoH,MAAAvG,KACAmE,KAAAqC,mBAAA1B,KAAA,KAAA3F,KAGAgF,KAAA6B,UAIA3G,EAAAqD,IAAA4D,GAAAtG,KAAA,WACAqG,EAAAJ,YAAA,EACAI,EAAAH,aAGAM,mBAAA,SAAArH,EAAAsH,GACAtH,EAAA+C,MAAAuE,EAAAvE,MACA/C,EAAAoC,SAAAkF,EAAAlF,UAEAS,SAAA,WACA,MAAAmC,MAAA8B,aAIA,GAAAnE,OACAlE,QAAA8I,OAAAC,gBAAA5I","file":"app.min.js","sourcesContent":["/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/* jshint unused: false */\nvar app = angular.module('Notes', ['restangular', 'ngRoute']).\nconfig(function($provide, $routeProvider, RestangularProvider, $httpProvider,\n $windowProvider) {\n 'use strict';\n\n // Always send the CSRF token by default\n $httpProvider.defaults.headers.common.requesttoken = requestToken;\n\n // you have to use $provide inside the config method to provide a globally\n // shared and injectable object\n $provide.value('Constants', {\n saveInterval: 5*1000 // miliseconds\n });\n\n // define your routes that that load templates into the ng-view\n $routeProvider.when('/notes/:noteId', {\n templateUrl: 'note.html',\n controller: 'NoteController',\n resolve: {\n // $routeParams does not work inside resolve so use $route\n // note is the name of the argument that will be injected into the\n // controller\n /* @ngInject */\n note: function ($route, $q, is, Restangular) {\n\n var deferred = $q.defer();\n var noteId = $route.current.params.noteId;\n is.loading = true;\n\n Restangular.one('notes', noteId).get().then(function (note) {\n is.loading = false;\n deferred.resolve(note);\n }, function () {\n is.loading = false;\n deferred.reject();\n });\n\n return deferred.promise;\n }\n }\n }).otherwise({\n redirectTo: '/'\n });\n\n var baseUrl = OC.generateUrl('/apps/notes');\n RestangularProvider.setBaseUrl(baseUrl);\n\n\n\n}).run(function ($rootScope, $location, NotesModel) {\n 'use strict';\n\n $('link[rel=\"shortcut icon\"]').attr(\n\t\t 'href',\n\t\t OC.filePath('notes', 'img', 'favicon.png')\n );\n\n // handle route errors\n $rootScope.$on('$routeChangeError', function () {\n var notes = NotesModel.getAll();\n\n // route change error should redirect to the latest note if possible\n if (notes.length > 0) {\n var sorted = notes.sort(function (a, b) {\n if(a.modified > b.modified) {\n return 1;\n } else if(a.modified < b.modified) {\n return -1;\n } else {\n return 0;\n }\n });\n\n var note = notes[sorted.length-1];\n $location.path('/notes/' + note.id);\n } else {\n $location.path('/');\n }\n });\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('AppController', function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote) {\n if(lastViewedNote !== 0) {\n $location.path('/notes/' + lastViewedNote);\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('NoteController', function($routeParams, $scope, NotesModel,\n SaveQueue, note) {\n 'use strict';\n\n NotesModel.updateIfExists(note);\n\n $scope.note = NotesModel.get($routeParams.noteId);\n\n $scope.isSaving = function () {\n return SaveQueue.isSaving();\n };\n\n $scope.updateTitle = function () {\n $scope.note.title = $scope.note.content.split('\\n')[0] ||\n t('notes', 'New note');\n };\n\n $scope.save = function() {\n var note = $scope.note;\n SaveQueue.add(note);\n };\n\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', function($routeParams, $scope, $location,\n Restangular, NotesModel) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notes = NotesModel.getAll();\n\n var notesResource = Restangular.all('notes');\n\n // initial request for getting all notes\n notesResource.getList().then(function (notes) {\n NotesModel.addAll(notes);\n });\n\n $scope.create = function () {\n notesResource.post().then(function (note) {\n NotesModel.add(note);\n $location.path('/notes/' + note.id);\n });\n };\n\n $scope.delete = function (noteId) {\n var note = NotesModel.get(noteId);\n note.remove().then(function () {\n NotesModel.remove(noteId);\n $scope.$emit('$routeChangeError');\n });\n };\n\n $scope.toggleFavorite = function (noteId) {\n var note = NotesModel.get(noteId);\n note.customPUT({favorite: !note.favorite},\n 'favorite', {}, {}).then(function (favorite) {\n note.favorite = favorite ? true : false;\n });\n };\n\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesAutofocus', function () {\n 'use strict';\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.focus();\n }\n };\n});\n","/*global mdEdit*/\napp.directive('editor', ['$timeout', function ($timeout) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\t\t\tvar editor = mdEdit(element[0], {change: function(value) {\n\t\t\t\t$timeout(function(){\n\t\t\t\t\tscope.$apply(function() {\n\t\t\t\t\t\tscope.note.content = value;\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}});\n\t\t\teditor.setValue(scope.note.content);\n\t\t\telement.on('click', '.link', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = $(this).find('.link-params-inner').text();\n\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n}]);\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesIsSaving', function ($window) {\n 'use strict';\n return {\n restrict: 'A',\n scope: {\n 'notesIsSaving': '='\n },\n link: function (scope) {\n $window.onbeforeunload = function () {\n if (scope.notesIsSaving) {\n return t('notes', 'Note is currently saving. Leaving ' +\n 'the page will delete all changes!');\n } else {\n return null;\n }\n };\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/**\n * Like ng-change only that it does not fire when you type faster than\n * 300 ms\n */\napp.directive('notesTimeoutChange', function ($timeout) {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element, attributes) {\n var interval = 300; // 300 miliseconds timeout after typing\n var timeout;\n\n $(element).bind('input propertychange paste', function () {\n $timeout.cancel(timeout);\n\n timeout = $timeout(function () {\n scope.$apply(attributes.notesTimeoutChange);\n }, interval);\n });\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip();\n }\n };\n});\n","/**\n * removes whitespaces and leading #\n */\napp.filter('noteTitle', function () {\n\t'use strict';\n\treturn function (value) {\n \tvalue = value.split('\\n')[0] || 'newNote';\n\t\treturn value.trim().replace(/^#+/g, '');\n\t};\n});\n","app.filter('wordCount', function () {\n\t'use strict';\n\treturn function (value) {\n\t\tif (value && (typeof value === 'string')) {\n\t\t\tvar wordCount = value.split(/\\s+/).filter(\n\t\t\t\t// only count words containing\n\t\t\t\t// at least one alphanumeric character\n\t\t\t\tfunction(value) {\n\t\t\t\t\treturn value.search(/[A-Za-z0-9]/) !== -1;\n\t\t\t\t}\n\t\t\t).length;\n\t\t\treturn window.n('notes', '%n word', '%n words', wordCount);\n\t\t} else {\n\t\t\treturn 0;\n\t\t}\n\t};\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('is', function () {\n 'use strict';\n\n return {\n loading: false\n };\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// take care of fileconflicts by appending a number\napp.factory('NotesModel', function () {\n 'use strict';\n\n var NotesModel = function () {\n this.notes = [];\n this.notesIds = {};\n };\n\n NotesModel.prototype = {\n addAll: function (notes) {\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('SaveQueue', function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n _flush: function () {\n // if there are no changes dont execute the requests\n var keys = Object.keys(this._queue);\n if(keys.length === 0 || this._flushLock) {\n return;\n } else {\n this._flushLock = true;\n }\n\n var self = this;\n var requests = [];\n\n // iterate over updated objects and run an update request for\n // each one of them\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/* jshint unused: false */\nvar app = angular.module('Notes', ['restangular', 'ngRoute']).\nconfig(function($provide, $routeProvider, RestangularProvider, $httpProvider,\n $windowProvider) {\n 'use strict';\n\n // Always send the CSRF token by default\n $httpProvider.defaults.headers.common.requesttoken = requestToken;\n\n // you have to use $provide inside the config method to provide a globally\n // shared and injectable object\n $provide.value('Constants', {\n saveInterval: 5*1000 // miliseconds\n });\n\n // define your routes that that load templates into the ng-view\n $routeProvider.when('/notes/:noteId', {\n templateUrl: 'note.html',\n controller: 'NoteController',\n resolve: {\n // $routeParams does not work inside resolve so use $route\n // note is the name of the argument that will be injected into the\n // controller\n /* @ngInject */\n note: function ($route, $q, is, Restangular) {\n\n var deferred = $q.defer();\n var noteId = $route.current.params.noteId;\n is.loading = true;\n\n Restangular.one('notes', noteId).get().then(function (note) {\n is.loading = false;\n deferred.resolve(note);\n }, function () {\n is.loading = false;\n deferred.reject();\n });\n\n return deferred.promise;\n }\n }\n }).otherwise({\n redirectTo: '/'\n });\n\n var baseUrl = OC.generateUrl('/apps/notes');\n RestangularProvider.setBaseUrl(baseUrl);\n\n\n\n}).run(function ($rootScope, $location, NotesModel) {\n 'use strict';\n\n $('link[rel=\"shortcut icon\"]').attr(\n\t\t 'href',\n\t\t OC.filePath('notes', 'img', 'favicon.png')\n );\n\n // handle route errors\n $rootScope.$on('$routeChangeError', function () {\n var notes = NotesModel.getAll();\n\n // route change error should redirect to the latest note if possible\n if (notes.length > 0) {\n var sorted = notes.sort(function (a, b) {\n if(a.modified > b.modified) {\n return 1;\n } else if(a.modified < b.modified) {\n return -1;\n } else {\n return 0;\n }\n });\n\n var note = notes[sorted.length-1];\n $location.path('/notes/' + note.id);\n } else {\n $location.path('/');\n }\n });\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('AppController', function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote) {\n if(lastViewedNote !== 0) {\n $location.path('/notes/' + lastViewedNote);\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('NoteController', function($routeParams, $scope, NotesModel,\n SaveQueue, note) {\n 'use strict';\n\n NotesModel.updateIfExists(note);\n\n $scope.note = NotesModel.get($routeParams.noteId);\n\n $scope.isSaving = function () {\n return SaveQueue.isSaving();\n };\n\n $scope.updateTitle = function () {\n $scope.note.title = $scope.note.content.split('\\n')[0] ||\n t('notes', 'New note');\n };\n\n $scope.save = function() {\n var note = $scope.note;\n SaveQueue.add(note);\n };\n\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', function($routeParams, $scope, $location,\n Restangular, NotesModel) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notes = NotesModel.getAll();\n\n var notesResource = Restangular.all('notes');\n\n // initial request for getting all notes\n notesResource.getList().then(function (notes) {\n NotesModel.addAll(notes);\n });\n\n $scope.create = function () {\n notesResource.post().then(function (note) {\n NotesModel.add(note);\n $location.path('/notes/' + note.id);\n });\n };\n\n $scope.delete = function (noteId) {\n var note = NotesModel.get(noteId);\n note.remove().then(function () {\n NotesModel.remove(noteId);\n $scope.$emit('$routeChangeError');\n });\n };\n\n $scope.toggleFavorite = function (noteId) {\n var note = NotesModel.get(noteId);\n note.customPUT({favorite: !note.favorite},\n 'favorite', {}, {}).then(function (favorite) {\n note.favorite = favorite ? true : false;\n });\n };\n\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesAutofocus', function () {\n 'use strict';\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.focus();\n }\n };\n});\n","/*global mdEdit*/\napp.directive('editor', ['$timeout', function ($timeout) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\t\t\tvar editor = mdEdit(element[0], {change: function(value) {\n\t\t\t\t$timeout(function(){\n\t\t\t\t\tscope.$apply(function() {\n\t\t\t\t\t\tscope.note.content = value;\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t}});\n\t\t\teditor.setValue(scope.note.content);\n\t\t\telement.on('click', '.link', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = $(this).find('.link-params-inner').text();\n\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n}]);\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesIsSaving', function ($window) {\n 'use strict';\n return {\n restrict: 'A',\n scope: {\n 'notesIsSaving': '='\n },\n link: function (scope) {\n $window.onbeforeunload = function () {\n if (scope.notesIsSaving) {\n return t('notes', 'Note is currently saving. Leaving ' +\n 'the page will delete all changes!');\n } else {\n return null;\n }\n };\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/**\n * Like ng-change only that it does not fire when you type faster than\n * 300 ms\n */\napp.directive('notesTimeoutChange', function ($timeout) {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element, attributes) {\n var interval = 300; // 300 miliseconds timeout after typing\n var timeout;\n\n $(element).bind('input propertychange paste', function () {\n $timeout.cancel(timeout);\n\n timeout = $timeout(function () {\n scope.$apply(attributes.notesTimeoutChange);\n }, interval);\n });\n }\n };\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip();\n }\n };\n});\n","/**\n * removes whitespaces and leading #\n */\napp.filter('noteTitle', function () {\n\t'use strict';\n\treturn function (value) {\n \tvalue = value.split('\\n')[0] || 'newNote';\n\t\treturn value.trim().replace(/^#+/g, '');\n\t};\n});\n","app.filter('wordCount', function () {\n\t'use strict';\n\treturn function (value) {\n\t\tif (value && (typeof value === 'string')) {\n\t\t\tvar wordCount = value.split(/\\s+/).filter(\n\t\t\t\t// only count words containing\n\t\t\t\t// at least one alphanumeric character\n\t\t\t\tfunction(value) {\n\t\t\t\t\treturn value.search(/[A-Za-z0-9]/) !== -1;\n\t\t\t\t}\n\t\t\t).length;\n\t\t\treturn window.n('notes', '%n word', '%n words', wordCount);\n\t\t} else {\n\t\t\treturn 0;\n\t\t}\n\t};\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('is', function () {\n 'use strict';\n\n return {\n loading: false\n };\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// take care of fileconflicts by appending a number\napp.factory('NotesModel', function () {\n 'use strict';\n\n var NotesModel = function () {\n this.notes = [];\n this.notesIds = {};\n };\n\n NotesModel.prototype = {\n addAll: function (notes) {\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('SaveQueue', function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n _flush: function () {\n // if there are no changes dont execute the requests\n var keys = Object.keys(this._queue);\n if(keys.length === 0 || this._flushLock) {\n return;\n } else {\n this._flushLock = true;\n }\n\n var self = this;\n var requests = [];\n\n // iterate over updated objects and run an update request for\n // each one of them\n for(var i=0; i Date: Wed, 19 Oct 2016 21:24:03 +0200 Subject: [PATCH 4/7] remove array annotation --- db/note.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/note.php b/db/note.php index 8d48d605..48955974 100644 --- a/db/note.php +++ b/db/note.php @@ -44,7 +44,7 @@ public function __construct() { * @param File $file * @return static */ - public static function fromFile(File $file, array $tags=[]){ + public static function fromFile(File $file, $tags=[]){ $note = new static(); $note->setId($file->getId()); $note->setContent($file->getContent()); From 7dfe6bc4d14348f6b39bf4f8109d695d70830904 Mon Sep 17 00:00:00 2001 From: korelstar Date: Thu, 20 Oct 2016 13:22:38 +0200 Subject: [PATCH 5/7] check if tagger is null --- service/notesservice.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/service/notesservice.php b/service/notesservice.php index 84f5f270..19f7e634 100644 --- a/service/notesservice.php +++ b/service/notesservice.php @@ -51,7 +51,11 @@ public function getAll ($userId){ } } $tagger = \OC::$server->getTagManager()->load('files'); - $tags = $tagger->getTagsForObjects(array_keys($filesById)); + if($tagger==null) { + $tags = []; + } else { + $tags = $tagger->getTagsForObjects(array_keys($filesById)); + } $notes = []; foreach($filesById as $id=>$file) { @@ -76,8 +80,12 @@ public function get ($id, $userId) { private function getTags ($id) { $tagger = \OC::$server->getTagManager()->load('files'); - $tags = $tagger->getTagsForObjects([$id]); - return $tags[$id]; + if($tagger==null) { + $tags = []; + } else { + $tags = $tagger->getTagsForObjects([$id]); + } + return array_key_exists($id, $tags) ? $tags[$id] : []; } /** From 94b5d812d1a10d1004e5da21e6acce59a389cc02 Mon Sep 17 00:00:00 2001 From: korelstar Date: Thu, 20 Oct 2016 13:31:28 +0200 Subject: [PATCH 6/7] adjust tests for new favorite feature --- tests/unit/controller/NotesApiControllerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/controller/NotesApiControllerTest.php b/tests/unit/controller/NotesApiControllerTest.php index 10bb7253..ec34b778 100644 --- a/tests/unit/controller/NotesApiControllerTest.php +++ b/tests/unit/controller/NotesApiControllerTest.php @@ -88,10 +88,12 @@ public function testGetAllHide(){ $this->assertEquals(json_encode([ [ 'modified' => 123, + 'favorite' => false, 'id' => 3, ], [ 'modified' => 111, + 'favorite' => false, 'id' => 4, ] ]), json_encode($response->getData())); @@ -136,6 +138,7 @@ public function testGetHide(){ $this->assertEquals(json_encode([ 'modified' => 123, + 'favorite' => false, 'id' => 3, ]), json_encode($response->getData())); $this->assertTrue($response instanceof DataResponse); From 3aed82d2b61d97b5db4f783d2af5a3281a0d8acb Mon Sep 17 00:00:00 2001 From: korelstar Date: Fri, 21 Oct 2016 13:48:23 +0200 Subject: [PATCH 7/7] allow setting favorite over API --- controller/notesapicontroller.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/controller/notesapicontroller.php b/controller/notesapicontroller.php index e8252419..7071bbcf 100644 --- a/controller/notesapicontroller.php +++ b/controller/notesapicontroller.php @@ -125,11 +125,19 @@ public function create($content) { * * @param int $id * @param string $content + * @param boolean $favorite * @return DataResponse */ - public function update($id, $content) { + public function update($id, $content=null, $favorite=null) { + if($favorite!==null) { + $this->service->favorite($id, $favorite, $this->userId); + } return $this->respond(function () use ($id, $content) { - return $this->service->update($id, $content, $this->userId); + if($content===null) { + return $this->service->get($id, $this->userId); + } else { + return $this->service->update($id, $content, $this->userId); + } }); }