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/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); + } }); } 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..0ff92022 100644 --- a/css/notes.css +++ b/css/notes.css @@ -42,10 +42,15 @@ display: none; } -#app-navigation .active > .utils .icon-delete { - display: block; +#app-navigation .active > .utils .icon-delete, +#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..48955974 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,24 +33,29 @@ class Note extends Entity { public $modified; public $title; public $content; + public $favorite = false; public function __construct() { $this->addType('modified', 'integer'); + $this->addType('favorite', 'boolean'); } /** * @param File $file * @return static */ - public static function fromFile(File $file){ + public static function fromFile(File $file, $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 + 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; } - -} \ 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..3505bb57 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; @@ -53,4 +54,4 @@ app.factory('NotesModel', function () { }; 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..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});\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; 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'); + if($tagger==null) { + $tags = []; + } else { + $tags = $tagger->getTagsForObjects(array_keys($filesById)); + } + + $notes = []; + foreach($filesById as $id=>$file) { + $notes[] = Note::fromFile($file, array_key_exists($id, $tags) ? $tags[$id] : []); + } return $notes; } @@ -65,9 +75,18 @@ 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'); + if($tagger==null) { + $tags = []; + } else { + $tags = $tagger->getTagsForObjects([$id]); + } + return array_key_exists($id, $tags) ? $tags[$id] : []; + } /** * Creates a note and returns the empty note @@ -131,7 +150,31 @@ public function update ($id, $content, $userId){ $file->putContent($content); - return Note::fromFile($file); + return Note::fromFile($file, $this->getTags($id)); + } + + + /** + * 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]); } @@ -161,7 +204,6 @@ private function getFileById ($folder, $id) { if(count($file) <= 0 || !$this->isNote($file[0])) { throw new NoteDoesNotExistException(); } - 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)"> +
  • 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);