diff --git a/README.md b/README.md
index 9859b6ca5..6e32d3276 100644
--- a/README.md
+++ b/README.md
@@ -7,21 +7,22 @@
The Notes app is a distraction free notes taking app. It supports formatting using [Markdown](https://en.wikipedia.org/wiki/Markdown) syntax. Notes are saved as files in your Nextcloud, so you can view and edit them with every Nextcloud client. Furthermore, a separate [RESTful API](https://github.com/nextcloud/notes/wiki/API-0.2) allows for an easy integration into third-party apps (currently, there are notes apps for [Android](https://github.com/stefan-niedermann/nextcloud-notes) and [iOS](https://github.com/owncloud/notes-iOS-App) which allow convenient access to your Nextcloud notes). Further features include marking notes as favorites and future versions will provide categories for better organization.
-
+
For further developer and user documentation please visit [the wiki](https://github.com/nextcloud/notes/wiki)
## :busts_in_silhouette: Maintainers
- [Hendrik Leppelsack](https://github.com/Henni)
- [Lukas Reschke](https://github.com/LukasReschke)
+- [Kristof Hamann](https://github.com/korelstar)
## :link: Requirements
**Minimum PHP Version**
-* PHP >= 5.4
+* PHP >= 5.6
**Minimum Nextcloud / ownCloud Version**
-* Nextcloud >= 9.0
-* ownCloud >= 8.1
+* Nextcloud >= 12.0
+* ownCloud >= 9.1
**Supported Webservers**
* Apache
diff --git a/appinfo/info.xml b/appinfo/info.xml
index f25480ad5..f00ca6474 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -7,9 +7,12 @@
- 2.3.2
+ 2.4.0
agpl
- Bernhard Posselt, Jan-Christoph Borchardt, Hendrik Leppelsack
+ Bernhard Posselt
+ Hendrik Leppelsack
+ Jan-Christoph Borchardt
+ Kristof Hamann
Notes
https://github.com/nextcloud/notes/wiki
@@ -23,7 +26,7 @@ The Notes app is a distraction free notes taking app. It supports formatting usi
https://github.com/nextcloud/notes.git
https://raw.githubusercontent.com/nextcloud/screenshots/master/apps/Notes/notes.png
-
-
+
+
diff --git a/js/public/app.min.js b/js/public/app.min.js
index 5b7219570..de4ec9919 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 u=e.module("Notes",["restangular","ngRoute"]).config(["$provide","$routeProvider","RestangularProvider","$httpProvider","$windowProvider",function(e,t,n,i,r){i.defaults.headers.common.requesttoken=o,e.value("Constants",{saveInterval:5e3}),t.when("/notes/:noteId",{templateUrl:"note.html",controller:"NoteController",resolve:{note:["$route","$q","is","Restangular",function(e,t,n,o){var i=t.defer(),r=e.current.params.noteId;return n.loading=!0,o.one("notes",r).get().then(function(e){n.loading=!1,i.resolve(e)},function(){n.loading=!1,i.reject()}),i.promise}]}}).otherwise({redirectTo:"/"});var u=OC.generateUrl("/apps/notes");n.setBaseUrl(u)}]).run(["$rootScope","$location","NotesModel",function(e,t,o){n('link[rel="shortcut icon"]').attr("href",OC.filePath("notes","img","favicon.png")),e.$on("$routeChangeError",function(){var e=o.getAll();if(e.length>0){var n=e.sort(function(e,t){return e.modified>t.modified?1:e.modified\?]/g,""),e=e.replace(/^[\. ]+/gm,""),n.note.title=e.trim().split(/\r?\n/,2)[0]||t("notes","New note")},n.onEdit=function(){var e=n.note;e.unsaved=!0,n.autoSave(e)},n.autoSave=u(function(e){i.add(e)},1e3),n.manualSave=function(){var e=n.note;e.error=!1,i.addManual(e)},s.unbind("keypress.notes.save"),s.bind("keypress.notes.save",function(e){if(e.ctrlKey||e.metaKey)switch(String.fromCharCode(e.which).toLowerCase()){case"s":e.preventDefault(),n.manualSave()}}),n.toggleDistractionFree=function(){function e(e){e.requestFullscreen?e.requestFullscreen():e.mozRequestFullScreen?e.mozRequestFullScreen():e.webkitRequestFullscreen?e.webkitRequestFullscreen():e.msRequestFullscreen&&e.msRequestFullscreen()}function t(){document.exitFullscreen?document.exitFullscreen():document.mozCancelFullScreen?document.mozCancelFullScreen():document.webkitExitFullscreen&&document.webkitExitFullscreen()}document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement?t():e(document.getElementById("app-content"))},n.$watch(function(){return n.note.title},function(e){e?document.title=e+" - "+n.defaultTitle:document.title=n.defaultTitle})}]),u.controller("NotesController",["$routeParams","$scope","$location","Restangular","NotesModel","$window",function(e,n,o,i,r,u){n.route=e,n.notesLoaded=!1,n.notes=r.getAll();var s=i.all("notes");s.getList().then(function(e){r.addAll(e),n.notesLoaded=!0}),n.create=function(){s.post().then(function(e){r.add(e),o.path("/notes/"+e.id)})},n["delete"]=function(e){var t=r.get(e);t.remove().then(function(){r.remove(e),n.$emit("$routeChangeError")})},n.toggleFavorite=function(e){var t=r.get(e);t.customPUT({favorite:!t.favorite},"favorite",{},{}).then(function(e){t.favorite=!!e})},u.onbeforeunload=function(){for(var e=r.getAll(),n=0;n0){var n=e.sort(function(e,t){return e.modified>t.modified?1:e.modified\?]/g,""),e=e.replace(/^[\. ]+/gm,""),n.note.title=e.trim().split(/\r?\n/,2)[0]||t("notes","New note")},n.onEdit=function(){var e=n.note;e.unsaved=!0,n.autoSave(e)},n.autoSave=u(function(e){i.add(e)},1e3),n.manualSave=function(){var e=n.note;e.error=!1,i.addManual(e)},s.unbind("keypress.notes.save"),s.bind("keypress.notes.save",function(e){if(e.ctrlKey||e.metaKey)switch(String.fromCharCode(e.which).toLowerCase()){case"s":e.preventDefault(),n.manualSave()}}),n.toggleDistractionFree=function(){function e(e){e.requestFullscreen?e.requestFullscreen():e.mozRequestFullScreen?e.mozRequestFullScreen():e.webkitRequestFullscreen?e.webkitRequestFullscreen():e.msRequestFullscreen&&e.msRequestFullscreen()}function t(){document.exitFullscreen?document.exitFullscreen():document.mozCancelFullScreen?document.mozCancelFullScreen():document.webkitExitFullscreen&&document.webkitExitFullscreen()}document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement?t():e(document.getElementById("app-content"))},n.$watch(function(){return n.note.title},function(e){e?document.title=e+" - "+n.defaultTitle:document.title=n.defaultTitle})}]),u.controller("NotesController",["$routeParams","$scope","$location","Restangular","NotesModel","$window",function(e,n,o,i,r,u){n.route=e,n.notesLoaded=!1,n.notes=r.getAll();var s=i.all("notes");s.getList().then(function(e){r.addAll(e),n.notesLoaded=!0}),n.create=function(){s.post().then(function(e){r.add(e),o.path("/notes/"+e.id)})},n["delete"]=function(e){var t=r.get(e);t.remove().then(function(){r.remove(e),n.$emit("$routeChangeError")})},n.toggleFavorite=function(e){var t=r.get(e);t.customPUT({favorite:!t.favorite},"favorite",{},{}).then(function(e){t.favorite=!!e})},u.onbeforeunload=function(){for(var e=r.getAll(),n=0;n\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 * filter by multiple words (AND operation)\n */\napp.filter('and', ['$filter', function ($filter) {\n\t'use strict';\n\treturn function (items, searchString) {\n\t\tvar searchValues = searchString.split(' ');\n\t\tvar filtered = items;\n\t\tfor(var i in searchValues) {\n\t\t\tfiltered = $filter('filter')(filtered, searchValues[i]);\n\t\t}\n\t\treturn filtered;\n\t};\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 SimpleMDE*/\napp.directive('editor', ['$timeout',\n 'urlFinder',\n function ($timeout, urlFinder) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\n\t\t\tvar simplemde = new SimpleMDE({\n\t\t\t\telement: element[0],\n\t\t\t\tspellChecker: false,\n\t\t\t\tautoDownloadFontAwesome: false,\n\t\t\t\ttoolbar: false,\n\t\t\t\tstatus: false,\n\t\t\t\tforceSync: true\n\t\t\t});\n\t\t\tvar editorElement = $(simplemde.codemirror.getWrapperElement());\n\n\t\t\tsimplemde.value(scope.note.content);\n\t\t\tsimplemde.codemirror.focus();\n\n\t\t\tsimplemde.codemirror.on('change', function() {\n\t\t\t\t$timeout(function() {\n\t\t\t\t\tscope.$apply(function () {\n\t\t\t\t\t\tscope.note.content = simplemde.value();\n\t\t\t\t\t\tscope.onEdit();\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\n\t\t\teditorElement.on('click', '.cm-link, .cm-url', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = urlFinder(this);\n\t\t\t\t\tif(angular.isDefined(url)) {\n\t\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t\t}\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('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip({'container': 'body'});\n\n element.on('$destroy', function() {\n element.tooltip('hide');\n });\n }\n };\n});\n","/**\n * Copyright (c) 2016, Hendrik Leppelsack\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('debounce', ['$timeout', function($timeout) {\n\t'use strict';\n\n\treturn function debounce(func, delay) {\n\t\tvar timeout;\n\n\t\treturn function() {\n\t\t\tvar context = this, args = arguments;\n\n\t\t\tif(timeout) {\n\t\t\t\t$timeout.cancel(timeout);\n\t\t\t}\n\t\t\ttimeout = $timeout(function() {\n\t\t\t\tfunc.apply(context, args);\n\t\t\t}, delay);\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 this._manualSaveActive = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n addManual: function (note) {\n this._manualSaveActive = true;\n this.add(note);\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\napp.controller('AppController', function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote, errorMessage, useSearchAPI) {\n $scope.defaultTitle = document.title;\n\n if(lastViewedNote !== 0) {\n $location.path('/notes/' + lastViewedNote);\n }\n if(errorMessage) {\n OC.Notification.showTemporary(errorMessage);\n }\n if(useSearchAPI) {\n $scope.initSearch();\n }\n };\n\n $scope.search = '';\n $scope.defaultTitle = null;\n\n $scope.initSearch = function() {\n new OCA.Search(\n function (query) {\n $scope.search = query;\n $scope.$apply();\n if($('#app-navigation-toggle').css('display')!=='none' &&\n !$('body').hasClass('snapjs-left')) {\n $('#app-navigation-toggle').click();\n }\n },\n function () {\n $scope.search = '';\n $scope.$apply();\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\napp.controller('NoteController', function($routeParams, $scope, NotesModel,\n SaveQueue, note, debounce,\n $document) {\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 $scope.isManualSaving = function () {\n return SaveQueue.isManualSaving();\n };\n\n $scope.updateTitle = function () {\n var content = $scope.note.content;\n\n // prepare content: remove markdown characters and empty spaces\n content = content.replace(/^\\s*[*+-]\\s+/mg, ''); // list item\n content = content.replace(/^#+\\s+(.*?)\\s*#*$/mg, '$1'); // headline\n content = content.replace(/^(=+|-+)$/mg, ''); // separate headline\n content = content.replace(/(\\*+|_+)(.*?)\\1/mg, '$2'); // emphasis\n\n // prevent directory traversal, illegal characters\n content = content.replace(/[\\*\\|\\/\\\\\\:\\\"<>\\?]/g, '');\n // prevent unintended file names\n content = content.replace(/^[\\. ]+/mg, '');\n\n // generate title from the first line of the content\n $scope.note.title = content.trim().split(/\\r?\\n/, 2)[0] ||\n t('notes', 'New note');\n };\n\n $scope.onEdit = function() {\n var note = $scope.note;\n note.unsaved = true;\n $scope.autoSave(note);\n };\n\n $scope.autoSave = debounce(function(note) {\n SaveQueue.add(note);\n }, 1000);\n\n $scope.manualSave = function() {\n var note = $scope.note;\n note.error = false;\n SaveQueue.addManual(note);\n };\n\n $document.unbind('keypress.notes.save');\n $document.bind('keypress.notes.save', function(event) {\n if(event.ctrlKey || event.metaKey) {\n switch(String.fromCharCode(event.which).toLowerCase()) {\n case 's':\n event.preventDefault();\n $scope.manualSave();\n break;\n }\n }\n });\n\n $scope.toggleDistractionFree = function() {\n function launchIntoFullscreen(element) {\n if(element.requestFullscreen) {\n element.requestFullscreen();\n } else if(element.mozRequestFullScreen) {\n element.mozRequestFullScreen();\n } else if(element.webkitRequestFullscreen) {\n element.webkitRequestFullscreen();\n } else if(element.msRequestFullscreen) {\n element.msRequestFullscreen();\n }\n }\n\n function exitFullscreen() {\n if(document.exitFullscreen) {\n document.exitFullscreen();\n } else if(document.mozCancelFullScreen) {\n document.mozCancelFullScreen();\n } else if(document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n }\n }\n\n if(document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement) {\n exitFullscreen();\n } else {\n launchIntoFullscreen(document.getElementById('app-content'));\n }\n };\n\n $scope.$watch(function() {\n return $scope.note.title;\n }, function(newValue) {\n if(newValue) {\n document.title = newValue + ' - ' + $scope.defaultTitle;\n } else {\n document.title = $scope.defaultTitle;\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// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', function($routeParams, $scope, $location,\n Restangular, NotesModel, $window) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notesLoaded = false;\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 $scope.notesLoaded = true;\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 $window.onbeforeunload = function() {\n var notes = NotesModel.getAll();\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([\"$provide\", \"$routeProvider\", \"RestangularProvider\", \"$httpProvider\", \"$windowProvider\", 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: [\"$route\", \"$q\", \"is\", \"Restangular\", 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([\"$rootScope\", \"$location\", \"NotesModel\", 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/**\n * filter by multiple words (AND operation)\n */\napp.filter('and', ['$filter', function ($filter) {\n\t'use strict';\n\treturn function (items, searchString) {\n\t\tvar searchValues = searchString.split(' ');\n\t\tvar filtered = items;\n\t\tfor(var i in searchValues) {\n\t\t\tfiltered = $filter('filter')(filtered, searchValues[i]);\n\t\t}\n\t\treturn filtered;\n\t};\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\napp.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/**\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\n/*global SimpleMDE*/\napp.directive('editor', ['$timeout',\n 'urlFinder',\n function ($timeout, urlFinder) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\n\t\t\tvar simplemde = new SimpleMDE({\n\t\t\t\telement: element[0],\n\t\t\t\tspellChecker: false,\n\t\t\t\tautoDownloadFontAwesome: false,\n\t\t\t\ttoolbar: false,\n\t\t\t\tstatus: false,\n\t\t\t\tforceSync: true\n\t\t\t});\n\t\t\tvar editorElement = $(simplemde.codemirror.getWrapperElement());\n\n\t\t\tsimplemde.value(scope.note.content);\n\t\t\tsimplemde.codemirror.focus();\n\n\t\t\tsimplemde.codemirror.on('change', function() {\n\t\t\t\t$timeout(function() {\n\t\t\t\t\tscope.$apply(function () {\n\t\t\t\t\t\tscope.note.content = simplemde.value();\n\t\t\t\t\t\tscope.onEdit();\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\n\t\t\teditorElement.on('click', '.cm-link, .cm-url', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = urlFinder(this);\n\t\t\t\t\tif(angular.isDefined(url)) {\n\t\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\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({'container': 'body'});\n\n element.on('$destroy', function() {\n element.tooltip('hide');\n });\n }\n };\n});\n\n/**\n * Copyright (c) 2016, Hendrik Leppelsack\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('debounce', ['$timeout', function($timeout) {\n\t'use strict';\n\n\treturn function debounce(func, delay) {\n\t\tvar timeout;\n\n\t\treturn function() {\n\t\t\tvar context = this, args = arguments;\n\n\t\t\tif(timeout) {\n\t\t\t\t$timeout.cancel(timeout);\n\t\t\t}\n\t\t\ttimeout = $timeout(function() {\n\t\t\t\tfunc.apply(context, args);\n\t\t\t}, delay);\n\t\t};\n\t};\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/**\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', [\"$q\", function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n this._manualSaveActive = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n addManual: function (note) {\n this._manualSaveActive = true;\n this.add(note);\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\napp.controller('AppController', [\"$scope\", \"$location\", \"is\", function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote, errorMessage, useSearchAPI) {\n $scope.defaultTitle = document.title;\n\n if(lastViewedNote !== 0) {\n $location.path('/notes/' + lastViewedNote);\n }\n if(errorMessage) {\n OC.Notification.showTemporary(errorMessage);\n }\n if(useSearchAPI) {\n $scope.initSearch();\n }\n };\n\n $scope.search = '';\n $scope.defaultTitle = null;\n\n $scope.initSearch = function() {\n new OCA.Search(\n function (query) {\n $scope.search = query;\n $scope.$apply();\n if($('#app-navigation-toggle').css('display')!=='none' &&\n !$('body').hasClass('snapjs-left')) {\n $('#app-navigation-toggle').click();\n }\n },\n function () {\n $scope.search = '';\n $scope.$apply();\n }\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\napp.controller('NoteController', [\"$routeParams\", \"$scope\", \"NotesModel\", \"SaveQueue\", \"note\", \"debounce\", \"$document\", function($routeParams, $scope, NotesModel,\n SaveQueue, note, debounce,\n $document) {\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 $scope.isManualSaving = function () {\n return SaveQueue.isManualSaving();\n };\n\n $scope.updateTitle = function () {\n var content = $scope.note.content;\n\n // prepare content: remove markdown characters and empty spaces\n content = content.replace(/^\\s*[*+-]\\s+/mg, ''); // list item\n content = content.replace(/^#+\\s+(.*?)\\s*#*$/mg, '$1'); // headline\n content = content.replace(/^(=+|-+)$/mg, ''); // separate headline\n content = content.replace(/(\\*+|_+)(.*?)\\1/mg, '$2'); // emphasis\n\n // prevent directory traversal, illegal characters\n content = content.replace(/[\\*\\|\\/\\\\\\:\\\"<>\\?]/g, '');\n // prevent unintended file names\n content = content.replace(/^[\\. ]+/mg, '');\n\n // generate title from the first line of the content\n $scope.note.title = content.trim().split(/\\r?\\n/, 2)[0] ||\n t('notes', 'New note');\n };\n\n $scope.onEdit = function() {\n var note = $scope.note;\n note.unsaved = true;\n $scope.autoSave(note);\n };\n\n $scope.autoSave = debounce(function(note) {\n SaveQueue.add(note);\n }, 1000);\n\n $scope.manualSave = function() {\n var note = $scope.note;\n note.error = false;\n SaveQueue.addManual(note);\n };\n\n $document.unbind('keypress.notes.save');\n $document.bind('keypress.notes.save', function(event) {\n if(event.ctrlKey || event.metaKey) {\n switch(String.fromCharCode(event.which).toLowerCase()) {\n case 's':\n event.preventDefault();\n $scope.manualSave();\n break;\n }\n }\n });\n\n $scope.toggleDistractionFree = function() {\n function launchIntoFullscreen(element) {\n if(element.requestFullscreen) {\n element.requestFullscreen();\n } else if(element.mozRequestFullScreen) {\n element.mozRequestFullScreen();\n } else if(element.webkitRequestFullscreen) {\n element.webkitRequestFullscreen();\n } else if(element.msRequestFullscreen) {\n element.msRequestFullscreen();\n }\n }\n\n function exitFullscreen() {\n if(document.exitFullscreen) {\n document.exitFullscreen();\n } else if(document.mozCancelFullScreen) {\n document.mozCancelFullScreen();\n } else if(document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n }\n }\n\n if(document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement) {\n exitFullscreen();\n } else {\n launchIntoFullscreen(document.getElementById('app-content'));\n }\n };\n\n $scope.$watch(function() {\n return $scope.note.title;\n }, function(newValue) {\n if(newValue) {\n document.title = newValue + ' - ' + $scope.defaultTitle;\n } else {\n document.title = $scope.defaultTitle;\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// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', [\"$routeParams\", \"$scope\", \"$location\", \"Restangular\", \"NotesModel\", \"$window\", function($routeParams, $scope, $location,\n Restangular, NotesModel, $window) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notesLoaded = false;\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 $scope.notesLoaded = true;\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 $window.onbeforeunload = function() {\n var notes = NotesModel.getAll();\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.directive('notesAutofocus', function () {\n 'use strict';\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.focus();\n }\n };\n});\n","/*global SimpleMDE*/\napp.directive('editor', ['$timeout',\n 'urlFinder',\n function ($timeout, urlFinder) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\n\t\t\tvar simplemde = new SimpleMDE({\n\t\t\t\telement: element[0],\n\t\t\t\tspellChecker: false,\n\t\t\t\tautoDownloadFontAwesome: false,\n\t\t\t\ttoolbar: false,\n\t\t\t\tstatus: false,\n\t\t\t\tforceSync: true\n\t\t\t});\n\t\t\tvar editorElement = $(simplemde.codemirror.getWrapperElement());\n\n\t\t\tsimplemde.value(scope.note.content);\n\t\t\tsimplemde.codemirror.focus();\n\n\t\t\tsimplemde.codemirror.on('change', function() {\n\t\t\t\t$timeout(function() {\n\t\t\t\t\tscope.$apply(function () {\n\t\t\t\t\t\tscope.note.content = simplemde.value();\n\t\t\t\t\t\tscope.onEdit();\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\n\t\t\teditorElement.on('click', '.cm-link, .cm-url', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = urlFinder(this);\n\t\t\t\t\tif(angular.isDefined(url)) {\n\t\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t\t}\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('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip({'container': 'body'});\n\n element.on('$destroy', function() {\n element.tooltip('hide');\n });\n }\n };\n});\n","/**\n * Copyright (c) 2016, Hendrik Leppelsack\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('debounce', ['$timeout', function($timeout) {\n\t'use strict';\n\n\treturn function debounce(func, delay) {\n\t\tvar timeout;\n\n\t\treturn function() {\n\t\t\tvar context = this, args = arguments;\n\n\t\t\tif(timeout) {\n\t\t\t\t$timeout.cancel(timeout);\n\t\t\t}\n\t\t\ttimeout = $timeout(function() {\n\t\t\t\tfunc.apply(context, args);\n\t\t\t}, delay);\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 this._manualSaveActive = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n addManual: function (note) {\n this._manualSaveActive = true;\n this.add(note);\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\napp.controller('AppController', function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote, errorMessage, useSearchAPI) {\n $scope.defaultTitle = document.title;\n\n if(lastViewedNote !== 0) {\n $location.path('/notes/' + lastViewedNote);\n }\n if(errorMessage) {\n OC.Notification.showTemporary(errorMessage);\n }\n if(useSearchAPI) {\n $scope.initSearch();\n }\n };\n\n $scope.search = '';\n $scope.defaultTitle = null;\n\n $scope.initSearch = function() {\n new OCA.Search(\n function (query) {\n $scope.search = query;\n $scope.$apply();\n if($('#app-navigation-toggle').css('display')!=='none' &&\n !$('body').hasClass('snapjs-left')) {\n $('#app-navigation-toggle').click();\n }\n },\n function () {\n $scope.search = '';\n $scope.$apply();\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\napp.controller('NoteController', function($routeParams, $scope, NotesModel,\n SaveQueue, note, debounce,\n $document) {\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 $scope.isManualSaving = function () {\n return SaveQueue.isManualSaving();\n };\n\n $scope.updateTitle = function () {\n var content = $scope.note.content;\n\n // prepare content: remove markdown characters and empty spaces\n content = content.replace(/^\\s*[*+-]\\s+/mg, ''); // list item\n content = content.replace(/^#+\\s+(.*?)\\s*#*$/mg, '$1'); // headline\n content = content.replace(/^(=+|-+)$/mg, ''); // separate headline\n content = content.replace(/(\\*+|_+)(.*?)\\1/mg, '$2'); // emphasis\n\n // prevent directory traversal, illegal characters\n content = content.replace(/[\\*\\|\\/\\\\\\:\\\"<>\\?]/g, '');\n // prevent unintended file names\n content = content.replace(/^[\\. ]+/mg, '');\n\n // generate title from the first line of the content\n $scope.note.title = content.trim().split(/\\r?\\n/, 2)[0] ||\n t('notes', 'New note');\n };\n\n $scope.onEdit = function() {\n var note = $scope.note;\n note.unsaved = true;\n $scope.autoSave(note);\n };\n\n $scope.autoSave = debounce(function(note) {\n SaveQueue.add(note);\n }, 1000);\n\n $scope.manualSave = function() {\n var note = $scope.note;\n note.error = false;\n SaveQueue.addManual(note);\n };\n\n $document.unbind('keypress.notes.save');\n $document.bind('keypress.notes.save', function(event) {\n if(event.ctrlKey || event.metaKey) {\n switch(String.fromCharCode(event.which).toLowerCase()) {\n case 's':\n event.preventDefault();\n $scope.manualSave();\n break;\n }\n }\n });\n\n $scope.toggleDistractionFree = function() {\n function launchIntoFullscreen(element) {\n if(element.requestFullscreen) {\n element.requestFullscreen();\n } else if(element.mozRequestFullScreen) {\n element.mozRequestFullScreen();\n } else if(element.webkitRequestFullscreen) {\n element.webkitRequestFullscreen();\n } else if(element.msRequestFullscreen) {\n element.msRequestFullscreen();\n }\n }\n\n function exitFullscreen() {\n if(document.exitFullscreen) {\n document.exitFullscreen();\n } else if(document.mozCancelFullScreen) {\n document.mozCancelFullScreen();\n } else if(document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n }\n }\n\n if(document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement) {\n exitFullscreen();\n } else {\n launchIntoFullscreen(document.getElementById('app-content'));\n }\n };\n\n $scope.$watch(function() {\n return $scope.note.title;\n }, function(newValue) {\n if(newValue) {\n document.title = newValue + ' - ' + $scope.defaultTitle;\n } else {\n document.title = $scope.defaultTitle;\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// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', function($routeParams, $scope, $location,\n Restangular, NotesModel, $window) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notesLoaded = false;\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 $scope.notesLoaded = true;\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 $window.onbeforeunload = function() {\n var notes = NotesModel.getAll();\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([\"$provide\", \"$routeProvider\", \"RestangularProvider\", \"$httpProvider\", \"$windowProvider\", 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: [\"$route\", \"$q\", \"is\", \"Restangular\", 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([\"$rootScope\", \"$location\", \"NotesModel\", 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/**\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\n/*global SimpleMDE*/\napp.directive('editor', ['$timeout',\n 'urlFinder',\n function ($timeout, urlFinder) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\n\t\t\tvar simplemde = new SimpleMDE({\n\t\t\t\telement: element[0],\n\t\t\t\tspellChecker: false,\n\t\t\t\tautoDownloadFontAwesome: false,\n\t\t\t\ttoolbar: false,\n\t\t\t\tstatus: false,\n\t\t\t\tforceSync: true\n\t\t\t});\n\t\t\tvar editorElement = $(simplemde.codemirror.getWrapperElement());\n\n\t\t\tsimplemde.value(scope.note.content);\n\t\t\tsimplemde.codemirror.focus();\n\n\t\t\tsimplemde.codemirror.on('change', function() {\n\t\t\t\t$timeout(function() {\n\t\t\t\t\tscope.$apply(function () {\n\t\t\t\t\t\tscope.note.content = simplemde.value();\n\t\t\t\t\t\tscope.onEdit();\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\n\t\t\teditorElement.on('click', '.cm-link, .cm-url', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = urlFinder(this);\n\t\t\t\t\tif(angular.isDefined(url)) {\n\t\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\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({'container': 'body'});\n\n element.on('$destroy', function() {\n element.tooltip('hide');\n });\n }\n };\n});\n\n/**\n * Copyright (c) 2016, Hendrik Leppelsack\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('debounce', ['$timeout', function($timeout) {\n\t'use strict';\n\n\treturn function debounce(func, delay) {\n\t\tvar timeout;\n\n\t\treturn function() {\n\t\t\tvar context = this, args = arguments;\n\n\t\t\tif(timeout) {\n\t\t\t\t$timeout.cancel(timeout);\n\t\t\t}\n\t\t\ttimeout = $timeout(function() {\n\t\t\t\tfunc.apply(context, args);\n\t\t\t}, delay);\n\t\t};\n\t};\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/**\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', [\"$q\", function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n this._manualSaveActive = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n addManual: function (note) {\n this._manualSaveActive = true;\n this.add(note);\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\napp.controller('AppController', [\"$scope\", \"$location\", \"is\", function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote, errorMessage, useSearchAPI) {\n $scope.defaultTitle = document.title;\n\n if(lastViewedNote !== 0) {\n $location.path('/notes/' + lastViewedNote);\n }\n if(errorMessage) {\n OC.Notification.showTemporary(errorMessage);\n }\n if(useSearchAPI) {\n $scope.initSearch();\n }\n };\n\n $scope.search = '';\n $scope.defaultTitle = null;\n\n $scope.initSearch = function() {\n new OCA.Search(\n function (query) {\n $scope.search = query;\n $scope.$apply();\n if($('#app-navigation-toggle').css('display')!=='none' &&\n !$('body').hasClass('snapjs-left')) {\n $('#app-navigation-toggle').click();\n }\n },\n function () {\n $scope.search = '';\n $scope.$apply();\n }\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\napp.controller('NoteController', [\"$routeParams\", \"$scope\", \"NotesModel\", \"SaveQueue\", \"note\", \"debounce\", \"$document\", function($routeParams, $scope, NotesModel,\n SaveQueue, note, debounce,\n $document) {\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 $scope.isManualSaving = function () {\n return SaveQueue.isManualSaving();\n };\n\n $scope.updateTitle = function () {\n var content = $scope.note.content;\n\n // prepare content: remove markdown characters and empty spaces\n content = content.replace(/^\\s*[*+-]\\s+/mg, ''); // list item\n content = content.replace(/^#+\\s+(.*?)\\s*#*$/mg, '$1'); // headline\n content = content.replace(/^(=+|-+)$/mg, ''); // separate headline\n content = content.replace(/(\\*+|_+)(.*?)\\1/mg, '$2'); // emphasis\n\n // prevent directory traversal, illegal characters\n content = content.replace(/[\\*\\|\\/\\\\\\:\\\"<>\\?]/g, '');\n // prevent unintended file names\n content = content.replace(/^[\\. ]+/mg, '');\n\n // generate title from the first line of the content\n $scope.note.title = content.trim().split(/\\r?\\n/, 2)[0] ||\n t('notes', 'New note');\n };\n\n $scope.onEdit = function() {\n var note = $scope.note;\n note.unsaved = true;\n $scope.autoSave(note);\n };\n\n $scope.autoSave = debounce(function(note) {\n SaveQueue.add(note);\n }, 1000);\n\n $scope.manualSave = function() {\n var note = $scope.note;\n note.error = false;\n SaveQueue.addManual(note);\n };\n\n $document.unbind('keypress.notes.save');\n $document.bind('keypress.notes.save', function(event) {\n if(event.ctrlKey || event.metaKey) {\n switch(String.fromCharCode(event.which).toLowerCase()) {\n case 's':\n event.preventDefault();\n $scope.manualSave();\n break;\n }\n }\n });\n\n $scope.toggleDistractionFree = function() {\n function launchIntoFullscreen(element) {\n if(element.requestFullscreen) {\n element.requestFullscreen();\n } else if(element.mozRequestFullScreen) {\n element.mozRequestFullScreen();\n } else if(element.webkitRequestFullscreen) {\n element.webkitRequestFullscreen();\n } else if(element.msRequestFullscreen) {\n element.msRequestFullscreen();\n }\n }\n\n function exitFullscreen() {\n if(document.exitFullscreen) {\n document.exitFullscreen();\n } else if(document.mozCancelFullScreen) {\n document.mozCancelFullScreen();\n } else if(document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n }\n }\n\n if(document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement) {\n exitFullscreen();\n } else {\n launchIntoFullscreen(document.getElementById('app-content'));\n }\n };\n\n $scope.$watch(function() {\n return $scope.note.title;\n }, function(newValue) {\n if(newValue) {\n document.title = newValue + ' - ' + $scope.defaultTitle;\n } else {\n document.title = $scope.defaultTitle;\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// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', [\"$routeParams\", \"$scope\", \"$location\", \"Restangular\", \"NotesModel\", \"$window\", function($routeParams, $scope, $location,\n Restangular, NotesModel, $window) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notesLoaded = false;\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 $scope.notesLoaded = true;\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 $window.onbeforeunload = function() {\n var notes = NotesModel.getAll();\n for(var i=0; i