Repository: Laverna/laverna Branch: master Commit: 9e2caf95d69a Files: 421 Total size: 1.2 MB Directory structure: gitextract_t164g6mh/ ├── .bowerrc ├── .codeclimate.yml ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .gitmodules ├── .jshintrc ├── .travis.yml ├── CONTRIBUTE.md ├── LICENSE ├── README.md ├── app/ │ ├── .htaccess │ ├── 404.html │ ├── config.xml │ ├── docs/ │ │ └── howto.md │ ├── dropbox.html │ ├── images/ │ │ ├── icon/ │ │ │ └── icon-512x512.icns │ │ └── laverna.icon.xcf │ ├── index.html │ ├── locales/ │ │ ├── ar/ │ │ │ └── translation.json │ │ ├── bs_ba/ │ │ │ └── translation.json │ │ ├── da/ │ │ │ └── translation.json │ │ ├── de/ │ │ │ └── translation.json │ │ ├── de_ch/ │ │ │ └── translation.json │ │ ├── el/ │ │ │ └── translation.json │ │ ├── en/ │ │ │ └── translation.json │ │ ├── eo/ │ │ │ └── translation.json │ │ ├── es/ │ │ │ └── translation.json │ │ ├── fr/ │ │ │ └── translation.json │ │ ├── gl/ │ │ │ └── translation.json │ │ ├── hi_in/ │ │ │ └── translation.json │ │ ├── it/ │ │ │ └── translation.json │ │ ├── ja/ │ │ │ └── translation.json │ │ ├── ko/ │ │ │ └── translation.json │ │ ├── locales.json │ │ ├── lt/ │ │ │ └── translation.json │ │ ├── lv/ │ │ │ └── translation.json │ │ ├── mr_in/ │ │ │ └── translation.json │ │ ├── nb/ │ │ │ └── translation.json │ │ ├── nl/ │ │ │ └── translation.json │ │ ├── nn/ │ │ │ └── translation.json │ │ ├── oc/ │ │ │ └── translation.json │ │ ├── pl/ │ │ │ └── translation.json │ │ ├── pt/ │ │ │ └── translation.json │ │ ├── pt_br/ │ │ │ └── translation.json │ │ ├── ru/ │ │ │ └── translation.json │ │ ├── se/ │ │ │ └── translation.json │ │ ├── sq/ │ │ │ └── translation.json │ │ ├── tr/ │ │ │ └── translation.json │ │ ├── zh_cn/ │ │ │ └── translation.json │ │ └── zh_tw/ │ │ └── translation.json │ ├── manifest.webapp │ ├── migrate.html │ ├── robots.txt │ ├── scripts/ │ │ ├── app.js │ │ ├── apps/ │ │ │ ├── confirm/ │ │ │ │ ├── appConfirm.js │ │ │ │ └── show/ │ │ │ │ ├── controller.js │ │ │ │ ├── template.html │ │ │ │ └── view.js │ │ │ ├── encryption/ │ │ │ │ ├── appEncrypt.js │ │ │ │ ├── auth/ │ │ │ │ │ ├── app.js │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── errorConfirm.html │ │ │ │ │ ├── template.html │ │ │ │ │ └── view.js │ │ │ │ └── encrypt/ │ │ │ │ ├── app.js │ │ │ │ ├── backup.html │ │ │ │ ├── backupView.js │ │ │ │ ├── controller.js │ │ │ │ ├── template.html │ │ │ │ └── view.js │ │ │ ├── help/ │ │ │ │ ├── about/ │ │ │ │ │ ├── app.js │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── template.html │ │ │ │ │ └── view.js │ │ │ │ ├── appHelp.js │ │ │ │ ├── firstStart/ │ │ │ │ │ ├── app.js │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── template.html │ │ │ │ │ └── view.js │ │ │ │ └── show/ │ │ │ │ ├── app.js │ │ │ │ ├── controller.js │ │ │ │ ├── template.html │ │ │ │ └── view.js │ │ │ ├── navbar/ │ │ │ │ ├── appNavbar.js │ │ │ │ └── show/ │ │ │ │ ├── controller.js │ │ │ │ ├── template.html │ │ │ │ └── view.js │ │ │ ├── notebooks/ │ │ │ │ ├── appNotebooks.js │ │ │ │ ├── form/ │ │ │ │ │ ├── notebook/ │ │ │ │ │ │ ├── app.js │ │ │ │ │ │ ├── controller.js │ │ │ │ │ │ ├── formView.js │ │ │ │ │ │ └── templates/ │ │ │ │ │ │ └── form.html │ │ │ │ │ └── tag/ │ │ │ │ │ ├── app.js │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── formView.js │ │ │ │ │ └── templates/ │ │ │ │ │ └── form.html │ │ │ │ ├── list/ │ │ │ │ │ ├── app.js │ │ │ │ │ ├── behaviors/ │ │ │ │ │ │ ├── compositeBehavior.js │ │ │ │ │ │ └── itemBehavior.js │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── templates/ │ │ │ │ │ │ ├── layout.html │ │ │ │ │ │ ├── notebooksItem.html │ │ │ │ │ │ ├── notebooksList.html │ │ │ │ │ │ ├── tagsItem.html │ │ │ │ │ │ └── tagsList.html │ │ │ │ │ └── views/ │ │ │ │ │ ├── layout.js │ │ │ │ │ ├── notebooksComposite.js │ │ │ │ │ ├── notebooksItem.js │ │ │ │ │ ├── tagsComposite.js │ │ │ │ │ └── tagsItem.js │ │ │ │ └── remove/ │ │ │ │ ├── controller.js │ │ │ │ └── notebooks.html │ │ │ ├── notes/ │ │ │ │ ├── appNote.js │ │ │ │ ├── form/ │ │ │ │ │ ├── app.js │ │ │ │ │ ├── behaviors/ │ │ │ │ │ │ ├── desktop.js │ │ │ │ │ │ └── mobile.js │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── templates/ │ │ │ │ │ │ ├── form.html │ │ │ │ │ │ └── notebooks.html │ │ │ │ │ └── views/ │ │ │ │ │ ├── formView.js │ │ │ │ │ ├── notebook.js │ │ │ │ │ └── notebooks.js │ │ │ │ ├── list/ │ │ │ │ │ ├── controller.js │ │ │ │ │ ├── listApp.js │ │ │ │ │ ├── templates/ │ │ │ │ │ │ ├── sidebarList.html │ │ │ │ │ │ └── sidebarListItem.html │ │ │ │ │ └── views/ │ │ │ │ │ ├── noteSidebar.js │ │ │ │ │ └── noteSidebarItem.js │ │ │ │ ├── remove/ │ │ │ │ │ └── controller.js │ │ │ │ └── show/ │ │ │ │ ├── app.js │ │ │ │ ├── controller.js │ │ │ │ ├── noteView.js │ │ │ │ └── templates/ │ │ │ │ └── item.html │ │ │ └── settings/ │ │ │ ├── appSettings.js │ │ │ ├── controller.js │ │ │ ├── module/ │ │ │ │ ├── app.js │ │ │ │ └── controller.js │ │ │ ├── show/ │ │ │ │ ├── app.js │ │ │ │ ├── controller.js │ │ │ │ ├── formBehavior.js │ │ │ │ ├── module.js │ │ │ │ ├── templates/ │ │ │ │ │ ├── editor.html │ │ │ │ │ ├── encryption.html │ │ │ │ │ ├── general.html │ │ │ │ │ ├── importExport.html │ │ │ │ │ ├── keybindings.html │ │ │ │ │ ├── modules.html │ │ │ │ │ ├── profiles.html │ │ │ │ │ ├── showTemplate.html │ │ │ │ │ └── sync.html │ │ │ │ └── views/ │ │ │ │ ├── editor.js │ │ │ │ ├── encryption.js │ │ │ │ ├── general.js │ │ │ │ ├── importExport.js │ │ │ │ ├── keybindings.js │ │ │ │ ├── modules.js │ │ │ │ ├── profiles.js │ │ │ │ ├── showView.js │ │ │ │ └── sync.js │ │ │ └── sidebar/ │ │ │ ├── app.js │ │ │ ├── controller.js │ │ │ ├── template.html │ │ │ ├── templates/ │ │ │ │ └── navbar.html │ │ │ ├── view.js │ │ │ └── views/ │ │ │ └── navbar.js │ │ ├── backbone.noworker.sync.js │ │ ├── backbone.sync.js │ │ ├── behaviors/ │ │ │ ├── content.js │ │ │ ├── modal.js │ │ │ ├── modalForm.js │ │ │ ├── sidebar.js │ │ │ └── sidemenu.js │ │ ├── classes/ │ │ │ ├── encryption.js │ │ │ ├── sjcl.js │ │ │ └── sjcl.worker.js │ │ ├── collections/ │ │ │ ├── configs.js │ │ │ ├── files.js │ │ │ ├── modules/ │ │ │ │ ├── configs.js │ │ │ │ ├── files.js │ │ │ │ ├── module.js │ │ │ │ ├── notebooks.js │ │ │ │ ├── notes.js │ │ │ │ └── tags.js │ │ │ ├── notebooks.js │ │ │ ├── notes.js │ │ │ ├── pageable.js │ │ │ └── tags.js │ │ ├── constants.js │ │ ├── helpers/ │ │ │ ├── db.js │ │ │ ├── fileSaver.js │ │ │ ├── i18next.js │ │ │ ├── keybindings.js │ │ │ ├── migrate.js │ │ │ ├── radio.shim.js │ │ │ ├── storage.js │ │ │ ├── title.js │ │ │ ├── underscore-util.js │ │ │ └── uri.js │ │ ├── init.js │ │ ├── initializers.js │ │ ├── main.js │ │ ├── migrate.js │ │ ├── models/ │ │ │ ├── config.js │ │ │ ├── file.js │ │ │ ├── note.js │ │ │ ├── notebook.js │ │ │ └── tag.js │ │ ├── moduleLoader.js │ │ ├── modules/ │ │ │ ├── codemirror/ │ │ │ │ ├── controller.js │ │ │ │ ├── module.js │ │ │ │ ├── templates/ │ │ │ │ │ └── editor.html │ │ │ │ └── views/ │ │ │ │ └── editor.js │ │ │ ├── dropbox/ │ │ │ │ ├── classes/ │ │ │ │ │ ├── adapter.js │ │ │ │ │ └── sync.js │ │ │ │ └── module.js │ │ │ ├── electronSearch/ │ │ │ │ ├── controller.js │ │ │ │ ├── module.js │ │ │ │ ├── template.html │ │ │ │ └── view.js │ │ │ ├── fileDialog/ │ │ │ │ ├── controller.js │ │ │ │ ├── helper.js │ │ │ │ ├── module.js │ │ │ │ ├── templates/ │ │ │ │ │ ├── dialog.html │ │ │ │ │ └── dropzone.html │ │ │ │ └── views/ │ │ │ │ └── dialog.js │ │ │ ├── fs/ │ │ │ │ ├── classes/ │ │ │ │ │ ├── adapter.js │ │ │ │ │ └── sync.js │ │ │ │ ├── module.js │ │ │ │ ├── templates/ │ │ │ │ │ └── settings.html │ │ │ │ └── views/ │ │ │ │ └── settings.js │ │ │ ├── fuzzySearch/ │ │ │ │ ├── controllers/ │ │ │ │ │ └── main.js │ │ │ │ ├── module.js │ │ │ │ ├── regions/ │ │ │ │ │ └── sidebar.js │ │ │ │ ├── templates/ │ │ │ │ │ ├── composite.html │ │ │ │ │ └── item.html │ │ │ │ └── views/ │ │ │ │ ├── composite.js │ │ │ │ └── item.js │ │ │ ├── importExport/ │ │ │ │ ├── controller.js │ │ │ │ └── module.js │ │ │ ├── linkDialog/ │ │ │ │ ├── controller.js │ │ │ │ ├── module.js │ │ │ │ ├── templates/ │ │ │ │ │ ├── dialog.html │ │ │ │ │ └── item.html │ │ │ │ └── views/ │ │ │ │ ├── collection.js │ │ │ │ ├── dialog.js │ │ │ │ └── item.js │ │ │ ├── markdown/ │ │ │ │ ├── libs/ │ │ │ │ │ ├── markdown-it-file.js │ │ │ │ │ ├── markdown-it-task.js │ │ │ │ │ ├── markdown-it.js │ │ │ │ │ └── markdown.js │ │ │ │ ├── module.js │ │ │ │ └── workers/ │ │ │ │ └── markdown.js │ │ │ ├── mathjax/ │ │ │ │ ├── libs/ │ │ │ │ │ └── mathjax.js │ │ │ │ └── module.js │ │ │ ├── modules.json │ │ │ └── remotestorage/ │ │ │ ├── classes/ │ │ │ │ ├── module.js │ │ │ │ ├── rs.js │ │ │ │ └── sync.js │ │ │ └── module.js │ │ ├── modules.js │ │ ├── regions/ │ │ │ └── regionManager.js │ │ ├── templates/ │ │ │ └── loader.html │ │ ├── views/ │ │ │ ├── brand.js │ │ │ ├── loader.js │ │ │ └── modal.js │ │ └── workers/ │ │ ├── localForage.js │ │ └── sjcl.js │ └── styles/ │ ├── core/ │ │ ├── bootstrap.less │ │ ├── codemirror/ │ │ │ ├── core.less │ │ │ └── theme.less │ │ ├── codemirror.less │ │ ├── editor.less │ │ ├── fontello/ │ │ │ ├── LICENSE.txt │ │ │ ├── README.txt │ │ │ ├── config.json │ │ │ ├── css/ │ │ │ │ ├── animation.less │ │ │ │ ├── fontello-codes.less │ │ │ │ ├── fontello-embedded.less │ │ │ │ ├── fontello-ie7-codes.less │ │ │ │ ├── fontello-ie7.less │ │ │ │ └── fontello.less │ │ │ └── demo.html │ │ ├── fontello.less │ │ ├── fuzzy.less │ │ ├── header.less │ │ ├── layout.less │ │ ├── list.less │ │ ├── main.less │ │ ├── responsive.less │ │ ├── sidemenu.less │ │ ├── utils.less │ │ └── variables.less │ └── theme-default/ │ ├── buttons.less │ ├── checkbox.less │ ├── codemirror.less │ ├── dropzone.less │ ├── editor.less │ ├── forms.less │ ├── header.less │ ├── layout.less │ ├── list.less │ ├── loading-animation.less │ ├── main.less │ ├── modal.less │ ├── prism.less │ ├── settings.less │ ├── sidemenu.less │ ├── utils.less │ └── variables.less ├── bower.json ├── config.xml ├── electron.js ├── gulpfile.js ├── gulps/ │ ├── clean.js │ ├── copy.js │ ├── copyDist.js │ ├── copyRelease.js │ ├── cssmin.js │ ├── electron.js │ ├── htmlManifest.js │ ├── htmlmin.js │ ├── jshint.js │ ├── jsonlint.js │ ├── less.js │ ├── mobile.js │ ├── mocha.js │ ├── nightwatch.js │ ├── npm.js │ ├── prism.js │ ├── require.js │ └── serve.js ├── karma.conf.js ├── package.json ├── preload.js ├── server.js └── test/ ├── .bowerrc ├── bower.json ├── index.html ├── nightwatch.json ├── spec/ │ ├── app.js │ ├── apps/ │ │ ├── confirm/ │ │ │ ├── show/ │ │ │ │ └── view.js │ │ │ └── test.js │ │ ├── encryption/ │ │ │ ├── encrypt/ │ │ │ │ ├── controller.js │ │ │ │ └── view.js │ │ │ └── test.js │ │ ├── help/ │ │ │ ├── about/ │ │ │ │ └── view.js │ │ │ ├── show/ │ │ │ │ └── view.js │ │ │ └── test.js │ │ ├── navbar/ │ │ │ ├── show/ │ │ │ │ └── view.js │ │ │ └── test.js │ │ ├── notebooks/ │ │ │ ├── list/ │ │ │ │ ├── layout.js │ │ │ │ └── views/ │ │ │ │ ├── notebookList.js │ │ │ │ └── tagList.js │ │ │ ├── notebooksForm/ │ │ │ │ └── formView.js │ │ │ ├── tagsForm/ │ │ │ │ └── tagForm.js │ │ │ └── test.js │ │ ├── notes/ │ │ │ ├── list/ │ │ │ │ ├── app.js │ │ │ │ ├── controller.js │ │ │ │ └── views/ │ │ │ │ ├── noteSidebar.js │ │ │ │ └── noteSidebarItem.js │ │ │ └── test.js │ │ └── settings/ │ │ ├── show/ │ │ │ ├── formBehavior.js │ │ │ └── views/ │ │ │ ├── basic.js │ │ │ ├── importExport.js │ │ │ ├── profiles.js │ │ │ ├── shortcuts.js │ │ │ └── showView.js │ │ └── test.js │ ├── backbone.sync.js │ ├── classes/ │ │ ├── encryption.js │ │ ├── helpers.js │ │ └── sjcl.js │ ├── collections/ │ │ ├── configs.js │ │ ├── modules/ │ │ │ ├── configs.js │ │ │ ├── files.js │ │ │ ├── module.js │ │ │ ├── notebooks.js │ │ │ ├── notes.js │ │ │ └── tags.js │ │ ├── notebooks.js │ │ ├── notes.js │ │ ├── pageable.js │ │ └── tags.js │ ├── helpers/ │ │ ├── db.js │ │ ├── i18next.js │ │ ├── storage.js │ │ ├── underscore-util.js │ │ └── uri.js │ ├── init.js │ ├── initializers.js │ ├── models/ │ │ ├── config.js │ │ ├── file.js │ │ ├── note.js │ │ ├── notebook.js │ │ └── tag.js │ ├── moduleLoader.js │ └── test.js └── spec-ui/ ├── commands/ │ ├── addNote.js │ ├── addNotebook.js │ ├── addTag.js │ ├── changeEncryption.js │ ├── closeWelcome.js │ └── findAll.js ├── modules/ │ └── remotestorage/ │ ├── auth.js │ ├── client1.js │ └── client2.js └── tests/ ├── apps/ │ ├── encryption/ │ │ └── encrypt.js │ ├── navbar/ │ │ └── navbar.js │ ├── notebooks/ │ │ ├── form.js │ │ ├── formEdit.js │ │ ├── list.js │ │ └── remove.js │ ├── notes/ │ │ ├── form.js │ │ ├── list.js │ │ └── show.js │ └── settings/ │ ├── general.js │ ├── import.js │ ├── keybindings.js │ └── profiles.js └── modules/ └── fuzzySearch/ └── fuzzySearch.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .bowerrc ================================================ { "directory": "app/bower_components" } ================================================ FILE: .codeclimate.yml ================================================ engines: eslint: enabled: true duplication: enabled: true config : languages: - javascript languages: JavaScript: true ratings: paths: - app/scripts/** exclude_paths: - test/** ================================================ FILE: .editorconfig ================================================ # EditorConfig helps developers define and maintain consistent # coding styles between different editors and IDEs # editorconfig.org root = true [*] # Change these settings to your own preference indent_style = space indent_size = 4 # We recommend you to keep these unchanged end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitattributes ================================================ * text=auto ================================================ FILE: .gitignore ================================================ # Cordova build configs and keys build.json **/*.keystore # Compiled files app/styles/**/*.css dist/ cordova/ release/ .tmp/ # Bower app/bower_components test/bower_components .bower-cache .bower-registry .bower-tmp # Test reports (Nightwatch) .reports test/visual/screenshots # Old files that should not appear again if someone pushes them app/scripts/libs/remotestorage.js app/scripts/apps/settings/show/encryptView.js app/scripts/apps/settings/show/encryptTemplate.html app/scripts/helpers/syncStatus.js # Intellij .idea # Created by https://www.gitignore.io/api/node,vim,osx,linux,windows ### Node ### # Logs logs *.log npm-debug.log* # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules jspm_packages # Optional npm cache directory .npm # Optional REPL history .node_repl_history ### Vim ### # swap [._]*.s[a-w][a-z] [._]s[a-w][a-z] # session Session.vim # temporary .netrwhist *~ # auto-generated tag files tags ### OSX ### *.DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Linux ### *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* ### Windows ### # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Windows shortcuts *.lnk ================================================ FILE: .gitmodules ================================================ ================================================ FILE: .jshintrc ================================================ { "node": true, "browser": true, "esnext": true, "bitwise": true, "camelcase": true, "curly": true, "eqeqeq": true, "immed": true, "indent": 4, "latedef": true, "newcap": true, "noarg": true, "quotmark": "single", "regexp": true, "undef": true, "unused": true, "strict": true, "trailing": true, "smarttabs": true, "jquery": true, "mocha": true, "globals": { "expect": false, "define": false } } ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 6 - 'stable' before_install: - npm install -g gulp bower jshint install: - npm install - bower install - cd test && bower install && cd ../ script: - gulp build - gulp --root dist & - set -e - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && gulp nightwatch --env ci; echo $? || false' - sleep 1 - killall gulp after_failure: - killall gulp addons: sauce_connect: username: secure: "F2waNLSycvKfroVB0wIExO9dimxFCrdoKdhj3k8NzcB8a1OFfmu9JqGf+NIHUOXqW3r6LEOmtH522II3AsEAz2a5En+rzl83CVBYiYylYzvnnGZHgTMttemYWRqbH8pEYGA+o/m47NDJo4AbAJ3A5lQvHPKylbPVuuHMGgHwrdXgGAktw2mq15mo65LEhSVKRm08b6tB+AD1OesaZ//dzf/uhhmB6ukvXVI5CaGCJV13OVXHf1vIkutTqx3JrDwi2ErGhwTg/wc1ZKnWG7+EAKxsf0lfgNFCkAL3xS8309LyvrFDIoDCY0T3VeR0WDQtr+ryRDIcOROoo3Q2LUqqERD1W4a1Tsn3yxeEbyHzOPMo2o14AK04y3Alel+5en4V0hNEhDbSgV/pyzkrzZ1KVHNyNkZA5i1aCWG5rfO6oeVhPtUI/oAwom1Xa8FwvK96osbUmv5TDeax/dtMxYLtOZDJo6AZx6aVN7wqjDtLuYg0oZ7/+JAIaHfngxYgKE400sied6v2VNxYdqP+Duf2ZaiuOXfnnQB74/zy29uQtbztmAKuLF36/Wi1PWTBnezIo9vRLTzB8JVZ2echeJYbOpk5dqfnjAE2xwOiHzbqRfRvd699C+x9WCXgNZmySog7MEY5SJ31JqVfUzrhhSgo7xbpWfig9vNdExAcaDJpHSo=" access_key: secure: "yDTvc3TpWhwDLtF6LnVduEtfdi1lmA265XAAiirV2Hn/2NonOS4M5eW7j5RB9JY/psLpd9/l5SZZTIYxCxURrMBlxnHyOYlrNnA4aTXGVPulDECPmQFdXuMHJclnCx4GL/YOg5EXhsjD8EFB/ihavDaFr2740tuYet8KNqqB0RI5pkmp7RP8G3x3AiTu2zv72e5C0TukKBdCUpIhSX34ofsmvQ8KQ/DOAUK1MjNYxb55Mhf7MCgskC+bypzWZDuuOvM0w48OzwDuF6LBnHQCxvUmcUwuxRTuJNJZUFoOIEEP44r546fi0vzt9/1gLI2Qsc/YtuvSsPKquMb+iV1Rn8F77vGUN0woiYlcAAtrCIe+fk2Kl6FVNPY9KlldQZPwogLLwGpPN6h+kGVOgNBl0WSmiekDVbFDqo0TcNhlwkkpR8Iuy+WfWZI5l+pmZrBCnQIIFNd+SlLXlftp1aiaghBs6654tp+jULfUzjeL8yxBJpcsosi8+erpx4u7ldbtWi94z1LCstbU65nSPlEaKEmjGPWppFOL0nzORTT2trSil0WrQoAkBmsFkzxI7KOhFPoVuk26E4bY/o7Jz1Y2OGPYKdsZppB3pDDH6BxUYW/u5YLveEU64Kz6ixgDkOi3rHFrDiq81v7OKAINytqik6FV4N1zpsurQ2JUV9aPmf8=" tunnel_domains: localhost ================================================ FILE: CONTRIBUTE.md ================================================ Contribution ================ Note, all contributions should be done on `dev` branch. ### Localizations ---------------- 1. Copy ./app/locales/en and rename it to locale name - ./app/locales/[localeName] 2. Open ./app/locales/[localeName]/translation.json 3. Replace the values 4. Make a *pull request* ================================================ FILE: LICENSE ================================================ Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: README.md ================================================ # Laverna - note taking web app [![Join the chat at https://gitter.im/Laverna/laverna](https://badges.gitter.im/Laverna/laverna.svg)](https://gitter.im/Laverna/laverna?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/Laverna/laverna.svg?branch=dev)](https://travis-ci.org/Laverna/laverna) [![devDependency Status](https://david-dm.org/Laverna/laverna/dev-status.svg)](https://david-dm.org/Laverna/laverna#info=devDependencies) [![Code Climate](https://codeclimate.com/github/Laverna/laverna/badges/gpa.svg)](https://codeclimate.com/github/Laverna/laverna) Laverna is a JavaScript note-taking web application with a Markdown editor and encryption support. It's built to be an open source alternative to Evernote. The application stores all your notes in your browser databases such as indexedDB or localStorage, which is good for security reasons, because only you have access to them. **Demo**: https://laverna.cc/ OR http://laverna.github.io/static-laverna ## Features ----------- * Markdown editor based on Pagedown * Manage your notes, even when you're offline * Secure client-side encryption * Synchronizes with cloud storage services (currently only with Dropbox and RemoteStorage) * Three editing modes: distraction free, preview, and normal mode * WYSIWYG control buttons * MathJax support * Syntax highlighting * No registration required * Web based * Keybindings ## Tools On the front-end this project uses JavaScript and the [Marionette JS](http://marionettejs.com/) framework while [Node JS](https://nodejs.org/en/), [Bower](https://bower.io/), and [Gulp.js](http://gulpjs.com/) are used on the back-end. The test runner used is [karma](https://karma-runner.github.io/1.0/index.html) however, contributors are free to utilize whatever testing tools they desire. ## Installation --------------- There are several ways to start using Laverna: 1. Open [laverna.cc][10] and start using it. No extra steps are needed. 2. Use a desktop app. 3. Use a prebuilt version from [Laverna/static-laverna][9] repository. 4. Build it from the source code. ### Desktop app installation --------------- Download the latest [Laverna release][13] for your operating system. After downloading the archive, you need to unpack it. Then, in the unpacked folder you need to run an executable (laverna.exe for Windows, laverna for Linux and Mac). #### Arch Linux (or derived distributions) The package can be found [here](https://aur.archlinux.org/packages/laverna/). For installation please use : ```bash $ pacaur -S laverna ``` For issue about installation please report [here](https://github.com/funilrys/PKGBUILD/issues/new) or contact [@funilrys](https://github.com/funilrys) on gitter [here](https://gitter.im/funilrys_/PKGBUILD) ### Installation of a prebuilt version ------------ #### 1. Download ```bash $ wget https://github.com/Laverna/static-laverna/archive/gh-pages.zip -O laverna.zip ``` #### 2. Unpack the downloaded archive ```bash $ unzip laverna.zip ``` #### 3. Open index.html in a browser Open in your favorite browser the index.html file which is located inside *laverna* directory. ## Installation from source --------------- To install, do the following: #### 1. Install Git This project requires that you have the latest version of git installed. To do so, see [Installing Git][14] (first-time users of git might want to check out the next section for configuring git). **Note:** Windows users will have to set the PATH variable for git after installing it. #### 2. Clone repository: For those who plan on contributing to the project's development , hit the fork button at the top of the page first (others can go on to the next step). Open a terminal, or command line, and navigate to the desired location of where you want to download the repository. Then enter the following commands to clone the repo: ```bash # clone the repository $ git clone git@github.com:Laverna/laverna.git # navigate to the project directory cd laverna ``` **3. Ensure you have the node.js platform installed.** (See OS-specific instructions on their [website][8]). **4. Ensure you have the bower and gulp packages installed** (locally and globally): ```bash $ npm install bower $ npm install -g bower $ npm install gulp $ npm install -g gulp ``` #### 5. Install Laverna's dependencies: ```bash $ npm install $ bower install $ cd test $ bower install $ cd .. ``` #### 6. Build minified version of Laverna: ```bash $ gulp build ``` #### 7. Start Laverna: ```bash $ gulp ``` ## MacOS notes on accepting incoming connections Because currently Laverna does not sign it's Mac packages, if you want to avoid the "Accept incoming connections" warning message everytime the application is launched, you can run the following commands. Assuming your current direction contains the laverna application: ```bash codesign -s - -f ./laverna.app/Contents/Frameworks/Electron\ Framework.framework codesign -s - -f ./laverna.app/Contents/Frameworks/Electron\ Helper\ EH.app codesign -s - -f ./laverna.app/Contents/Frameworks/Electron\ Helper\ NP.app codesign -s - -f ./laverna.app/Contents/Frameworks/Electron\ Helper.app codesign -s - -f ./laverna.app/Contents/Frameworks/Mantle.framework codesign -s - -f ./laverna.app/Contents/Frameworks/ReactiveCocoa.framework codesign -s - -f ./laverna.app/Contents/Frameworks/Squirrel.framework codesign --verify -vv ./laverna.app ``` ## Do you have questions? --------------- Please have a look in our [wiki][15]. ## Support --------------- * Hit star button on [github][6] * Like us on [alternativeto.net][5] * [Contribute][7] ### Coding Style Guidelines For those wanting to contribute code, we ask that you use either plain JavaScript or the Marionette.js framework. (For more details on the preferred coding style see [.editorconfig](https://github.com/Laverna/laverna/blob/master/.editorconfig)). Also, all experimental changes are being pushed on the **dev** branch, so any feature changes are preferred to be done on either this branch or a branch that uses the dev branch as its parent. ## Donation: ----------- * [Bitcoin][3] * [BountySource][12] ## Security -------------- Laverna uses the [SJCL] [1] library implementing the AES algorithm. You can review the code at: * https://github.com/Laverna/laverna/blob/master/app/scripts/classes/encryption.js * https://github.com/Laverna/laverna/blob/master/app/scripts/apps/encryption/ ## License -------------- Published under [MPL-2.0 License][11]. Laverna uses a lot of other libraries and each of these [libraries use different licenses][2]. [1]: http://bitwiseshiftleft.github.io/sjcl/ [2]: https://github.com/Laverna/laverna/blob/master/bower.json [3]: http://blockchain.info/address/1Q68HfLjNvWbLFr3KGK6nfXg7vc3hpDr11 [4]: https://www.gittip.com/Laverna/ [5]: http://alternativeto.net/software/laverna/ [6]: https://github.com/Laverna/laverna [7]: https://github.com/Laverna/laverna/blob/master/CONTRIBUTE.md [8]: http://nodejs.org [9]: https://github.com/Laverna/static-laverna/archive/gh-pages.zip [10]: https://laverna.cc/index.html [11]: https://www.mozilla.org/en-US/MPL/2.0/ [12]: https://www.bountysource.com/teams/laverna [13]: https://github.com/Laverna/laverna/releases [14]: https://git-scm.com/book/en/v2 [15]: https://github.com/Laverna/laverna/wiki ================================================ FILE: app/.htaccess ================================================ # Apache Server Configs v2.2.0 | MIT License # https://github.com/h5bp/server-configs-apache # (!) Using `.htaccess` files slows down Apache, therefore, if you have access # to the main server config file (usually called `httpd.conf`), you should add # this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html. # ############################################################################## # # CROSS-ORIGIN RESOURCE SHARING (CORS) # # ############################################################################## # ------------------------------------------------------------------------------ # | Cross-domain AJAX requests | # ------------------------------------------------------------------------------ # Allow cross-origin AJAX requests. # http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity # http://enable-cors.org/ # # Header set Access-Control-Allow-Origin "*" # # ------------------------------------------------------------------------------ # | CORS-enabled images | # ------------------------------------------------------------------------------ # Send the CORS header for images when browsers request it. # https://developer.mozilla.org/en-US/docs/HTML/CORS_Enabled_Image # http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html # http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/ SetEnvIf Origin ":" IS_CORS Header set Access-Control-Allow-Origin "*" env=IS_CORS # ------------------------------------------------------------------------------ # | Web fonts access | # ------------------------------------------------------------------------------ # Allow access to web fonts from all domains. Header set Access-Control-Allow-Origin "*" # ############################################################################## # # ERRORS # # ############################################################################## # ------------------------------------------------------------------------------ # | 404 error prevention for non-existing redirected folders | # ------------------------------------------------------------------------------ # Prevent Apache from returning a 404 error as the result of a rewrite # when the directory with the same name does not exist. # http://httpd.apache.org/docs/current/content-negotiation.html#multiviews # http://www.webmasterworld.com/apache/3808792.htm Options -MultiViews # ------------------------------------------------------------------------------ # | Custom error messages / pages | # ------------------------------------------------------------------------------ # Customize what Apache returns to the client in case of an error. # http://httpd.apache.org/docs/current/mod/core.html#errordocument ErrorDocument 404 /404.html # ############################################################################## # # INTERNET EXPLORER # # ############################################################################## # ------------------------------------------------------------------------------ # | Better website experience | # ------------------------------------------------------------------------------ # Force Internet Explorer to render pages in the highest available mode # in the various cases when it may not. # http://hsivonen.iki.fi/doctype/ie-mode.pdf Header set X-UA-Compatible "IE=edge" # `mod_headers` cannot match based on the content-type, however, this # header should be send only for HTML pages and not for the other resources Header unset X-UA-Compatible # ------------------------------------------------------------------------------ # | Cookie setting from iframes | # ------------------------------------------------------------------------------ # Allow cookies to be set from iframes in Internet Explorer. # http://msdn.microsoft.com/en-us/library/ms537343.aspx # http://www.w3.org/TR/2000/CR-P3P-20001215/ # # Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" # # ############################################################################## # # MIME TYPES AND ENCODING # # ############################################################################## # ------------------------------------------------------------------------------ # | Proper MIME types for all files | # ------------------------------------------------------------------------------ # Audio AddType audio/mp4 m4a f4a f4b AddType audio/ogg oga ogg opus # Data interchange AddType application/json json map AddType application/ld+json jsonld # JavaScript # Normalize to standard type. # http://tools.ietf.org/html/rfc4329#section-7.2 AddType application/javascript js # Video AddType video/mp4 f4v f4p m4v mp4 AddType video/ogg ogv AddType video/webm webm AddType video/x-flv flv # Web fonts AddType application/font-woff woff AddType application/vnd.ms-fontobject eot # Browsers usually ignore the font MIME types and simply sniff the bytes # to figure out the font type. # http://mimesniff.spec.whatwg.org/#matching-a-font-type-pattern # Chrome however, shows a warning if any other MIME types are used for # the following fonts. AddType application/x-font-ttf ttc ttf AddType font/opentype otf # Make SVGZ fonts work on the iPad. # https://twitter.com/FontSquirrel/status/14855840545 AddType image/svg+xml svgz AddEncoding gzip svgz # Other AddType application/octet-stream safariextz AddType application/x-chrome-extension crx AddType application/x-opera-extension oex AddType application/x-web-app-manifest+json webapp AddType application/x-xpinstall xpi AddType application/xml atom rdf rss xml AddType image/webp webp AddType image/x-icon cur AddType text/cache-manifest appcache manifest AddType text/vtt vtt AddType text/x-component htc AddType text/x-vcard vcf # ------------------------------------------------------------------------------ # | UTF-8 encoding | # ------------------------------------------------------------------------------ # Use UTF-8 encoding for anything served as `text/html` or `text/plain`. AddDefaultCharset utf-8 # Force UTF-8 for certain file formats. AddCharset utf-8 .atom .css .js .json .jsonld .rss .vtt .webapp .xml # ############################################################################## # # URL REWRITES # # ############################################################################## # ------------------------------------------------------------------------------ # | Rewrite engine | # ------------------------------------------------------------------------------ # Turn on the rewrite engine and enable the `FollowSymLinks` option (this is # necessary in order for the following directives to work). # If your web host doesn't allow the `FollowSymlinks` option, you may need to # comment it out and use `Options +SymLinksIfOwnerMatch`, but be aware of the # performance impact. # http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks # Also, some cloud hosting services require `RewriteBase` to be set. # http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site Options +FollowSymlinks # Options +SymLinksIfOwnerMatch RewriteEngine On # RewriteBase / # ------------------------------------------------------------------------------ # | Suppressing / Forcing the `www.` at the beginning of URLs | # ------------------------------------------------------------------------------ # The same content should never be available under two different URLs, # especially not with and without `www.` at the beginning. This can cause # SEO problems (duplicate content), and therefore, you should choose one # of the alternatives and redirect the other one. # By default `Option 1` (no `www.`) is activated. # http://no-www.org/faq.php?q=class_b # If you would prefer to use `Option 2`, just comment out all the lines # from `Option 1` and uncomment the ones from `Option 2`. # IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME! # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Option 1: rewrite www.example.com → example.com RewriteCond %{HTTPS} !=on RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Option 2: rewrite example.com → www.example.com # Be aware that the following might not be a good idea if you use "real" # subdomains for certain parts of your website. # # RewriteCond %{HTTPS} !=on # RewriteCond %{HTTP_HOST} !^www\. [NC] # RewriteCond %{SERVER_ADDR} !=127.0.0.1 # RewriteCond %{SERVER_ADDR} !=::1 # RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] # # ############################################################################## # # SECURITY # # ############################################################################## # ------------------------------------------------------------------------------ # | Clickjacking | # ------------------------------------------------------------------------------ # Protect website against clickjacking. # The example below sends the `X-Frame-Options` response header with the value # `DENY`, informing browsers not to display the web page content in any frame. # This might not be the best setting for everyone. You should read about the # other two possible values for `X-Frame-Options`: `SAMEORIGIN` & `ALLOW-FROM`. # http://tools.ietf.org/html/rfc7034#section-2.1 # Keep in mind that while you could send the `X-Frame-Options` header for all # of your site’s pages, this has the potential downside that it forbids even # non-malicious framing of your content (e.g.: when users visit your site using # a Google Image Search results page). # Nonetheless, you should ensure that you send the `X-Frame-Options` header for # all pages that allow a user to make a state changing operation (e.g: pages # that contain one-click purchase links, checkout or bank-transfer confirmation # pages, pages that make permanent configuration changes, etc.). # Sending the `X-Frame-Options` header can also protect your website against # more than just clickjacking attacks: https://cure53.de/xfo-clickjacking.pdf. # http://tools.ietf.org/html/rfc7034 # http://blogs.msdn.com/b/ieinternals/archive/2010/03/30/combating-clickjacking-with-x-frame-options.aspx # https://www.owasp.org/index.php/Clickjacking # # Header set X-Frame-Options "DENY" # # Header unset X-Frame-Options # # # ------------------------------------------------------------------------------ # | Content Security Policy (CSP) | # ------------------------------------------------------------------------------ # Mitigate the risk of cross-site scripting and other content-injection attacks. # This can be done by setting a `Content Security Policy` which whitelists # trusted sources of content for your website. # The example header below allows ONLY scripts that are loaded from the current # site's origin (no inline scripts, no CDN, etc). This almost certainly won't # work as-is for your site! # For more details on how to craft a reasonable policy for your site, read: # http://html5rocks.com/en/tutorials/security/content-security-policy (or the # specification: http://w3.org/TR/CSP). Also, to make things easier, you can # use an online CSP header generator such as: http://cspisawesome.com/. # # Header set Content-Security-Policy "script-src 'self'; object-src 'self'" # # Header unset Content-Security-Policy # # # ------------------------------------------------------------------------------ # | File access | # ------------------------------------------------------------------------------ # Block access to directories without a default document. # You should leave the following uncommented, as you shouldn't allow anyone to # surf through every directory on your server (which may includes rather private # places such as the CMS's directories). Options -Indexes # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Block access to hidden files and directories. # This includes directories used by version control systems such as Git and SVN. RewriteCond %{SCRIPT_FILENAME} -d [OR] RewriteCond %{SCRIPT_FILENAME} -f RewriteRule "(^|/)\." - [F] # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Block access to files that can expose sensitive information. # By default, block access to backup and source files that may be left by some # text editors and can pose a security risk when anyone has access to them. # http://feross.org/cmsploit/ # IMPORTANT: Update the `` regular expression from below to include # any files that might end up on your production server and can expose sensitive # information about your website. These files may include: configuration files, # files that contain metadata about the project (e.g.: project dependencies), # build scripts, etc.. # Apache < 2.3 Order allow,deny Deny from all Satisfy All # Apache ≥ 2.3 Require all denied # ------------------------------------------------------------------------------ # | Reducing MIME-type security risks | # ------------------------------------------------------------------------------ # Prevent some browsers from MIME-sniffing the response. # This reduces exposure to drive-by download attacks and should be enable # especially if the web server is serving user uploaded content, content # that could potentially be treated by the browser as executable. # http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-v-comprehensive-protection.aspx # http://msdn.microsoft.com/en-us/library/ie/gg622941.aspx # http://mimesniff.spec.whatwg.org/ # # Header set X-Content-Type-Options "nosniff" # # ------------------------------------------------------------------------------ # | Reflected Cross-Site Scripting (XSS) attacks | # ------------------------------------------------------------------------------ # (1) Try to re-enable the Cross-Site Scripting (XSS) filter built into the # most recent web browsers. # # The filter is usually enabled by default, but in some cases it may be # disabled by the user. However, in Internet Explorer for example, it can # be re-enabled just by sending the `X-XSS-Protection` header with the # value of `1`. # # (2) Prevent web browsers from rendering the web page if a potential reflected # (a.k.a non-persistent) XSS attack is detected by the filter. # # By default, if the filter is enabled and browsers detect a reflected # XSS attack, they will attempt to block the attack by making the smallest # possible modifications to the returned web page. # # Unfortunately, in some browsers (e.g.: Internet Explorer), this default # behavior may allow the XSS filter to be exploited, thereby, it's better # to tell browsers to prevent the rendering of the page altogether, instead # of attempting to modify it. # # http://hackademix.net/2009/11/21/ies-xss-filter-creates-xss-vulnerabilities # # IMPORTANT: Do not rely on the XSS filter to prevent XSS attacks! Ensure that # you are taking all possible measures to prevent XSS attacks, the most obvious # being: validating and sanitizing your site's inputs. # # http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-iv-the-xss-filter.aspx # http://blogs.msdn.com/b/ieinternals/archive/2011/01/31/controlling-the-internet-explorer-xss-filter-with-the-x-xss-protection-http-header.aspx # https://www.owasp.org/index.php/Cross-site_Scripting_%28XSS%29 # # # (1) (2) # Header set X-XSS-Protection "1; mode=block" # # Header unset X-XSS-Protection # # # ------------------------------------------------------------------------------ # | Secure Sockets Layer (SSL) | # ------------------------------------------------------------------------------ # Rewrite secure requests properly in order to prevent SSL certificate warnings. # E.g.: prevent `https://www.example.com` when your certificate only allows # `https://secure.example.com`. # # RewriteCond %{SERVER_PORT} !^443 # RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L] # # ------------------------------------------------------------------------------ # | HTTP Strict Transport Security (HSTS) | # ------------------------------------------------------------------------------ # Force client-side SSL redirection. # If a user types `example.com` in his browser, the above rule will redirect # him to the secure version of the site. That still leaves a window of # opportunity (the initial HTTP connection) for an attacker to downgrade or # redirect the request. # The following header ensures that browser will ONLY connect to your server # via HTTPS, regardless of what the users type in the address bar. # http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-14#section-6.1 # http://www.html5rocks.com/en/tutorials/security/transport-layer-security/ # IMPORTANT: Remove the `includeSubDomains` optional directive if the subdomains # are not using HTTPS. # # Header set Strict-Transport-Security "max-age=16070400; includeSubDomains" # # ------------------------------------------------------------------------------ # | Server software information | # ------------------------------------------------------------------------------ # Avoid displaying the exact Apache version number, the description of the # generic OS-type and the information about Apache's compiled-in modules. # ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`! # ServerTokens Prod # ############################################################################## # # WEB PERFORMANCE # # ############################################################################## # ------------------------------------------------------------------------------ # | Compression | # ------------------------------------------------------------------------------ # Force compression for mangled headers. # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding # Compress all output labeled with one of the following MIME-types # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` # and can remove the `` and `` lines # as `AddOutputFilterByType` is still in the core directives). AddOutputFilterByType DEFLATE application/atom+xml \ application/javascript \ application/json \ application/ld+json \ application/rss+xml \ application/vnd.ms-fontobject \ application/x-font-ttf \ application/x-web-app-manifest+json \ application/xhtml+xml \ application/xml \ font/opentype \ image/svg+xml \ image/x-icon \ text/css \ text/html \ text/plain \ text/x-component \ text/xml # ------------------------------------------------------------------------------ # | Content transformations | # ------------------------------------------------------------------------------ # Prevent mobile network providers from modifying the website's content. # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5. # # Header set Cache-Control "no-transform" # # ------------------------------------------------------------------------------ # | ETags | # ------------------------------------------------------------------------------ # Remove `ETags` as resources are sent with far-future expires headers. # http://developer.yahoo.com/performance/rules.html#etags. # `FileETag None` doesn't work in all cases. Header unset ETag FileETag None # ------------------------------------------------------------------------------ # | Expires headers | # ------------------------------------------------------------------------------ # The following expires headers are set pretty far in the future. If you # don't control versioning with filename-based cache busting, consider # lowering the cache time for resources such as style sheets and JavaScript # files to something like one week. ExpiresActive on ExpiresDefault "access plus 1 month" # CSS ExpiresByType text/css "access plus 1 year" # Data interchange ExpiresByType application/json "access plus 0 seconds" ExpiresByType application/ld+json "access plus 0 seconds" ExpiresByType application/xml "access plus 0 seconds" ExpiresByType text/xml "access plus 0 seconds" # Favicon (cannot be renamed!) and cursor images ExpiresByType image/x-icon "access plus 1 week" # HTML components (HTCs) ExpiresByType text/x-component "access plus 1 month" # HTML ExpiresByType text/html "access plus 0 seconds" # JavaScript ExpiresByType application/javascript "access plus 1 year" # Manifest files ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" ExpiresByType text/cache-manifest "access plus 0 seconds" # Media ExpiresByType audio/ogg "access plus 1 month" ExpiresByType image/gif "access plus 1 month" ExpiresByType image/jpeg "access plus 1 month" ExpiresByType image/png "access plus 1 month" ExpiresByType video/mp4 "access plus 1 month" ExpiresByType video/ogg "access plus 1 month" ExpiresByType video/webm "access plus 1 month" # Web feeds ExpiresByType application/atom+xml "access plus 1 hour" ExpiresByType application/rss+xml "access plus 1 hour" # Web fonts ExpiresByType application/font-woff "access plus 1 month" ExpiresByType application/vnd.ms-fontobject "access plus 1 month" ExpiresByType application/x-font-ttf "access plus 1 month" ExpiresByType font/opentype "access plus 1 month" ExpiresByType image/svg+xml "access plus 1 month" # ------------------------------------------------------------------------------ # | Filename-based cache busting | # ------------------------------------------------------------------------------ # If you're not using a build process to manage your filename version revving, # you might want to consider enabling the following directives to route all # requests such as `/css/style.12345.css` to `/css/style.css`. # To understand why this is important and a better idea than `*.css?v231`, read: # http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring # # RewriteCond %{REQUEST_FILENAME} !-f # RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpe?g|gif)$ $1.$3 [L] # # ------------------------------------------------------------------------------ # | File concatenation | # ------------------------------------------------------------------------------ # Allow concatenation from within specific style sheets and JavaScript files. # e.g.: # # If you have the following content in a file # # # # # Apache will replace it with the content from the specified files. # # # Options +Includes # AddOutputFilterByType INCLUDES application/javascript application/json # SetOutputFilter INCLUDES # # # Options +Includes # AddOutputFilterByType INCLUDES text/css # SetOutputFilter INCLUDES # # ================================================ FILE: app/404.html ================================================ Page Not Found :(

Not found :(

Sorry, but the page you were trying to view does not exist.

It looks like this was the result of either:

================================================ FILE: app/config.xml ================================================ Laverna Open source note taking application Laverna project ================================================ FILE: app/docs/howto.md ================================================ ### How to use tags in Laverna When you are editing or creating notes, write "@" before any word to create a tag. ### How to use tasks in Laverna You can create tasks by prefacing line with [ ] or [x] (incomplete or complete, respectively). Tasks will be automatically rendered as checkboxes that you can check on and off. ================================================ FILE: app/dropbox.html ================================================ ================================================ FILE: app/index.html ================================================ Laverna
================================================ FILE: app/locales/ar/translation.json ================================================ { "Search": "بحث", "All notes": "كل الملاحظات", "Favourites": "المفضلة", "Favorite": "مفضلة", "Trash": "سلة المهملات", "Open tasks": "فتح المهام", "Notebooks": "دفاتر الملاحظات", "Settings": "الإعدادات", "About": "حول", "Save": "حفظ", "Save & Exit": "حفظ وخروج", "Cancel": "إلغاء", "Full screen": "كامل الشاشة", "Preview": "معاينة", "Normal": "عادي", "Select notebook": "إختر دفتر", "Title": "العنوان", "Submit": "أرسل", "Tags": "علامات", "Tag": "علامة", "Parent": "والد", "Root": "جذر", "Notebooks & tags": "الدفاتر والعلامات", "Notebook": "دفتر", "Restore": "استعادة", "Delete": "حذف", "New tag": "علامة جديدة", "Edit": "تحرير", "Remove": "إزالة", "Forever": "للأبد", "No": "لا", "Yes": "نعم", "Basic": "أساسي", "Cloud storage": "تخزين سحابي", "Notes per page": "الملاحظات في الصفحة", "Sort notebooks": "رتّب الملاحظات", "Name": "الاسم", "Created": "أنشأ", "Default edit mode": "وضع التحرير الافتراضي", "Fullscreen with preview": "معاينة على كامل الشاشة", "Use encryption": "استخدم التشفير", "Encryption parameters": "موسطات التشفير", "Encryption Password": "كلمة سر التشفير", "Salt": "حافظ", "Random": "عشوائي", "Key size": "حجم المفتاح", "Strengthen by a factor of": "تقوية بمعامل مقداره هو", "Authentication strength": "قوة الاستيثاق", "Unlock": "فك", "Your new encryption password": "كلمة سر التشفير الجديدة لك هي", "Your old encryption password": "كلمة سر التشفير السابقة هي", "Show sidebar": "عرض الشريط الجانبي", "Previous": "السابقة", "Next": "التالية", "Navigation": "استعراض", "navigateTop": "أعلى", "navigateBottom": "أسفل", "Jump": "قفز", "jumpInbox": "إذهب لصندوق الوارد", "jumpNotebook": "إذهب إلى قائمة الدفاتر", "jumpFavorite": "إذهب إلى الملاحظات المفضلة", "jumpRemoved": "إذهب إلى الملاحظات الزائلة", "jumpOpenTasks": "إذهب إلى الملاحظات ذات المهام القائمة", "Actions": "إجراءات", "actionsEdit": "تحرير", "actionsOpen": "فتح", "actionsRemove": "إزالة", "actionsRotateStar": "دوّر النجمة", "App": "التطبيق", "appCreateNote": "أنشئ ملاحظة جديدة", "appSearch": "إبحث عن ملاحظة", "appKeyboardHelp": "مساعدة لوحة المفاتيح", "Change keybindings": "تغيير إعدادات ارتباطات المفاتيح", "Donate": "تبرَّع", "Github page": "Github صفحة", "Report bugs and issues here": "أخبرنا عن العلل والمشكلات هنا", "Report bugs through email": "أبلغ عن العلل بالبريد", "Credits": "شكر وتقدير", "List of contributors": "قائمة المساهمين", "List of all used libraries": "قائمة جميع المكتبات المستخدمة", "Are you sure?": "هل أنت متأكد؟", "You have unsaved changes": "لديك تغييرات لم تحفظها.", "Dropbox API key": "مفتاح API لـ Dropbox", "Required": "مطلوب", "Optional": "اختياري", "Language": "اللغة", "Action": "الإجراء", "Select": "اختر", "General": "عامّ", "Encryption": "التشفير", "Keybindings": "ارتباطات المفاتيح", "Sync": "مزامنة", "Profiles": "ملفات التعريف", "Import": "استيراد", "Transfer data": "استيراد وتصدير", "Import settings": "استيراد الإعدادات", "Export settings": "تصدير الإعدادات", "Wrong format": "صيغة خطأ", "useDefaultConfigs": "استخدم الإعدادات من ملف التعريف الافتراضي", "File chould be in json format": "يجب أن يكون الملف بصيغة JSON", "Close": "إغلاق", "Hyperlink": "ارتباط تشعبي", "Editor": "المحرر", "Preview": "معاينة", "Download": "سحب", "Transfer everything": "كل شيء", "encryption": { "wait": "فضلاً انتظر حتى يكتمل التشفير", "error": "خطأ في التشفير", "errorConfirm": "خطأ أثناء فك تشفير البيانات. \r\r **حدّث إعداداتك** في هذا المستعرض كذلك إن كنت قد غيّرت إعدادات التشفير في مستعرض آخر ، أو حاول استيراد الإعدادات. \r\r وإن كنت لم تغيّر شيئاً **حاول الدخول** مرة ثانية.", "errorConfirmSettings": "غيّر إعدادات التشفير", "errorConfirmAuth": "أعد المحاولة", "backup": { "title": "نسخ احتياطي للبيانات", "content": "قبل الاستمرار للخطوة التالية ، فضلاً اسحب ملف النسخة الاحتياطية. يحتوي الملف بيانات ملفات التعريف المتغيرة السابقة دون تشفير. إحفظه في مكان آمن.", "next": "واصل دون سحب ملف النسخة الاحتياطية" }, "state": { "decrypt": "جار فك تشفير كل شيء", "encrypt": "تشفير كل شيء", "save": "جار حفظ التغييرات" } }, "profile": { "confirm remove": "سيتم حذف ملف التعريف **{{profile}}** بجميع البيانات بما فيها الملاحظات والعلامات والدفاتر‫.‬ هذا الإجراء لا يمكن التراجع عنه‫!‬", "type name": "اكتب اسم الملف التعريفي" }, "files": { "file-url": "عنوان URL للملف أو الصورة", "attach": "أرفق ملفاً", "attachLink": "أرفق رابطاً", "attachImage": "أرفق صورة" }, "notes": { "confirm trash": "سيتم نقل الملاحظة **{{title}}** إلى سلة المهملات.", "confirm remove": "سيتم حذف الملاحظة **{{title}}** ‫**‬إلى الأبد‫**‬!", "create and attach": "أنشئ ملاحظة جديدة وأرفق هذا الرابط", "create": "أنشئ ملاحظة جديدة", "hyperlink-dialog": "عنوان الملاحظة أو العنوان URL" }, "notebooks": { "select": "إختر دفتر ملاحظات", "add": "أضف دفتر ملاحظات جديد", "edit": "عدّل دفتر ملاحظات", "name": "فضلاً أكتب اسماً لهذا الدفتر", "confirm remove": "الدفتر **{{name}}** سيتم حذفه ‫**‬إلى الأبد‫**‬!", "remove with notes": "نعم إحذفه والملاحظات المرفقة", "remove": "نعم إحذفه" }, "tags": { "name": "اسم العلامة مطلوب", "add": "أضف علامة جديدة", "edit": "حرّر علامة", "confirm remove": "العلامة **{{name}}** سيتم حذفها ‫**‬إلى الأبد‫**‬!" }, "dropbox": { "auth confirm": "ستتم إعادة التوجيه الآن إلى صفحة تسجيل الدخول في **Dropbox**.\r> فضلاً إضغط زر **OK**.", "auth title": "Dropbox auth", "api info 1": "يمكنك إنشاء مفتاح API الخاص بك على", "api info 2": "ضع في حسبانك أنك عندما تنشئ تطبيقاً جديداً على موقع المطورين على Dropbox فإن :", "api info li 1": "نوع التطبيق يجب أن يكون Dropbox API app", "api info li 2": "نوع البيانات يجب أن يكون Files and datastores" }, "help": { "firststart title": "مرحباً بك في Laverna", "firststart import": "إن كنت قد استخدمت Laverna من قبل فيمكنك استيراد إعداداتك السابقة بالضغط على زر 'استيراد' أدناه", "firststart next": "إذا لم تستخدم Laverna من قبل فاضغط على زر 'التالي' لبدء عملية التثبيت", "firststart encryption": "إن كنت ترغب استعمال التشفير فاكتب كلمة سر التشفير‫.‬", "firststart sync": "تحتاج لتمكين المزامنة مع أحد المحوّلات لتتمكن من عرض ملاحظاتك على أجهزة أخرى ، نظراً لأننا لانخزّن أي بيانات على خوادمنا.", "firststart backup": "لقد اكتمل كل شيء تقريباً‫.‬ يمكنك سحب نسخة احتياطية من إعداداتك والتقدم للخطوة التالية." } } ================================================ FILE: app/locales/bs_ba/translation.json ================================================ { "en" : "Engleski", "ru" : "Ruski", "nl" : "Holandski", "fr" : "Francuski", "pt_br" : "Portugalski (Brazil)", "eo": "Esperanto", "es": "Španski", "de": "Njemački", "se": "Švedski", "el": "Grčki", "nb": "Norveški (Bokmal)", "nn": "Norveški (Nynorsk)", "Search": "Pretraži", "All notes": "Sve bilješke", "Favourites": "Omiljene", "Favorite": "Stavi u omiljene", "Trash": "Smeće", "Notebooks": "Bilježnice", "Settings": "Postavke", "About": "O programu", "Save": "Sačuvaj", "Save & Exit": "Sačuvaj i izađi", "Cancel": "Odustani", "Full screen": "Preko cijelog ekrana", "Preview": "Pregledaj", "Normal": "Normalno", "Select notebook": "Izaberi bilježnicu", "Title": "Naslov", "Submit": "Pošalji", "Tags": "Tagovi", "Tag": "Tag", "Parent": "Roditelj", "Root": "Glavna bilježnica", "Notebooks & tags": "Bilježnice i tagovi", "Notebook": "Bilježnica", "Restore": "Vrati prethodno", "Delete": "Izbriši", "New tag": "Novi tag", "Edit": "Uredi", "Remove": "Ukloni", "Forever": "Zauvijek", "No": "Ne", "Yes": "Da", "Basic": "Osnovno", "Cloud storage": "Cloud smještaj", "Notes per page": "Bilješki po stranici", "Sort notebooks": "Sortiraj bilježnice", "Name": "Ime", "Created": "Kreirano", "Default edit mode": "Podrazumijevana forma za uređivanje", "Fullscreen with preview": "Preko cijelog ekrana sa prikazom", "Use encryption": "Koristi enkripciju", "Encryption parameters": "Enkripcijski parametri", "Encryption Password": "Enkripcijska šifra", "Salt": "Salt", "Random": "Nasumično", "Key size": "Veličina ključa", "Strengthen by a factor of": "Pojačana sa faktorom od", "Authentication strength": "Snaga autentifikacije", "Unlock": "Otključaj", "Your new encryption password": "Vaša nova šifra za enkripciju", "Your old encryption password": "Vaša stara šifra za enkripciju", "Please wait until the encryption will be completed": "Molimo sačekajte dok enkripcija ne bude završena.", "Shortcuts": "Prečice", "Newer": "Novije", "Older": "Starije", "Navigation": "Navigacije", "navigateTop": "Vrh", "navigateBottom": "Dno", "Jump": "Skoči", "jumpInbox": "Idi u inboks", "jumpNotebook": "Prikaži listu bilježnica", "jumpFavorite": "Prikaži omiljene bilješke", "jumpRemoved": "Prikaži obrisane bilješke", "Actions": "Akcije", "actionsEdit": "Uredi", "actionsOpen": "Otvori", "actionsRemove": "Izbriši", "actionsRotateStar": "Rotacijska zvijezda", "App": "Aplikacija", "appCreateNote": "Kreiraj novu bilješku", "appSearch": "Pretraži unutar bilješke", "appKeyboardHelp": "Pomoć oko tastature" } ================================================ FILE: app/locales/da/translation.json ================================================ { "Search": "Søg", "All notes": "Alle noter", "Favourites": "Foretrukne", "Favorite": "Favorit", "Trash": "Papirkurv", "Open tasks": "Åbne opgaver", "Notebooks": "Notesbøger", "Settings": "Indstillinger", "About": "Om", "Save": "Gem", "Save & Exit": "Gem & Luk", "Cancel": "Annuler", "Full screen": "Fuld skærm", "Preview": "Smugkig", "Normal": "Normal", "Select notebook": "Vælg notesbog", "Title": "Titel", "Submit": "Indsend", "Tags": "Tags", "Tag": "Tag", "Parent": "Overliggende", "Root": "Rod", "Notebooks & tags": "Notesbøger & tags", "Notebook": "Notesbog", "Restore": "Gendan", "Delete": "Slet", "New tag": "Nyt tag", "Edit": "Rediger", "Remove": "Fjern", "Forever": "Permanent", "No": "Nej", "Yes": "Ja", "Basic": "Basalt", "Cloud storage": "Opbevaring i Skyen", "Notes per page": "Noter per side", "Sort notebooks": "Sortér notesbøger", "Name": "Navn", "Created": "Oprettet", "Default edit mode": "Standard redigeringsvindue", "Fullscreen with preview": "Fuldskærm med smugkig", "Use encryption": "Benyt kryptering", "Encryption parameters": "Krypteringsparametre", "Encryption Password": "Krypteringskodeord", "Salt": "Salt", "Random": "Tilfældig", "Key size": "Nøglestørrelse", "Strengthen by a factor of": "Styrk med en faktor på", "Authentication strength": "Autentifikationsstyrke", "Unlock": "Lås op", "Your new encryption password": "Dit nye krypteringskodeord", "Your old encryption password": "Dit gamle krypteringskodeord", "Show sidebar": "Vis sidepanelet", "Previous": "Forrige", "Next": "Næste", "Navigation": "Navigation", "navigateTop": "Top", "navigateBottom": "Bund", "Jump": "Hop", "jumpInbox": "Vis indbakken", "jumpNotebook": "Åben liste over notesbøger", "jumpFavorite": "Vis foretrukne noter", "jumpRemoved": "Vis slettede noter", "jumpOpenTasks": "Vis noter med ufærdige opgaver", "Actions": "Handlinger", "actionsEdit": "Rediger", "actionsOpen": "Åben", "actionsRemove": "Fjern", "actionsRotateStar": "Rotér stjerne", "App": "App", "appCreateNote": "Opret ny note", "appSearch": "Søg efter note", "appKeyboardHelp": "Tastaturhjælp", "Change keybindings": "Ændre indstillinger for genvejstaster", "Donate": "Donér", "Github page": "Github side", "Report bugs and issues here": "Anmeld fejl og problemer her", "Report bugs through email": "Anmeld fejl via email", "Credits": "Anerkendelse", "List of contributors": "Liste over bidragsydere", "List of all used libraries": "Liste over alle benyttede libraries", "Are you sure?": "Er du helt sikker?", "You have unsaved changes": "Du har ikke-gemte ændringer.", "Dropbox API key": "Dropbox API nøgle", "Required": "Obligatorisk", "Optional": "Valgfrit", "Language": "Sprog", "Action": "Handling", "Select": "Vælg", "General": "Generelt", "Encryption": "Kryptering", "Keybindings": "Genvejstaster", "Sync": "Synkronisering", "Profiles": "Profiler", "Import": "Importer", "Transfer data": "Overfør data", "Import settings": "Importer indstillinger", "Export settings": "Eksporter indstillinger", "Wrong format": "Forkert format", "useDefaultConfigs": "Brug standardinstillingerne", "File chould be in json format": "Filen kunne være i json format", "Close": "Luk", "Hyperlink": "Hyperink", "Editor": "Redigeringsvindue", "Preview": "Smugkig", "Download": "Download", "Transfer everything": "Overfør alt", "encryption": { "wait": "Vent venligst indtil krypteringen er fuldført", "error": "Krypteringsfejl", "errorConfirm": "Der opstod en fejl under dekrypteringen.\r\r Hvis du har foretaget ændringer af krypteringen i en anden browser, **skal du også ændre dette** i denne browser. Eller forsøge at importere disse ændringer.\r\r Hvis du ikke har foretaget nogen ændringer, kan du **forsøge logge ind** igen.", "errorConfirmSettings": "Ændre indstillinger for kryptering", "errorConfirmAuth": "Prøv igen", "backup": { "title": "Foretag backup af data", "content": "Vær venlig at downloade din backup-fil, før du forsætter. Denne indeholder dekrypteret tidligere data fra tidligere profiler. Opbevar denne et sikkert sted.", "next": "Fortsæt uden at downloade en backup-fil" }, "state": { "decrypt": "Dekrypter alt", "encrypt": "Krypter alt", "save": "Gem ændringerne" } }, "profile": { "confirm remove": "Al data for profilen **{{profile}}** , såsom noter, tags og notesbøger, vil blive fjernet permanent !", "type name": "Skriv profilnavn" }, "files": { "file-url": "Sti til fil eller billede", "attach": "Vedhæft en fil", "attachLink": "Vedhæft som et link", "attachImage": "Vedhæft som et billede" }, "notes": { "confirm trash": "Noten **{{title}}** vil blive flyttet til papirskurven.", "confirm remove": "Noten **{{title}}** vil blive **fjernet permanent**!", "create and attach": "Opret en ny note og vedhæft denne som et link", "create": "Opret en ny note", "hyperlink-dialog": "Titel på note eller sti" }, "notebooks": { "select": "Vælg en notesbog", "add": "Tilføj en ny notesbog", "edit": "Rediger notesbog", "name": "Angiv venligst et navn for notesbogen", "confirm remove": "Notesbogen **{{name}}** vil blive **fjernet permanent**!", "remove with notes": "Ja, fjern med vedhæftede noter", "remove": "Ja tak, fjern" }, "tags": { "name": "Tagnavn er obligatorisk", "add": "Tilføj et nyt tag", "edit": "Rediger et tag", "confirm remove": "Tagget **{{name}}** vil blive **fjernet permanent**!" }, "dropbox": { "auth confirm": "Du vil nu blive omdirigeret til **Dropboxs** autentifikationsside.\r> Klik venligst på **OK**.", "auth title": "Dropbox auth", "api info 1": "Du kan angive din egen API nøgle", "api info 2": "Bemærk følgende når du opretter en ny app på Dropboxs side for udviklere:", "api info li 1": "Applikationstypen skal være Dropbox API app", "api info li 2": "Datatypen skal være Files and datastores" }, "help": { "firststart title": "Velkommen til Laverna", "firststart import": "Hvis du har benyttet Laverna før, kan du importere dine data på knappen 'importer'.", "firststart next": "Hvis ikke du har benyttet dig af Laverna før, kan du gå videre ved at trykke på knappen 'næste' og starte installationen.", "firststart encryption": "Hvis du vil kryptere dit indhold angiv venligst et kodeord.", "firststart sync": "Da vi ikke lagre nogen form for data på vores servere, skal du benytte en af synkroniseringstjenesterne for at se dit indhold på dine andre enheder.", "firststart backup": "Alt er næsten klart. Du kan derfor downloade en backup af dine indstillinger og forsætte til det sidste trin." } } ================================================ FILE: app/locales/de/translation.json ================================================ { "Search": "Suchen", "All notes": "Alle Notizen", "Favourites": "Favoriten", "Favorite": "Favorit", "Trash": "Mülleimer", "Open tasks": "Offene Aufgaben", "Notebooks": "Notizbücher", "Settings": "Einstellungen", "About": "Über", "Save": "Speichern", "Save & Exit": "Speichern & Schließen", "Cancel": "Abbrechen", "Full screen": "Vollbild", "Preview": "Vorschau", "Normal": "Normal", "Select notebook": "Notizbuch wählen", "Title": "Titel", "Submit": "Senden", "Tags": "Tags", "Tag": "Tag", "Parent": "Elternelement", "Root": "Hauptverzeichnis", "Notebooks & tags": "Notizbücher & Tags", "Notebook": "Notizbuch", "Restore": "Wiederherstellen", "Delete": "Löschen", "New tag": "Neuer Tag", "Edit": "Bearbeiten", "Remove": "Entfernen", "Forever": "Endgültig", "No": "Nein", "Yes": "Ja", "Basic": "Basis", "Cloud storage": "Cloudspeicher", "Notes per page": "Notizen pro Seite", "Sort notebooks": "Notizbücher sortieren nach", "Name": "Name", "Created": "Erstellungsdatum", "Default edit mode": "Standard Bearbeitungsmodus", "Fullscreen with preview": "Vollbild mit Vorschau", "Use encryption": "Verschlüsselung verwenden", "Encryption parameters": "Verschlüsselungsparameter", "Encryption Password": "Verschlüsselungspasswort", "Salt": "Salt", "Random": "Zufällig", "Key size": "Schlüsselgröße", "Strengthen by a factor of": "Verstärken um den Faktor", "Authentication strength": "Verschlüsselungsstärke", "Unlock": "Entsperren", "Your new encryption password": "Ihr neues Verschlüsselungspasswort", "Your old encryption password": "Ihr altes Verschlüsselungspasswort", "Show sidebar": "Seitenleiste anzeigen", "Previous": "Zurück", "Next": "Weiter", "Navigation": "Navigation", "navigateTop": "Nach oben", "navigateBottom": "Nach unten", "Jump": "Springen", "jumpInbox": "Zur Inbox", "jumpNotebook": "Zur Notizbuchliste", "jumpFavorite": "Zu den Favoriten", "jumpRemoved": "Zu gelöschten Notizen", "jumpOpenTasks": "Zu Notizen mit offenen Aufgaben", "Actions": "Aktionen", "actionsEdit": "Bearbeiten", "actionsOpen": "Öffnen", "actionsRemove": "Entfernen", "actionsRotateStar": "Favorisieren", "App": "Anwendung", "appCreateNote": "Neue Notiz", "appSearch": "Notizen Durchsuchen", "appKeyboardHelp": "Tastaturhilfe", "Change keybindings": "Tastenkürzel ändern", "Donate": "Spenden", "Github page": "Github Seite", "Report bugs and issues here": "Fehler und Anregungen hier melden", "Report bugs through email": "Fehler per E-Mail melden", "Credits": "Danksagung", "List of contributors": "Liste der Mitwirkenden", "List of all used libraries": "Liste aller benutzten Bibliotheken", "Are you sure?": "Sind Sie sicher?", "You have unsaved changes": "Sie haben nicht gespeicherte Änderungen.", "Dropbox API key": "Dropbox API Schlüssel", "Required": "Erforderlich", "Optional": "Optional", "Language": "Sprache", "Action": "Aktion", "Select": "Wählen", "General": "Allgemein", "Encryption": "Verschlüsselung", "Keybindings": "Tastenkürzel", "Sync": "Synchronisierung", "Profiles": "Profile", "Import": "Importieren", "Transfer data": "Übertrage Daten", "Transfer settings": "Übertrage Einstellungen", "Import settings": "Importeinstellungen", "Export settings": "Exporteinstellungen", "Wrong format": "Falsches Format", "useDefaultConfigs": "Einstellungen des Standardprofils verwenden", "File chould be in json format": "Datei sollte im JSON-Format vorliegen", "Close": "Schließen", "Hyperlink": "Link", "Editor": "Editor", "Download": "Download", "Transfer everything": "Übertrage alles", "Other": "Sonstiges", "Default": "Standard", "Modules": "Module", "Import data": "Importiere Daten", "Export data": "Exportiere Daten", "Enabled": "Aktiviert", "Disabled": "Deaktiviert", "Untitled": "Unbenannt", "Line of": "Zeile {{currentLine}} von {{numberOfLines}}", "Drop files": "Ziehen Sie hier die Dateien zum Hochladen hinein", "Spaces per indent": "Leerzeichen pro Einrückung", "Sort notes": "Notizen sortieren nach", "Updated date": "Änderungsdatum", "Created date": "Erstelldatum", "Text editor": "Texteditor", "Vim": "Vim", "Emacs": "Emacs", "Sublime": "Sublime", "encryption": { "provide password": "Bitte, geben Sie Ihr Passwort ein", "change password": "Geben Sie Ihr Passwort hier ein, um es zu ändern", "wait": "Bitte warten, bis die Verschlüsselung abgeschlossen ist", "error": "Verschüsselungsfehler", "errorConfirm": "Fehler beim Verschlüsseln der Daten.\r\r Falls Sie die Verschlüsselungseinstellungen in einem anderen Browser geändert haben, **ändern Sie die Einstellungen** auch in diesem Browser. Oder probieren Sie andere Einstellungen.\r\r Und falls Sie nichts verändert haben, versuchen Sie, sich **erneut anzumelden**.", "errorConfirmSettings": "Verschlüsselungseinstellungen speichern", "errorConfirmAuth": "Nochmal versuchen", "backup": { "title": "Datensicherung", "content": "Bevor Sie fortfahren, laden Sie bitte Ihre Sicherungsdatei herunter. Sie enthält die entschlüsselten vorherigen Daten veränderter Profile. Verwahren Sie diese an einem sicheren Ort.", "next": "Fortfahren, ohne die Sicherungsdatei herunterzuladen" }, "state": { "decrypt": "Alles entschlüsseln", "encrypt": "Alles verschlüsseln", "save": "Änderungen speichern" } }, "profile": { "profile name": "Profilname", "confirm remove": "Das Profil **{{profile}}** wird mit allen Daten inklusive Notizen, Tags und Notizbüchern gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!", "type name": "Profilnamen eingeben" }, "files": { "file-url": "Datei oder Bild-URL", "attach": "Datei anhängen", "attachLink": "Als Link anhängen", "attachImage": "Als Bild anhängen" }, "notes": { "confirm trash": "Die Notiz **{{title}}** wird in den Papierkorb verschoben.", "confirm remove": "Die Notiz **{{title}}** wird **endgültig** gelöscht!", "create and attach": "Neue Notiz erstellen und deren Link anhängen", "create": "Neue Notiz erstellen", "hyperlink-dialog": "Titel einer Notiz oder URL" }, "notebooks": { "select": "Notizbuch wählen", "add": "Neues Notizbuch hinzufügen", "edit": "Notizbuch bearbeiten", "name": "Bitte geben Sie den Namen des Notizbuchs an", "confirm remove": "Das Notizbuch **{{name}}** wird **endgültig** gelöscht!", "remove with notes": "Ja, mit angehängten Notizen löschen", "remove": "Ja, löschen" }, "tags": { "name": "Tagname ist erforderlich", "add": "Neuen Tag hinzufügen", "edit": "Tag bearbeiten", "confirm remove": "Der Tag **{{name}}** wird **endgültig** gelöscht!" }, "dropbox": { "auth confirm": "Sie werden jetzt zur **Dropbox** Autorisierungsseite umgeleitet.\r> Bitte klicken Sie den **OK** Button.", "auth title": "Dropbox auth", "api info 1": "Sie können Ihren eigenen API-Schlüssel haben bei", "api info 2": "Wenn Sie eine neue App auf der Entwicklerwebsite von Dropbox erstellen, beachten Sie, dass:", "api info li 1": "der API-Typ \"Dropbox API\" sein sollte", "api info li 2": "der \"type of access\" \"Full Dropbox\" sein sollte" }, "help": { "firststart title": "Willkommen zu Laverna", "firststart import": "Wenn Sie Laverna bereits verwendet haben, können Sie Ihre alten Einstellungen über die 'Importieren' Schaltfläche unten importieren.", "firststart next": "Wenn Sie Laverna noch nie benutzt haben, klicken Sie auf 'Weiter' um die Installation zu starten.", "firststart encryption": "Falls Sie Verschlüsselung verwenden wollen, geben Sie bitte das Verschlüsselungspasswort an.", "firststart sync": "Damit Sie Ihre Notizen auch auf anderen Geräten sehen können, müssen Sie die Synchronisierung mit einem Adapter aktivieren, da wir keine Daten auf unseren Servern speichern.", "firststart backup": "Fast fertig. Sie können Ihre Einstellungen herunterladen und mit dem letzten Schritt fortfahren." } } ================================================ FILE: app/locales/de_ch/translation.json ================================================ { "en" : "Änglisch", "ru" : "Russisch", "nl" : "Dänisch", "fr" : "Französisch", "pt_br" : "Portugisich (Brasilien)", "nb" : "Norwegisch (Bokmal)", "nn" : "Norwegisch (Nynorsk)", "ru" : "Russisch", "eo": "Esperanto", "es": "Spanisch", "se": "Schwedisch", "el": "Griechisch", "bs_ba": "Bosnisch", "hi_in": "Hindi", "mr-in": "Marathi", "Search": "Sueche", "All notes": "Alli Notize", "Favourites": "Favoritä", "Favorite": "Favorit", "Trash": "Abfall", "Notebooks": "Notizbüecher", "Settings": "Istellige", "About": "Über uns", "Save": "Speichere", "Save & Exit": "Speichere & Zuemache", "Cancel": "Abbräche", "Full screen": "Full screen", "Preview": "Vorschau", "Normal": "Normal", "Select notebook": "Wähl es Notizbuech", "Title": "Titel", "Submit": "Sändä", "Tags": "Tags", "Tag": "Tag", "Parent": "Parent", "Root": "Root", "Notebooks & tags": "Notizbüecher & Tags", "Notebook": "Notizbuech", "Restore": "Wiederhärstelle", "Delete": "Lösche", "New tag": "Neue Tag", "Edit": "Bearbeite", "Remove": "Entfärne", "Forever": "für immer", "No": "Nei", "Yes": "Jo", "Basic": "Basis", "Cloud storage": "Cloudspeicher", "Notes per page": "Notize pro Site", "Default edit mode": "Standard Bearbeitigsmodus", "Fullscreen with preview": "Vollbid mit Vorschau", "Use encryption": "Bruch Verschlüsselig", "Encryption parameters": "Verschlüsseligsparameter", "Encryption Password": "Verschlüsseligspasswort", "Salt": "Salt", "Random": "Zufällig", "Key size": "Schlüsselgrössi", "Strengthen by a factor of": "Verstärkt um de Faktor", "Authentication strength": "Verschlüsseligsstärke", "Unlock": "Entsperre", "Your new encryption password": "Dis neue Verschlüsseligspasswort", "Your old encryption password": "Dis alte Verschlüsseligspasswort", "Please wait until the encryption will be completed": "Bitte wart bis d Verschlüsselig beändet isch", "Shortcuts": "Shortcuts", "Newer": "Zrugg", "Older": "Vorwärts", "Navigation": "Navigation", "navigateTop": "nach oobe", "navigateBottom": "nach unde", "Jump": "gumpe", "jumpInbox": "Gang zur Inbox", "jumpNotebook": "Gang zur Notizbüecherliste", "jumpFavorite": "Gang zu de favorisierte Notize", "jumpRemoved": "Gang zu de glöschten Notize", "Actions": "Aktione", "actionsOpen": "Öffne", "actionsRotateStar": "Stern dreie", "App": "App", "appCreateNote": "Erstell e neui Notiz", "appSearch": "Durchsuch d Notizen", "appKeyboardHelp": "Tastatur Hilfe" } ================================================ FILE: app/locales/el/translation.json ================================================ { "en" : "Αγγλικά", "ru" : "Ρώσικα", "nl" : "Ολλανδικά", "fr" : "Γαλλικά", "pt_br" : "Πορτογαλικά Βραζιλίας", "Search": "Εύρεση", "All notes": "Όλες οι σημειώσεις", "Favourites": "Αγαπημένα", "Favorite": "Αγαπημένο", "Trash": "Κάδος", "Notebooks": "Τετράδιο", "Settings": "Ρυθμίσεις", "About": "Περί", "Save": "Αποθήκευση", "Save & Exit": "Αποθήκευση & Έξοδος", "Cancel": "Ακύρωση", "Full screen": "Ολόκληρη οθόνη", "Preview": "Προεπισκόπηση", "Normal": "Κανονικό", "Select notebook": "Επιλογή τετραδίου", "Title": "Τίτλος", "Submit": "Αποστολή", "Tags": "Ετικέτες", "Tag": "Ετικέτα", "Parent": "Πηγή", "Root": "Ρίζα", "Notebooks & tags": "Τετράδια & ετικέτες", "Notebook": "Τετράδιο", "Restore": "Επαναφορά", "Delete": "Διαγραφή", "New tag": "Νέα ετικέτα", "Edit": "Επεξεργασία", "Remove": "Αφαίρεση", "Forever": "Για πάντα", "No": "Όχι", "Yes": "Ναι", "Basic": "Βασική", "Cloud storage": "Αποθήκευση στο σύννεφο", "Notes per page": "Σημειώσεις ανά σελίδα", "Default edit mode": "Προεπιλεγμένος τρόπος επεξερασγίας", "Fullscreen with preview": "Προεπισκόπηση σε πλήρη οθόνη", "Use encryption": "Χρησιμοποίησε κρυπτογράφηση", "Encryption parameters": "Ρυθμίσεις κρυπτογράφησης", "Encryption Password": "Κωδικός κρυπτογράφησης", "Salt": "Άλατι", "Random": "Τυχαίο", "Key size": "Μέγεθος κλειδιού", "Strengthen by a factor of": "Ενίσχυση με συντελεστή", "Authentication strength": "Ενίσχυση ταυτοποίησης", "Unlock": "Ξεκλείδωσε", "Your new encryption password": "Νέος κωδικός κρυπτογράφησης", "Your old encryption password": "Παλιός κωδικός κρυπτογράφησης", "Please wait until the encryption will be completed": "Παρακαλώ περιμένετε να ολοκληρωθεί η διαδικασία κρυπτογράφησης", "Shortcuts": "Συντομεύσεις", "Newer": "Προηγούμενο", "Older": "Επόμενο", "Navigation": "Πλοήγηση", "navigateTop": "Πάνω", "navigateBottom": "Κάτω", "Jump": "Άλμα", "jumpInbox": "Πάνε στα εισερχόμενα", "jumpNotebook": "Πήγαινε στην κατάσταση τετραδίων", "jumpFavorite": "Πήγαινε στις αγαπημένες σημειώσεις", "jumpRemoved": "Πήγαινε στην διαγραφή σημειώσεων", "Actions": "Ενέργειες", "actionsOpen": "Άνοιξε", "actionsRotateStar": "Περιστροφή αστεριού", "App": "App", "appCreateNote": "Δημιουργεία νέας σημείωσης", "appSearch": "Ψάξε σημείωση", "appKeyboardHelp": "Βοήθεια πληκτρολογίου" } ================================================ FILE: app/locales/en/translation.json ================================================ { "Search": "Search", "All notes": "All notes", "Favourites": "Favourites", "Favorite": "Favourites", "Trash": "Trash", "Open tasks": "Open tasks", "Notebooks": "Notebooks", "Settings": "Settings", "About": "About", "Save": "Save", "Save & Exit": "Save & Exit", "Cancel": "Cancel", "Full screen": "Full screen", "Preview": "Preview", "Normal": "Normal", "Select notebook": "Select notebook", "Title": "Title", "Submit": "Submit", "Tags": "Tags", "Tag": "Tag", "Parent": "Parent", "Root": "Root", "Notebooks & tags": "Notebooks & Tags", "Notebook": "Notebook", "Restore": "Restore", "Delete": "Delete", "New tag": "New tag", "Edit": "Edit", "Remove": "Remove", "Forever": "Forever", "No": "No", "Yes": "Yes", "Basic": "Basic", "Cloud storage": "Cloud storage", "Notes per page": "Notes per page", "Sort notebooks": "Sort notebooks by", "Name": "Name", "Created": "Created", "Default edit mode": "Default edit mode", "Fullscreen with preview": "Fullscreen with preview", "Use encryption": "Use encryption", "Encryption parameters": "Encryption parameters", "Encryption Password": "Encryption Password", "Salt": "Salt", "Random": "Random", "Key size": "Key size", "Strengthen by a factor of": "Strengthen by a factor of", "Authentication strength": "Authentication strength", "Unlock": "Unlock", "Your new encryption password": "Your new encryption password", "Your old encryption password": "Your old encryption password", "Show sidebar": "Show sidebar", "Previous": "Previous", "Next": "Next", "Navigation": "Navigation", "navigateTop": "Top", "navigateBottom": "Bottom", "Jump": "Jump", "jumpInbox": "Go to inbox", "jumpNotebook": "Go to notebook list", "jumpFavorite": "Go to favourite notes", "jumpRemoved": "Go to removed notes", "jumpOpenTasks": "Go to notes with open tasks", "Actions": "Actions", "actionsEdit": "Edit", "actionsOpen": "Open", "actionsRemove": "Remove", "actionsRotateStar": "Toggle Star", "App": "App", "appCreateNote": "Create new note", "appSearch": "Search note", "appKeyboardHelp": "Keyboard help", "Change keybindings": "Change keybinding settings", "Donate": "Donate", "Github page": "Github page", "Report bugs and issues here": "Report bugs and issues here", "Report bugs through email": "Report bugs through email", "Credits": "Credits", "List of contributors": "List of contributors", "List of all used libraries": "List of all used libraries", "Are you sure?": "Are you sure?", "You have unsaved changes": "You have unsaved changes.", "Dropbox API key": "Dropbox API key", "Required": "Required", "Optional": "Optional", "Language": "Language", "Action": "Action", "Select": "Select", "General": "General", "Encryption": "Encryption", "Keybindings": "Keybindings", "Sync": "Sync", "Profiles": "Profiles", "Import": "Import", "Transfer data": "Transfer data", "Transfer settings": "Transfer settings", "Import settings": "Import settings", "Export settings": "Export settings", "Wrong format": "Wrong format", "useDefaultConfigs": "Use settings from the default profile", "File should be in json format": "File should be in json format", "Close": "Close", "Hyperlink": "Hyperlink", "Editor": "Editor", "Preview": "Preview", "Download": "Download", "Transfer everything": "Transfer everything", "Find in page": "Find in page", "Other": "Other", "Default": "Default", "Modules": "Modules", "Import data": "Import data", "Export data": "Export data", "Enabled": "Enabled", "Disabled": "Disabled", "Untitled": "Untitled", "Line of": "Line {{currentLine}} of {{numberOfLines}}", "Drop files": "Drop files here to upload", "Spaces per indent": "Spaces per indent", "Sort notes": "Sort notes by", "Updated date": "Modification date", "Created date": "Creation date", "Text editor": "Text editor", "Vim": "Vim", "Emacs": "Emacs", "Sublime": "Sublime", "encryption": { "provide password": "Please, provide your password", "change password": "Type your password here to change it", "wait": "Please wait until the encryption is completed", "error": "Encryption error", "errorConfirm": "Error while decrypting data.\r\r If you changed encryption settings in another browser, **update your settings** in this browser too. Or try to import settings.\r\r And if you did not change anything, **try to login** again.", "errorConfirmSettings": "Change encryption settings", "errorConfirmAuth": "Retry again", "backup": { "title": "Backup Data", "content": "Please, before proceeding to the next step, download your backup file. It contains decrypted previous data of changed profiles. Keep it in a safe place.", "next": "Procceed without downloading the backup file" }, "state": { "decrypt": "Decrypting everything", "encrypt": "Encrypting everything", "save": "Saving changes" } }, "profile": { "profile name": "Profile name", "confirm remove": "Profile **{{profile}}** will be removed with all the data, including notes, tags, and notebooks. This action is irreversible!", "type name": "Type profile name" }, "files": { "file-url": "File or image URL", "attach": "Attach a file", "attachLink": "Attach as a link", "attachImage": "Attach as an image" }, "notes": { "confirm trash": "The note **{{title}}** will be moved to trash.", "confirm remove": "The note **{{title}}** will be removed **for ever**!", "create and attach": "Create a new note and attach its link", "create": "Create a new note", "hyperlink-dialog": "Title of a note or URL" }, "notebooks": { "select": "Select a Notebook", "add": "Add a new notebook", "edit": "Edit a notebook", "name": "Please, provide notebook's name", "confirm remove": "The notebook **{{name}}** will be removed **for ever**!", "remove with notes": "Yes, remove with attached notes", "remove": "Yes, remove" }, "tags": { "name": "Tag name is required", "add": "Add a new tag", "edit": "Edit a tag", "confirm remove": "The tag **{{name}}** will be removed **for ever**!" }, "dropbox": { "auth confirm": "Now you will be redirected to **Dropbox** authorization page.\r> Please click **OK** button.", "auth title": "Dropbox auth", "api info 1": "You can have your own API key on", "api info 2": "When you create a new app at Dropbox's Developer site you should keep in mind that:", "api info li 1": "Type of API should be **Dropbox API**", "api info li 2": "Type of access should be **App Folder**" }, "help": { "firststart title": "Welcome to Laverna", "firststart import": "If you have already used Laverna before, you can import your old settings by clicking on 'import' button bellow.", "firststart next": "If you have never used Laverna before, click on 'next' button to start installation process.", "firststart encryption": "If you want to use encryption, please, provide encryption password.", "firststart sync": "Since we don't store any data on our servers, you need to enable synchronization with one of the adapters to be able to view your notes on other devices.", "firststart backup": "Everything is almost ready. You can download your settings backup and proceed to the last step." } } ================================================ FILE: app/locales/eo/translation.json ================================================ { "en" : "Angla", "ru" : "Rusa", "nl" : "Nederlanda", "fr" : "Franca", "pt_br" : "Brazila Portugala", "es": "Hispana", "Search": "Serĉi", "All notes": "Ĉiuj notoj", "Favourites": "Ŝatataj", "Favorite": "Ŝatata", "Trash": "Rubo", "Notebooks": "Kajeroj", "Settings": "Agordoj", "About": "Pri", "Save": "Konservi", "Save & Exit": "Konservu & Eliri", "Cancel": "Nuligi", "Full screen": "Plena ekrano", "Preview": "Antaŭrigardo", "Normal": "Normala", "Select notebook": "Elektu kajero", "Title": "Titolo", "Submit": "Submetiĝi", "Tags": "Etikedoj", "Tag": "Etikejo", "Parent": "Patro", "Root": "Radiko", "Notebooks & tags": "Kajeroj & etikedoj", "Notebook": "Kajero", "Restore": "Restarigi", "Delete": "Forviŝi", "New tag": "Novaj etikedon", "Edit": "Redakti", "Remove": "Forigi", "Forever": "Eterne", "No": "Ne", "Yes": "Jes", "Basic": "Bazaj", "Cloud storage": "Nubo stokado", "Notes per page": "Notoj po paĝo", "Default edit mode": "Defaŭlta redaktu modo", "Fullscreen with preview": "Plena ekrano kun antaŭvido", "Use encryption": "Uzu ĉifrado", "Encryption parameters": "Ĉifrado parametroj", "Encryption Password": "Ĉifrado Pasvorto", "Salt": "Salo", "Random": "Hazarda", "Key size": "Ŝlosila amplekso", "Strengthen by a factor of": "Plifortigi per faktoro de", "Authentication strength": "Aŭtentigo forto", "Unlock": "Malŝlosi", "Your new encryption password": "Via nova ĉifrada pasvorto", "Your old encryption password": "Via malnova ĉifrada pasvorto", "Please wait until the encryption will be completed": "Bonvolu atendi ĝis la ĉifrado estos kompletigita", "Shortcuts": "Klavkombinoj", "Newer": "Antaŭa", "Older": "Sekva", "Navigation": "Navigado", "Top": "Supro", "Bottom": "Malsupro", "Jump": "Salti", "Go to inbox": "Iru al enirkesto", "Go to notebook list": "Iru al kajero listo", "Go to favourite notes": "Iru al preferataj notoj", "Go to removed notes": "Iru al forigita notoj", "Actions": "Agoj", "Open": "Malferma", "Rotate Star": "Rotacii stelo", "App": "Programo", "Create new note": "Krei novan noto", "Search note": "Serĉi noto", "Keyboard help": "Klavaro helpo" } ================================================ FILE: app/locales/es/translation.json ================================================ { "en" : "Inglés", "ru" : "Ruso", "nl" : "Neerlandés", "fr" : "Francés", "pt_br" : "Portugués de Brasil", "eo": "Esperanto", "es" : "Español", "Search": "Búsqueda", "All notes": "Notas", "Favourites": "Favoritos", "Favorite": "Favorito", "Trash": "Papelera de reciclaje", "Notebooks": "Cuadernos", "Settings": "Configuraciones", "About": "Acerca de", "Save": "Guardar", "Save & Exit": "Guardar y salir", "Cancel": "Cancelar", "Full screen": "Pantalla completa", "Preview": "Vista previa", "Normal": "Normal", "Select notebook": "Seleccionar cuaderno", "Title": "Título", "Submit": "Enviar", "Tags": "Etiquetas", "Tag": "Etiqueta", "Parent": "Padre", "Root": "Raiz", "Notebooks & tags": "Cuadernos y etiquetas", "Notebook": "Cuaderno", "Restore": "Restaurar", "Delete": "Borrar", "New tag": "Nueva etiqueta", "Edit": "Editar", "Remove": "Borrar", "Forever": "Para siempre", "No": "No", "Yes": "Sí", "Basic": "Básico", "Cloud storage": "Almacenamiento en la nube", "Notes per page": "Notas por página", "Sort notebooks": "Ordenar cuadernos", "Name": "Nombre", "Created": "Creado", "Default edit mode": "Modo editar predeterminado", "Fullscreen with preview": "Pantalla completa con previsualización", "Use encryption": "Usar cifrado", "Encryption parameters": "Parámetros de cifrado", "Encryption Password": "Contraseña de cifrado", "Salt": "Sal", "Random": "Aleatorio", "Key size": "Tamaño de la clave", "Strengthen by a factor of": "Fortalecer por un factor de", "Authentication strength": "Fortaleza de la autenticación", "Unlock": "Desbloquear", "Your new encryption password": "Su nueva contraseña de cifrado", "Your old encryption password": "Su antigüa contraseña de cifrado", "Please wait until the encryption will be completed": "Por favor espere mientras el cifrado se completa", "Shortcuts": "Accesos directos", "Newer": "Anterior", "Older": "Siguiente", "Navigation": "Navegación", "Top": "Superior", "Bottom": "Inferior", "Jump": "Saltar", "Go to inbox": "Ir a la bandeja de entrada", "Go to notebook list": "Ir a la lista de cuadernos", "Go to favourite notes": "Ir a los cuadernos favoritos", "Go to removed notes": "Ir a las notas borradas", "Actions": "Acciones", "Open": "Abrir", "Rotate Star": "Girar la Estrella", "App": "App", "Create new note": "Crear nueva nota", "Search note": "Buscar nota", "Keyboard help": "Ayuda del teclado" } ================================================ FILE: app/locales/fr/translation.json ================================================ { "Search": "Chercher", "All notes": "Toutes les notes", "Favourites": "Favoris", "Favorite": "Favori", "Trash": "Corbeille", "Open tasks": "Tâches en cours", "Notebooks": "Bloc-notes", "Settings": "Paramètres", "About": "À propos", "Save": "Sauvegarder", "Save & Exit": "Sauvegarder & Quitter", "Cancel": "Annuler", "Full screen": "Plein écran", "Preview": "Prévisualisation", "Normal": "Normal", "Select notebook": "Sélectionner un bloc-notes", "Title": "Titre", "Submit": "Envoyer", "Tags": "Étiquettes", "Tag": "Étiquette", "Parent": "Parent", "Root": "Racine", "Notebooks & tags": "Bloc-notes et étiquette", "Notebook": "Bloc-notes", "Restore": "Restaurer", "Delete": "Supprimer", "New tag": "Nouvelle étiquette", "Edit": "Éditer", "Remove": "Supprimer", "Forever": "Effacer", "No": "Non", "Yes": "Oui", "Basic": "Basique", "Cloud storage": "Stockage dans le cloud", "Notes per page": "Notes par page", "Sort notebooks": "Trier les bloc-notes", "Name": "Nom", "Created": "Créé", "Default edit mode": "Mode d'édition par défaut", "Fullscreen with preview": "Plein écran avec visualisation", "Use encryption": "Utiliser le chiffrement", "Encryption parameters": "Paramètres de chiffrement", "Encryption Password": "Mot de passe de chiffrement", "Salt": "Salage", "Random": "Aléatoire", "Key size": "Taille de la clef", "Strengthen by a factor of": "Renforcer par un facteur de", "Authentication strength": "Force d'authentification", "Unlock": "Déverrouiller", "Your new encryption password": "Votre nouveau mot de passe de chiffrement", "Your old encryption password": "Votre ancien mot de passe de chiffrement", "Show sidebar": "Montrer le panneau latéral", "Previous": "Précédent", "Next": "Suivant", "Navigation": "Navigation", "navigateTop": "Haut", "navigateBottom": "Bas", "Jump": "Déplacement", "jumpInbox": "Aller à la boîte de réception", "jumpNotebook": "Aller à la liste de bloc-notes", "jumpFavorite": "Aller aux notes favoris", "jumpRemoved": "Aller aux notes supprimées", "jumpOpenTasks": "Aller aux tâches ouvertes", "Actions": "Actions", "actionsEdit": "Modifier", "actionsOpen": "Ouvrir", "actionsRemove": "Supprimer", "actionsRotateStar": "Ajouter/Supprimer des favoris", "App": "Application", "appCreateNote": "Créer une nouvelle note", "appSearch": "Rechercher une note", "appKeyboardHelp": "Afficher les raccourcis clavier", "Change keybindings": "Changer les raccourcis clavier", "Donate": "Faire un don", "Github page": "Page Github", "Report bugs and issues here": "Rapporter les bugs et les problèmes ici", "Report bugs through email": "Rapporter les bugs par courriel", "Credits": "Crédits", "List of contributors": "Liste des contributeurs", "List of all used libraries": "Liste de toutes les librairies utilisées", "Are you sure?": "Êtes-vous sûr", "You have unsaved changes": "Certaines modifications ne sont pas sauvegardées.", "Dropbox API key": "Clé de l'API Dropbox", "Required": "Requis", "Optional": "Optionel", "Language": "Langue", "Action": "Action", "Select": "Sélectionner", "General": "Général", "Encryption": "Chiffrement", "Keybindings": "Raccourcis claviers", "Sync": "Synchroniser", "Profiles": "Profiles", "Import": "Importer", "Transfer data": "Importer & exporter", "Import settings": "Paramètres d'import", "Export settings": "Paramètres d'export", "Wrong format": "Mauvais format", "useDefaultConfigs": "Utiliser les paramètres du profile par défaut", "File should be in json format": "Le fichier devrait être au format json", "Close": "Fermer", "Hyperlink": "Hyperlien", "Editor": "Éditeur", "Download": "Télécharger", "Transfer everything": "Tout", "Find in page": "Chercher dans la page", "Other": "Divers", "Default": "Par défaut", "Modules": "Modules", "Import data": "Importer les données", "Export data": "Exporter les données", "Enabled": "Activé", "Disabled": "Désactivé", "encryption": { "provide password": "Veuillez saisir votre mot de passe", "change password": "Saisissez votre mot de passe ici pour le modifier", "wait": "Veuillez attendre que le chiffrement soit terminé", "error": "Erreur de chiffrement", "errorConfirm": "Erreur lors du déchiffrement des données.\r\r Si vous avez changé les paramètres de chiffrement sur un autre navigateur, veuillez aussi **mettre à jour vos paramètres** dans ce navigateur. Ou essayez d'importer les paramètres.\r\r Et si vous n'avez rien changé, **essayez de vous connecter** à nouveau.", "errorConfirmSettings": "Changer les paramètres de chiffrement", "errorConfirmAuth": "Réessayer", "backup": { "title": "Sauvegarder les données", "content": "Veuillez télécharger votre fichier de sauvegarde avant de passer à l'étape suivante. Il contient des données déchiffrées préalablement au changement de profile. Gardez le en lieu sûr.", "next": "Continuer sans télécharger le fichier de sauvegarde" }, "state": { "decrypt": "Déchiffrement en cours", "encrypt": "Chiffrement en cours", "save": "Sauvegarde en cours" } }, "profile": { "confirm remove": "Le profile **{{profile}}** sera supprimé avec toutes les données qu'il contient, en particulier les notes, les étiquettes, et les bloc-notes. Cette action est irréversible !", "type name": "Saisir le nom du profile" }, "files": { "file-url": "URL du fichier ou de l'image", "attach": "Joindre un fichier", "attachLink": "Joindre en tant que lien", "attachImage": "Joindre en tant qu'image" }, "notes": { "confirm trash": "La note **{{title}}** va être déplacée dans la corbeille.", "confirm remove": "La note **{{title}}** va être supprimée **pour toujours**!", "create and attach": "Créer une note et joindre son lien", "create": "Créer une nouvelle note", "hyperlink-dialog": "Titre d'une note ou URL" }, "notebooks": { "select": "Sélectionner un bloc-notes", "add": "Créer un nouveau bloc-notes", "edit": "Modifier un bloc-notes", "name": "Veuillez saisir le nom du bloc-notes", "confirm remove": "Le bloc-notes **{{name}}** sera supprimé **pour toujours**!", "remove with notes": "Oui, supprimer avec les notes attachées", "remove": "Oui, supprimer" }, "tags": { "name": "Le nom de l'étiquette est requis", "add": "Créer une nouvelle étiquette", "edit": "Modifier une étiquette", "confirm remove": "L'étiquette **{{name}}** sera supprimée **pour toujours**!" }, "dropbox": { "auth confirm": "Vous allez maintenant être redirigé sur la page d'autorisation de **Dropbox**.\r> Veuillez cliquer sur le bouton **OK**.", "auth title": "Autentification de Dropbox", "api info 1": "Vous pouvez utiliser votre propre clé d'API", "api info 2": "Lorsque vous créez une nouvelle application sur le site développeur de Dropbox, vous devez garder ceci en tête:", "api info li 1": "Le type d'app doit être Dropbox API app", "api info li 2": "Le type de donnée doit être Files et datastores" }, "help": { "firststart title": "Bienvenue sur Laverna", "firststart import": "Si vous avez déja utilisé Laverna auparavant, vous pouvez importer vos paramètres en cliquant sur le bouton 'importer' ci-dessous.", "firststart next": "Si vous n'avez jamais utilisé auparavant, cliquez sur le bouton 'suivant' et commencez le processus d'installation.", "firststart encryption": "Si vous voulez utiliser le chiffrement, veuillez fournir un mot de passe de chiffrement.", "firststart sync": "Comme nous ne stockons aucune donnée sur nos serveurs, vous devez activer la synchronisation avec l'un des services pour pouvoir visualiser vos notes sur d'autres terminaux.", "firststart backup": "L'installation est presque terminée. Vous pouvez télécharger une sauvegarde de vos paramètres et continuer à la prochaine étape." } } ================================================ FILE: app/locales/gl/translation.json ================================================ { "Search": "Buscar", "All notes": "Todas as notas", "Favourites": "Favoritos", "Favorite": "Favorito", "Trash": "Lixo", "Open tasks": "Abrir as tarefas", "Notebooks": "Cadernos de notas", "Settings": "Configuracións", "About": "Sobre de", "Save": "Gardar", "Save & Exit": "Gardar e saír", "Cancel": "Cancelar", "Full screen": "Pantalla completa", "Preview": "Visualización previa", "Normal": "Normal", "Select notebook": "Escoller o caderno", "Title": "Título", "Submit": "Enviar", "Tags": "Etiquetas", "Tag": "Etiquetas", "Parent": "Nai", "Root": "Raíz", "Notebooks & tags": "Caderno e etiquetas", "Notebook": "Caderno", "Restore": "Restaurar", "Delete": "Borrar", "New tag": "Nova etiqueta", "Edit": "Editar", "Remove": "Borrar", "Forever": "Para sempre", "No": "Non", "Yes": "Si", "Basic": "Básico", "Cloud storage": "Almacenamento na nube", "Notes per page": "Notas por páxina", "Sort notebooks": "Ordenar os cadernos por", "Name": "Nome", "Created": "Creado", "Default edit mode": "Modo de edición por defecto", "Fullscreen with preview": "Pantalla completa con vista previa", "Use encryption": "Usar o cifrado", "Encryption parameters": "Parámetros de cifrado", "Encryption Password": "Contrasinal para o cifrado", "Salt": "Saltar", "Random": "Ao chou", "Key size": "Tamaño da clave", "Strengthen by a factor of": "Fortalecer a un nivel de", "Authentication strength": "Forza da autenticación", "Unlock": "Desbloquear", "Your new encryption password": "O teu novo contrasinal de cifrado", "Your old encryption password": "O teu anterior contrasinal de cifrado", "Show sidebar": "Mostrar a barra lateral", "Previous": "Vista previa", "Next": "Seguinte", "Navigation": "Navegación", "navigateTop": "Cara a arriba", "navigateBottom": "Cara a abaixo", "Jump": "Saltar", "jumpInbox": "Ir á caixa de entrada", "jumpNotebook": "Ir á lista de cadernos", "jumpFavorite": "Ir ás notas favoritas", "jumpRemoved": "Ir ás notas borradas", "jumpOpenTasks": "Ir ás notas con tarefas abertas", "Actions": "Accións", "actionsEdit": "Editar", "actionsOpen": "Abrir", "actionsRemove": "Borrar", "actionsRotateStar": "Mudar o favorito", "App": "Aplicación", "appCreateNote": "Crear unha nova nota", "appSearch": "Buscar notas", "appKeyboardHelp": "Axuda co teclado", "Change keybindings": "Cambiar a configuración dos atallos", "Donate": "Doar", "Github page": "Páxina de Github", "Report bugs and issues here": "Informar desde aquí de erros e problemas", "Report bugs through email": "Informar de erros polo correo", "Credits": "Créditos", "List of contributors": "Lista dos que contribuíron", "List of all used libraries": "Lista de todas as librarías usadas", "Are you sure?": "Estás seguro?", "You have unsaved changes": "Tes cambios sen gardar.", "Dropbox API key": "Clave da API de Dropbox", "Required": "Obrigatorio", "Optional": "Opcional", "Language": "Lingua", "Action": "Acción", "Select": "Escoller", "General": "Xeral", "Encryption": "Cifrado", "Keybindings": "Teclas dos atallos", "Sync": "Sincronización", "Profiles": "Perfís", "Import": "Importar", "Transfer data": "Transferir datos", "Transfer settings": "Transferir configuracións", "Import settings": "Importar configuracións", "Export settings": "Exportar configuracións", "Wrong format": "Formato equivocado", "useDefaultConfigs": "Usar a configuración do perfil por defecto", "File should be in json format": "O ficheiro ten que estar nun formato json", "Close": "Pechar", "Hyperlink": "Hiperligazón", "Editor": "Editor", "Preview": "Vista previa", "Download": "Descargar", "Transfer everything": "Transferir todo", "Find in page": "Atopar na páxina", "Other": "Outros", "Default": "Por defecto", "Modules": "Módulos", "Import data": "Importar datos", "Export data": "Exportar datos", "Enabled": "Activado", "Disabled": "Desactivado", "Untitled": "Sen título", "Line of": "Liña {{currentLine}} de {{numberOfLines}}", "Drop files": "Arrastra aquí os ficheiros a subir", "Spaces per indent": "Espazos por indentación", "Sort notes": "Ordenar as notar por", "Updated date": "Data de cambios", "Created date": "Data de creación", "Text editor": "Editor de texto", "Vim": "Vim", "Emacs": "Emacs", "Sublime": "Sublime", "encryption": { "provide password": "Introduce o contrasinal", "change password": "Escribe o contrasinal para cambialo", "wait": "Agarda a que acabe o cifrado", "error": "Erro de cifrado", "errorConfirm": "Houbo un erro cifrando os datos.\r\r Se mudaches as configuracións de cifrado noutro navegador **actualiza as túas configuracións** aquí tamén. Senón, proba a importar a configuración.\r\r E se aínda así non houbo cambios, **intenta rexistrarte** de novo.", "errorConfirmSettings": "Cambiar a configuración do cifrado", "errorConfirmAuth": "Volver a intentalo", "backup": { "title": "Copia de seguridade de datos", "content": "Antes de continuar co seguinte paso, descarga o ficheiro coa copia de seguridade de datos. Iso contén os datos previos sen cifrar que se empregaron nos perfís anteriores. Gárdao nun lugar seguro.", "next": "Continuar sen descargar a copia de seguridade de datos" }, "state": { "decrypt": "Descifrar todo", "encrypt": "Cifrar todo", "save": "Gardando os cambios" } }, "profile": { "profile name": "Nome do perfil", "confirm remove": "O perfil **{{profile}}** vaise mover con todos os datos, incluíndo as notas, as etiquetas e os cadernos. Isto é algo que non se poderá desfacer!", "type name": "Escribe o nome do perfil" }, "files": { "file-url": "URL de ficheiro ou imaxe", "attach": "Anexar un ficheiro", "attachLink": "Anexar unha ligazón", "attachImage": "Anexar como unha imaxe" }, "notes": { "confirm trash": "Vaise tirar a nota **{{title}}** ao lixo.", "confirm remove": "Vaise borrar a nota **{{title}}** **para sempre**!", "create and attach": "Crear unha nova nota e engadir a súa ligazón", "create": "Crear unha nova nota", "hyperlink-dialog": "Título da nota ou URL" }, "notebooks": { "select": "Escoller un caderno", "add": "Engadir un novo caderno", "edit": "Editar un caderno", "name": "Dálle un nome ao caderno", "confirm remove": "Vaise borrar o caderno **{{name}}** **para sempre**!", "remove with notes": "Si, e borralo coas notas que teña anexas", "remove": "Si, borrar" }, "tags": { "name": "Fai falla un nome de etiqueta", "add": "Engadir unha nova etiqueta", "edit": "Editar a etiqueta", "confirm remove": "Vaise borrar a etiqueta **{{name}}** **para sempre**!" }, "dropbox": { "auth confirm": "Agora mostraráseche a páxina de permisos para **Dropbox**.\r> Prémelle ao botón de **Aceptar**.", "auth title": "Autenticación de Dropbox", "api info 1": "Agora podes ter activa a túa propia clave da API", "api info 2": "Cando se crea unha nova aplicación no sitio de desenvolvemento de Dropbox hai que ter en conta que:", "api info li 1": "O tipo de API ten que ser **Dropbox API**", "api info li 2": "O tipo de acceso ten que ser **App Folder**" }, "help": { "firststart title": "Benvido/as a Laverna", "firststart import": "Se xa usaches Laverna antes podes importar a configuración anterior premendo o botón de 'importar' que está abaixo.", "firststart next": "Se aínda non usaches Laverna, preme en 'seguinte' e comeza a súa instalación.", "firststart encryption": "Se queres usar un cifrado, introduce un contrasinal de cifrado.", "firststart sync": "Xa que non ofrecemos un servizo de almacenamento de datos nos nosos servidores, para ver as túas notas noutros dispositivos precisas activar a sincronización con un dos adaptadores que temos.", "firststart backup": "Xa case está todo listo. Podes descargar unha copia de seguridade da configuración e continuar co último destes pasos." } } ================================================ FILE: app/locales/hi_in/translation.json ================================================ { "en" : "अंग्रेजी", "ru" : "रूसी", "nl" : "डच", "fr" : "फ्रेंच", "pt_br" : "ब्राजील पुर्तगाली", "eo": "एस्पेरान्तो", "es": "स्पेनिश", "de": "जर्मन", "de_ch": "स्विस जर्मन", "se": "स्वीडिश", "el": "यूनानी", "nb": "नार्वेजियन (बोकमाल)", "nn": "नॉर्वेजियाई (नायनोर्स्क)", "bs_ba": "बोस्नियाई", "hi_in": "हिन्दी", "mr_in": "मराठी", "zh_cn": "सरलीकृत चीनी", "Search": "खोजें", "All notes": "सभी नोट्स", "Favourites": "पसंदीदा", "Favorite": "पसंदीदा", "Trash": "कचरा", "Notebooks": "नोटबुक", "Settings": "सेटिंग्स", "About": "बारे में", "Save": "बचालें", "Save & Exit": "बचाके निकलें", "Cancel": "रद्द करें", "Full screen": "पूर्ण स्क्रीन", "Preview": "पूर्वावलोकन", "Normal": "सामान्य", "Select notebook": "नोटबुक चुनें", "Title": "शीर्षक", "Submit": "प्रस्तुत करें", "Tags": "टैग", "Tag": "टैग", "Parent": "पैत्रिक", "Root": "मूल", "Notebooks & tags": "नोटबुक और टैग", "Notebook": "नोटबुक", "Restore": "वापस लाऐं", "Delete": "मिटाऐं", "New tag": "नया टैग", "Edit": "संपादित करें", "Remove": "हटाऐं", "Forever": "हमेशा के लिये", "No": "नहीं", "Yes": "हाँ", "Basic": "बुनियादी", "Cloud storage": "बादल भंडारण", "Notes per page": "प्रति पृष्ठ नोट्स", "Sort notebooks": "नोटबुक क्रमबद्ध करें", "Name": "नाम", "Created": "जब बनाया", "Default edit mode": "डिफ़ॉल्ट संपादन मोड", "Fullscreen with preview": "फुल स्क्रीन के साथ पूर्वावलोकन", "Use encryption": "एन्क्रिप्शन उपयोग करें", "Encryption parameters": "एन्क्रिप्शन के मापदंडों", "Encryption Password": "एन्क्रिप्शन का पासवर्ड", "Salt": "साल्ट", "Random": "यादृच्छिक करें", "Key size": "की साईज", "Strengthen by a factor of": "ताकत का कारक", "Authentication strength": "प्रमाणीकरण ताकत", "Unlock": "अनलॉक", "Your new encryption password": "आपका नया एन्क्रिप्शन पासवर्ड", "Your old encryption password": "आपका पुराना एन्क्रिप्शन पासवर्ड", "Please wait until the encryption will be completed": "कृपया एन्क्रिप्शन पूरा होने तक प्रतीक्षा करें", "Shortcuts": "शॉर्टकट", "Newer": "नए", "Older": "पुराने", "Navigation": "नेविगेशन", "navigateTop": "ऊपर", "navigateBottom": "नीचे", "Jump": "कूदो", "jumpInbox": "इनबॉक्स में जाओ", "jumpNotebook": "नोटबुक सूची में जाओ", "jumpFavorite": "पसंदीदा नोट्स पर जाएँ", "jumpRemoved": "हटाऐं हुऐ नोट्स पर जाएँ", "Actions": "प्रक्रियाऐं", "actionsEdit": "संपादित करें", "actionsOpen": " खोलें", "actionsRemove": "हटाऐं", "actionsRotateStar": "तारा घुमाएं", "App": "एप्लिकेशन", "appCreateNote": "नयी नोट बनाएं", "appSearch": "नोट खोजें", "appKeyboardHelp": "कीबोर्ड मदद" } ================================================ FILE: app/locales/it/translation.json ================================================ { "en" : "Inglese", "ru" : "Russo", "nl" : "Olandese", "fr" : "Francese", "pt_br" : "Portoghese Brasiliano", "eo": "Esperanto", "es": "Spagnolo", "de": "Tedesco", "de_ch": "Tedesco Svizzero", "Search": "Cerca", "All notes": "Tutte le note", "Favourites": "Preferiti", "Favorite": "Preferito", "Trash": "Cestino", "Notebooks": "Quaderni", "Settings": "Impostazioni", "About": "Informazioni", "Save": "Salva", "Save & Exit": "Salva ed esci", "Cancel": "Annulla", "Full screen": "Schermo intero", "Preview": "Anteprima", "Normal": "Normale", "Select notebook": "Scegli quaderno", "Title": "Titolo", "Submit": "Invia", "Tags": "Tags", "Tag": "Tag", "Parent": "Superiore", "Root": "Root", "Notebooks & tags": "Quaderni & tag", "Notebook": "Quaderno", "Restore": "Ripristina", "Delete": "Elimina", "New tag": "Nuovo tag", "Edit": "Modifica", "Remove": "Rimuovi", "Forever": "Per sempre", "No": "No", "Yes": "Sì", "Basic": "Basic", "Cloud storage": "Cloud storage", "Notes per page": "Note per pagina", "Sort notebooks": "Ordina quaderni", "Name": "Nome", "Created": "Creato", "Default edit mode": "Modalità default", "Fullscreen with preview": "Schermo intero con anteprima", "Use encryption": "Usa crittografia", "Encryption parameters": "Parametri crittografia", "Encryption Password": "Password crittografia", "Salt": "Salt", "Random": "Casuale", "Key size": "Dimensione chiave", "Strengthen by a factor of": "Rafforza di un fattore di", "Authentication strength": "Resistenza autenticazione", "Unlock": "Sblocca", "Your new encryption password": "La tua nuova password di cifratura", "Your old encryption password": "La tua vecchia password di cifratura", "Please wait until the encryption will be completed": "Aspetta fino a che la cifratura non è completa", "Shortcuts": "Collegamenti", "Previous": "Precedente", "Next": "Prossimo", "Navigation": "Navigazione", "navigateTop": "Su", "navigateBottom": "Giù", "Jump": "Salta", "jumpInbox": "Vai alla inbox", "jumpNotebook": "Vai alla lista dei quaderni", "jumpFavorite": "Vai alle note preferite", "jumpRemoved": "Vai alle note cancellate", "Actions": "Azioni", "actionsEdit": "Modifica", "actionsOpen": "Apri", "actionsRemove": "Cancella", "actionsRotateStar": "Ruota la stella", "App": "App", "appCreateNote": "Crea nuova nota", "appSearch": "Cerca nota", "appKeyboardHelp": "Aiuto tastiera", "Remove profile": "Sei sicuro di voler cancellare il profilo '__profile__'?", "Change shortcuts": "Cambia impostazioni collegamento", "Donate": "Dona", "Github page": "Pagina Github", "Report bugs and issues here": "Segnala bug e problemi qui", "Report bugs through email": "Segnala bug via e-mail", "Credits": "Riconoscimenti", "List of contributors": "Lista di contributori", "List of all used libraries": "Lista di tutte le librerie usate", "notebooks": { "name": "Fornisci un nome per il quaderno" }, "tags": { "name": "Il nome del tag è necessario" } } ================================================ FILE: app/locales/ja/translation.json ================================================ { "Search": "検索", "All notes": "すべてのノート", "Favourites": "お気に入り集", "Favorite": "お気に入り", "Trash": "ゴミ箱", "Open tasks": "タスク集", "Notebooks": "ノートブック集", "Settings": "設定", "About": "Lavernaについて", "Save": "保存", "Save & Exit": "保存して終了", "Cancel": "キャンセル", "Full screen": "フルスクリーン", "Preview": "プレビュー", "Normal": "標準", "Select notebook": "ノートブックを選択", "Title": "タイトル", "Submit": "実行", "Tags": "タグ集", "Tag": "タグ", "Parent": "親", "Root": "ルート", "Notebooks & tags": "ノートブック集とタグ集", "Notebook": "ノートブック", "Restore": "元に戻す", "Delete": "削除", "New tag": "新しいタグ", "Edit": "編集", "Remove": "削除", "Forever": "完全に削除", "No": "いいえ", "Yes": "はい", "Basic": "Basic", "Cloud storage": "クラウドストレージ", "Notes per page": "ノート数/ページ", "Sort notebooks": "ノートブックの並び順", "Name": "名前", "Created": "作成された", "Default edit mode": "デフォルトの編集モード", "Fullscreen with preview": "プレビューありのフルスクリーン", "Use encryption": "暗号化する", "Encryption parameters": "暗号化パラメータ", "Encryption Password": "暗号化パスワード", "Salt": "ソルト", "Random": "ランダムな値", "Key size": "鍵長(ビット)", "Strengthen by a factor of": "鍵生成iteration回数", "Authentication strength": "認証強度", "Unlock": "ロック解除", "Your new encryption password": "新しいパスワード", "Your old encryption password": "古いパスワード", "Show sidebar": "サイドバーを表示", "Previous": "前", "Next": "次", "Navigation": "キー操作", "navigateTop": "上", "navigateBottom": "下", "Jump": "ジャンプ", "jumpInbox": "すべてを表示", "jumpNotebook": "ノートブック集", "jumpFavorite": "お気に入りノート集", "jumpRemoved": "ゴミ箱", "jumpOpenTasks": "タスク集", "Actions": "アクション", "actionsEdit": "編集", "actionsOpen": "開く", "actionsRemove": "削除", "actionsRotateStar": "星マークをON/OFF", "App": "App", "appCreateNote": "ノートを新規作成", "appSearch": "ノートを検索", "appKeyboardHelp": "キー割り当て表を表示", "Change keybindings": "キー割り当ての変更", "Donate": "カンパする", "Github page": "Github ページ", "Report bugs and issues here": "バグや課題を報告する", "Report bugs through email": "メールでバグを報告", "Credits": "クレジット", "List of contributors": "貢献者リスト", "List of all used libraries": "使用ライブラリのリスト", "Are you sure?": "本当にいいですか?", "You have unsaved changes": "変更が保存されていません", "Dropbox API key": "Dropbox API キー", "Required": "必須", "Optional": "任意", "Language": "言語", "Action": "操作", "Select": "選択", "General": "総合", "Encryption": "暗号化", "Keybindings": "キー割り当て", "Sync": "同期", "Profiles": "プロファイル", "Import": "インポート", "Transfer data": "データを移す", "Transfer settings": "設定を移す", "Import settings": "設定を取り込む", "Export settings": "設定を取り出す", "Wrong format": "形式が異なります", "useDefaultConfigs": "デフォルトのプロファイルの設定を使う", "File should be in json format": "ファイルはjson形式でないといけません", "Close": "閉じる", "Hyperlink": "ハイパーリンク", "Editor": "エディタ", "Preview": "プレビュー", "Download": "ダウンロード", "Transfer everything": "すべて移す", "Find in page": "ページ内で探す", "Other": "その他", "Default": "デフォルト", "Modules": "モジュール", "Import data": "データを取り込む", "Export data": "データを取り出す", "Enabled": "有効", "Disabled": "無効", "Untitled": "無題", "Line of": "{{numberOfLines}}行中{{currentLine}}行目", "Drop files": "ファイルをここにドロップしてアップロード", "Spaces per indent": "タブの空白数(半角)", "Sort notes": "ノートの並び順", "Updated date": "変更日時", "Created date": "作成日時", "Text editor": "テキストエディタ", "Vim": "Vim", "Emacs": "Emacs", "Sublime": "Sublime", "encryption": { "provide password": "パスワードを入力してください", "change password": "パスワードを変更したい場合ここに入力してください", "wait": "暗号化が完了するまでお待ちください", "error": "暗号化エラー", "errorConfirm": "データを復号中にエラーが発生しました。\r\r 別のブラウザでパスワードを変更した場合, このブラウザでも**設定を変更**してください。または設定を取り込みます。\r\r 何の設定を変更していなければもう一度**ログイン**してください。", "errorConfirmSettings": "暗号の設定を変更", "errorConfirmAuth": "再試行してくださ", "backup": { "title": "データをバックアップ", "content": "続行する前にバックアップをダウンロードしてください。バックアップファイルには暗号化されていないデータが入っています。安全な場所で保管してください。", "next": "バックアップファイルをダウンロードせずに続行する。" }, "state": { "decrypt": "すべてを復号", "encrypt": "すべてを暗号化", "save": "変更を保存" } }, "profile": { "profile name": "プロファイル名", "confirm remove": "これはもとに戻せない操作です!ノート・タグ・ノートブックを含め、「**{{profile}}**」のすべてのデータは削除されます。", "type name": "プロファイル名を入力" }, "files": { "file-url": "ファイル、または画像のURL", "attach": "ファイルを添付する", "attachLink": "リンクを添付する", "attachImage": "画像を添付する" }, "notes": { "confirm trash": "ノート「**{{title}}**」をゴミ箱に移動します", "confirm remove": "「**{{title}}**」は**完全に**削除されます", "create and attach": "リンクを添付して新しいノートを作成", "create": "新しノートを作成", "hyperlink-dialog": "ノートの題名またはURL" }, "notebooks": { "select": "ノートブックを選択", "add": "ノートブックを追加", "edit": "ノートブック名を変更", "name": "ノートブック名を入力してください", "confirm remove": "「**{{name}}**」は**完全に**削除されます!", "remove with notes": "はい、中のノートも削除する", "remove": "はい、中のノートは削除しない" }, "tags": { "name": "タグ名が必要です", "add": "タグを追加", "edit": "タグを編集", "confirm remove": "「**{{name}}**」は**完全に**削除されます!" }, "dropbox": { "auth confirm": "それでは**Dropbox**認証ページにリダイレクトされます。\r> **OK** ボタンをクリックしてください", "auth title": "Dropbox 認証", "api info 1": "あなた専用のAPIキーを使うことができます。", "api info 2": "Dropbox's Developer で新しいアプリを作成するとき次のことに注意してください:", "api info li 1": "API タイプは **Dropbox API** です", "api info li 2": "アクセスタイプは **App Folder** です" }, "help": { "firststart title": "Lavernaへようこそ", "firststart import": "既にLavernaをご利用の場合、下の'import'ボタンをクリックしてその設定を取り込むことができます。", "firststart next": "Lavernaを初めて使う場合、'next'ボタンをクリックしてインストールを進めてください。", "firststart encryptiON": "暗号化機能を使う場合、パスワードを入力してください。", "firststart sync": "Lavernaはサーバにデータを保管しないので、他のデバイスでもノートを使いたい場合は同期を機能を有効にしてください。", "firststart backup": "もう少しで完了です。設定のバックアップを取れるようになりました。そして、最後のステップへ進んでください。" } } ================================================ FILE: app/locales/ko/translation.json ================================================ { "Search": "검색", "All notes": "모든 노트", "Favourites": "즐겨찾기", "Favorite": "즐겨찾기", "Trash": "휴지통", "Open tasks": "진행중인 작업", "Notebooks": "노트북", "Settings": "설정", "About": "정보", "Save": "저장", "Save & Exit": "저장 후 종료", "Cancel": "취소", "Full screen": "전체화면", "Preview": "미리보기", "Normal": "기본", "Select notebook": "노트북 선택", "Title": "제목", "Submit": "제출하기", "Tags": "태그", "Tag": "태그", "Parent": "위치", "Root": "최상위", "Notebooks & tags": "노트북 & 태그", "Notebook": "노트북", "Restore": "복원", "Delete": "삭제", "New tag": "새로운 태그", "Edit": "편집", "Remove": "제거", "Forever": "영원히", "No": "아니오", "Yes": "예", "Basic": "기본", "Cloud storage": "클라우드 저장소", "Notes per page": "페이지 당 노트수", "Sort notebooks": "노트북 정렬", "Name": "이름순으로 정렬", "Created": "시간순으로 정렬", "Default edit mode": "기본 편집모드", "Fullscreen with preview": "전체화면과 미리보기", "Use encryption": "암호화 사용", "Encryption parameters": "암호화 변수", "Encryption Password": "암호화 비밀번호", "Salt": "소금", "Random": "무작위", "Key size": "암호키 길이", "Strengthen by a factor of": "강화 인자", "Authentication strength": "인증 강도", "Unlock": "Unlock", "Your new encryption password": "새로운 암호화 비밀번호", "Your old encryption password": "기존 암호화 비밀번호", "Show sidebar": "사이드바 보이기", "Previous": "이전", "Next": "다음", "Navigation": "탐색", "navigateTop": "위로", "navigateBottom": "아래로", "Jump": "빠른 이동", "jumpInbox": "모든 노트 목록으로 이동", "jumpNotebook": "노트북 목록으로 이동", "jumpFavorite": "즐겨찾기로 이동", "jumpRemoved": "휴지통으로 이동", "jumpOpenTasks": "진행중인 작업으로 이동", "Actions": "작업", "actionsEdit": "편집", "actionsOpen": "열기", "actionsRemove": "삭제", "actionsRotateStar": "즐겨찾기 토글", "App": "앱", "appCreateNote": "새 노트 만들기", "appSearch": "노트 검색", "appKeyboardHelp": "단축키 보기", "Change keybindings": "단축키 변경하기", "Donate": "기부하기", "Github page": "Github 페이지", "Report bugs and issues here": "Github에 버그 제보하기", "Report bugs through email": "이메일로 버그 제보하기", "Credits": "크레딧", "List of contributors": "기여자 목록", "List of all used libraries": "사용된 라이브러리 목록", "Are you sure?": "정말이세요?", "You have unsaved changes": "변경한 사항이 저장되지 않습니다.", "Dropbox API key": "Dropbox API 키", "Required": "필수사항", "Optional": "선택사항", "Language": "언어", "Action": "작업", "Select": "선택", "General": "일반", "Encryption": "암호화", "Keybindings": "단축키", "Sync": "동기화", "Profiles": "프로필", "Import": "가져오기", "Transfer data": "가져오기 & 내보내기", "Import settings": "가져오기 설정", "Export settings": "내보내기 설정", "Wrong format": "잘못된 형식의 파일", "useDefaultConfigs": "기본 프로필의 설정을 사용합니다", "File should be in json format": "json 형식의 파일이여야 합니다", "Close": "닫기", "Hyperlink": "하이퍼링크", "Editor": "편집자", "Preview": "미리보기", "Download": "다운로드", "Transfer everything": "전부", "Find in page": "페이지에서 찾기", "Other": "기타", "Default": "기본", "Modules": "모듈", "Import data": "데이터 가져오기", "Export data": "데이터 내보내기", "Enabled": "활성화", "Disabled": "비활성화", "encryption": { "provide password": "비밀번호를 입력해주세요", "change password": "변경할 비밀번호를 입력해주세요", "wait": "암호화가 끝날때까지 기다려주세요", "error": "암호화 에러", "errorConfirm": "데이터 암호화중 에러가 발생했습니다.\r\r 만약 또 다른 Laverna 윈도우에서 암호화 설정을 변경하셨다면 현재 윈도우에서도 **설정을 갱신하거나** 설정 가져오기를 시도해보세요.\r\r 그래도 아무것도 변하지 않는다면 **다시 로그인해주세요**.", "errorConfirmSettings": "암호화 설정 변경", "errorConfirmAuth": "다시 시도", "backup": { "title": "데이터 백업", "content": "계속 진행하기 전에, 백업 파일을 다운로드 해주세요. 이 파일은 변경된 프로필의 암호화되지 않은 데이터로 구성되어 있습니다. 안전한 장소에 보관해주세요.", "next": "백업 파일을 다운로드 하지 않고 진행하기" }, "state": { "decrypt": "모든 데이터 해독 중", "encrypt": "모든 데이터 암호화 중", "save": "변경사항 저장중" } }, "profile": { "confirm remove": "**{{profile}}** 프로필이 지워집니다. 프로필에 포함된 노트, 태그, 노트북 등 모든 데이터가 삭제되고, 되돌릴 수 없어요!", "type name": "새로운 프로필 이름을 입력하세요" }, "files": { "file-url": "파일 또는 이미지 URL", "attach": "파일 첨부", "attachLink": "링크로 첨부", "attachImage": "이미지로 첨부" }, "notes": { "confirm trash": "**{{title}}** 노트가 휴지통으로 이동합니다.", "confirm remove": "**{{title}}** 노트가 **영원히** 지워집니다!", "create and attach": "새 노트를 만들고 링크를 연결합니다", "create": "새 노트 만들기", "hyperlink-dialog": "노트 제목 또는 URL" }, "notebooks": { "select": "노트북 선택", "add": "노트북 추가", "edit": "노트북 편집", "name": "노트북 이름을 입력해주세요", "confirm remove": "**{{name}}** 노트북이 **영원히** 지워집니다!", "remove with notes": "네, 포함된 노트도 함께 지워주세요", "remove": "네, 지워주세요" }, "tags": { "name": "태그 이름을 입력해주세요", "add": "태그 추가", "edit": "태그 편집", "confirm remove": "**{{name}}** 태그가 **영원히** 지워집니다!" }, "dropbox": { "auth confirm": "이제 **Dropbox** 권한 승인 페이지로 이동합니다.\r> **OK** 버튼을 눌러주세요.", "auth title": "Dropbox 권한 승인", "api info 1": "여기서 여러분의 API key를 만들 수 있습니다:", "api info 2": "Dropbox 개발자 사이트에서 새 앱을 만들 때 이것을 기억해주세요:", "api info li 1": "앱의 종류는 Dropbox API 앱이여야 합니다.", "api info li 2": "데이터의 종류는 파일과 데이터저장소여야 합니다." }, "help": { "firststart title": "Laverna에 오신걸 환영합니다", "firststart import": "이미 Laverna를 사용하고 계신다면, 아래의 '가져오기' 버튼으로 이전 설정을 가져올 수 있습니다.", "firststart next": "Laverna를 처음 사용하신다면, '다음' 버튼으로 설치를 시작하세요.", "firststart encryption": "암호화를 사용하길 원하신다면, 암호화 비밀번호를 입력해주세요.", "firststart sync": "우리는 어떠한 데이터도 서버에 저장하지 않기 때문에, 다른 장치에서 여러분의 노트를 보기 위해서는 적용된 서비스들 중 하나와 동기화를 사용할 필요가 있습니다.", "firststart backup": "모든 준비가 거의 완료되었습니다. 여러분의 설정 백업을 다운로드하고 마지막 단계를 진행할 수 있습니다." } } ================================================ FILE: app/locales/locales.json ================================================ { "ar": { "name": "Arabic", "nativeName": "العربية" }, "it": { "name": "Italian", "nativeName": "Italiano" }, "bs_ba": { "name": "Bosnian", "nativeName": "Bosnian" }, "cs": { "name": "Czech", "nativeName": "Čeština" }, "da": { "name": "Danish", "nativeName": "Dansk" }, "de": { "name": "German", "nativeName": "Deutsch" }, "de_ch": { "name": "Swiss german", "nativeName": "Schwiizerdütsch" }, "el": { "name": "Greek", "nativeName": "Ελληνικά" }, "en": { "name": "English", "nativeName": "English" }, "eo": { "name": "Esperanto", "nativeName": "Esperanto" }, "es": { "name": "Spanish", "nativeName": "Español" }, "fr": { "name": "French", "nativeName": "Français" }, "gl": { "name": "Galician", "nativeName": "Galego" }, "hi_in": { "name": "Hindi", "nativeName": "Hindi" }, "ja": { "name": "Japanese", "nativeName": "日本語" }, "ko": { "name": "Korean", "nativeName": "한국어" }, "mr_in": { "name": "Marathi", "nativeName": "Marathi" }, "nb": { "name": "Norwegian Bokmål", "nativeName": "Norsk bokmål" }, "nl": { "name": "Dutch", "nativeName": "Nederlands" }, "nn": { "name": "Norwegian Nynorsk", "nativeName": "Norsk nynorsk" }, "oc": { "name": "Occitan", "nativeName": "Occitan" }, "lt": { "name": "Lithuanian", "nativeName": "Lietuvių" }, "lv": { "name": "Latvian", "nativeName": "Latviešu" }, "pl": { "name": "Polish", "nativeName": "Polski" }, "pt_br": { "name": "Portugisich (Brasilien)", "nativeName": "Portugisich (Brasilien)" }, "ru": { "name": "Russian", "nativeName": "Русский" }, "se": { "name": "Swedish", "nativeName": "Svenska" }, "sq": { "name": "Albanian", "nativeName": "Shqip" }, "tr": { "name": "Turkish", "nativeName": "Türkçe" }, "zh_cn": { "name": "Simplified Chinese", "nativeName": "Simplified Chinese" }, "zh_tw": { "name": "Traditional Chinese (Taiwan)", "nativeName": "Traditional Chinese (Taiwan)" } } ================================================ FILE: app/locales/lt/translation.json ================================================ { "Search": "Paieška", "All notes": "Visi užrašai", "Favourites": "Mėgstamiausi", "Favorite": "Mėgstamiausi", "Trash": "Šiukšlinė", "Open tasks": "Nebaigtos užduotys", "Notebooks": "Užrašinės", "Settings": "Nustatymai", "About": "Apie", "Save": "Išsaugoti", "Save & Exit": "Išsaugoti ir išeiti", "Cancel": "Atšaukti", "Full screen": "Pilnas ekranas", "Preview": "Peržiūra", "Normal": "Normalus", "Select notebook": "Pasirinkti užrašinę", "Title": "Pavadinimas", "Submit": "Pateikti", "Tags": "Gairės", "Tag": "Gairė", "Parent": "Užrašinė", "Root": "Pagrindinė", "Notebooks & tags": "Užrašinės ir gairės", "Notebook": "Užrašinė", "Restore": "Atkurti", "Delete": "Ištrinti", "New tag": "Nauja gairė", "Edit": "Keisti", "Remove": "Pašalinti", "Forever": "Visam laikui", "No": "Ne", "Yes": "Taip", "Basic": "Bazinis", "Cloud storage": "Saugykla debesyse", "Notes per page": "Užrašų puslapiuose", "Sort notebooks": "Rūšiuoti užrašines", "Name": "Pavadinimas", "Created": "Sukurta", "Default edit mode": "Numatytasis keitimo rėžimas", "Fullscreen with preview": "Pilno ekrano peržiūra", "Use encryption": "Naudoti šifravimą", "Encryption parameters": "Šifravimo parametrai", "Encryption Password": "Slaptažodis", "Salt": "Druska", "Random": "Atsitiktinis", "Key size": "Rakto dydis", "Strengthen by a factor of": "Sustiprinti daugikliu", "Authentication strength": "Autentifikacijos stiprumas", "Unlock": "Atrakinti", "Your new encryption password": "Jūsų naujas slaptažodis", "Your old encryption password": "Jūsų senas slaptažodis", "Show sidebar": "Rodyti šoninę juostą", "Previous": "Ankstesnis", "Next": "Kitas", "Navigation": "Navigacija", "navigateTop": "Aukštyn", "navigateBottom": "Žemyn", "Jump": "Pereiti į", "jumpInbox": "Gautuosius", "jumpNotebook": "Užrašinių sąrašą", "jumpFavorite": "Mėgstamiausių sąrašą", "jumpRemoved": "Pašalintus užrašus", "jumpOpenTasks": "Užrašus su nebaigtomis užduotimis", "Actions": "Veiksmai", "actionsEdit": "Keisti", "actionsOpen": "Atidaryti", "actionsRemove": "Šalinti", "actionsRotateStar": "Perjungti žvaigždutę", "App": "Programa", "appCreateNote": "Sukurti naują užrašą", "appSearch": "Ieškoti užrašų", "appKeyboardHelp": "Klaviatūros pagalba", "Change keybindings": "Keisti klaviatūros nustatymus", "Donate": "Parama", "Github page": "Github puslapis", "Report bugs and issues here": "Pranešti apie klaidas ir problemas čia", "Report bugs through email": "Praneši apie klaidas naudojant el. paštą", "Credits": "Kreditai", "List of contributors": "Prisidėjusių sąrašas", "List of all used libraries": "Naudotų bibliotekų sąrašas", "Are you sure?": "Esate tuo tikras?", "You have unsaved changes": "Turite neišsaugotų pakeitimų.", "Dropbox API key": "Dropbox API raktas", "Required": "Privaloma", "Optional": "Nebūtina", "Language": "Kalba", "Action": "Veiksmas", "Select": "Pasirinkti", "General": "Bendra", "Encryption": "Šifravimas", "Keybindings": "Spartieji klavišai", "Sync": "Sinchronizacija", "Profiles": "Profiliai", "Import": "Importuoti", "Transfer data": "Importuoti ir eksportuoti", "Import settings": "Imporavimo nustatymai", "Export settings": "Eksportavimo nustatymai", "Wrong format": "Blogas formatas", "useDefaultConfigs": "Naudoti nustatymus iš numatytojo profilio", "File should be in json format": "Failas turi būti json formate", "Close": "Uždaryti", "Hyperlink": "Hypernuoroda", "Editor": "Redaktorius", "Preview": "Peržiūra", "Download": "Atsisiųsti", "Transfer everything": "Viskas", "Find in page": "Rasti puslapyje", "Other": "Kita", "Default": "Numatyta", "Modules": "Mobuliai", "Import data": "Importuoti duomenis", "Export data": "Eksportuoti duomenis", "Enabled": "Įjungta", "Disabled": "Išjungta", "encryption": { "provide password": "Prašome nurodyti šifravimui skirtą slaptažodį", "change password": "Norėdami pakeisti slaptažodį įveskite jį čia", "wait": "Prašome palaukti, kol bus užbaigtas šifravimas", "error": "Šifravimo klaida", "errorConfirm": "Klaida iššifruojant duomenis.\r\r If you changed encryption settings in another browser, **update your settings** in this browser too. Or try to import settings.\r\r And if you did not change anything, **try to login** again.", "errorConfirmSettings": "Change encryption settings", "errorConfirmAuth": "Bandyti dar kartą", "backup": { "title": "Atkūrimo duomenys", "content": "Please, before proceeding to the next step, download your backup file. It contains decrypted previous data of changed profiles. Keep it in a safe place.", "next": "Procceed without downloading the backup file" }, "state": { "decrypt": "Viskas iššifruojama", "encrypt": "Viskas šifruojama", "save": "Išsaugomi pakeitimai" } }, "profile": { "confirm remove": "Profile **{{profile}}** will be removed with all the data, including notes, tags, and notebooks. This action is irreversible!", "type name": "Įvesti profilio pavadimą" }, "files": { "file-url": "Failo arba vaizdo URL", "attach": "Pridėti failą", "attachLink": "Pridėti kaip nuorodą", "attachImage": "Pridėti kaip vaizdą" }, "notes": { "confirm trash": "Užrašai **{{title}}** bus perkelti į šiukšlinę.", "confirm remove": "Užrašai **{{title}}** bus pašalinti **visam laikui**!", "create and attach": "Sukurti naujus užrašus ir pridėti jų nuorodą", "create": "Sukurti naujus užrašus", "hyperlink-dialog": "Užrašų pavadinimas arba URL" }, "notebooks": { "select": "Pasirinkti užrašinę", "add": "Pridėti naują užrašinę", "edit": "Keisti užrašinę", "name": "Prašome įvesti užrašinės pavadinimą", "confirm remove": "Užrašinė **{{name}}** bus pašalinta **visam laikui**!", "remove with notes": "Taip, pašalinti priskirtus užrašus", "remove": "Taip, pašalinti" }, "tags": { "name": "Gairės pavadinimas yra būtinas", "add": "Pridėti naują gairę", "edit": "Keisti gairę", "confirm remove": "Gairė **{{name}}** bus pašalinta **visam laikui**!" }, "dropbox": { "auth confirm": "Now you will be redirected to **Dropbox** authorization page.\r> Please click **OK** button.", "auth title": "Dropbox auth", "api info 1": "You can have your own API key on", "api info 2": "When you create a new app at Dropbox's Developer site you should keep in mind that:", "api info li 1": "Type of app should be Dropbox API app", "api info li 2": "Type of data should be Files and datastores" }, "help": { "firststart title": "Sveiki atvykę į Laverna", "firststart import": "Jeigu naudojotės Laverna anksčiau, tai galite importuoti senuosius nustatymus spausdami žemiau esantį mytuką „Imporuoti“", "firststart next": "Jeigu niekada nesinaudojote Laverna, spauskite mygtuką „Toliau“, kad pradėtumėte įdiegimo procesą.", "firststart encryption": "Jeigu norite naudoti kodavimą, prašome nurodyti kodavimo slaptažodį.", "firststart sync": "Kadangi mes nesaugome jokių duomenų mūsų serveriuose, jums reikia įgalinti sinchronizaciją su vienu iš adapterių, kuris galėtų peržiūrėti jūsų užrašus kituose įrenginiuose.", "firststart backup": "Viskas yra beveik paruošta. Galite atsisiųsti savo nustatymų kopiją ir eiti į paskutinį žingsnį." } } ================================================ FILE: app/locales/lv/translation.json ================================================ { "Search": "Meklēt", "All notes": "Piezīmes", "Favourites": "Izlase", "Favorite": "Izlase", "Trash": "Atkritne", "Open tasks": "Atvērtie uzdevumi", "Notebooks": "Klades", "Settings": "Iestatījumi", "About": "Par", "Save": "Saglabāt", "Save & Exit": "Saglabāt un iziet", "Cancel": "Atcelt", "Full screen": "Pilnekrānā", "Preview": "Priekšskatījums", "Normal": "Parasts skats", "Select notebook": "Izvēlēties kladi", "Title": "Nosaukums", "Submit": "Iesniegt", "Tags": "Tagi", "Tag": "Tags", "Parent": "Vecāki", "Root": "Sakne", "Notebooks & tags": "Klades un tagi", "Notebook": "Klade", "Restore": "Atjaunot", "Delete": "Izdzēst", "New tag": "Jauns tags", "Edit": "Rediģēt", "Remove": "Noņemt", "Forever": "Uz visiem laikiem", "No": "Nē", "Yes": "Jā", "Basic": "Pamata", "Cloud storage": "Mākoņglabātuve", "Notes per page": "Piezīmes uz lapu", "Sort notebooks": "Kārtot klades", "Name": "Pēc nosaukuma", "Created": "Pēc izveidošanas datuma", "Default edit mode": "Noklusējuma rediģēšanas režīms", "Fullscreen with preview": "Pilnekrāna ar priekšskatījumu", "Use encryption": "Izmantot šifrēšanu", "Encryption parameters": "Šifrēšanas parametri", "Encryption Password": "Šifrēšanas parole", "Salt": "Sāls", "Random": "Nejauša", "Key size": "Atslēgas garums", "Strengthen by a factor of": "Stiprināt par koeficientu", "Authentication strength": "Autentifikācijas stiprums", "Unlock": "Atslēgt", "Your new encryption password": "Jūsu jaunā šifra parole", "Your old encryption password": "Jūsu vecā šifra parole", "Show sidebar": "Rādīt sānjoslu", "Previous": "Iepriekšējā", "Next": "Nākamā", "Navigation": "Navigācija", "navigateTop": "Uz augšu", "navigateBottom": "Uz leju", "Jump": "Pārlēkt", "jumpInbox": "Iet uz iesūtni", "jumpNotebook": "Iet uz klažu sarakstu", "jumpFavorite": "Iet uz piezīmju izlasi", "jumpRemoved": "Iet uz noņemtajām piezīmēm", "jumpOpenTasks": "Iet uz piezīmēm ar atvērtiem uzdevumiem", "Actions": "Darbības", "actionsEdit": "Rediģēt", "actionsOpen": "Atvērt", "actionsRemove": "Noņemt", "actionsRotateStar": "Rotēt zvaigzi", "App": "Aplikācija", "appCreateNote": "Izveidot jaunu piezīmi", "appSearch": "Meklēt piezīmi", "appKeyboardHelp": "Tastatūras palīdzība", "Change keybindings": "Mainīt taustiņu iestatījumus", "Donate": "Ziedot", "Github page": "GitHub lapa", "Report bugs and issues here": "Ziņot par kļūdām un problēmām šeit", "Report bugs through email": "Ziņot par kļūdām caur e-pastu", "Credits": "Titri", "List of contributors": "Līdzstrādnieku saraksts", "List of all used libraries": "Izmantoto koda bibliotēku saraksts", "Are you sure?": "Vai esat pārliecināts/-a?", "You have unsaved changes": "Jums ir nesaglabātas izmaiņas.", "Dropbox API key": "Dropbox API atslēga", "Required": "Obligāts", "Optional": "Neobligāts", "Language": "Valoda", "Action": "Darbība", "Select": "Izvēlēties", "General": "Vispārēji", "Encryption": "Šifrēšana", "Keybindings": "Taustiņu iestatījumi", "Sync": "Sinhronizēt", "Profiles": "Profili", "Import": "Importēt", "Transfer data": "Importēt un eksportēt", "Import settings": "Importa iestatījumi", "Export settings": "Eksporta iestatījumi", "Wrong format": "Nepareizs formāts", "useDefaultConfigs": "Izmantot iestatījumus no noklusējuma profila", "File should be in json format": "Failam vajag būt JSON formātā", "Close": "Aizvērt", "Hyperlink": "Hipersaite", "Editor": "Redaktors", "Preview": "Priekšskatījums", "Download": "Lejupielāde", "encryption": { "wait": "Lūdzu uzgaidīt, līdz šifrēšana būs pabeigta", "error": "Šifrēšanas kļūda", "errorConfirm": "Kļūda šifrējot datus.\r\r Ja Jūs mainījāt šifrēšanas iestātījumus citā pārlūkā, **atjauniniet Jūsu iestatījumus** arī šajā pārlūkā vai arī mēģiniet importēt iestatījumus.\r\r Ja tas neko nemaina, **mēģiniet ielogoties** atkal.", "errorConfirmSettings": "Mainīt šifrēšanas parametrus", "errorConfirmAuth": "Mēģiniet atkal", "backup": { "title": "Datu rezerves kopija", "content": "Lūdzu, pirms ejiet uz nākamo soli, lejupielādējiet Jūsu rezerves kopiju. Tā satur atšifrētus iepriekšējos datus un mainītos profilus. Glabājiet to drošā vietā.", "next": "Turpināt, nelejupielādējot rezerves kopiju" }, "state": { "decrypt": "Atšifrē visu", "encrypt": "Šifrē visu", "save": "Saglabā izmaiņas" } }, "profile": { "confirm remove": "Profils **{{profile}}** tiks noņemts ar visiem datiem, tai skaitā piezīmēm, tagiem un piezīmju grāmatiņām. Šī darbība ir neatgriezeniska!", "type name": "Ierakstiet profila vārdu" }, "files": { "file-url": "Faila vai attēla URL", "attach": "Pievienot failu", "attachLink": "Pievienot kā saiti", "attachImage": "Pievienot kā attēlu" }, "notes": { "confirm trash": "Piezīme **{{title}}** nonāks atkritnē.", "confirm remove": "Piezīme**{{title}}** tiks izdzēsta **uz visiem laikiem**!", "create and attach": "Izveidot jaunu piezīmi un pievienot tās saiti", "create": "Izveidot jaunu piezīmi", "hyperlink-dialog": "Piezīmes vai URL nosaukums" }, "notebooks": { "select": "Izvēlēties kladi", "add": "Pievienot jaunu kladi", "edit": "Rediģēt kladi", "name": "Lūdzu, sniedziet kladei nosaukumu", "confirm remove": "Klade **{{name}}** tiks izdzēsta **uz visiem laikiem**!", "remove with notes": "Jā, izdzēst ar pievienotajām piezīmēm", "remove": "Jā, izdzēst" }, "tags": { "name": "Taga nosaukums ir obligāts", "add": "Pievienot jaunu tagu", "edit": "Rediģēt tagu", "confirm remove": "Tags **{{name}}** tiks dzēsts **uz visiem laikiem**!" }, "dropbox": { "auth confirm": "Tagad Jūs tiksiet novirzīts/-a uz **Dropbox** autorizācijas lapu.\r> Lūdzu noklikšķiniet **OK** pogu.", "auth title": "Dropbox autentifikācija" }, "help": { "firststart title": "Laipni lūgti Laverna", "firststart import": "Ja Jūs jau esat lietojis/-usi Laverna iepriekš, Jūs varat importēt Jūsu iepriekšējos iestatījumu klikšķinot 'importēt' pogu lejup.", "firststart next": "Ja nekad neesat lietojis/-usi Laverna līdz šim, klikšķiniet uz 'nākamā' pogas, lai sāktu instalācijas procesu.", "firststart encryption": "Ja vēlaties izmantot šifrēšanu, lūdzu sniedziet šifrēšans paroli.", "firststart sync": "Tā kā mēs neglabājam jebkādus datus mūsu serveros, Jums vajag ieslēgt sinhronizāciju ar vienu no no adapteriem lai varētu skatīt savas piezīmes arī uz citām ierīcēm.", "firststart backup": "Viss ir gandrīz gatavs. Jūs varat lejupielādēt savu iestatījumu rezerves kopiju un pāriet uz nākamo soli." } } ================================================ FILE: app/locales/mr_in/translation.json ================================================ { "en": "इंग्रजी", "ru": "रशियन", "nl": "डच", "fr": "फ्रेंच", "pt_br": "ब्राझिलियन पोर्तुगीज", "eo": "एस्पेरान्तो", "es": "स्पॅनिश", "de": "जर्मन", "de_ch": "स्विस जर्मन", "se": "स्वीडिश", "el": "ग्रीक", "nb": "नॉर्वेजियन (बोकमाल)", "nn": "नॉर्वेजियन (न्योर्स्क)", "bs_ba": "बोस्नियन", "hi_in": "हिंदी", "mr_in": "मराठी", "zh_cn": "सरलीकृत चीनी", "Search": "शोधा", "All notes": "सर्व नोट्स", "Favourites": "आवडत्या", "Favorite": "आवडता", "Trash": "कचरा", "Notebooks": "नोटबुक्स", "Settings": "सेटिंग्ज", "About": "विषयक", "Save": "जतन करा", "Save & Exit": "जतन करून बंद करा", "Cancel": "रद्द करा", "Full screen": "पूर्ण स्क्रीन", "Preview": "पूर्वावलोकन", "Normal": "सामान्य", "Select notebook": "नोटबुक निवडा", "Title": "शीर्षक", "Submit": "सबमिट करा", "Tags": "टॅग्ज", "Tag": "टॅग", "Parent": "पूर्वज", "Root": "मूळे", "Notebooks & tags": "नोटबुक्स आणि टॅग्ज", "Notebook": "नोटबुक", "Restore": "पुनर्संचयित करा", "Delete": "मिटवा", "New tag": "नवीन टॅग", "Edit": "संपादन करा", "Remove": "काढा", "Forever": "कायमचे", "No": "नाही", "Yes": "होय", "Basic": "मूलभूत", "Cloud storage": "मेघ संचय", "Notes per page": "नोट्स प्रत्येक पानावर", "Sort notebooks": "नोटबुक वर्गीकरण करा", "Name": "नाव", "Created": "निर्माण", "Default edit mode": "मुलभूत संपादन मोड", "Fullscreen with preview": "पूर्वावलोकन सह पूर्णस्क्रीन", "Use encryption": "एनक्रिप्शन वापरा", "Encryption parameters": "एनक्रिप्शनचे मापदंड", "Encryption Password": "एन्क्रिप्शनचे पासवर्ड", "Salt": "साल्त", "Random": "यादृच्छिक करा", "Key size": "की साईझ", "Strengthen by a factor of": "शक्तीचा घटक", "Authentication strength": "प्रमाणीकरण शक्ती", "Unlock": "अनलॉक", "Your new encryption password": "आपले नवीन एनक्रिप्शनचे पासवर्ड", "Your old encryption password": "आपल्या जुन्या एनक्रिप्शनचे पासवर्ड", "Please wait until the encryption will be completed": "एन्क्रिप्शन पूर्ण होईल तोपर्यंत प्रतीक्षा करा", "Shortcuts": "शॉर्टकत्स", "Newer": "नवीन", "Older": "जुने", "Navigation": "नेव्हिगेशन", "navigateTop": "सुरवातीला", "navigateBottom": "तळाशी", "Jump": "उडी", "jumpInbox": "इनबॉक्सला जा", "jumpNotebook": "नोटबुक यादीला जा", "jumpFavorite": "आवडत्या नोट्सना जा", "jumpRemoved": "काढलेल्या नोट्सना जा", "Actions": "क्रिया", "actionsEdit": "संपादन करा", "actionsOpen": "उघडा", "actionsRemove": "काढा", "actionsRotateStar": "तारा फिरवा", "App": "अनुप्रयोग", "appCreateNote": "नवीन नोट तयार करा", "appSearch": "नोट शोधा", "appKeyboardHelp": "कीबोर्ड मदत" } ================================================ FILE: app/locales/nb/translation.json ================================================ { "About": "Om", "Actions": "Handlinger", "All notes": "Alle notat", "App": "Applikasjon", "Authentication strength": "Autentiseringsstyrke", "Basic": "Enkel", "navigateBottom": "Ned", "Cancel": "Avbryt", "Cloud storage": "Skylagring", "appCreateNote": "Lag nytt notat", "Default edit mode": "Standard redigeringsmodus", "Delete": "Slett", "Edit": "Rediger", "Encryption Password": "Krypteringspassord", "Encryption parameters": "Krypteringsparametere", "Favorite": "Favoritt", "Favourites": "Favoritter", "Forever": "Alltid", "Full screen": "Fullskjerm", "Fullscreen with preview": "Fullskjerm med forhåndsvisning", "jumpFavorite": "Gå til favorittnotat", "jumpInbox": "Gå til innboks", "jumpNotebook": "Gå til notatblokkliste", "jumpRemoved": "Gå til fjernede notat", "Jump": "Hopp", "Key size": "Nøkkelstørrelse", "appKeyboardHelp": "Tastaturhjelp", "Navigation": "Navigering", "New tag": "Ny emneknagg", "Older": "Neste", "No": "Nei", "Normal": "Vanlig", "Notebook": "Notatblokk", "Notebooks & tags": "Notatblokker & emneknagger", "Notebooks": "Notatblokker", "Notes per page": "Notat per side", "actionsOpen": "Åpne", "Parent": "Forelder", "Please wait until the encryption will be completed": "Vennligst vent til krypteringen er ferdig", "Preview": "Forhåndsvis", "Newer": "Forrige", "Random": "Tilfeldig", "Remove": "Fjern", "Restore": "Gjenopprett", "Root": "Rot", "actionsRotateStar": "Vend stjerne", "Salt": "Salt", "Save & Exit": "Lagre og avslutt", "Save": "Lagre", "appSearch": "Søk etter notat", "Search": "Søk", "Select notebook": "Velg notatblokk", "Settings": "Innstillinger", "Shortcuts": "Snarveier", "Strengthen by a factor of": "Styrk med faktor", "Submit": "Legg inn", "Tag": "Emneknagg", "Tags": "Emneknagger", "Title": "Tittel", "navigateTop": "Opp", "Trash": "Papirkurv", "Unlock": "Lås opp", "Use encryption": "Bruk kryptering", "Yes": "Ja", "Your new encryption password": "Ditt nye krypteringspassord", "Your old encryption password": "Ditt gamle krypteringspassord", "en": "Engelsk", "fr": "Fransk", "nl": "Nederlandsk", "pt_br": "Brasiliansk portugisisk", "ru": "Russisk" } ================================================ FILE: app/locales/nl/translation.json ================================================ { "Search": "Zoeken", "All notes": "Inbox", "Favourites": "Favorieten", "Trash": "Prullenbak", "Notebooks": "Notitieboeken", "Settings": "Instellingen", "About": "Over", "Save": "Opslaan", "Cancel": "Annuleren", "Full screen": "Volledig scherm", "Preview": "Voorbeeld", "Normal": "Normaal", "Select notebook": "Selecteer notitieblok", "Title": "Titel", "Submit": "Toepassen", "Tags": "Labels", "Tag": "Label", "Parent": "Parent", "Root": "Root", "Notebooks & tags": "Notitieblokken & labels", "Notebook": "Notitie", "Restore": "Herstellen", "Delete": "Verwijderen", "New tag": "Nieuw label", "Edit": "Aanpassen", "Remove": "Weghalen", "Forever": "Altijd", "No": "Nee", "Yes": "Ja", "Basic": "Basis", "Cloud storage": "Cloud opslag", "Notes per page": "Notities per pagina", "Default edit mode": "Standaard bewerkingsmodus", "Fullscreen with preview": "Volledig scherm met voorbeeld", "Use encryption": "Gebruik encryptie", "Encryption parameters": "Encryptie waarden", "Encryption Password": "Encryptie Wachtwoord", "Salt": "Zout", "Random": "Willekeurig", "Key size": "Sleutel grootte", "Strengthen by a factor of": "Versterken met de factor", "Authentication strength": "Authenticatie sterkte", "Unlock": "Ontgrendelen", "Your new encryption password": "Uw nieuwe encryptie wachtwoord", "Your old encryption password": "Uw oude encryptie wachtwoord", "Please wait until the encryption will be completed": "Wacht alstublieft tot de versleuteling voltooid is", "Shortcuts": "Snelkoppelingen", "Newer": "Vorige", "Older": "Volgende", "Navigation": "Navigatie", "navigateTop": "Boven", "navigateBottom": "Onder", "Jump": "Spring", "jumpInbox": "Ga naar inbox", "jumpNotebook": "Ga naar notitieblok lijst", "jumpFavorite": "Ga naar favorite notities", "jumpRemoved": "Ga naar verwijderde notities", "Actions": "Acties", "actionsOpen": "Open", "actionsRotateStar": "Draai Ster", "App": "App", "appCreateNote": "Maak nieuwe notitie", "appSearch": "Zoek notitie", "appKeyboardHelp": "Toetsenbord help" } ================================================ FILE: app/locales/nn/translation.json ================================================ { "en" : "Engelsk", "ru" : "Russisk", "nl" : "Nederlandsk", "fr" : "Fransk", "pt_br" : "Brasiliansk portugisisk", "Search": "Søk", "All notes": "Alle notat", "Favourites": "Favorittar", "Favorite": "Favoritt", "Trash": "Papirkorga", "Notebooks": "Notatblokkar", "Settings": "Innstillingar", "About": "Om", "Save": "Lagra", "Save & Exit": "Lagra og avslutt", "Cancel": "Avbryt", "Full screen": "Fullskjerm", "Preview": "Førehandsvis", "Normal": "Vanleg", "Select notebook": "Vel notatblokk", "Title": "Tittel", "Submit": "Legg inn", "Tags": "Emneknaggar", "Tag": "Emneknagg", "Parent": "Forelder", "Root": "Rot", "Notebooks & tags": "Notatblokkar & emneknaggar", "Notebook": "Notatblokk", "Restore": "Gjenopprett", "Delete": "Slett", "New tag": "Ny emneknagg", "Edit": "Rediger", "Remove": "Fjern", "Forever": "Alltid", "No": "Nei", "Yes": "Ja", "Basic": "Enkel", "Cloud storage": "Skylagring", "Notes per page": "Notat per side", "Default edit mode": "Standard redigeringsmodus", "Fullscreen with preview": "Fullskjerm med førehandsvisning", "Use encryption": "Bruk kryptering", "Encryption parameters": "Krypteringsparameterar", "Encryption Password": "Krypteringspassord", "Salt": "Salt", "Random": "Tilfeldig", "Key size": "Nøkkelstorleik", "Strengthen by a factor of": "Styrk med faktor", "Authentication strength": "Autentiseringsstyrke", "Unlock": "Lås opp", "Your new encryption password": "Det nye krypteringspassordet ditt", "Your old encryption password": "Det gamle krypteringspassordet ditt", "Please wait until the encryption will be completed": "Ver vennleg og vent til krypteringa er ferdig", "Shortcuts": "Snarvegar", "Newer": "Førre", "Older": "Neste", "Navigation": "Navigering", "navigateTop": "Opp", "navigateBottom": "Ned", "Jump": "Hopp", "jumpInbox": "Gå til innboks", "jumpNotebook": "Gå til notatblokkliste", "jumpFavorite": "Gå til favorittnotat", "jumpRemoved": "Gå til fjerna notat", "Actions": "Handlingar", "actionsOpen": "Opna", "actionsRotateStar": "Vend stjerne", "App": "Applikasjon", "appCreateNote": "Lag nytt notat", "appSearch": "Søk etter notat", "appKeyboardHelp": "Tastaturhjelp" } ================================================ FILE: app/locales/oc/translation.json ================================================ { "Search": "Cercar", "All notes": "Totas las nòtas", "Favourites": "Favorits", "Favorite": "Favorit", "Trash": "Escobilhièr", "Open tasks": "Prètzfaits en escritura", "Notebooks": "Blòts", "Settings": "Paramètres", "About": "A prepaus", "Save": "Salvar", "Save & Exit": "Salvar & Sortir", "Cancel": "Anullar", "Full screen": "Plen ecran", "Preview": "Apercebut", "Normal": "Normal", "Select notebook": "Seleccionar un blòt", "Title": "Títol", "Submit": "Mandar", "Tags": "Etiquetas", "Tag": "Etiqueta", "Parent": "Parent", "Root": "Raiç", "Notebooks & tags": "Blòts & Etiquetas", "Notebook": "Blòt", "Restore": "Restablir", "Delete": "Suprimir", "New tag": "Novèla etiqueta", "Edit": "Modificar", "Remove": "Levar", "Forever": "Per totjorn", "No": "Non", "Yes": "Òc", "Basic": "Basic", "Cloud storage": "Enregistrament alonhat sul Cloud", "Notes per page": "Nòtas per pagina", "Sort notebooks": "Triar los blòts", "Name": "Nom", "Created": "Creat", "Default edit mode": "Mòde d'edicion per defaut", "Fullscreen with preview": "Plen ecran amb apercebut", "Use encryption": "Utilizar lo chiframent", "Encryption parameters": "Paramètres de chiframent", "Encryption Password": "Senhal de chiframent", "Salt": "Sal", "Random": "Aleatòri", "Key size": "Talha de la clau", "Strengthen by a factor of": "Renfortir per un factor de", "Authentication strength": "Autentificacion fòrta", "Unlock": "Desclavar", "Your new encryption password": "Vòstre novèl senhal", "Your old encryption password": "Vòstre ancian senhal", "Show sidebar": "Afichar la barra laterala", "Previous": "Precedent", "Next": "Seguent", "Navigation": "Navigacion", "navigateTop": "Naut", "navigateBottom": "Bas", "Jump": "Sautar", "jumpInbox": "Anar a la bóstia de recepcion", "jumpNotebook": "Anar a la lista de blòts", "jumpFavorite": "Anar a las nòtas preferidas", "jumpRemoved": "Anar a las nòtas levadas", "jumpOpenTasks": "Anar a los prètzfaits en escritura", "Actions": "Accions", "actionsEdit": "Modificar", "actionsOpen": "Dobrir", "actionsRemove": "Levar", "actionsRotateStar": "Ajustar/Levar dels favorits", "App": "Aplicacion", "appCreateNote": "Crear una novèla nòta", "appSearch": "Cercar una nòta", "appKeyboardHelp": "Ajuda pel clavièr", "Change keybindings": "Cambiar los acorchis de clavièr", "Donate": "Far un don", "Github page": "Pagina Github", "Report bugs and issues here": "Senhalar los bugs e problèmas aquí", "Report bugs through email": "Senhalar los bugs per corrièl", "Credits": "Crèdits", "List of contributors": "Lista dels contributors", "List of all used libraries": "Lista de totas las bibliotècas utilizadas", "Are you sure?": "Sètz segur ?", "You have unsaved changes": "Avètz pas salvat los cambiaments.", "Dropbox API key": "Clau de l'API Dropbox", "Required": "Requesit", "Optional": "Opcional", "Language": "Lenga", "Action": "Accion", "Select": "Seleccionar", "General": "General", "Encryption": "Chiframent", "Keybindings": "Acorchis de clavièr", "Sync": "Sincronizar", "Profiles": "Perfils", "Import": "Importar", "Transfer data": "Importar & Exportar", "Transfer settings": "Paramètres de transferiment", "Import settings": "Importar los paramètres", "Export settings": "Exportar los paramètres", "Wrong format": "Marrit format", "useDefaultConfigs": "Utilizar los paramètres del perfil per defaut", "File should be in json format": "Cal que lo fichièr siá al format json", "Close": "Tampar", "Hyperlink": "Iperligam", "Editor": "Editor", "Preview": "Apercebut", "Download": "Telecargar", "Transfer everything": "Tot", "Find in page": "Trobar dins la pagina", "Other": "Autre", "Default": "Per defaut", "Modules": "Moduls", "Import data": "Importar las donadas", "Export data": "Exportar las donadas", "Enabled": "Activat", "Disabled": "Desactivat", "Untitled": "Cap de nom", "Line of": "Linha {{currentLine}} sus {{numberOfLines}}", "Drop files": "Pausar los fichièrs aquí per los enviar", "Spaces per indent": "Espacis pels alineas", "Sort notes": "Triar las nòtas per", "Updated date": "Data de modificacion", "Created date": "Data de creacion", "Text editor": "Editor de tèxtes", "Vim": "Vim", "Emacs": "Emacs", "Sublime": "Sublime", "encryption": { "provide password": "Mercés de fornir vòstre senhal", "change password": "Marcatz vòstre senhal aquí per o cambiar", "wait": "Mercés d'esperar dusca que lo chiframent siá acabat", "error": "Error al moment de chifrar", "errorConfirm": "Error pendent lo deschiframent de las donadas.\r\r S'avètz cambiat los paramètres de chiframent sus un navigator mai, **metètz a jorn vòstres paramètres** sus aiceste navigator tanben. O ensajatz d'importar los paramètres.\r\r E s'avètz pas cambiat res, **ensajatz de vos connectar** de nòu.", "errorConfirmSettings": "Cambiar los paramètres de chiframent", "errorConfirmAuth": "Tornatz ensajar", "backup": { "title": "Donadas de salvagarda", "content": "Mercés de telecargar vòstre fichièr de salvagarda abans de passar a l'etapa seguenta. Conten las donadas deschifradas d'abans los cambiaments de perfils. Gardatz-lo en un luòc segur.", "next": "Contunhar sens telecargar lo fichièr de salvament" }, "state": { "decrypt": "O deschifrar tot", "encrypt": "O chifrar tot", "save": "Enregistrament dels cambiaments" } }, "profile": { "profile name": "Nom del perfil", "confirm remove": "Lo perfil **{{profile}}** serà levat amb totas las donadas, inclús las nòtas, etiquetas e los blòts. Aquesta accion es irreversibla !", "type name": "Marcatz lo nom del perfil" }, "files": { "file-url": "URL d'un fichièr o d'un imatge", "attach": "Ligar coma un fichièr", "attachLink": "Ligar coma un ligam", "attachImage": "Ligar coma un imatge" }, "notes": { "confirm trash": "La nòta **{{title}}** serà desplaçada a l'escobilhièr.", "confirm remove": "La nòta **{{title}}** serà levada **per totjorn** !", "create and attach": "Crear una nòta novèla e ligar sos ligams", "create": "Crear una nòta novèla", "hyperlink-dialog": "Títol d'una nòta o una URL" }, "notebooks": { "select": "Seleccionatz un blòt", "add": "Ajustar un blòt novèl", "edit": "Modificar un blòt", "name": "Mercés de donar un nom al blòt", "confirm remove": "Lo blòt **{{name}}** serà levat **per totjorn** !", "remove with notes": "Òc-ben, levatz-lo amb las nòtas ligadas", "remove": "Òc-ben, levatz-lo" }, "tags": { "name": "Lo nom de l'etiqueta es requesit", "add": "Ajustar una novèla etiqueta", "edit": "Modificar una etiqueta", "confirm remove": "L'etiqueta **{{name}}** serà levada **per totjorn** !" }, "dropbox": { "auth confirm": "Seretz menat a la pagina d'autorizacion de **Dropbox**.\r> Mercés de far clic sul boton **OK**.", "auth title": "Dropbox auth", "api info 1": "Podètz aver vòstra pròpria clau d'API", "api info 2": "Quand creatz una novèla app sul site Dropbox's Developer vos cal gardar al cap aquò : ", "api info li 1": "Tip d'API deu èsser **Dropbox API**", "api info li 2": "Tip d'accès deu èsser **App Folder**" }, "help": { "firststart title": "Benvengut a Laverna", "firststart import": "S'avètz ja utilizat Laverna, podètz importar vòstres paramètres d'abans en clicant sul boton 'importar' çaijós.", "firststart next": "S'avètz pas jamai utilizat Laverna, clicatz sul boton 'seguent' per començar lo processús d'installacion.", "firststart encryption": "Se volètz utilizar lo chiframent, mercés de fornir un senhal de chiframent.", "firststart sync": "Ja que gardem pas cap de donadas sus nòstres servidors, vos cal activar la sincronizacion amb un de los adaptadors per poder veire vòstras nòtas en d'aparelhs mai.", "firststart backup": "Tot es gaireben prèst. Podètz telecargar vòstra salvagarda de paramètres e anar a la darrièra etapa." } } ================================================ FILE: app/locales/pl/translation.json ================================================ { "Search": "Szukaj...", "All notes": "Notatki", "Favourites": "Oznaczone", "Favorite": "Oznaczone", "Trash": "Kosz", "Open tasks": "Otwarte zadania", "Notebooks": "Notesy", "Settings": "Ustawienia", "About": "Laverna - informacje", "Save": "Zapisz", "Save & Exit": "Zapisz i wyjdź", "Cancel": "Anuluj", "Full screen": "Pełen ekran", "Preview": "Podgląd", "Normal": "Standardowy", "Select notebook": "Wybierz notes", "Title": "Tytuł", "Submit": "Zapisz zmiany", "Tags": "Etykiety", "Tag": "Etykieta", "Parent": "Nadrzędny", "Root": "Root", "Notebooks & tags": "Notesy", "Notebook": "Notes", "Restore": "Przywróć", "Delete": "Usuń", "New tag": "Nowa etykieta", "Edit": "Edytuj", "Remove": "Usuń", "Forever": "Usuń", "No": "Nie", "Yes": "Tak", "Basic": "Podstawowe", "Cloud storage": "Synchronizacja", "Notes per page": "Notatki na stronę", "Sort notebooks": "Sortowanie notesów", "Name": "Nazwa", "Created": "Data utworzenia", "Default edit mode": "Domyślny tryb edycji", "Fullscreen with preview": "Pełen ekran z podglądem", "Use encryption": "Szyfrowanie", "Encryption parameters": "Parametry szyfrowania", "Encryption Password": "Hasło", "Salt": "Salt", "Random": "Losowy", "Key size": "Długość klucza", "Strengthen by a factor of": "Wzmocnij o krotność", "Authentication strength": "Siła uwierzytelniania", "Unlock": "Odblokuj", "Your new encryption password": "Podaj nowe hasło", "Your old encryption password": "Podaj stare hasło", "Show sidebar": "Pokaż pasek boczny", "Previous": "Wstecz", "Next": "Dalej", "Navigation": "Nawigacja", "navigateTop": "Góra", "navigateBottom": "Dół", "Jump": "Przejdź do", "jumpInbox": "Wszystkich notatek", "jumpNotebook": "Notesów", "jumpFavorite": "Oznaczonych notatek", "jumpRemoved": "Kosza", "jumpOpenTasks": "Otwartych zadań", "Actions": "Akcje", "actionsEdit": "Edytuj", "actionsOpen": "Otwórz", "actionsRemove": "Usuń", "actionsRotateStar": "Oznacz notatkę", "App": "Aplikacja", "appCreateNote": "Stwórz nową notatkę", "appSearch": "Szukaj notatki", "appKeyboardHelp": "Szybka pomoc", "Change keybindings": "Zmień skróty klawiszowe", "Donate": "Wspomóż", "Github page": "Strona Github", "Report bugs and issues here": "Zgłaszanie błędów i sugestii", "Report bugs through email": "Zgłaszanie błędów przez mail", "Credits": "Podziękowania", "List of contributors": "Lista współtwórców", "List of all used libraries": "Lista użytych bibliotek", "Are you sure?": "Jesteś pewien?", "You have unsaved changes": "Niezapisane zmiany zostaną utracone.", "Dropbox API key": "Klucz API Dropbox", "Required": "Wymagane", "Optional": "Opcjonalne", "Language": "Język", "Action": "Akcja", "Select": "Wybierz", "General": "Podstawowe", "Encryption": "Szyfrowanie", "Keybindings": "Skróty klawiszowe", "Sync": "Synchronizacja", "Profiles": "Profile", "Import": "Import", "Transfer data": "Import i Eksport", "Transfer settings": "Ustawienia", "Import settings": "Importuj ustawienia", "Export settings": "Eksportuj ustawienia", "Wrong format": "Niewłaściwy format", "useDefaultConfigs": "Użyj ustawień z profilu domyślnego", "File should be in json format": "Plik powinien być w formacie json", "Close": "Zamknij", "Hyperlink": "Hiperłącze", "Editor": "Edytor", "Preview": "Podgląd", "Download": "Pobierz", "Transfer everything": "Wszystko", "Find in page": "Znajdź na stronie", "Other": "Inne", "Default": "Domyślny", "Modules": "Moduły", "Import data": "Importuj dane", "Export data": "Eksportuj dane", "Enabled": "Włączony", "Disabled": "Wyłączony", "Untitled": "Bez nazwy", "Line of": "Linia {{currentLine}} z {{numberOfLines}}", "Drop files": "Przeciągnij tutaj plik, żeby przesłać", "Spaces per indent": "Liczba spacji wcięcia akapitu", "Sort notes": "Sortowanie notatek", "Updated date": "Dacie modyfikacji", "Created date": "Dacie utworzenia", "Text editor": "Edytor tekstowy", "Vim": "Vim", "Emacs": "Emacs", "Sublime": "Sublime", "encryption": { "provide password": "Proszę podać hasło", "change password": "Wprowadź nowe hasło", "wait": "Trwa szyfrowanie", "error": "Błąd szyfrowania", "errorConfirm": "Błąd odszyfrowywania.\r\r Jeżeli zmieniałeś ustawienia szyfrowania w innej przeglądarce, **zaktualizuj ustawienia** również w tej przeglądarce lub spróbuj zaimportować ustawienia.\r\r Jeżeli nie zmieniałeś niczego, **spróbuj zalogować się jeszcze raz**.", "errorConfirmSettings": "Zmień ustawienia szyfrowania", "errorConfirmAuth": "Spróbuj ponownie", "backup": { "title": "Kopia zapasowa", "content": "Przed kolejnym krokiem pobierz proszę kopię zapasową, zawiera ona odszyfrowaną wersję zmienianego profilu. Trzymaj w bezpiecznym miejscu.", "next": "Przejdź dalej bez pobierania kopii zapasowej" }, "state": { "decrypt": "Odszyfruj wszystko", "encrypt": "Zaszyfruj wszystko", "save": "Zapisz ustawienia" } }, "profile": { "profile name": "Nazwa profilu", "confirm remove": "Profil **{{profile}}** zostanie usunięty wraz ze wszystkimi powiązanymi danymi. Akcja jest nieodwracalna!", "type name": "Wpisz nazwę profilu" }, "files": { "file-url": "URL pliku lub obrazka", "attach": "Dołącz plik", "attachLink": "Dołącz hiperłącze", "attachImage": "Dołącz obrazek" }, "notes": { "confirm trash": "Notatka **{{title}}** zostanie przeniesiona do kosza", "confirm remove": "Notatka **{{title}}** zostanie **nieodwracalnie** usunięta!", "create and attach": "Stwórz notatkę i załącz jej adres", "create": "Stwórz nową notatkę", "hyperlink-dialog": "Tytuł notatki lub URL" }, "notebooks": { "select": "Wybierz notes", "add": "Stwórz notes", "edit": "Edytuj notes", "name": "Podaj nazwę notesu", "confirm remove": "Notes **{{name}}** zostanie **nieodwracalnie** usunięty!", "remove with notes": "Tak, usuń wraz z powiązanymi notatkami", "remove": "Tak, usuń" }, "tags": { "name": "Nazwa etykiety jest wymagana", "add": "Dodaj nową etykietę", "edit": "Edytuj etykietę", "confirm remove": "Etykieta **{{name}}** zostanie **nieodwracalnie** usunięta!" }, "dropbox": { "auth confirm": "Zostaniesz teraz przekierowany na stronę autoryzacyjną **Dropbox**.\r> Kliknij przycisk **OK**.", "auth title": "Dropbox - autoryzacja", "api info 1": "Możesz posiadać własny klucz API", "api info 2": "Kiedy tworzysz nową aplikacje na stronie Dropbox's Developer pamiętaj o:" , "api info li 1": "Typ API powinien być **Dropbox API**", "api info li 2": "Typ dostępu powinien być **App Folder**" }, "help": { "firststart title": "Witaj w Laverna", "firststart import": "Jeżeli chcesz zaimportować swoje ustawienia Laverna klikając przycisk 'Import'.", "firststart next": "Jeżeli to twój pierwszy raz z Laverna kliknij 'Dalej', aby rozpocząć proces instalacji.", "firststart encryption": "Jeżeli chcesz zaszyfrować swoje notesy podaj hasło.", "firststart sync": "Nie posiadamy własnego serwera synchronizacji notesów, ale możesz skorzystać z jednej z podanych usług.", "firststart backup": "Wszytko już prawie gotowe. Możesz teraz pobrać kopię zapasową ustawień i przejść do ostatniego kroku." } } ================================================ FILE: app/locales/pt/translation.json ================================================ { "en" : "Inglês", "ru" : "Russo", "nl" : "Holandês", "fr" : "Francês", "pt" : "Português", "pt_br" : "Português (Brasil) ", "Search": "Busca", "All notes": "Todas as Anotações", "Favourites": "Favorictos", "Favorite": "Favoricto", "Trash": "Balde do lixo", "Notebooks": "Blocos de Anoctações", "Settings": "Definições", "About": "Acerca de", "Save": "Salvar", "Save & Exit": "Salvar e Sair", "Cancel": "Cancelar", "Full screen": "Ecrã inteiro", "Preview": "Pré-visualizar", "Normal": "Normal", "Select notebook": "Selecionar bloco de anotações", "Title": "Título", "Submit": "Submeter", "Tags": "Marcações", "Tag": "Marcação", "Parent": "Pai", "Root": "Raíz", "Notebooks & tags": "Blocos de Anotações & marcações", "Notebook": "Bloco de Anotações", "Restore": "Restaurar", "Delete": "Eliminar", "New tag": "Nova marcação", "Edit": "Editar", "Remove": "Remover", "Forever": "Para sempre", "No": "Não", "Yes": "Sim", "Basic": "Básico", "Cloud storage": "Armazenamento na nuvem", "Notes per page": "Anotações por página", "Default edit mode": "Modo de edição padrão", "Fullscreen with preview": "Ecrã cheio com pré-visualização", "Use encryption": "Usar criptografia", "Encryption parameters": "Parâmetros de criptografia", "Encryption Password": "Palavra-passe da criptografia", "Salt": "Sal", "Random": "Aleatório", "Key size": "Tamanho da chave", "Strengthen by a factor of": "Fator de reforço", "Authentication strength": "Força da autenticação", "Unlock": "Desbloquear", "Your new encryption password": "Sua nova palavra-passe de criptografia", "Your old encryption password": "Sua antiga palavra-passe de criptografia", "Please wait until the encryption will be completed": "Por favor, aguarde até que a criptografia seja concluída", "Shortcuts": "Atalhos", "Newer": "Anterior", "Older": "Próximo", "Navigation": "Navegação", "navigateTop": "Topo", "navigateBottom": "Rodapé", "Jump": "Pular", "jumpInbox": "Ir para a caixa de entrada", "jumpNotebook": "Ir para a lista de blocos de notas", "jumpFavorite": "Ir para as notas favoritas", "jumpRemoved": "Ir para as notas removidas", "Actions": "Acções", "actionsOpen": "Abrir", "actionsRotateStar": "Remover dos favoritos", "App": "Aplicação", "appCreateNote": "Criar nova anotação", "appSearch": "Procurar anotação", "appKeyboardHelp": "Ajuda com o teclado" } ================================================ FILE: app/locales/pt_br/translation.json ================================================ { "en" : "Inglês", "ru" : "Russo", "nl" : "Holandês", "fr" : "Francês", "pt_br" : "Português (Brasil) ", "Search": "Pesquisar", "All notes": "Todas as Notas", "Favourites": "Favoritos", "Favorite": "Favorito", "Trash": "Lixeira", "Notebooks": "Blocos de Notas", "Settings": "Configurações", "About": "Sobre", "Save": "Salvar", "Save & Exit": "Salvar e Sair", "Cancel": "Cancelar", "Full screen": "Tela cheia", "Preview": "Pré-visualizar", "Normal": "Normal", "Select notebook": "Selecionar bloco de notas", "Title": "Título", "Submit": "Enviar", "Tags": "Tags", "Tag": "Tag", "Parent": "Pai", "Root": "Raiz", "Notebooks & tags": "Blocos de Notas & tags", "Notebook": "Bloco de Notas", "Restore": "Restaurar", "Delete": "Excluir", "New tag": "Nova tag", "Edit": "Editar", "Remove": "Remover", "Forever": "Para sempre", "No": "Não", "Yes": "Sim", "Basic": "Básico", "Cloud storage": "Armazenamento na nuvem", "Notes per page": "Notas por página", "Default edit mode": "Modo de edição padrão", "Fullscreen with preview": "Tela cheia com pré-visualização", "Use encryption": "Usar criptografia", "Encryption parameters": "Parâmetros de criptografia", "Encryption Password": "Senha da criptografia", "Salt": "Sal", "Random": "Aleatório", "Key size": "Tamanho da chave", "Strengthen by a factor of": "Fator de reforço", "Authentication strength": "Força da autenticação", "Unlock": "Desbloquear", "Your new encryption password": "Sua nova senha de criptografia", "Your old encryption password": "Sua antiga senha de criptografia", "Please wait until the encryption will be completed": "Por favor, aguarde até que a criptografia seja concluída", "Shortcuts": "Atalhos", "Newer": "Anterior", "Older": "Próximo", "Navigation": "Navegação", "navigateTop": "Topo", "navigateBottom": "Rodapé", "Jump": "Pular", "jumpInbox": "Ir para a caixa de entrada", "jumpNotebook": "Ir para a lista de blocos de notas", "jumpFavorite": "Ir para as notas favoritas", "jumpRemoved": "Ir para as notas removidas", "Actions": "Ações", "actionsOpen": "Abrir", "actionsRotateStar": "Remover dos favoritos", "App": "App", "appCreateNote": "Criar nova nota", "appSearch": "Procurar nota", "appKeyboardHelp": "Ajuda com o teclado" } ================================================ FILE: app/locales/ru/translation.json ================================================ { "en" : "Английский", "ru" : "Русский", "nl" : "Голландский", "fr" : "Французский", "pt_br" : "Бразильский португальский", "eo": "Эсперанто", "es": "Испанский", "de": "Немецкий", "de_ch": "Швейцарский немецкий", "se": "Шведский", "el": "Греческий", "nb": "Норвежский (Букмол)", "nn": "Норвежский (Нюнорск)", "bs_ba": "Боснийский", "hi_in": "Хинди", "mr_in": "Маратхи", "zh_cn": "Упрощенный китайский", "Search": "Поиск", "All notes": "Все заметки", "Favourites": "Избранное", "Favorite": "Избранное", "Trash": "Корзина", "Notebooks": "Блокноты", "Settings": "Настройки", "About": "О программе", "Save": "Сохранить", "Save & Exit": "Сохранить и выйти", "Cancel": "Отмена", "Fullscreen": "Полный экран", "Full screen": "Полный экран", "Preview": "Предпросмотр", "Normal": "Нормальный", "Select notebook": "Выбрать блокнот", "Title": "Название", "Submit": "Отправить", "Tags": "Теги", "Tag": "Тег", "Parent": "Родительский", "Root": "Корень", "Notebooks & Tags": "Блокноты и теги", "Notebook": "Блокнот", "Restore": "Восстановить", "Delete": "Удалить", "New tag": "Новый тег", "Edit": "Изменить", "Remove": "Убрать", "Forever": "Навсегда", "No": "Нет", "Yes": "Да", "Basic": "Основные", "Cloud storage": "Облачное хранилище", "Notes per page": "Заметок на странице", "Sort notebooks": "Сортировать блокноты", "Name": "По имени", "Created": "По дате создания", "Default edit mode": "Режим по умолчанию", "Fullscreen with preview": "Полный экран с предпросмотром", "Use encryption": "Использовать шифрование", "Encryption parameters": "Параметры шифрования", "Encryption Password": "Пароль шифрования", "Salt": "Соль", "Random": "Случайно", "Key size": "Длина ключа", "Strengthen by a factor of": "Коэффициент усиления", "Authentication strength": "Надежность аутентификации", "Unlock": "Разблокировать", "Your new encryption password": "Новый пароль шифрования", "Your old encryption password": "Старый пароль шифрования", "Please wait until the encryption will be completed": "Подождите завершения шифрования", "Shortcuts": "Горячие клавиши", "Newer": "Назад", "Older": "Вперед", "Navigation": "Навигация", "navigateTop": "Вверх", "navigateBottom": "Вниз", "Jump": "Переходы", "jumpInbox": "Перейти ко всем заметкам", "jumpNotebook": "К списку блокнотов", "jumpFavorite": "К избранным заметкам", "jumpRemoved": "К убранным заметкам", "Actions": "Действия", "actionsEdit": "Изменить", "actionsOpen": "Открыть", "actionsRemove": "Убрать", "actionsRotateStar": "Поставить/убрать звезду", "App": "Приложение", "appCreateNote": "Создать новую заметку", "appSearch": "Искать заметку", "appKeyboardHelp": "Помощь по горячим клавишам", "Other": "Другое", "Profiles": "Профили", "Import settings": "Импортировать настройки", "Export settings": "Экспортировать настройки", "Action": "Действие", "Trashed": "Корзина", "New notebook": "Новый блокнот", "Are you sure? You have unsaved changes": "Вы уверены? У вас есть несохраненные изменения", "profile": { "profile name": "Имя профиля" } } ================================================ FILE: app/locales/se/translation.json ================================================ { "en" : "Engelska", "ru" : "Ryska", "nl" : "Holländska", "fr" : "Franska", "pt_br" : "Portugisiska (Brasilien)", "eo": "Esperanto", "es": "Spanska", "de": "Tyska", "se": "Svenska", "nb" : "Norska (Bokmål)", "nn" : "Norska (Nynorsk)", "Search": "Sök", "All notes": "Alla anteckningar", "Favourites": "Favoriter", "Favorite": "Favoriter", "Trash": "Papperskorg", "Notebooks": "Anteckningsböcker", "Settings": "Inställningar", "About": "Om Laverna", "Save": "Spara", "Save & Exit": "Spara och avsluta", "Cancel": "Avbryt", "Full screen": "Helskärm", "Preview": "Förhandsvisning", "Normal": "Normal", "Select notebook": "Välj anteckningsbok", "Title": "Rubrik", "Submit": "Tillämpa", "Tags": "Taggar", "Tag": "Tagg", "Parent": "Förälder", "Root": "Rot", "Notebooks & tags": "Anteckningsböcker & Taggar", "Notebook": "Anteckningsbok", "Restore": "Återskapa", "Delete": "Radera", "New tag": "Ny tagg", "Edit": "Redigera", "Remove": "Ta bort", "Forever": "För alltid", "No": "Nej", "Yes": "Ja", "Basic": "Grundläggande", "Cloud storage": "Molnlagring", "Notes per page": "Anteckningar per sida", "Sort notebooks": "Sortera anteckningar", "Name": "Namn", "Created": "Skapad", "Default edit mode": "Standardredigeringsläge", "Fullscreen with preview": "Helskärm med förhandsvisning", "Use encryption": "Använd kryptering", "Encryption parameters": "Krypteringsparametrar", "Encryption Password": "Lösenord", "Salt": "Salt", "Random": "Slumpmässig", "Key size": "Nyckelstorlek", "Strengthen by a factor of": "Förstärk med en faktor av", "Authentication strength": "Autentiseringsstyrka", "Unlock": "Lås upp", "Your new encryption password": "Ditt nya lösenord", "Your old encryption password": "Ditt gamla lösenord", "Please wait until the encryption will be completed": "Var god vänta tills kryptering är slutförd", "Shortcuts": "Genvägar", "Previous": "Föregående", "Next": "Nästa", "Navigation": "Navigation", "navigateTop": "Till toppen", "navigateBottom": "Till botten", "Jump": "Håppa", "jumpInbox": "Gå till inkorg", "jumpNotebook": "Gå till Anteckningsböker", "jumpFavorite": "Gå till Favoriter", "jumpRemoved": "Gå till Papperskorgen", "Actions": "Handlingar", "actionsEdit": "Redigera", "actionsOpen": "Öppna", "actionsRemove": "Radera", "actionsRotateStar": "Lägg till/ta bort favorit", "App": "Applikation", "appCreateNote": "Skapa ny anteckning", "appSearch": "Sök i anteckningar", "appKeyboardHelp": "Skrivbordshjälp" } ================================================ FILE: app/locales/sq/translation.json ================================================ { "Search": "Kërkoni", "All notes": "Krejt shënimet", "Favourites": "Të parapëlqyera", "Favorite": "Të parapëlqyera", "Trash": "Hedhurina", "Open tasks": "Punë të hapura", "Notebooks": "Blloqe", "Settings": "Rregullime", "About": "Mbi", "Save": "Ruaje", "Save & Exit": "Ruaje & Dil", "Cancel": "Anuloje", "Full screen": "Sa krejt ekrani", "Preview": "Paraparje", "Normal": "Normal", "Select notebook": "Përzgjidhni bllok", "Title": "Titull", "Submit": "Parashtroje", "Tags": "Etiketa", "Tag": "Etiketë", "Parent": "Mëmë", "Root": "Rrënjë", "Notebooks & tags": "Blloqe & etiketa", "Notebook": "Bllok", "Restore": "Riktheje", "Delete": "Fshije", "New tag": "Etiketë e re", "Edit": "Përpunojeni", "Remove": "Hiqe", "Forever": "Përgjithmonë", "No": "Jo", "Yes": "Po", "Basic": "Bazë", "Cloud storage": "Depozitë në re", "Notes per page": "Shënime për faqe", "Sort notebooks": "Renditni blloqet", "Name": "Emër", "Created": "Krijuar më", "Default edit mode": "Mënyrë parazgjedhje për përpunime", "Fullscreen with preview": "Sa krejt ekrani, me paraparje", "Use encryption": "Përdor fshehtëzim", "Encryption parameters": "Parametra fshehtëzimi", "Encryption Password": "Fjalëkalim Fshehtëzimi", "Salt": "Salt", "Random": "Kuturu", "Key size": "Madhësi kyçi", "Strengthen by a factor of": "Fuqizoje me", "Authentication strength": "Fuqi mirëfilltësimi", "Unlock": "Shkyçe", "Your new encryption password": "Fjalëkalimi juaj i ri i fshehtëzimit", "Your old encryption password": "Fjalëkalimi juaj i vjetër i fshehtëzimit", "Show sidebar": "Shfaqni anështyllën", "Previous": "I mëparshmi", "Next": "Pasuesi", "Navigation": "Lëvizje", "navigateTop": "Sipër", "navigateBottom": "Poshtë", "Jump": "Hidhu", "jumpInbox": "Shko te të marrët", "jumpNotebook": "Shko te listë blloqesh", "jumpFavorite": "Shko te shënime të parapëlqyera", "jumpRemoved": "Shko te shënime të hequra", "jumpOpenTasks": "Shko te shënime me punë të pambaruara", "Actions": "Veprime", "actionsEdit": "Përpunoni", "actionsOpen": "Hape", "actionsRemove": "Hiqe", "actionsRotateStar": "Rrotullo Yllin", "App": "Aplikacion", "appCreateNote": "Krijoni shënim të ri", "appSearch": "Kërkoni shënim", "appKeyboardHelp": "Ndihmë për tastierën", "Change keybindings": "Ndryshoni rregullime tastesh", "Donate": "Dhuroni", "Github page": "Faqja Github", "Report bugs and issues here": "Njoftoni këtu të meta dhe probleme", "Report bugs through email": "Njoftoni të meta me email", "Credits": "Falënderime", "List of contributors": "Listë kontribuesish", "List of all used libraries": "Listë e krejt librarive të përdorura", "Are you sure?": "Jeni i sigurt?", "You have unsaved changes": "Keni ndryshime të paruajtura.", "Dropbox API key": "Kyç API Dropbox-i", "Required": "E domosdoshme", "Optional": "Opsionale", "Language": "Gjuhë", "Action": "Veprim", "Select": "Përzgjidhni", "General": "Të përgjithshme", "Encryption": "Fshehtëzim", "Keybindings": "Shkurtore tastiere", "Sync": "Njëkohësim", "Profiles": "Profile", "Import": "Importim", "Transfer data": "Import & eksport", "Import settings": "Rregullime importimesh", "Export settings": "Rregullime eksportimesh", "Wrong format": "Format i gabuar", "useDefaultConfigs": "Përdor rregullime nga profili parazgjedhje", "File should be in json format": "Kartela duhet të jetë në format json", "Close": "Mbylle", "Hyperlink": "Tejlidhje", "Editor": "Përpunues", "Preview": "Paraparje", "Download": "Shkarkim", "Transfer everything": "Gjithçka", "encryption": { "wait": "Ju lutemi, pritni deri sa të plotësohet fshehtëzimi", "error": "Gabim fshehtëzimi", "errorConfirm": "Gabim gjatë shfshehtëzimit të të dhënave.\r\r Nëse i ndryshuat rregullimet mbi fshehtëzimin në një tjetër shfletues, **përditësojini rregullimet tuaja** edhe në këtë shfletues. Ose provoni t’i importoni rregullimet.\r\r Dhe, nëse s’keni ndryshuar gjë, **provoni të bëni hyrjen** sërish.", "errorConfirmSettings": "Ndryshoni rregullime fshehtëzimi", "errorConfirmAuth": "Riprovoni sërish", "backup": { "title": "Kopjeruani të Dhëna", "content": "Ju lutemi, përpara se të vazhdoni me hapin pasues, shkarkoni kartelën tuaj kopjeruajtje. Ajo përmban të dhëna të shfshehtëzuara profilesh të mëparshëm të ndryshuar. Ruajeni në një vend të sigurt.", "next": "Vazhdoni pa e shkarkuar kartelën kopjeruajtje" }, "state": { "decrypt": "Po shfshehtëzohet gjithçka", "encrypt": "Po fshehtëzohet gjithçka", "save": "Po ruhen ndryshimet" } }, "profile": { "confirm remove": "Profili **{{profile}}** do të hiqet me gjithë të dhënat, përfshi shënime, etiketa dhe blloqe shënimesh. Ky veprim është i pakthyeshëm!", "type name": "Shtypni emër profili" }, "files": { "file-url": "URL kartele ose pamjeje", "attach": "Bashkëngjitni një kartelë", "attachLink": "Bashkëngjiteni si lidhje", "attachImage": "Bashkëngjiteni si figurë" }, "notes": { "confirm trash": "Shënimi **{{title}}** do të kalohet te hedhurinat.", "confirm remove": "Shënimi **{{title}}** do të hiqet **përgjithmonë**!", "create and attach": "Krijoni një shënim të ri dhe bashkëngjitni lidhjen e tij", "create": "Krijoni një shënim të ri", "hyperlink-dialog": "Titulli i një shënimi ose URL-je" }, "notebooks": { "select": "Përzgjidhni një Bllok Shënimesh", "add": "Shtoni një bllok të ri shënimesh", "edit": "Përpunoni një bllok shënimesh", "name": "Ju lutemi, jepni emrin e bllokut të shënimeve", "confirm remove": "Blloku i shënimeve **{{name}}** do të hiqet **përgjithmonë**!", "remove with notes": "Po, hiqe me gjithë shënimet e bashkëngjitura", "remove": "Po, hiqe" }, "tags": { "name": "Emri i etiketës është i domosdoshëm", "add": "Shtoni një etiketë të re", "edit": "Përpunoni etiketë", "confirm remove": "Etiketa **{{name}}** do të hiqet **përgjithmonë**!" }, "dropbox": { "auth confirm": "Tani do të ridrejtoheni për te faqja e autorizimeve **Dropbox**.\r> Ju lutemi, klikoni mbi butonin **OK**.", "auth title": "Autorizim Dropbox", "api info 1": "Mund të keni kyçin tuaj API", "api info 2": "Kur krijoni një aplikacion të ri te sajti Dropbox's Developer duhet të keni parasysh se:", "api info li 1": "Lloji i aplikacionit duhet të jetë aplikacion API Dropbox", "api info li 2": "Llojet e të dhënave duhet të jenë Kartela dhe depo të dhënash" }, "help": { "firststart title": "Mirë se vini te Laverna", "firststart import": "Nëse e keni përdorur Laverna-n më parë, mund të importoni rregullimet tuaja të vjetra duke klikuar mbi butonin 'Importo' më poshtë.", "firststart next": "Nëse s’e keni përdorur Laverna-n më parë, klikoni mbi butonin 'pasuesi' që të fillojë procesi i instalimit.", "firststart encryption": "Nëse dëshironi të përdorni fshehtëzim, ju lutemi, jepni fjalëkalim fshehtëzimesh.", "firststart sync": "Meqë nuk depozitojmë ndonjë të dhënë në shërbyesit tanë, lypset të aktivizoni njëkohësimin me një nga përshtatësit, që të jeni në gjendje të shihni shënimet tuaja në pajisje të tjera.", "firststart backup": "Gjithçka thuajse është gati. Mund të shkarkoni kopjeruajtjen e rregullimeve tuaja dhe të vazhdoni me hapin e fundit." } } ================================================ FILE: app/locales/tr/translation.json ================================================ { "Search": "Ara", "All notes": "Bütün notlar", "Favourites": "Sık kullanılanlar", "Favorite": "Sık kullanılan", "Trash": "Çöp kutusu", "Open tasks": "Yapacaklarım", "Notebooks": "Not defteri", "Settings": "Kaydet", "About": "Hakkında", "Save": "Save", "Save & Exit": "Kaydet & Çık", "Cancel": "İptal", "Full screen": "Tam Ekran", "Normal": "Normal", "Select notebook": "Not defteri seç", "Title": "Başlık", "Submit": "Gönder", "Tags": "Etiketler", "Tag": "Etiket", "Parent": "Üst öğe", "Root": "Kök", "Notebooks & Tags": "Not defterleri & Etiketler", "Notebook": "Not defteri", "Restore": "Geri yükle", "Delete": "Sil", "New tag": "Yeni etiket", "Edit": "Düzenle", "Remove": "Kaldır", "Forever": "Kalıcı sil", "No": "Hayır", "Yes": "Evet", "Basic": "Temel", "Cloud storage": "Bulut depolama", "Notes per page": "Sayfa başına not", "Sort notebooks": "Not def. sıralama", "Name": "Ad", "Created": "Oluşturulma", "Default edit mode": "Varsayılan düzenleme modu", "Fullscreen with preview": "Önizlemeli tam ekran", "Use encryption": "Şifreleme kullan", "Encryption parameters": "Şifreleme parametreleri", "Encryption Password": "Parola", "Salt": "Salt", "Random": "Rasgele", "Key size": "Anahtar büyüklüğü", "Strengthen by a factor of": "Sağlamlık faktörü", "Authentication strength": "Kimlik doğrulama dayanıklılığı", "Unlock": "Kilidi aç", "Your new encryption password": "Yeni parolan", "Your old encryption password": "Eski parolan", "Show sidebar": "Kenar çubuğu göster", "Previous": "Önceki", "Next": "Sonraki", "Navigation": "Navigasyon", "navigateTop": "Üst", "navigateBottom": "Alt", "Jump": "Atla", "jumpInbox": "Gelen kutusuna git", "jumpNotebook": "Not listesine git", "jumpFavorite": "Sık kullanılanlara git", "jumpRemoved": "Silinen notlara git", "jumpOpenTasks": "Yapacak olan notlara git", "Actions": "Eylemler", "actionsEdit": "Düzenle", "actionsOpen": "Aç", "actionsRemove": "Kaldır", "actionsRotateStar": "Yıldız", "App": "Uygulama", "appCreateNote": "Yeni not oluştur", "appSearch": "Not ara", "appKeyboardHelp": "Klavye yardım", "Change keybindings": "Kısayolları değiştir", "Donate": "Başış Yap", "Github page": "Github kaynak kod sayfası", "Report bugs and issues here": "Hata ve sorunları buraya bildir", "Report bugs through email": "Hataları email yolu ile bildir", "Credits": "Katkılar", "List of contributors": "Katkıda bulunanlar", "List of all used libraries": "Kullanılan kütüphaneler", "Are you sure?": "Emin misin?", "You have unsaved changes": "Kaydedilmemiş değişikliklerin var", "Dropbox API key": "Dropbox API anahtarı", "Required": "Gerekli", "Optional": "İsteğe bağlı", "Language": "Dil", "Action": "Eylem", "Select": "Seç", "General": "Genel", "Encryption": "Şifre", "Keybindings": "Kısayollar", "Sync": "Senkronize", "Profiles": "Profiller", "Import": "İçe aktarım", "Import & export": "İçe & Dışa Aktarım", "Import settings": "İçe aktarıma ayarları", "Export settings": "Dışa aktarım ayarları", "Wrong format": "Yanlış biçim", "useDefaultConfigs": "Varsayılar profil ayarları kullan", "File should be in json format": "Dosya json formatında olmalı", "Close": "Kapat", "Hyperlink": "Köprü", "Editor": "Editör", "Preview": "Önizleme", "Download": "İndir", "Find in page": "Sayfada ara", "Other": "Diğer", "Default": "Varsayılan", "Modules": "Modüller", "Import data": "İçeri veri aktar", "Export data": "Dışarı veri aktar", "Everything": "Herşey", "Enabled": "Etkin", "Disabled": "Devredışı", "Untitled": "Başlıksız", "Line of": "Satır {{mevcutSatır}} of {{SatırSayısı}}", "encryption": { "provide password": "Lütfen şifrenizi doğrulayınız", "change password": "Şifrenizi değiştirmek için buraya giriniz", "wait": "Şifreleme bitene kadar lütfen bekleyiniz", "error": "Encryption error", "errorConfirm": "Şifreleme anında hata gerçekleşti.\r\r Eğer şifre ayarlarınızı başka bir tarayıcıda değiştirdiyseniz, **ayarlarınızı** bu tarayıcıda da değiştiriniz. Veya içeri veri aktarımı deneyiniz .\r\r Hala bir sonuç alamıyorsanız, **tekrar giriş** yapmayı deneyiniz.", "errorConfirmSettings": "Şifreleme ayarlarını değiştirin", "errorConfirmAuth": "Tekrar deneyiniz", "backup": { "title": "Veri yedekle", "content": "Lütfen, bir değişiklik yapmadan önce, yedek dosyanızı indirin. Yedek dosyanız bir önceki profil verilerinizi barındırır ve güvenli bir yerde tutar.", "next": "Yedek almadan işleme devam et" }, "state": { "decrypt": "Herşeyi çöz", "encrypt": "Herşeyi şifrele", "save": "Değişiklikler kaydediliyor." } }, "profile": { "profile name": "Profil ismi", "confirm remove": "Profiliniz **{{profiliniz}}** bütün kayıtıları ile beraber silenecektir, notlarınız, etiketleriniz, not defterleriniz ile beraber. Bu eylem tersine çevrilemez!", "type name": "Profil ismi tipi" }, "files": { "file-url": "Dosya veya resim URL'si", "attach": "Dosya ekle", "attachLink": "Link ekle", "attachImage": "Resim ekle" }, "notes": { "confirm trash": "Bu not **{{başlık}}** çöpe taşınacaktır.", "confirm remove": "Bu not **{{başlık}}** **geri dönmemek üzere** silenecektir!", "create and attach": "Yeni bir not ekle ve link ekle", "create": "Yeni not ekle", "hyperlink-dialog": "Notun başlığı yada URL" }, "notebooks": { "select": "Not defteri seç", "add": "Yeni not defteri ekle", "edit": "Not defterini düzenle", "name": "Lütfen not defterinin adını tanımlayınız", "confirm remove": "Not defteri **{{adı}}** **geri dönmemek üzere** silinecektir!", "remove with notes": "Evet, ekli dosyalar ile beraber kaldır", "remove": "Evet, kaldır" }, "tags": { "name": "Etiket ismi gerekli", "add": "Yeni etiket ekle", "edit": "Etiket düzenle", "confirm remove": "Etiket**{{adı}}** **geri dönmemek üzere** silinecektir!" }, "dropbox": { "auth confirm": "**Dropbox** yetkilendirme sayfasına yönlendiriliyorsunuz.\r> Lütfen **tamama** tıklayınız", "auth title": "Dropbox yetkilendirme", "api info 1": "Kendi API anahtarın olabilir", "api info 2": "Dropbox's Developer sayfasında yeni bir uygulama başlattığında bunları aklında tutsan iyi olur:", "api info li 1": "API tipin **Dropbox API** olmalı", "api info li 2": "Erişim tipi **Uygulama Dosyası** olmalı" }, "help": { "firststart title": "Laverna'ya hoş geldin!", "firststart import": "Eğer daha önce Laverna kullanıysan, aşağıdaki 'veri aktara' tıklayarak önceki ayarlarını kullanabilirsin.", "firststart next": "Eğer daha önce Laverna kullanıysan, 'ileri' butonuna tıklayarak yükleme işlemine bağlayabilirsin", "firststart encryption": "Eğer şifrelemeyi kullanmak istiyorsan, lütfen şifrenizi tanımlayınız.", "firststart sync": "Sunucumuzda veri depolamadığımızdan beri, notlarını diğer cihazlarda görüntülemek için senkronizasyonu adaptörlerden biriyle çalışır kılman gerekiyor.", "firststart backup": "Herşey neredeyse hazır. Ayarlarının yedeğini indirdikten sonra sonra adıma geçebilirsin" } } ================================================ FILE: app/locales/zh_cn/translation.json ================================================ { "en" : "英语", "ru" : "俄语", "nl" : "荷兰语", "fr" : "法语", "pt_br" : "巴西葡萄牙语", "eo": "世界语", "es": "西班牙语", "de": "德语", "se": "瑞典语", "el": "希腊语", "nb": "挪威语 (波克默尔语)", "nn": "挪威语 (尼诺斯克语)", "Search": "搜索", "All notes": "所有笔记", "Favourites": "收藏夹", "Favorite": "收藏", "Trash": "垃圾桶", "Open tasks": "待处理任务", "Notebooks": "所有笔记本", "Settings": "设置", "About": "关于", "Save": "保存", "Save & Exit": "保存退出", "Cancel": "取消", "Full screen": "全屏", "Preview": "预览", "Normal": "正常", "Select notebook": "选择笔记本", "Title": "标题", "Submit": "提交", "Tags": "所有标签", "Tag": "标签", "Parent": "父", "Root": "根", "Notebooks & tags": "笔记本和标签", "Notebook": "笔记本", "Restore": "重新保存", "Delete": "删除", "New tag": "新建标签", "Edit": "编辑", "Remove": "移除", "Forever": "永远", "No": "否", "Yes": "是", "Basic": "基本", "Cloud storage": "云存储", "Notes per page": "每页笔记数", "Sort notebooks": "笔记本排序", "Name": "名称", "Created": "创建者", "Default edit mode": "默认编辑模式", "Fullscreen with preview": "全屏预览", "Use encryption": "加密", "Encryption parameters": "加密参数", "Encryption Password": "加密密码", "Salt": "加盐", "Random": "随机", "Key size": "密钥长度", "Strengthen by a factor of": "加强元素", "Authentication strength": "验证强度", "Unlock": "解锁", "Your new encryption password": "新加密密码", "Your old encryption password": "旧加密密码", "Show sidebar": "显示侧边栏", "Shortcuts": "快捷方式", "Previous": "上一步", "Next": "下一步", "Navigation": "导航", "navigateTop": "顶部", "navigateBottom": "底部", "Jump": "跳转", "jumpInbox": "跳转到收件夹", "jumpNotebook": "跳转到笔记列表", "jumpFavorite": "跳转到收藏夹", "jumpRemoved": "跳转到已删除笔记列表", "jumpOpenTasks": "跳转到待处理任务笔记列表", "Actions": "动作", "actionsEdit": "编辑", "actionsOpen": "打开", "actionsRemove": "移除", "actionsRotateStar": "加入收藏", "App": "应用", "appCreateNote": "新建笔记", "appSearch": "搜索笔记", "appKeyboardHelp": "按键帮助", "Change keybindings": "修改按键绑定", "Donate": "捐助", "Github page": "Github page", "Report bugs and issues here": "Report bugs and issues here", "Report bugs through email": "通过邮件报告Bug", "Credits": "Credits", "List of contributors": "List of contributors", "List of all used libraries": "List of all used libraries", "Are you sure?": "你确定 ?", "You have unsaved changes": "你有未保存的修改.", "Dropbox API key": "Dropbox API key", "Required": "必须", "Optional": "可选", "Language": "语言", "Action": "动作", "Select": "选择", "General": "通用", "Encryption": "加密", "Keybindings": "按键绑定", "Sync": "同步", "Profiles": "配置文件", "Import": "导入", "Transfer data": "导入 & 导出", "Import settings": "导入设置", "Export settings": "导出设置", "Wrong format": "格式错误", "useDefaultConfigs": "使用默认设置", "File should be in json format": "文件应该是json格式", "Close": "关闭", "Hyperlink": "超链接", "Editor": "编辑器", "Preview": "预览", "Download": "下载", "Transfer everything": "所有内容", "Find in page": "Find in page", "Other": "其他", "Default": "默认", "Modules": "模块", "Import data": "导入数据", "Export data": "导出数据", "Enabled": "启用", "Disabled": "停用", "Untitled": "无标题", "Line of": "Line {{currentLine}} of {{numberOfLines}}", "encryption": { "provide password": "请提供你的密码", "change password": "输入新密码", "wait": "请等待加密完成", "error": "加密错误", "errorConfirm": "解密数据错误。\r\r 如果你在其他浏览器修改了加密设置, 请在次浏览器**更新设置**。或者试试导入设置。\r\r 如果你没有修改加密设置, 试试**重新登录**。", "errorConfirmSettings": "修改加密设置", "errorConfirmAuth": "重试一次", "backup": { "title": "备份数据", "content": "请在下一步之前,下载你的备份文件。它包括配置修改之前的未加密数据,请放在安全的地方。", "next": "处理,但不下载备份文件" }, "state": { "decrypt": "解密所有内容", "encrypt": "加密所有内容", "save": "保存修改" } }, "profile": { "profile name": "配置名", "confirm remove": "配置 **{{profile}}** 将会和数据一块删除, 包括笔记,标签以及笔记本。该操作不可恢复。", "type name": "输入配置名称" }, "files": { "file-url": "文件或图片URL", "attach": "添加文件", "attachLink": "添加为链接", "attachImage": "添加为图片" }, "notes": { "confirm trash": "笔记 **{{title}}** 将被移动到垃圾桶。", "confirm remove": "笔记 **{{title}}** 将会**彻底**删除 !", "create and attach": "创建新的笔记并且添加链接", "create": "创建新的笔记", "hyperlink-dialog": "笔记的名称或是URL" }, "notebooks": { "select": "选择笔记本", "add": "添加新的笔记本", "edit": "编辑笔记本", "name": "请输入笔记本名", "confirm remove": "笔记本 **{{name}}** 将会**彻底**删除!", "remove with notes": "是的,和笔记一块删除", "remove": "是的,删除" }, "tags": { "name": "需要标签名称", "add": "添加新的标签", "edit": "编辑标签", "confirm remove": "标签 **{{name}}** 将会**彻底**删除!" }, "dropbox": { "auth confirm": "现在你会被重定向到 **Dropbox** 授权页面。\r> 请点击 **OK** 按钮。", "auth title": "Dropbox 授权", "api info 1": "You can have your own API key on", "api info 2": "When you create a new app at Dropbox's Developer site you should keep in mind that:", "api info li 1": "Type of API should be **Dropbox API**", "api info li 2": "Type of access should be **App Folder**" }, "help": { "firststart title": "欢迎使用 Laverna", "firststart import": "如果你以前使用过 Laveran,你可以点击下面的 '导入' 按钮导入你以前的配置。", "firststart next": "如果你从未用过 Laverna, 点击 '下一步' 开始安装。", "firststart encryption": "如果你想使用加密,请输入密码。", "firststart sync": "由于我们不在我们的服务器存储任何数据, 你需要启用一个同步方式以便在其他设备上查看你的笔记。", "firststart backup": "几乎所有的东西都准备好了。 你可以下载配置备份并且进行最后一步啦!" } } ================================================ FILE: app/locales/zh_tw/translation.json ================================================ { "en" : "英語", "ru" : "俄語", "nl" : "荷蘭語", "fr" : "法語", "pt_br" : "巴西葡萄牙語", "eo": "世界語", "es": "西班牙語", "de": "德語", "se": "瑞典語", "el": "希臘語", "nb": "挪威語 (波克默爾語)", "nn": "挪威語 (尼諾斯克語)", "Search": "搜尋", "All notes": "所有筆記", "Favourites": "已加星號", "Favorite": "星號", "Trash": "垃圾桶", "Open tasks": "待處理任務", "Notebooks": "所有筆記本", "Settings": "偏好設定", "About": "關於", "Save": "儲存", "Save & Exit": "儲存並退出", "Cancel": "取消", "Fullscreen": "全螢幕編輯", "Full screen": "全螢幕編輯", "Preview": "編輯及預覽", "Normal": "一般編輯", "Select notebook": "選擇筆記本", "Title": "標題", "Submit": "提交", "Tags": "所有標籤", "Tag": "標籤", "Parent": "上一層", "Root": "根目錄", "Notebooks & tags": "筆記本和標籤", "Notebook": "筆記本", "Restore": "還原", "Delete": "刪除", "New tag": "建立標籤", "Edit": "編輯", "Remove": "移除", "Forever": "永遠", "No": "否", "Yes": "是", "Basic": "基本", "Cloud storage": "雲端儲存", "Notes per page": "每頁筆記數", "Sort notebooks": "筆記本排序", "Name": "名稱", "Created": "建立者", "Default edit mode": "預設編輯模式", "Fullscreen with preview": "編輯及預覽", "Use encryption": "加密", "Encryption parameters": "參數", "Encryption Password": "密碼", "Salt": "加鹽", "Random": "隨機", "Key size": "金鑰長度", "Strengthen by a factor of": "加強元素", "Authentication strength": "驗證長度", "Unlock": "解鎖", "Your new encryption password": "新密碼", "Your old encryption password": "舊密碼", "Show sidebar": "顯示側邊欄", "Shortcuts": "熱鍵", "Previous": "上一步", "Next": "下一步", "Navigation": "瀏覽", "navigateTop": "頂部", "navigateBottom": "底部", "Jump": "切換", "jumpInbox": "切換到收件夾", "jumpNotebook": "切換到筆記列表", "jumpFavorite": "切換到已加星號列表", "jumpRemoved": "切換到已刪除筆記列表", "jumpOpenTasks": "切換到待處理任務筆記列表", "Actions": "動作", "actionsEdit": "編輯", "actionsOpen": "打開", "actionsRemove": "移除", "actionsRotateStar": "加入星號", "App": "應用", "appCreateNote": "新增筆記", "appSearch": "搜尋筆記", "appKeyboardHelp": "查看熱鍵", "Change keybindings": "修改熱鍵綁定", "Donate": "贊助", "Github page": "Github page", "Report bugs and issues here": "回報臭蟲", "Report bugs through email": "寄發郵件回報臭蟲", "Credits": "Credits", "List of contributors": "List of contributors", "List of all used libraries": "List of all used libraries", "Are you sure?": "確定嗎?", "You have unsaved changes": "你的修改未儲存,真的要放棄修改嗎?", "Dropbox API key": "Dropbox API key", "Required": "必須", "Optional": "可選", "Language": "語言", "Action": "動作", "Select": "選擇", "General": "通用", "Encryption": "加密", "Keybindings": "熱鍵", "Sync": "同步", "Profiles": "使用者設定檔", "Import": "匯入", "Transfer data": "備份與還原", "Transfer settings": "偏好設定", "Import settings": "匯入偏好設定", "Export settings": "匯出偏好設定", "Wrong format": "格式錯誤", "useDefaultConfigs": "使用預設設定", "File should be in json format": "文件應該是 json 格式", "Close": "關閉", "Hyperlink": "超連結", "Editor": "編輯器", "Preview": "編輯及預覽", "Download": "下載", "Transfer everything": "資料", "Find in page": "Find in page", "Other": "其他", "Default": "預設", "Modules": "擴充功能", "Import data": "匯入資料", "Export data": "匯出資料", "Enabled": "啟用", "Disabled": "停用", "Untitled": "無標題", "Line of": "{{currentLine}} / {{numberOfLines}}", "Drop files": "將檔案拖放到這裡上傳", "Spaces per indent": "Spaces per indent", "Sort notes": "筆記排序", "Updated date": "修改日期", "Created date": "新增日期", "Text editor": "文字編輯器", "Vim": "Vim", "Emacs": "Emacs", "Sublime": "Sublime", "encryption": { "provide password": "請提供你的密碼", "change password": "輸入新密碼", "wait": "加密中", "error": "加密錯誤", "errorConfirm": "資料解密錯誤。\r\r 如果你在其他瀏覽器調整了加密設置, 請在此瀏覽器**更新設置**。或試著匯入設置。\r\r 如果你沒有修改加密設置, 請嘗試**重新登入**。", "errorConfirmSettings": "加密設置", "errorConfirmAuth": "重試一次", "backup": { "title": "資料備份", "content": "請在下一步之前,下載你的備份文件。它包括了修改設定之前的未加密數據,請放在安全的地方。", "next": "執行,但不下載備份文件" }, "state": { "decrypt": "解密所有內容", "encrypt": "加密所有內容", "save": "儲存修改" } }, "profile": { "profile name": "設定檔名稱", "confirm remove": "使用者設定檔 **{{profile}}** 將會連同所有資料一起刪除,包括筆記、標籤以及筆記本。此操作不可還原。", "type name": "輸入設定檔名稱" }, "files": { "file-url": "檔案或圖片URL", "attach": "附加文件", "attachLink": "附加連結", "attachImage": "附加圖片" }, "notes": { "confirm trash": "筆記 **{{title}}** 將被移動到垃圾桶。", "confirm remove": "筆記 **{{title}}** 將會**徹底**刪除 !", "create and attach": "建立新的筆記並且附加連結", "create": "建立新的筆記", "hyperlink-dialog": "筆記的名稱或是URL" }, "notebooks": { "select": "選擇筆記本", "add": "建立新的筆記本", "edit": "編輯筆記本", "name": "請輸入筆記本名稱", "confirm remove": "筆記本 **{{name}}** 將會**徹底**刪除!", "remove with notes": "是的,和筆記一塊刪除", "remove": "是的,刪除" }, "tags": { "name": "需要標籤名稱", "add": "建立新的標籤", "edit": "編輯標籤", "confirm remove": "標籤 **{{name}}** 將會**徹底**刪除!" }, "dropbox": { "auth confirm": "現在你將會被重新導向 **Dropbox** 授權頁面。\r> 請點擊 **OK** 按鈕。", "auth title": "Dropbox 授權", "api info 1": "You can have your own API key on", "api info 2": "When you create a new app at Dropbox's Developer site you should keep in mind that:", "api info li 1": "Type of API should be **Dropbox API**", "api info li 2": "Type of access should be **App Folder**" }, "help": { "firststart title": "歡迎使用 Laverna", "firststart import": "如果你以前使用過 Laveran,你可以點選的 '匯入' 按鈕匯入你以前的設定。", "firststart next": "如果你從未用過 Laverna, 點選 '下一步' 開始安裝。", "firststart encryption": "如果你想使用加密,請輸入密碼。", "firststart sync": "由於我們不在服務器存儲任何數據, 你需要啟用一個同步方式以便在其他裝置上查瀏覽你的筆記。", "firststart backup": "幾乎所有的東西都準備好了。 你可以下載設定備份並且進行最後一步啦!" } } ================================================ FILE: app/manifest.webapp ================================================ { "name": "laverna", "description": "Open source note taking web application", "version": "0.6.2", "launch_path": "/index.html", "icons": { "512": "/images/icon/icon.png", "128": "/images/icon/icon-128x128.png", "16": "/images/icon/icon-16x16.png", "36": "/images/icon/icon-36x36.png", "48": "/images/icon/icon-48x48.png", "72": "/images/icon/icon-72x72.png" }, "developer": { "name": "Laverna project", "url": "https://github.com/Laverna/laverna/" }, "default_locale": "en", "installs_allowed_from": ["*"] } ================================================ FILE: app/migrate.html ================================================ Laverna
================================================ FILE: app/robots.txt ================================================ # robotstxt.org User-agent: * ================================================ FILE: app/scripts/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define, Modernizr*/ define([ 'helpers/underscore-util', 'jquery', 'backbone', 'backbone.radio', 'devicejs', 'regions/regionManager', 'marionette', 'i18next' ], function(_, $, Backbone, Radio, Device) { 'use strict'; var App = new Backbone.Marionette.Application(), env = { isWebkit : ('WebkitAppearance' in document.documentElement.style), isMobile : (Device.mobile() === true || Device.tablet() === true), platform : 'browser', ua : window.navigator.userAgent }, render; env.useWorkers = (Modernizr.webworkers && window.location.protocol !== 'file:' && !env.isWebkit); if (/(palemoon|sailfish)/i.test(env.ua)) { env.useWorkers = false; } if (env.isMobile) { env.platform = 'mobile'; } else if (window.requireNode) { env.platform = 'electron'; } // Customize underscore template _.templateSettings = { evaluate : /<%([\s\S]+?)%>/g, interpolate : /\{=([\s\S]+?)\}/g, escape : /\{\{([\s\S]+?)\}\}/g, }; /** * Overrite renderer in order to have access to * additional functions in templates (like, i18n). */ render = Backbone.Marionette.Renderer.render; Backbone.Marionette.Renderer.render = function(template, data) { data = _.extend(data || {}, { i18n : $.t, cleanXSS : _.cleanXSS, stripTags : _.stripTags }); return render(template, data); }; // Start a module App.startSubApp = function(appName, args) { var currentApp = appName ? App.module(appName) : null; if (App.currentApp === currentApp) { return; } // Stop previous app if current app is not modal if (App.currentApp && (!currentApp.options.modal || env.isMobile)) { App.currentApp.stop(); } App.currentApp = currentApp; if (currentApp) { App.channel.trigger('app:module', appName); currentApp.start(args); } return true; }; // Returns current app Radio.reply('global', 'app:current', function() { return App.currentApp; }); // @ToMove somewhere else App.channel.on('app:start', function() { $('.-loading').removeClass('-loading'); }); Radio.reply('global', 'device', function(method) { return Device[method](); }); Radio.reply('global', 'platform', function() { return env.platform; }); Radio.reply('global', 'use:webworkers', function() { return env.useWorkers; }); App.on('before:start', function() { Radio.trigger('global', 'app:init'); }); App.on('start', function() { console.timeEnd('App'); Backbone.history.start({pushState: false}); Radio.trigger('global', 'app:start'); }); return App; }); ================================================ FILE: app/scripts/apps/confirm/appConfirm.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'modules', 'apps/confirm/show/controller' ], function(_, Marionette, Radio, Modules, Controller) { 'use strict'; /** * Confirm module. We use it as a replacement for window.confirm. * * Replies on channel `Confirm` to: * 1. request `start` - starts itself. * 2. request `stop` - stops itself. */ var Confirm = Modules.module('Confirm', {startWithParent: false}); Confirm.on('start', function(options) { Confirm.controller = new Controller(options); }); Confirm.on('stop', function() { Confirm.controller.destroy(); Confirm.controller = null; }); // Initializer Radio.request('init', 'add', 'app', function() { Radio.reply('Confirm', 'start', Confirm.start, Confirm); Radio.reply('Confirm', 'stop', Confirm.stop, Confirm); }); return Confirm; }); ================================================ FILE: app/scripts/apps/confirm/show/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'marionette', 'backbone.radio', 'apps/confirm/show/view' ], function(_, Q, Marionette, Radio, View) { 'use strict'; /** * Confirm controller. * * For default it triggers the following events on `Confirm` channel: * 1. `confirm` - when a user clicks on "OK" button. * 2. `cancel` - when a user clicks on "Cancel" button. */ var Controller = Marionette.Object.extend({ initialize: function(options) { var self = this; if (typeof options === 'string') { options = {content: options}; } this.options = options; new Q((function() { // If instead of text a view was provided, render it if (typeof options.content === 'object') { return options.content.render().$el.html(); } // Try to make HTML from supposedly Markdown string return Radio.request('markdown', 'render', options.content); })()) .then(function(content) { self.options.content = content; return self.show(); }); }, onDestroy: function() { this.stopListening(this.view); this.view.trigger('destroy'); }, show: function() { // Instantiate a view this.view = new View(this.options); Radio.request('global', 'region:show', 'modal', this.view); // Events this.listenTo(this.view, 'click', this.triggerEvent); }, triggerEvent: function(event) { if (this.options['on' + event]) { this.options['on' + event](); } Radio.trigger('Confirm', event); // Stop itself Radio.request('Confirm', 'stop'); } }); return Controller; }); ================================================ FILE: app/scripts/apps/confirm/show/template.html ================================================ ================================================ FILE: app/scripts/apps/confirm/show/view.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'text!apps/confirm/show/template.html', 'mousetrap' ], function(_, Marionette, Radio, Tmpl, Mousetrap) { 'use strict'; /** * Confirm view. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), className: 'modal fade', events: { 'click .modal-footer .btn': 'triggerEvent' }, serializeData: function() { return this.options; }, templateHelpers: function() { return { getTitle: function() { return this.i18n(this.title || 'Are you sure?'); } }; }, initialize: function() { if (this.options.template) { this.template = _.template(this.options.template); } _.bindAll(this, 'focusNextBtn'); Mousetrap.bind('tab', this.focusNextBtn); // Events this.on('shown.modal', this.onShown, this); this.on('hidden.modal', this.refuseOnHide, this); }, onDestroy: function() { Mousetrap.unbind('tab'); }, onShown: function() { var $btn = this.$('.btn:last'); $btn.focus(); }, triggerEvent: function(e) { var $btn = $(e.currentTarget); this.trigger('click', $btn.attr('data-event')); }, refuseOnHide: function() { this.trigger('click', 'cancel'); }, focusNextBtn: function(e) { var $btn = this.$('.modal-footer .btn:focus').next(); $btn = $btn.length ? $btn : this.$('.modal-footer .btn:first'); $btn.focus(); e.preventDefault(); } }); return View; }); ================================================ FILE: app/scripts/apps/encryption/appEncrypt.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requirejs */ define([ 'jquery', 'underscore', 'q', 'marionette', 'backbone.radio', 'app', 'text!apps/encryption/auth/errorConfirm.html' ], function($, _, Q, Marionette, Radio, App, ConfirmTmpl) { 'use strict'; /** * Encryption module. * * Listens to events: * 1. channel: `encrypt`, event: `changed` * navigate to re-encryption page. * * Requests: * 1. channel: `encrypt`, request: `check:auth` * in order to check whether the user is authorized. */ var Encrypt = App.module('AppEncrypt', {startWithParent: false}), controller; Encrypt.Router = Marionette.AppRouter.extend({ appRoutes: { '(p/:profile/)auth' : 'showAuth', '(p/:profile/)encrypt/all' : 'showEncrypt' }, // Starts itself onRoute: function() { if (!Encrypt._isInitialized) { App.startSubApp('AppEncrypt', {profile: arguments[2][0]}); } } }); function startModule(module, args) { if (!module) { return; } $('.-loading').removeClass('-loading'); // Stop previous module if (Encrypt.currentApp) { Encrypt.currentApp.stop(); } Encrypt.currentApp = module; args.profile = args.profile || Radio.request('uri', 'profile'); module.start(args); // If module has stopped, remove the variable module.on('stop', function() { Encrypt.currentApp = null; }); } controller = { showAuth: function(profile) { requirejs(['apps/encryption/auth/app'], function(Module) { startModule(Module, {profile: profile}); }); }, showEncrypt: function(profile) { requirejs(['apps/encryption/encrypt/app'], function(Module) { startModule(Module, {profile: profile}); }); }, // On encrypt/decrypt error, remove PBKDF2 key from the session _confirmAuth: function() { Radio.once('Confirm', 'auth', function() { Radio.request('encrypt', 'delete:secureKey'); window.location.reload(); }); Radio.once('Confirm', 'openSettings', function() { Radio.request('uri', 'navigate', '/settings/encryption', { trigger : true, includeProfile: true }); }); Radio.request('Confirm', 'start', { title : 'encryption.error', content : $.t('encryption.errorConfirm'), template : ConfirmTmpl }); }, _checkAuth: function() { var isAuthed = Radio.request('encrypt', 'check:auth'); if (isAuthed === true) { return Radio.trigger('appEncrypt', 'auth:success'); } else if (isAuthed && isAuthed.isChanged === true) { return; } // Show auth form controller.showAuth(); } }; /** * Initializers and finalizers */ Encrypt.on('before:start', function() { }); Encrypt.on('before:stop', function() { Encrypt.currentApp.stop(); Encrypt.currentApp = null; }); // Check whether a user is authorized when everything is ready Radio.request('init', 'add', 'auth', function() { var defer = Q.defer(); Radio.on('encrypt', 'changed', controller.showEncrypt, controller); Radio.on('encrypt', 'decrypt:error', controller._confirmAuth, controller); Radio.on('appEncrypt', 'auth:success', function() { if (Encrypt.currentApp) { Encrypt.currentApp.stop(); } Encrypt.stop(); defer.resolve(); }); controller._checkAuth(); return defer.promise; }); // Register the router App.on('before:start', function() { new Encrypt.Router({ controller: controller }); }); }); ================================================ FILE: app/scripts/apps/encryption/auth/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'app', 'apps/encryption/auth/controller' ], function(_, Marionette, Radio, App, Controller) { 'use strict'; var Auth = App.module('AppEncrypt.Auth', {startWithParent: false}); /** * Initializers and finalizers */ Auth.on('before:start', function(options) { Auth.controller = new Controller(options); }); Auth.on('before:stop', function() { Auth.controller.destroy(); Auth.controller = null; }); return Auth; }); ================================================ FILE: app/scripts/apps/encryption/auth/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'apps/encryption/auth/view' ], function(_, Marionette, Radio, View) { 'use strict'; /** * Auth controller. It shows authorization form. */ var Controller = Marionette.Object.extend({ initialize: function(options) { this.options = options; this.show(); }, onDestroy: function() { this.stopListening(); Radio.request('global', 'region:empty', 'brand'); }, show: function() { this.view = new View(); // Show auth form Radio.request('global', 'region:show', 'brand', this.view); this.view.trigger('shown'); // Events this.listenTo(this.view, 'login', this.login); }, login: function(pwd) { var self = this; Radio.request('encrypt', 'check:password', pwd) .then(function(isAuth) { if (!isAuth) { return self.view.trigger('invalid:password'); } self.onAuth(pwd); }); }, onAuth: function(pwd) { Radio.request('encrypt', 'save:secureKey', pwd) .then(function() { Radio.trigger('appEncrypt', 'auth:success'); if (document.location.hash.search('auth') !== -1) { Radio.request('uri', 'back'); } }); }, }); return Controller; }); ================================================ FILE: app/scripts/apps/encryption/auth/errorConfirm.html ================================================
================================================ FILE: app/scripts/apps/encryption/auth/template.html ================================================

Laverna

================================================ FILE: app/scripts/apps/encryption/auth/view.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'jquery', 'marionette', 'text!apps/encryption/auth/template.html' ], function (_, $, Marionette, Tmpl) { 'use strict'; /** * Auth view. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), events: { 'submit .form-wrapper' : 'login' }, ui: { password : 'input[name=password]' }, initialize: function () { this.on('shown', this.focusPassword, this); this.on('invalid:password', this.wrongPwd, this); }, focusPassword: function () { this.ui.password.focus(); }, login: function (e) { e.preventDefault(); this.trigger('login', this.ui.password.val()); }, wrongPwd: function(){ // Create the shake function from jQuery-UI // without using the whole package jQuery.fn.shake = function(duration, shakes, distance) { duration = duration || 10; shakes = shakes || 10; distance = distance || 5; this.css('position', 'relative'); for (var x=1; x<=shakes; x++) { this.animate({left:(distance*-1)},(((duration/shakes)/4))) .animate({left:distance}, ((duration/shakes)/2)) .animate({left:0}, (((duration/shakes)/4))); } return this; }; //Clear password form var pwdForm = $('.form-control.input-lg.input--brand'); pwdForm.val(''); //Shake button var button = $('.btn.btn-lg.btn-block.btn--brand'); button.shake(); } }); return View; }); ================================================ FILE: app/scripts/apps/encryption/encrypt/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'app', 'apps/encryption/encrypt/controller' ], function(_, Marionette, Radio, App, Controller) { 'use strict'; /** * Sub app which handles encryption and re-encryption. */ var Encrypt = App.module('AppEncrypt.Encrypt', {startWithParent: false}); /** * Initializers and finalizers */ Encrypt.on('before:start', function(options) { Encrypt.controller = new Controller(options); }); Encrypt.on('before:stop', function() { Encrypt.controller.destroy(); Encrypt.controller = null; }); return Encrypt; }); ================================================ FILE: app/scripts/apps/encryption/encrypt/backup.html ================================================

{{i18n('encryption.backup.title')}}

{{i18n('encryption.backup.content')}}

================================================ FILE: app/scripts/apps/encryption/encrypt/backupView.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'text!apps/encryption/encrypt/backup.html' ], function(_, Marionette, Tmpl) { 'use strict'; var View = Marionette.ItemView.extend({ template: _.template(Tmpl), triggers: { 'click #btn--download': 'confirm:download', 'click #btn--next' : 'next:step' } }); return View; }); ================================================ FILE: app/scripts/apps/encryption/encrypt/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'q', 'underscore', 'marionette', 'backbone.radio', 'apps/encryption/encrypt/view', 'apps/encryption/encrypt/backupView' ], function(Q, _, Marionette, Radio, View, BackupView) { 'use strict'; /** * Encryption controller. * * Listens to events: * 1. channel: `Encryption`, event: `password:valid` * initilizes encryption. * 2. channel: this.view, event: `check:passwords` * checks passwords * * Triggers: * 1. channel: `configs`, request: `get:object` * 2. channel: `configs`, request: `reset:encrypt` * 3. channel: `global`, request: `region:show` * 4. channel: `encrypt`, request: `change:configs` * 5. channel: `encrypt`, request: `save:secureKey` * 6. channel: `encrypt`, request: `decrypt:models` * 7. channel: `encrypt`, request: `encrypt:models` */ var Controller = Marionette.Object.extend({ // Collections to encrypt collectionNames : ['notes', 'tags', 'notebooks'], collections : {}, initialize: function(options) { _.bindAll(this, 'saveChanges', 'encrypt', 'redirect', 'show', 'encryptProfile', 'showBackup'); this.options = options; this.vent = Radio.channel('encrypt'); // Configs this.configs = Radio.request('configs', 'get:object'); this.backup = _.extend({}, this.configs, this.configs.encryptBackup); // Just to be save remove current secure key from the session this.vent.request('delete:secureKey'); // Show the view Radio.request('configs', 'get:profiles') .then(this.show) .fail(function(e) { console.error('Error:', e); }); // Events this.listenTo(Radio.channel('Encryption'), 'password:valid', this.initEncrypt); }, onDestroy: function() { this.stopListening(); Radio.request('global', 'region:empty', 'brand'); }, show: function(profiles) { this.profiles = profiles; // Instantiate and show the view this.view = new View({ collections : this.collectionNames, configs : this.configs }); Radio.request('global', 'region:show', 'brand', this.view); // Events this.listenTo(this.view, 'check:passwords', this.checkPasswords); }, checkPasswords: function(data) { var self = this, promises = []; /* * If encryption was enabled in old configs but the old password * was not provided by the user, try to use the new password instead. */ if (Number(this.backup.encrypt) && (!data.old && data.password)) { data.old = data.password; } // Switch to backup configs and check old password if (data.old) { this.vent.request('change:configs', this.backup); promises.push(this.vent.request('check:password', data.old)); } // Switch to new configs and check new password if (data.password) { this.vent.request('change:configs', this.configs); promises.push(this.vent.request('check:password', data.password)); } return Q.all(promises) .then(function(results) { if (!results.length || _.indexOf(results, false) > -1) { return self.view.trigger('password:invalid', results); } self.passwords = data; Radio.trigger('Encryption', 'password:valid'); }); }, /** * Initialize encryption. */ initEncrypt: function() { var promises = [], profile = (this.profiles.length === 1 ? this.profiles[0] : 'notes-db'), self = this; this.rawData = {}; this.rawData[profile] = {configs: _.map(this.configs, function(item, key) { if (key === 'encrypt') { item = '0'; } if (key === 'encryptBackup') { item = {}; } if (key === 'appProfiles') { item = JSON.stringify(item); } return {name: key, value: item}; })}; // Re-encrypt every profile _.each(this.profiles, function(profile) { promises.push(function() { // Use backup configs self.vent.request('change:configs', self.backup); // Generate PBKDF2 before starting re-encryption return self.vent.request('save:secureKey', self.passwords.old) .then(function() { return self.encryptProfile({ profile: profile }); }); }); }); return _.reduce(promises, Q.when, new Q()) .then(this.resetBackup) .then(this.showBackup) .then(this.redirect) .fail(function() { console.error('Error!', arguments); }); }, /** * Start encryption process */ encryptProfile: function(options) { var promises = [], self = this; // Fetch options options = options || this.options; options.pageSize = 0; this.rawData[options.profile] = this.rawData[options.profile] || {}; // Fetch all collections in a profile _.each(this.collectionNames, function(name) { promises.push( new Q(Radio.request(name, 'fetch', options)) ); }); /** * After the collections are fetched, start re-encryption process. */ return Q.all(promises) .spread(function() { // Re-encrypt the collections that are not empty self.collections = _.filter(arguments, function(collection) { self.rawData[options.profile][collection.storeName] = collection.toJSON(); return collection.length > 0; }); self.view.trigger('encrypt:init', self.collections.length); }) .then(this.encrypt) .then(this.saveChanges); }, /** * Encrypt every collection with new encryption configs. */ encrypt: function() { // Encryption is disabled if (Number(this.configs.encrypt) === 0) { _.each(this.collections, function(collection) { collection.each(function(model) { model.set('encryptedData', null); }); }); return; } var promises = [], self = this; // Use new encryption configs this.vent.request('change:configs', this.configs); // Encrypt every collection _.each(this.collections, function(collection) { promises.push(function() { return self.vent.request( 'encrypt:models', collection ).then(function() { return self.checkEncryption(collection); }); }); }); return this.vent.request('save:secureKey', this.passwords.password) .then(function() { return _.reduce(promises, Q.when, new Q()); }); }, /** * Validate encryption by picking one of the models in a collection, * decrypting it, and comparing to the original value. */ checkEncryption: function(collection) { if (!collection.length) { return new Q(); } var model = collection.at(0); return this.vent.request('decrypt:model', model) .fail(function(e) { console.error('Encryption error:', e); throw new Error('Error with encryption'); }); }, /** * Save all changes in every collection. */ saveChanges: function() { var promises = []; _.each(this.collections, function(collection) { promises.push(function() { return new Q(Radio.request(collection.storeName, 'save:collection', collection)); }); }); return _.reduce(promises, Q.when, new Q()); }, /** * Probably we don't need backup configs and we can safely remove them. */ resetBackup: function() { return new Q(Radio.request('configs', 'reset:encrypt')); }, /** * Advice to download backup with data. */ showBackup: function() { var defer = Q.defer(); this.view = new BackupView({ data: this.rawData }); this.view.once('confirm:download', this.downloadBackup, this); this.view.once('next:step', defer.resolve, defer); Radio.request('global', 'region:show', 'brand', this.view); return defer.promise; }, downloadBackup: function() { Radio.request('importExport', 'export', this.rawData); }, /** * Delete current secure key from session storage and reload the page. */ redirect: function() { this.vent.request('delete:secureKey'); Radio.request('uri', 'navigate', '/notes', { includeProfile : true, trigger : false }); window.location.reload(); } }); return Controller; }); ================================================ FILE: app/scripts/apps/encryption/encrypt/template.html ================================================

{{ i18n('Encryption') }}

{{ i18n('encryption.wait') }}

Laverna

<% if (needOldPassword()) { %>
<% } %> <% if (needNewPassword()) { %>
<% } %>
================================================ FILE: app/scripts/apps/encryption/encrypt/view.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'jquery', 'underscore', 'marionette', 'backbone.radio', 'text!apps/encryption/encrypt/template.html' ], function($, _, Marionette, Radio, Tmpl) { 'use strict'; /** * Re-encryption view. * Shows auth form or progress bar. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), events: { 'submit form': 'checkPasswords' }, ui: { containerProgress : '#encryption-progress', containerForm : '#encryption-password', progress : '#progress', state : '#state', // Passwords oldPassword : 'input[name=oldpass]', password : 'input[name=password]', }, initialize: function() { this.progress = {count: 0, max: 0}; this.vent = Radio.channel('encrypt'); // Events this.listenTo(Radio.channel('Encryption'), 'password:valid', this.showProgress); this.listenTo(this, 'encrypt:init', this.changeMax); this.listenTo(Radio.channel('collection'), 'saved:all', this.saveProgress); this.listenTo(this.vent, 'decrypting:models', this.decryptProgress); this.listenTo(this.vent, 'encrypting:models', this.encryptProgress); }, changeMax: function(max) { this.progress.max = max; }, saveProgress: function() { this._setProgress('encryption.state.save'); }, decryptProgress: function() { this._setProgress('encryption.state.decrypt'); }, encryptProgress: function() { this._setProgress('encryption.state.encrypt'); }, /** * Hide auth form and show progress bar. */ showProgress: function() { this.ui.containerForm.hide(); this.ui.containerProgress.removeClass('hide'); }, _setProgress: function(state) { var max = this.progress.max, width; // Change progress bar this.progress.count = ( this.progress.count >= max ? 1 : this.progress.count + 1 ); width = Math.floor((this.progress.count * 100) / max); this.ui.progress.css('width', width + '%'); // Change the status this.ui.state.text($.t(state)); }, checkPasswords: function(e) { e.preventDefault(); this.trigger('check:passwords', { password : this.ui.password.length ? this.ui.password.val().trim() : null, old : this.ui.oldPassword.length ? this.ui.oldPassword.val().trim() : null }); }, serializeData: function() { return { configs: this.options.configs }; }, templateHelpers: function() { return { needOldPassword: function() { return ( ( // Backup password shouldn't be empty ( !_.isUndefined(this.configs.encryptBackup.encryptPass) && this.configs.encryptBackup.encryptPass.length !== 0 ) && // Encryption should be enabled ( _.isUndefined(this.configs.encryptBackup.encrypt) || Number(this.configs.encryptBackup.encrypt) ) ) || // Encryption was disabled in new configs ( !Number(this.configs.encrypt) && Number(this.configs.encryptBackup.encrypt) ) ); }, needNewPassword: function() { return Number(this.configs.encrypt); } }; } }); return View; }); ================================================ FILE: app/scripts/apps/help/about/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'app', 'apps/help/about/controller' ], function(_, Marionette, App, Controller) { 'use strict'; /** * Sub module shows information about the app. */ var About = App.module('AppHelp.About', {startWithParent: false}); /** * Initializers and finalizers */ About.on('before:start', function(options) { About.controller = new Controller(options); // Stop module if controller stops this.listenTo(About.controller, 'destroy', About.stop); }); About.on('before:stop', function() { this.stopListening(); About.controller = null; }); return About; }); ================================================ FILE: app/scripts/apps/help/about/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'apps/help/about/view', 'constants' ], function(_, Marionette, Radio, View, constants) { 'use strict'; var Controller = Marionette.Object.extend({ initialize: function() { this.view = new View({ appVersion: constants.VERSION }); Radio.request('global', 'region:show', 'modal', this.view); this.listenTo(this.view, 'redirect', this.destroy); }, onDestroy: function() { Radio.request('global', 'region:empty', 'modal'); } }); return Controller; }); ================================================ FILE: app/scripts/apps/help/about/template.html ================================================ ================================================ FILE: app/scripts/apps/help/about/view.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'jquery', 'marionette', 'behaviors/modal', 'text!apps/help/about/template.html' ], function (_, $, Marionette, ModalBehavior, Tmpl) { 'use strict'; var View = Marionette.ItemView.extend({ template: _.template(Tmpl), className: 'modal fade', behaviors: { ModalBehavior: { behaviorClass: ModalBehavior } }, serializeData: function () { return { appVersion : this.options.appVersion }; } }); return View; }); ================================================ FILE: app/scripts/apps/help/appHelp.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requirejs */ define([ 'underscore', 'marionette', 'backbone.radio', 'app' ], function(_, Marionette, Radio, App) { 'use strict'; var Help = App.module('AppHelp', {startWithParent: false}), controller; function startModule(module, args) { if (!module) { return; } // Stop previous module if (Help.currentApp) { Help.currentApp.stop(); } // Start this subapp else { Help.start(); } Help.currentApp = module; module.start(args); // If module has stopped, remove the variable and stop itself module.on('stop', function() { Help.stop(); Help.currentApp = null; }); } controller = { keybindings: function() { requirejs(['apps/help/show/app'], function(Module) { startModule(Module); }); }, about: function() { requirejs(['apps/help/about/app'], function(Module) { startModule(Module); }); }, firstStart: function() { if (!Number(Radio.request('configs', 'get:config', 'firstStart'))) { return; } requirejs(['apps/help/firstStart/app'], function(Module) { startModule(Module); }); } }; Help.on('before:start', function() { }); Help.on('before:stop', function() { }); // Add initializer Radio.request('init', 'add', 'app', function() { Radio.once('global', 'app:start', controller.firstStart, controller); Radio.reply('Help', { 'show:about' : controller.about, 'show:keybindings' : controller.keybindings }, controller); }); return Help; }); ================================================ FILE: app/scripts/apps/help/firstStart/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'app', 'apps/help/firstStart/controller' ], function(_, App, Controller) { 'use strict'; /** * Submodule shows first-start guide. */ var FirstStart = App.module('AppHelp.FirstStart', {startWithParent: false}); /** * Initializers and finalizers */ FirstStart.on('before:start', function(options) { FirstStart.controller = new Controller(options); // Stop module if controller stops this.listenTo(FirstStart.controller, 'destroy', FirstStart.stop); }); FirstStart.on('before:stop', function() { this.stopListening(); FirstStart.controller = null; }); return FirstStart; }); ================================================ FILE: app/scripts/apps/help/firstStart/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'marionette', 'backbone.radio', 'apps/help/firstStart/view', 'fileSaver', ], function(_, Q, Marionette, Radio, View, fileSaver) { 'use strict'; var Controller = Marionette.Object.extend({ initialize: function() { _.bindAll(this, 'show', 'destroy', 'save', 'mark', 'close'); this.profile = Radio.request('uri', 'profile'); Q.all([ Radio.request('configs', 'get:config', 'encrypt'), Radio.request('notes', 'fetch', {profile: this.profile}), Radio.request('notebooks', 'fetch', {profile: this.profile}), Radio.request('tags', 'fetch', {profile: this.profile}) ]) .spread(this.show) .fail(function(e) { console.error('Error:', e); }); }, onDestroy: function() { Radio.request('global', 'region:empty', 'modal'); this.view = null; }, show: function(encrypt, notes, notebooks, tags) { /** * If encryption is enabled or there is some data, showing * installation process is not necessary. */ if (Number(encrypt) || (notes.length || notebooks.length || tags.length)) { return this.close(); } // Clear old encryption secure from session storage window.sessionStorage.clear(); this.view = new View(); Radio.request('global', 'region:show', 'modal', this.view); this.listenTo(this.view, 'save', this.save); this.listenTo(this.view, 'import', this.import); this.listenTo(this.view, 'close', this.reload); this.listenTo(this.view, 'redirect', this.close); this.listenTo(this.view, 'download', this.download); }, import: function() { Radio.request('uri', 'navigate', '/settings/importExport', { trigger : true, includeProfile: true }); this.close(); }, /** * Export user's settings. */ download: function() { Radio.request('configs', 'get:all', {profile: this.profile}) .then(function(configs) { var blob = new Blob( [JSON.stringify(configs)], {type: 'text/plain;charset=utf8'} ); fileSaver(blob, 'laverna-settings.json'); window.location.reload(); }); }, /** * Save settings. */ save: function() { var password = this.view.ui.password.val().trim(), cloudStorage = this.view.ui.cloudStorage.val().trim(), promises = [], self = this; if (password.length) { promises.push(this.savePassword(password)); } if (cloudStorage !== '0') { promises.push(this.saveCloud(cloudStorage)); } return Q.all(promises) .then(this.mark) .then(function() { if (!promises.length) { return self.close(); } self.view.trigger('save:after'); }); }, savePassword: function(password) { var encryptSalt = Radio.request('encrypt', 'randomize', 5, 0, true); return Q.all([ Radio.request('configs', 'save:object', { name: 'encrypt', value: '1' }, undefined, {profile: this.profile}), Radio.request('configs', 'save:object', { name: 'encryptSalt', value: encryptSalt }, undefined, {profile: this.profile}), Radio.request('configs', 'save:object', { name: 'encryptPass', value: password }, undefined, {profile: this.profile}), ]); }, saveCloud: function(cloudStorage) { return Radio.request('configs', 'save:object', { name: 'cloudStorage', value: cloudStorage }, undefined, {profile: this.profile}); }, /** * Mark that installation process was done. */ mark: function() { return Radio.request('configs', 'save:object', { name : 'firstStart', value : '0' }, undefined, {profile: this.profile}); }, reload: function() { window.location.reload(); }, close: function() { this.mark() .then(this.destroy); }, }); return Controller; }); ================================================ FILE: app/scripts/apps/help/firstStart/template.html ================================================ ================================================ FILE: app/scripts/apps/help/firstStart/view.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'jquery', 'marionette', 'behaviors/modal', 'text!apps/help/firstStart/template.html' ], function ( _, $, Marionette, ModalBehavior, Tmpl) { 'use strict'; var View = Marionette.ItemView.extend({ template: _.template(Tmpl), className: 'modal fade', behaviors: { ModalBehavior: { behaviorClass: ModalBehavior } }, ui: { 'settings' : '#welcome--settings', 'page' : '#welcome--page', 'backup' : '#welcome--backup', 'password' : 'input[name="password"]', 'cloudStorage' : 'select[name="cloudStorage"]' }, triggers: { 'click #welcome--import' : 'import', 'click #welcome--save' : 'save', 'click #welcome--last' : 'close', 'click #welcome--export' : 'download', }, events: { 'click #welcome--next': 'onNext', }, initialize: function() { this.listenTo(this, 'save:after', this.onSave); }, onNext: function() { this.ui.page.addClass('hidden'); this.ui.settings.removeClass('hidden'); }, onSave: function() { this.ui.settings.addClass('hidden'); this.ui.backup.removeClass('hidden'); }, }); return View; }); ================================================ FILE: app/scripts/apps/help/show/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'app', 'apps/help/show/controller' ], function(_, Marionette, App, Controller) { 'use strict'; /** * Sub module shows keybindings list. */ var Keybindings = App.module('AppHelp.Keybindings', {startWithParent: false}); /** * Initializers and finalizers */ Keybindings.on('before:start', function(options) { Keybindings.controller = new Controller(options); // Stop module if controller stops this.listenTo(Keybindings.controller, 'destroy', Keybindings.stop); }); Keybindings.on('before:stop', function() { this.stopListening(); Keybindings.controller = null; }); return Keybindings; }); ================================================ FILE: app/scripts/apps/help/show/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'apps/help/show/view' ], function(_, Marionette, Radio, View) { 'use strict'; /** * Keybindings help controller. */ var Controller = Marionette.Object.extend({ initialize: function() { _.bindAll(this, 'show'); // Fetch configs Radio.request('configs', 'get:all', { profile: Radio.request('uri', 'profile') }).then(this.show); }, onDestroy: function() { Radio.request('global', 'region:empty', 'modal'); }, show: function(configs) { configs = configs.clone(); configs.reset(configs.shortcuts()); this.view = new View({ collection: configs }); Radio.request('global', 'region:show', 'modal', this.view); this.listenTo(this.view, 'redirect', this.destroy); } }); return Controller; }); ================================================ FILE: app/scripts/apps/help/show/template.html ================================================ ================================================ FILE: app/scripts/apps/help/show/view.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'jquery', 'marionette', 'behaviors/modal', 'text!apps/help/show/template.html' ], function ( _, $, Marionette, ModalBehavior, Tmpl) { 'use strict'; var View = Marionette.ItemView.extend({ template: _.template(Tmpl), className: 'modal fade', behaviors: { ModalBehavior: { behaviorClass: ModalBehavior } } }); return View; }); ================================================ FILE: app/scripts/apps/navbar/appNavbar.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'modules', 'apps/navbar/show/controller' ], function(_, Marionette, Radio, Modules, Controller) { 'use strict'; /** * Navbar module. * * Replies to requests: * 1. channel: `navbar`, reply: `start` * starts itself. */ var Navbar = Modules.module('Navbar', {startWithParent: false}); Navbar.on('start', function(options) { Navbar.controller = new Controller(options); }); Navbar.on('stop', function() { Navbar.controller.destroy(); Navbar.controller = null; }); // Initializer Radio.request('init', 'add', 'app:before', function() { Radio.reply('navbar', 'stop', Navbar.stop, Navbar); Radio.reply('navbar', 'start', function(options) { // Just trigger an event if (Navbar._isInitialized) { return Navbar.controller.trigger('change:title', options); } Navbar.start(options); }); }); return Navbar; }); ================================================ FILE: app/scripts/apps/navbar/show/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'jquery', 'q', 'underscore', 'backbone.radio', 'marionette', 'apps/navbar/show/view' ], function($, Q, _, Radio, Marionette, View) { 'use strict'; /** * Navbar controller. * * Listens to events: * ------------------ * 1. channel: `global`, event: `filter:change` * re-renders the view on this event. * 3. this.view, event: `search:submit` * navigates to search page * * Triggers: * ------------------ * Requests: * 1. channel: `global`, request: `app:current` * 2. channel: `uri`, request: `link:profile` * 3. channel: `global`, request: `get:title` * * requests: * 1. channel: `uri`, request: `navigate` * 2. channel: `global`, request: `region:show` */ var Controller = Marionette.Object.extend({ initialize: function(options) { var profile = {profile: Radio.request('uri', 'profile')}; _.bindAll(this, 'show'); this.options = options; // Request notebooks and title Q.all([ Radio.request('configs', 'get:all', profile), Radio.request('notebooks', 'get:all', profile), Radio.request('configs', 'get:model', {name: 'appProfiles'}), Radio.request('global', 'get:title', options) ]) .spread(this.show); // Events this.listenTo(this, 'change:title', this.changeTitle); }, onDestroy: function() { this.stopListening(); Radio.request('global', 'region:empty', 'sidebarNavbar'); }, show: function(configs, notebooks, profiles, title) { var args; args = _.extend({title: title}, this.options); this.view = new View({ args : args, notebooks : notebooks, collection : configs, profiles : profiles }); // Render the view Radio.request('global', 'region:show', 'sidebarNavbar', this.view); // Listen to view events this.listenTo(this.view, 'search:submit', this.navigateSearch, this); }, /** * Changes current title */ changeTitle: function(options) { var self = this; Radio.request('global', 'get:title', options) .then(function(title) { self.view.trigger('change:title', {title: title, args: options}); }); }, /** * Navigate to the search page. */ navigateSearch: function(text) { Radio.request('uri', 'navigate', { filter : 'search', query : text }); } }); return Controller; }); ================================================ FILE: app/scripts/apps/navbar/show/template.html ================================================
{{i18n('Close')}}
{{i18n('All notes')}} {{i18n('Favourites')}} {{i18n('Trash')}} {{i18n('Notebooks & tags')}} {{ i18n('Open tasks') }} <% if (notebooks !== null && notebooks.length) { %>
{{i18n('Notebooks')}}
<% notebooks.forEach(function(notebook) { %> {=cleanXSS(notebook.name)} <% }); } %>
{{i18n('Profiles')}}
{{i18n('Default')}} <% _.forEach(profiles, function(prof) { %> <% if (prof !== 'notes-db') { %> {{prof}} <% } }); %>
{{i18n('Other')}}
{{i18n('Settings')}} {{i18n('About')}}
================================================ FILE: app/scripts/apps/navbar/show/view.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'jquery', 'underscore', 'marionette', 'backbone.radio', 'behaviors/sidemenu', 'text!apps/navbar/show/template.html' ], function($, _, Marionette, Radio, Sidemenu, Tmpl) { 'use strict'; /** * Navbar view. * * Listens to: * ---------- * Events: * 1. channel: `global`, event: `show:search` * focuses on search form * * Triggers events: * 1. channel: `global`, event: `search:shown` * when the user opens the search form. * 2. channel: `global`, event: `search:hidden` * when the search form is hidden or the navbar view is destroyed. * 3. event: `search:submit` to itself * when the search form is submitted. * 4. channel: `global`, event: `search:change` * every time when the user types something on the search form. * * Requests: * 1. channel: `uri`, request: `link:profile` * 2. channel: `uri`, request: `profile` */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), keyboardEvents: {}, behaviors: { Sidemenu: { behaviorClass: Sidemenu } }, ui: { navbar : '#sidebar--nav', search : '#header--search--input', title : '#header--title', icon : '#header--icon', sync : '#header--sync--icon' }, events: { 'click #header--add' : 'navigateAdd', 'click #header--sbtn' : 'showSearch', 'click #header--about' : 'showAbout', 'blur @ui.search' : 'hideSearch', 'keyup @ui.search' : 'searchKeyup', 'submit #header--search' : 'searchSubmit', 'click #header--sync' : 'triggerSync' }, collectionEvents: { 'change': 'render' }, initialize: function() { this.listenTo(this, 'change:title', this.changeTitle); this.listenTo(Radio.channel('global'), 'show:search', this.showSearch); this.listenTo(Radio.channel('sync'), 'start', this.onSyncStart); this.listenTo(Radio.channel('sync'), 'stop', this.onSyncStop); // Re-render the view when notebooks collection has changed this.listenTo(this.options.notebooks, 'change add remove', this.render); }, onDestroy: function() { Radio.trigger('global', 'search:hidden'); }, triggerSync: function() { Radio.request('sync', 'start'); }, onSyncStart: function() { console.log('start spin'); this.ui.sync.toggleClass('animate-spin', true); }, onSyncStop: function() { console.log('stop spin'); this.ui.sync.toggleClass('animate-spin', false); }, /** * Trigger form:show event when add button is clicked. */ navigateAdd: function() { Radio.trigger('global', 'form:show'); return false; }, /** * Change navbar title */ changeTitle: function(options) { var icon = this.templateHelpers().getIcon.apply(options); this.ui.title.text($.t(options.title)); this.ui.icon.attr('class', icon); this.options.args = options.args; }, searchSubmit: function() { this.ui.search.blur(); if (this.ui.search.val().trim().length) { this.trigger('search:submit', this.ui.search.val().trim()); } Radio.trigger('global', 'search:hidden'); return false; }, showSearch: function() { this.ui.navbar.addClass('-search'); this.ui.search.focus().select(); Radio.trigger('global', 'search:shown'); return false; }, hideSearch: function() { this.ui.navbar.removeClass('-search'); }, showAbout: function(e) { e.preventDefault(); Radio.request('Help', 'show:about'); }, searchKeyup: function(e) { if (e.which === 27) { Radio.trigger('global', 'search:hidden'); return this.ui.search.blur(); } Radio.trigger('global', 'search:change', this.ui.search.val()); }, serializeData: function() { var maxNotebooks = parseInt(Radio.request('configs', 'get:config', 'navbarNotebooksMax'), 10); return { args : this.options.args, configs : this.collection.getConfigs(), profiles : this.options.profiles.getValueJSON(), notebooks : _.first(this.options.notebooks.toJSON(), maxNotebooks), uri : Radio.request('uri', 'link:profile', '/'), profile : Radio.request('uri', 'profile') }; }, templateHelpers: function() { return { getIcon: function() { return 'icon-' + ( !this.args.filter ? 'note' : this.args.filter ); }, isSyncEnabled: function() { return this.configs.cloudStorage === 'dropbox'; }, profileLink: function(profileName) { return Radio.request('uri', 'link:profile', '/notes', profileName); } }; } }); return View; }); ================================================ FILE: app/scripts/apps/notebooks/appNotebooks.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requirejs */ define([ 'q', 'underscore', 'marionette', 'backbone.radio', 'app', 'apps/notebooks/list/app' ], function(Q, _, Marionette, Radio, App, SidebarApp) { 'use strict'; /** * AppNotebooks module. The module shows a list of notebooks and tags * in sidebar. It also handles adding, updating, and removing of notebooks * and tags. * * Listens to events: * 1. channel: `global`, event: `form:show` * shows notebooks form * * Replies to requests on channel `appNotebooks`: * 1. request: `notebooks:remove` * removes specified notebook. * 2. request: `tags:remove` * removes specified tag. * 3. request: `show:form` * it always replies to this request. After receiving the request, it * shows notebook form without starting this module. * * Triggers requests: * 1. channel: `navbar`, request: `start` */ var Notebooks = App.module('AppNotebooks', {startWithParent: false}), startModule, controller; /** * The router. */ Notebooks.Router = Marionette.AppRouter.extend({ appRoutes: { // Notebooks '(p/:profile/)notebooks' : 'showList', '(p/:profile/)notebooks/add' : 'notebookForm', '(p/:profile/)notebooks/edit/:id' : 'notebookForm', // Tags '(p/:profile/)tags/add' : 'tagForm', '(p/:profile/)tags/edit/:id' : 'tagForm', }, // Starts itself onRoute: function() { if (!Notebooks._isInitialized) { App.startSubApp('AppNotebooks', {profile: arguments[2][0]}); } } }); /** * Starts submodules */ startModule = function(module, args) { if (!module) { return; } // Stop previous module if (Notebooks.currentApp) { Notebooks.currentApp.stop(); } Notebooks.currentApp = module; module.start(args); // If module has stopped, remove the variable module.on('stop', function() { Notebooks.currentApp = null; }); }; controller = { /** * Shows a list of notebooks and tags. * Sidebar module starts when this module starts. * That is why we do not have to do anything here. */ showList: function() { }, // Edit or add notebooks notebookForm: function(profile, id) { /* * Return a promise that gets resolved when the new notebook is * successfully created. */ var defer = Q.defer(); requirejs(['apps/notebooks/form/notebook/app'], function(Module) { startModule(Module, {profile: profile, id: id, promise: defer}); }); return defer.promise; }, // Edit or add tags tagForm: function(profile, id) { requirejs(['apps/notebooks/form/tag/app'], function(Module) { startModule(Module, {profile: profile, id: id}); }); }, // Remove an existing notebook _removeNotebook: function(profile, id) { requirejs(['apps/notebooks/remove/controller'], function(Controller) { new Controller('notebooks', profile, id); }); }, // Remove an existing tag _removeTag: function(profile, id) { requirejs(['apps/notebooks/remove/controller'], function(Controller) { new Controller('tags', profile, id); }); }, _navigateForm: function() { Radio.request('uri', 'navigate', '/notebooks/add', {includeProfile: true}); } }; /** * Initializers and finalizers */ Notebooks.on('before:start', function(options) { // Start the sidebar module SidebarApp.start(options); // Reply to requests Radio.channel('appNotebooks') .reply('notebooks:remove', controller._removeNotebook, controller) .reply('tags:remove', controller._removeTag, controller); // Listen to events this.listenTo(Radio.channel('global'), 'form:show', controller._navigateForm); }); Notebooks.on('before:stop', function() { // Stop the sidebar module SidebarApp.stop(); // Stop the current module if (Notebooks.currentApp) { Notebooks.currentApp.stop(); Notebooks.currentApp = null; } // Stop responding to requests and requests Radio.channel('appNotebooks') .stopReplying('notebooks:remove tags:remove'); // Stop listening to events this.stopListening(); }); Radio.request('init', 'add', 'app', function() { Radio.reply('appNotebooks', 'show:form', controller.notebookForm, controller); }); App.on('before:start', function() { new Notebooks.Router({ controller: controller }); }); return Notebooks; }); ================================================ FILE: app/scripts/apps/notebooks/form/notebook/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'app', 'apps/notebooks/form/notebook/controller' ], function(_, Marionette, Radio, App, Controller) { 'use strict'; /** * Notebook form module. * * Replies to requests on channel `appNotebooks` * 1. request: `form:stop` - stops itself. */ var Form = App.module('AppNotebooks.Form.Notebook', {startWithParent: false}); Form.on('before:start', function(options) { Form.controller = new Controller(options); Radio.reply('appNotebooks', 'form:stop', Form.stop, Form); }); Form.on('before:stop', function() { Radio.stopReplying('appNotebooks', 'form:stop'); Form.controller.destroy(); Form.controller = null; }); return Form; }); ================================================ FILE: app/scripts/apps/notebooks/form/notebook/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'q', 'underscore', 'marionette', 'backbone.radio', 'apps/notebooks/form/notebook/formView' ], function(Q, _, Marionette, Radio, View) { 'use strict'; /** * Notebook form controller. * * Listens to events: * 1. channel: `notebooks`, event: `update:model` * triggers `close` event on the view. * 2. this.view, event: `save` * saves the changes. * 3. this.view, event: `redirect` * stops the module and redirects. * * requests: * 1. channel: `notebooks`, event: `save` * 2. channel: `uri`, event: `back` * 3. channel: `appNotebooks`, event: `form:stop` */ var Controller = Marionette.Object.extend({ initialize: function(options) { options.profile = options.profile || Radio.request('uri', 'profile'); _.bindAll(this, 'show'); this.options = options; // Events this.listenTo(Radio.channel('notebooks'), 'update:model', this.onSaveAfter); // Fetch notebooks Q.all([ Radio.request('notebooks', 'get:all', options), Radio.request('notebooks', 'get:model', options) ]) .spread(this.show); }, onDestroy: function() { this.stopListening(); Radio.request('global', 'region:empty', 'modal'); // If we still got an unresolved promise, resolve it with null-value. if (this.options.promise) { this.options.promise.resolve(null); } }, show: function(collection, model) { // Show only notebooks which are not related to the current model. collection = collection.clone(); collection.reset(collection.rejectTree(model.get('id'))); // Instantiate and show the form view this.view = new View({ collection : collection, model : model }); Radio.request('global', 'region:show', 'modal', this.view); // Listen to events this.listenTo(this.view, 'save' , this.save); this.listenTo(this.view, 'redirect', this.redirect); }, save: function() { var self = this, data = { name : this.view.ui.name.val(), parentId : this.view.ui.parentId.val() }; Radio.request('notebooks', 'save', this.view.model, data) .then(function() { // Resolve the promise. if (self.options.promise) { self.options.promise.resolve({ title: self.view.model.title, id: self.view.model.id }); self.options.promise = null; } }) .fail(function(e) { console.error('Error:', e); if (self.options.promise) { self.options.promise.reject(e); self.options.promise = null; } }); }, onSaveAfter: function() { this.view.trigger('close'); }, redirect: function() { var moduleName = Radio.request('global', 'app:current').moduleName; // Stop itself Radio.request('appNotebooks', 'form:stop'); // Redirect only if current active module is AppNotebooks if (moduleName === 'AppNotebooks') { Radio.request('uri', 'back', '/notebooks', { includeProfile : true }); } } }); return Controller; }); ================================================ FILE: app/scripts/apps/notebooks/form/notebook/formView.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'jquery', 'marionette', 'behaviors/modalForm', 'models/notebook', 'text!apps/notebooks/form/notebook/templates/form.html' ], function(_, $, Marionette, ModalForm, Notebook, Tmpl) { 'use strict'; /** * Notebook form view. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), className: 'modal fade', ui: { name : 'input[name="name"]', parentId : 'select[name="parentId"]' }, behaviors: { ModalForm: { behaviorClass: ModalForm } }, serializeData: function() { return _.extend(this.model.toJSON(), { notebooks: this.collection.toJSON() }); }, templateHelpers: function() { return { isParent: function(notebookId) { if (this.parentId === notebookId) { return ' selected="selected"'; } } }; } }); return View; }); ================================================ FILE: app/scripts/apps/notebooks/form/notebook/templates/form.html ================================================ ================================================ FILE: app/scripts/apps/notebooks/form/tag/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'app', 'apps/notebooks/form/tag/controller' ], function(_, Marionette, Radio, App, Controller) { 'use strict'; /** * Tag form module. * * Replies to requests on channel `appNotebooks` * 1. request: `form:stop` - stops itself. */ var Form = App.module('AppNotebooks.Form.Tag', {startWithParent: false}); Form.on('before:start', function(options) { Form.controller = new Controller(options); Radio.reply('appNotebooks', 'form:stop', Form.stop, Form); }); Form.on('before:stop', function() { Radio.stopReplying('appNotebooks', 'form:stop'); Form.controller.destroy(); Form.controller = null; }); return Form; }); ================================================ FILE: app/scripts/apps/notebooks/form/tag/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'q', 'underscore', 'marionette', 'backbone.radio', 'apps/notebooks/form/tag/formView' ], function(Q, _, Marionette, Radio, View) { 'use strict'; /** * Tag form controller. * * Listens to events: * 1. channel: `tags`, event: `update:model` * triggers `close` event on the view. * 2. this.view, event: `save` * saves the changes. * 3. this.view, event: `redirect` * stops the module and redirects. * * requests: * 1. channel: `tags`, event: `save` * 2. channel: `uri`, event: `back` * 3. channel: `appNotebooks`, event: `form:stop` */ var Controller = Marionette.Object.extend({ initialize: function(options) { _.bindAll(this, 'show'); // Events this.listenTo(Radio.channel('tags'), 'update:model', this.onSaveAfter); // Fetch the model and render the view Radio.request('tags', 'get:model', options) .then(this.show); }, onDestroy: function() { this.stopListening(); Radio.request('global', 'region:empty', 'modal'); }, show: function(model) { // Instantiate and show the form view this.view = new View({ model: model }); Radio.request('global', 'region:show', 'modal', this.view); // Listen to events this.listenTo(this.view, 'save' , this.save); this.listenTo(this.view, 'redirect', this.redirect); }, save: function() { var data = { name: this.view.ui.name.val().trim() }; Radio.request('tags', 'save', this.view.model, data) .fail(function(e) { console.error('Error:', e); }); }, onSaveAfter: function() { this.view.trigger('close'); }, redirect: function() { Radio.request('appNotebooks', 'form:stop'); Radio.request('uri', 'back', '/notebooks', { includeProfile : true }); } }); return Controller; }); ================================================ FILE: app/scripts/apps/notebooks/form/tag/formView.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'behaviors/modalForm', 'text!apps/notebooks/form/tag/templates/form.html' ], function(_, Marionette, ModalForm, Templ) { 'use strict'; /** * Tag form view. */ var View = Marionette.ItemView.extend({ template: _.template(Templ), className: 'modal fade', ui: { name : 'input[name="name"]' }, behaviors: { ModalForm: { behaviorClass: ModalForm } } }); return View; }); ================================================ FILE: app/scripts/apps/notebooks/form/tag/templates/form.html ================================================ ================================================ FILE: app/scripts/apps/notebooks/list/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'app', 'apps/notebooks/list/controller' ], function(_, Marionette, Radio, App, Controller) { 'use strict'; /** * Notebooks list sub module. * It shows notebooks and tags list. */ var List = App.module('AppNotebooks.List', {startWithParent: false}); List.on('before:start', function(options) { List.controller = new Controller(options); }); List.on('before:stop', function() { List.controller.destroy(); List.controller = null; }); return List; }); ================================================ FILE: app/scripts/apps/notebooks/list/behaviors/compositeBehavior.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio' ], function(_, Marionette, Radio) { 'use strict'; /** * Composite behavior class for notebooks and tags views. * * Triggers the following events: * 1. channel: `appNotebooks`, event: `change:region` * when the user has reached the last or the first model */ var CompositeBehavior = Marionette.Behavior.extend({ defaults: { channel : 'notebooks', regionToChange : 'tags', activeModel : null }, initialize: function() { this.collection = this.view.options.collection; this.uiBody = $('.-scroll'); this.channel = Radio.channel(this.options.channel); // Listen to events on a channel [notebooks|tags] this.listenTo(this.channel, 'model:navigate', this.modelFocus); // View events this.listenTo(this.view, 'navigate:next', this.navigateNext); this.listenTo(this.view, 'navigate:previous', this.navigatePrevious); this.listenTo(this.view, 'childview:scroll:top', this.changeScrollTop); // Collection events this.listenTo(this.collection, 'page:end', this.onPageEnd); this.listenTo(this.collection, 'page:start', this.onPageStart); }, onBeforeDestroy: function() { this.view.collection.trigger('reset:all'); this.stopListening(); this.collection = null; this.channel = null; this.uiBody = null; }, /** * Change scroll position. */ changeScrollTop: function(view, scrollTop) { this.uiBody.scrollTop( scrollTop - this.uiBody.offset().top + this.uiBody.scrollTop() - 100 ); }, /** * Trigger `focus` event on the received model */ modelFocus: _.debounce(function(model) { this.view.options.activeModel = model.id; model.trigger('focus'); }, 10), /** * If a user has reached the first model in a collection * and it is notebooks composite view, trigger change:region event */ onPageEnd: function() { if (this.options.regionToChange === 'tags') { Radio.trigger('appNotebooks', 'change:region', this.options.regionToChange, 'Next'); } }, /** * If a user has reached the first model in a collection * and it is tags composite view, trigger change:region event */ onPageStart: function() { if (this.options.regionToChange === 'notebooks') { Radio.trigger('appNotebooks', 'change:region', this.options.regionToChange, 'Previous'); } }, /** * Navigate to the next model */ navigateNext: function() { this.view.collection.getNextItem(this.view.options.activeModel); }, /** * Navigate to the previous model */ navigatePrevious: function() { this.view.collection.getPreviousItem(this.view.options.activeModel); } }); return CompositeBehavior; }); ================================================ FILE: app/scripts/apps/notebooks/list/behaviors/itemBehavior.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'jquery', 'marionette', 'backbone.radio' ], function($, Marionette, Radio) { 'use strict'; /** * Item behavior class for notebooks and tags views. * * Triggers requests: * 1. channel: `appNotebooks`, request: `[notebooks|tags]:remove` * expects that the provided model will be removed. */ var ItemBehavior = Marionette.Behavior.extend({ modelEvents: { 'focus': 'makeActive' }, events: { 'click .remove-link': 'triggerRemove' }, triggerRemove: function() { var event = this.view.model.storeName + ':remove'; Radio.request('appNotebooks', event, null, this.view.model.id); return false; }, makeActive: function() { // Search by data-id attribute because child objects have the same class name var $item = this.view.$('.list-group-item[data-id=' + this.view.model.get('id') + ']'); $('.list-group-item.active').removeClass('active'); $item.addClass('active'); this.view.trigger('scroll:top', $item.offset().top); } }); return ItemBehavior; }); ================================================ FILE: app/scripts/apps/notebooks/list/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'marionette', 'backbone.radio', 'apps/notebooks/list/views/layout', 'apps/notebooks/list/views/notebooksComposite', 'apps/notebooks/list/views/tagsComposite' ], function(_, Q, Marionette, Radio, View, NotebooksView, TagsView) { 'use strict'; /** * List controller. * * Triggers: * 1. channel: `global`, event: `filter:change` */ var Controller = Marionette.Object.extend({ initialize: function(options) { _.bindAll(this, 'show'); this.options = options; // Show the navbar and change document title Radio.request('navbar', 'start', { title : 'Notebooks & tags', filter : 'notebook' }); // Fetch Q.all([ Radio.request('notebooks', 'get:all', options), Radio.request('tags', 'get:all', options) ]).spread(this.show) .fail(function(e) { console.error('Error:', e); }); }, onDestroy: function() { Radio.request('global', 'region:empty', 'sidebar'); }, show: function(notebooks, tags) { this.view = new View({ notebooks : notebooks, tags : tags, configs : Radio.request('configs', 'get:object') }); Radio.request('global', 'region:show', 'sidebar', this.view); // Show notebooks this.view.notebooks.show(new NotebooksView({ collection: notebooks })); // Show tags this.view.tags.show(new TagsView({ collection: tags })); } }); return Controller; }); ================================================ FILE: app/scripts/apps/notebooks/list/templates/layout.html ================================================
================================================ FILE: app/scripts/apps/notebooks/list/templates/notebooksItem.html ================================================ {=cleanXSS(name)}
================================================ FILE: app/scripts/apps/notebooks/list/templates/notebooksList.html ================================================
================================================ FILE: app/scripts/apps/notebooks/list/templates/tagsItem.html ================================================ {=cleanXSS(name)} {{count}} ================================================ FILE: app/scripts/apps/notebooks/list/templates/tagsList.html ================================================
================================================ FILE: app/scripts/apps/notebooks/list/views/layout.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'behaviors/sidebar', 'text!apps/notebooks/list/templates/layout.html', 'mousetrap' ], function(_, Marionette, Radio, Behavior, Tmpl, Mousetrap) { 'use strict'; /** * Notebooks layout view. * It shows lists of tags and notebooks. * * Listens to events: * 1. channel: `appNotebooks`, event: `change:region` * switches to another region. * * Triggers events: * 1. `navigate:next` to currently active region * 2. `navigate:previous` to currently active region * * requests: * 1. channel: `uri`, request: `navigate` */ var View = Marionette.LayoutView.extend({ template: _.template(Tmpl), regions: { notebooks : '#notebooks', tags : '#tags' }, behaviors: { SidebarBehavior: { behaviorClass: Behavior } }, // Default active region is `notebooks` activeRegion: 'notebooks', initialize: function() { _.bindAll(this, 'triggerNext', 'triggerPrevious', 'openActive', 'openEdit', 'triggerRemove', 'focusRegion'); // Make tags active region if there aren't any notebooks if (!this.options.notebooks.length) { this.activeRegion = 'tags'; } this.listenTo(Radio.channel('notebooks'), 'model:navigate', this.focusRegion); this.listenTo(Radio.channel('tags'), 'model:navigate', this.focusRegion); // Register keyboard events Mousetrap.bind(this.options.configs.navigateBottom, this.triggerNext); Mousetrap.bind(this.options.configs.navigateTop, this.triggerPrevious); Mousetrap.bind(this.options.configs.actionsOpen, this.openActive); Mousetrap.bind(this.options.configs.actionsEdit, this.openEdit); Mousetrap.bind(this.options.configs.actionsRemove, this.triggerRemove); // Listen to events this.listenTo(Radio.channel('appNotebooks'), 'change:region', this.changeRegion); }, onBeforeDestroy: function() { Mousetrap.unbind([ this.options.configs.navigateBottom, this.options.configs.navigateTop, this.options.configs.actionsOpen, this.options.configs.actionsEdit, this.options.configs.actionsRemove ]); this.stopListening(); }, triggerNext: function() { this[this.activeRegion].currentView.trigger('navigate:next'); }, triggerPrevious: function() { this[this.activeRegion].currentView.trigger('navigate:previous'); }, triggerRemove: function() { var $a = this.$('.list-group-item.active').parent().find('.remove-link:first'); $a.trigger('click'); return false; }, openActive: function() { var $a = this.$('.list-group-item.active'); Radio.request('uri', 'navigate', $a.attr('href')); }, openEdit: function() { var $a = this.$('.list-group-item.active').parent().find('.edit-link:first'); Radio.request('uri', 'navigate', $a.attr('href')); }, /** * Make sure a model's region is active. * For example, if a user navigated to a tag, tags region should be * active. */ focusRegion: _.debounce(function(model) { this.activeRegion = model.storeName; }, 100), /** * Switch from one region to another. For example, from `tags` to * `notebooks`. */ changeRegion: function(regionName, direction) { // Don't change active region if (!this.options[regionName].length) { return; } this.activeRegion = regionName; /* * Reset active model variable and * call either triggerNext or triggerPrevious method */ this[this.activeRegion].currentView.options.activeModel = null; this['trigger' + direction](); } }); return View; }); ================================================ FILE: app/scripts/apps/notebooks/list/views/notebooksComposite.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'apps/notebooks/list/behaviors/compositeBehavior', 'apps/notebooks/list/views/notebooksItem', 'text!apps/notebooks/list/templates/notebooksList.html' ], function(_, Marionette, Behavior, ItemView, Tmpl) { 'use strict'; /** * Notebooks composite view. * Everything happens in its behavior class. */ var View = Marionette.CompositeView.extend({ template: _.template(Tmpl), childView : ItemView, childViewContainer : '.list-notebooks', behaviors: { CompositeBehavior: { behaviorClass: Behavior } }, /** * Build a tree structure */ showCollection: function() { Marionette.CompositeView.prototype.showCollection.apply(this, arguments); var fragment = document.createDocumentFragment(); this.children.each(function(view) { this.attachFragment(this, view, fragment); }, this); this.$(this.childViewContainer).append(fragment); }, /** * Don't use the default method of attaching items */ attachHtml: function() {}, /** * For performance's sake attach items into fragment. */ attachFragment: function(colView, itemView, fragment) { var parentId = String(itemView.model.get('parentId')); // It isn't a child notebook if (parentId === '0' || parentId === '' || !fragment.getElementById(parentId)) { fragment.appendChild(itemView.el); } else { fragment.getElementById(parentId).appendChild(itemView.el); } } }); return View; }); ================================================ FILE: app/scripts/apps/notebooks/list/views/notebooksItem.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'apps/notebooks/list/behaviors/itemBehavior', 'text!apps/notebooks/list/templates/notebooksItem.html' ], function(_, Marionette, Radio, ItemBehavior, Tmpl) { 'use strict'; /** * Notebooks item view. * Everything happens in its behaviour. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), className: 'list--group list-group', behaviors: { ItemBehavior: { behaviorClass: ItemBehavior } }, serializeData: function() { return _.extend(this.model.toJSON(), { uri : Radio.request('uri', 'link:profile', '') }); } }); return View; }); ================================================ FILE: app/scripts/apps/notebooks/list/views/tagsComposite.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'apps/notebooks/list/behaviors/compositeBehavior', 'apps/notebooks/list/views/tagsItem', 'text!apps/notebooks/list/templates/tagsList.html' ], function(_, Marionette, Radio, Behavior, ItemView, Tmpl) { 'use strict'; /** * Tags composite view. * Everything happens in its behavior class. */ var View = Marionette.CompositeView.extend({ template: _.template(Tmpl), childView: ItemView, childViewContainer: '.list--tags', behaviors: { CompositeBehavior: { behaviorClass : Behavior, channel : 'tags', regionToChange : 'notebooks' } }, collectionEvents: { 'page:next': 'onPageNext' }, onPageNext: function() { this.collection.getPage(this.collection.state.currentPage + 1); }, templateHelpers: function() { return { uri : function() { return Radio.request('uri', 'link:profile', ''); } }; } }); return View; }); ================================================ FILE: app/scripts/apps/notebooks/list/views/tagsItem.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'apps/notebooks/list/behaviors/itemBehavior', 'text!apps/notebooks/list/templates/tagsItem.html' ], function(_, Marionette, Radio, ItemBehavior, Tmpl) { 'use strict'; /** * Tags item view. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), className: 'list--group list-group', behaviors: { ItemBehavior: { behaviorClass: ItemBehavior } }, serializeData: function() { return _.extend(this.model.toJSON(), { uri : Radio.request('uri', 'link:profile', '') }); } }); return View; }); ================================================ FILE: app/scripts/apps/notebooks/remove/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'text!apps/notebooks/remove/notebooks.html' ], function(_, Marionette, Radio, Tmpl) { 'use strict'; /** * Remove controller. Handles removing of notebooks and tags. * * Listens to events on channel `[notebooks|tags]`: * 1. event: `destroy:model` * * requests: * 1. channel: [notebooks|tags], request: `remove` * expects that the model will be destroyed. * 2. channel: `Confirm`, request: `start` */ var Controller = Marionette.Object.extend({ initialize: function(modelType, profile, id) { _.bindAll(this, 'showConfirm'); this.channel = Radio.channel(modelType); profile = profile || Radio.request('uri', 'profile'); // Events this.listenTo(this.channel, 'destroy:model', this.destroy); this.listenTo(Radio.channel('Confirm'), 'cancel', this.destroy); this.listenTo(Radio.channel('Confirm'), 'confirm', this.remove); this.listenTo(Radio.channel('Confirm'), 'confirmNotes', this.removeWithNotes); // Fetch a tag or a notebook by ID this.channel.request('get:model', {profile: profile, id: id}) .then(this.showConfirm); }, onDestroy: function() { this.stopListening(); this.channel = null; }, showConfirm: function(model) { this.model = model; Radio.request('Confirm', 'start', { content : $.t(model.storeName + '.confirm remove', model.toJSON()), template: model.storeName === 'notebooks' ? Tmpl : undefined }); }, remove: function() { this.channel.request('remove', this.model, {profile: this.model.profileId}, false); }, removeWithNotes: function() { this.channel.request('remove', this.model, {profile: this.model.profileId}, true); } }); return Controller; }); ================================================ FILE: app/scripts/apps/notebooks/remove/notebooks.html ================================================
================================================ FILE: app/scripts/apps/notes/appNote.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requirejs */ define([ 'underscore', 'jquery', 'marionette', 'app', 'backbone.radio', 'enquire', 'apps/notes/list/listApp' ], function(_, $, Marionette, App, Radio, enquire, SidebarApp) { 'use strict'; /** * AppNote module. * * Listens to * -------- * Events on channel `appNote`: * 1. `form:show` - shows a form where a user can add/edit new notes * 2. `notes:toggle` - make the sidebar region active * * Replies on channel `appNote`: * 1. `route:args` - returns current route arguments */ var AppNote = App.module('AppNote', { startWithParent: false }), executeAction, API; /** * The router */ AppNote.Router = Marionette.AppRouter.extend({ appRoutes: { '' : 'showIndex', 'p/:profile' : 'filterNotes', // Edit/Add notes '(p/:profile/)notes/add' : 'noteForm', '(p/:profile/)notes/edit/:id' : 'noteForm', // Filter notes '(p/:profile/)notes(/f/:filter)(/q/:query)(/p:page)' : 'filterNotes', '(p/:profile/)notes(/f/:filter)(/q/:query)(/p:page)(/show/:id)' : 'showNote' }, // Start this module onRoute: function() { if (!AppNote._isInitialized) { var args = arguments[0] === 'noteForm' ? {} : arguments[2]; App.startSubApp('AppNote', API._getArgs.apply(this, args)); } } }); /** * Starts a submodule */ executeAction = function(module, args) { if (!module) { return; } // Stop previous module if (AppNote.currentApp) { AppNote.currentApp.stop(); } AppNote.currentApp = module; module.start(args); // If module has stopped, remove the variable module.on('stop', function() { AppNote.currentApp = null; }); }; /** * Router's controller */ API = { // Index page showIndex: function() { this.filterNotes(); }, // Filter collection filterNotes: function() { var args = this._getArgs.apply(this, arguments); // Wait until the SidebarApp has started if (!SidebarApp._isInitialized) { return SidebarApp.on('start', function() { API.filterNotes(args); }); } Radio.request('appNote', 'filter', args); }, // Show a note showNote: function() { var args = this._getArgs.apply(this, arguments); requirejs(['apps/notes/show/app'], function(Module) { executeAction(Module, args); }); }, // Shows a form for editing or adding notes noteForm: function(profile, id) { var args = _.extend({}, this.notesArg || {}, { id : id, profile : profile }); // Start 'Form' module requirejs(['apps/notes/form/app'], function(Module) { args.method = id ? 'edit' : 'add'; executeAction(Module, args); }); }, // Remove an existing note removeNote: function(id) { requirejs(['apps/notes/remove/controller'], function(Controller) { API.notesArg.id = null; new Controller(_.extend({}, API.notesArg || {}, {id: id})); }); }, // Make sidebar active _toggleSidebar: function(args) { this.$content = this.$content || $(App.content.el); this.$content.removeClass('active-row'); this.filterNotes.apply(this, args); }, // Builds an object from router arguments _getArgs: function(profile, filter, query, page, id) { if (arguments.length === 1 && typeof arguments[0] === 'object') { return arguments[0]; } this.notesArg = { id : id, page : Number(page || 0), query : query, filter : filter, profile : profile || Radio.request('uri', 'profile'), }; return this.notesArg; } }; /** * Module's initializer/finalizer */ AppNote.on('before:start', function(options) { // Show the sidebar SidebarApp.start(options); // Listen to events this.listenTo(Radio.channel('appNote'), 'notes:toggle', API._toggleSidebar); this.listenTo(Radio.channel('global'), 'form:show', function() { Radio.request('uri', 'navigate', '/notes/add', { trigger : true, includeProfile: true }); }); // Respond to requests and requests Radio.channel('appNote') .reply('remove:note', API.removeNote, API) .reply('route:args', function() {return API.notesArg;}, API); }); AppNote.on('before:stop', function() { // Stop the sidebar app SidebarApp.stop(); // Stop the current module if (AppNote.currentApp) { AppNote.currentApp.stop(); AppNote.currentApp = null; } // Stop listenning to events this.stopListening(); // Stop responding to requests and requests Radio.channel('appNote') .stopReplying('remove:note') .stopReplying('route:args'); }); /** * Register the router */ App.on('before:start', function() { new AppNote.Router({ controller: API }); }); return AppNote; }); ================================================ FILE: app/scripts/apps/notes/form/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'jquery', 'app', 'backbone.radio', 'marionette', 'apps/notes/form/controller' ], function(_, $, App, Radio, Marionette, Controller) { 'use strict'; /** * Form app. Instantiates form controller. * * Listens to the following events: * 1. channel: notesForm, event: stop * stops itself */ var Form = App.module('AppNote.Form', { startWithParent: false }); Form.on('before:start', function(options) { Form.controller = new Controller(options); Radio.on('notesForm', 'stop', Form.stop, Form); }); Form.on('before:stop', function() { Radio.off('notesForm', 'stop'); Form.controller.destroy(); Form.controller = null; }); return Form; }); ================================================ FILE: app/scripts/apps/notes/form/behaviors/desktop.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'marionette' ], function(Marionette) { 'use strict'; var Desktop = Marionette.Behavior.extend({ initialize: function() { console.warn('Hullo', 'desktop'); }, }); return Desktop; }); ================================================ FILE: app/scripts/apps/notes/form/behaviors/mobile.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'marionette' ], function(Marionette) { 'use strict'; var Desktop = Marionette.Behavior.extend({ initialize: function() { console.warn('Hullo', 'mobile'); }, }); return Desktop; }); ================================================ FILE: app/scripts/apps/notes/form/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requirejs */ define([ 'underscore', 'q', 'jquery', 'backbone.radio', 'marionette', 'i18next', 'apps/notes/form/views/formView', 'apps/notes/form/views/notebooks' ], function (_, Q, $, Radio, Marionette, i18n, View, NotebooksView) { 'use strict'; /** * Note form controller. * * Triggers the following events: * 1. channel: notesForm, event: stop * * Listens to the following events: * 1. channel: notes, event: save:after * * requests: * 1. channel: notes, request: save * to save the changes. */ var Controller = Marionette.Object.extend({ initialize: function(options) { this.options = options; // Saves data before you change anything, in case you cancel editing this.dataBeforeChange = null; // Data should be deleted if user wants to cancel editing this.deleteData = false; _.bindAll(this, 'show', 'redirect'); // Fetch everything Q.all([ Radio.request('notes', 'get:model:full', options), Radio.request('notebooks', 'get:all', _.pick(options, 'profile')) ]) .spread(this.show) .catch(function() { console.error('Editor error', arguments); }); // Events this.listenTo(Radio.channel('notes'), 'update:model', this.redirect); this.listenTo(Radio.channel('Confirm'), 'confirm', this.redirect); this.listenTo(Radio.channel('Confirm'), 'cancel', this.onConfirmCancel); }, onDestroy: function() { this.stopListening(); this.view.trigger('destroy'); }, show: function(note, notebooks) { var notebooksView; note = note[0]; // Set document title Radio.request('global', 'set:title', note.get('title')); // Use behaviours that are appropriate for a device. if (Radio.request('global', 'platform') === 'mobile') { delete View.prototype.behaviors.Desktop; } else { delete View.prototype.behaviors.Mobile; } this.view = new View({ model : note, profile : note.profileId }); // Show the view and trigger an event Radio.request('global', 'region:show', 'content', this.view); this.view.trigger('rendered'); /* * Resolve the notebook ID. * If the current note doesn't have a notebook attached, * try to use one from the filter if it specifies a notebook. */ var activeId = note.get('notebookId'); if (activeId === '0' && this.options.filter === 'notebook') { activeId = this.options.query; } // Show notebooks selector notebooksView = new NotebooksView({ collection : notebooks, activeId : activeId }); this.view.notebooks.show(notebooksView); // Listen to view events this.listenTo(this.view, 'save', this.save); this.listenTo(this.view, 'cancel', this.showConfirm); // Get data before any change is made // so that it can be reset when you cancel editing var self = this; this.getContent() .then(function(data) { self.dataBeforeChange = data; }) .fail(function(e) { console.error('Error getting data on start', e); }); }, save: function() { var self = this; return this.getContent() .then(function(data) { if (data.title === '') { var title = i18n.t('Untitled'); data.title = title; Radio.request('global', 'set:title', title); } return Radio.request('notes', 'save', self.view.model, data, self.view.options.saveTags); }) .fail(function(e) { console.error('Error', e); }); }, getContent: function() { var self = this; return Radio.request('editor', 'get:data') .then(function(data) { return _.extend(data, { title : self.view.ui.title.val().trim(), notebookId : self.view.notebooks.currentView.ui.notebookId.val().trim(), }); }); }, /** * Warn a user that they have made some changes. */ showConfirm: function() { var self = this; return this.getContent() .then(function(data) { // Redirect if data wasn't changed if (_.isEqual(self.dataBeforeChange, data)) { return self.redirect(); } // User perhaps wants to cancel editing, // if not, deleteData will be set false again in onConfirmCancel self.deleteData = true; Radio.request('Confirm', 'start', $.t('You have unsaved changes')); }) .fail(function(e) { console.error('form ShowConfirm', e); }); }, // Called when the cancel dialog was accepted redirect: function() { if (!this.view.getOption('redirect')) { return; } // Stop the module and navigate back if(this.deleteData){ this.deleteData = false; if (this.options.method === 'add') { // Delete the note if editing of a new note was canceled var self = this; requirejs(['apps/notes/remove/controller'], function(Controller) { new Controller(_.extend({}, {id: self.view.model.id, deleteDirect: true})); }); } else if (this.options.method === 'edit') { // Save the note without any change // if editing of an existing note was canceled Radio.request('notes', 'save', this.view.model, this.dataBeforeChange); } } Radio.trigger('notesForm', 'stop'); Radio.request('uri', 'back'); }, // Called when the cancel dialog was canceled onConfirmCancel: function() { // Rebind keybindings again because TW bootstrap modal overrites ESC. this.view.trigger('bind:keys'); this.view.options.isClosed = false; if (this.view.options.focus !== 'editor') { return this.view.ui[this.view.options.focus].focus(); } Radio.trigger('editor', 'focus'); } }); return Controller; }); ================================================ FILE: app/scripts/apps/notes/form/templates/form.html ================================================
================================================ FILE: app/scripts/apps/notes/form/templates/notebooks.html ================================================
================================================ FILE: app/scripts/apps/notes/form/views/formView.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'jquery', 'underscore', 'marionette', 'backbone.radio', 'mousetrap', 'text!apps/notes/form/templates/form.html', 'behaviors/content', 'apps/notes/form/behaviors/desktop', 'apps/notes/form/behaviors/mobile', 'mousetrap.global' ], function($, _, Marionette, Radio, Mousetrap, Tmpl, Behavior, Desktop, Mobile) { 'use strict'; /** * Note form view. * * Triggers the following * Events: * 1. channel: notesForm, event: view:ready * when the view is ready. * 2. channel: notesForm, event: view:destroy * before the view is destroyed. * 3. channel: notesForm, event: set:mode * when "Edit mode" has changed. * * Responds to the following * Requests: * 1. channel: notesForm, request: model * returns current model. * 2. channel: notesForm, request: show:editor * shows the provided view in the `editor` region. * * Listens to the following events: * 1. channel: notesForm, event: save:auto * saves then the note automaticaly */ var View = Marionette.LayoutView.extend({ template: _.template(Tmpl), className: 'layout--body', regions: { editor : '#editor', notebooks : '#editor--notebooks' }, behaviors: { Content: { behaviorClass: Behavior }, Desktop: { behaviorClass: Desktop }, Mobile: { behaviorClass: Mobile } }, ui: { // Form form : '.editor--form', saveBtn : '.editor--save', title : '#editor--input--title' }, events: { 'click .editor--mode a' : 'switchMode', // Handle saving 'submit @ui.form' : 'save', 'click @ui.saveBtn' : 'save', 'click .editor--cancel' : 'cancel' }, initialize: function() { _.bindAll(this, 'autoSave', 'save', 'cancel'); this.configs = Radio.request('configs', 'get:object'); this.$body = $('body'); // Events and replies Radio.channel('notesForm') .reply('model', this.model, this) .reply('show:editor', this.showEditor, this) .on('save:auto', this.autoSave, this); // Register keybindings this.bindKeys(); // The view is ready this.listenTo(this, 'rendered', this.onRendered); this.listenTo(this, 'bind:keys', this.bindKeys); }, bindKeys: function() { Mousetrap.bindGlobal(['ctrl+s', 'command+s'], this.save); Mousetrap.bindGlobal(['esc'], this.cancel); }, onRendered: function() { Radio.trigger('notesForm', 'view:ready'); // Focus on the 'title' this.ui.title.trigger('focus'); // Change edit mode if (this.configs.editMode !== 'normal') { this.switchMode(this.configs.editMode); } }, onBeforeDestroy: function() { this._normalMode(); // Trigger an event Radio.trigger('notesForm', 'view:destroy'); // Stop listening to events Radio.channel('notesForm') .stopReplying('model show:editor') .off('save:auto'); // Destroy shortcuts Mousetrap.unbind(['ctrl+s', 'command+s', 'esc']); }, showEditor: function(view) { this.editor.show(view); }, /** * Close the form without saving. */ cancel: function() { // Save which element was under focus this.options.focus = this.ui.title.is(':focus') ? 'title' : 'editor'; this.options.isClosed = true; this.options.redirect = true; this.trigger('cancel'); return false; }, autoSave: function() { if (this.options.isClosed) { return; } // Don't save tags when auto save notes // so that no unfinished tags are saved this.options.saveTags = false; this.options.redirect = false; console.log('Auto saving the note...'); this.trigger('save'); }, save: function(e) { if (e.preventDefault) { e.preventDefault(); } this.options.saveTags = true; this.options.isClosed = true; this.options.redirect = true; this.trigger('save'); return false; }, switchMode: function(e) { var mode = (typeof e !== 'string' ? $(e.currentTarget).attr('data-mode') : e); if (!mode) { return; } // Close a dropdown menu this.ui.form.trigger('click'); // Switch to another mode this['_' + mode + 'Mode'].apply(this); // Trigger an event Radio.trigger('notesForm', 'set:mode', mode); return false; }, _fullscreenMode: function() { this.$body .removeClass('-preview') .addClass('editor--fullscreen'); }, _previewMode: function() { this.$body.addClass('editor--fullscreen -preview'); }, _normalMode: function() { this.$body.removeClass('editor--fullscreen -preview'); } }); return View; }); ================================================ FILE: app/scripts/apps/notes/form/views/notebook.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette' ], function(_, Marionette) { 'use strict'; var View = Marionette.ItemView.extend({ template : _.template('{=cleanXSS(name)}'), tagName : 'option', onRender : function() { this.$el.attr('value', this.model.get('id')); } }); return View; }); ================================================ FILE: app/scripts/apps/notes/form/views/notebooks.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'apps/notes/form/views/notebook', 'text!apps/notes/form/templates/notebooks.html' ], function(_, Marionette, Radio, ItemView, Tmpl) { 'use strict'; /** * Notebooks view. It shows a selector of notebooks. * * requests: * 1. channel: `appNotebooks`, request: `show:form` * in order to show the notebook form. */ var View = Marionette.CompositeView.extend({ template: _.template(Tmpl), childView : ItemView, childViewContainer : '.editor--notebooks--list', ui: { notebookId : '[name="notebookId"]' }, events: { 'change @ui.notebookId': 'addNotebook' }, collectionEvents: { 'change' : 'render', 'add:model': 'selectModel' }, onRender: function() { this.ui.notebookId.val(this.options.activeId); }, selectModel: function(model) { this.ui.notebookId.val(model.id); }, addNotebook: function() { if (this.ui.notebookId.find('.addNotebook').is(':selected')) { var self = this; Radio.request('appNotebooks', 'show:form') .then(function(notebook) { // Set the active notebook if one was created by the user. if (notebook) { self.ui.notebookId.val(notebook.id); } }); this.ui.notebookId.val(this.options.activeId); } } }); return View; }); ================================================ FILE: app/scripts/apps/notes/list/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'jquery', 'underscore', 'backbone.radio', 'marionette', 'apps/notes/list/views/noteSidebar' ], function ($, _, Radio, Marionette, Sidebar) { 'use strict'; /** * Notes list controller - shows notes list in the sidebar * * Listens to * ---------- * Events: * 1. channel: `appNote`, event: `model:active` * triggers `focus` event on the passed model. * 2. channel: `notes`, event: `model:navigate` * navigates to the model which was passed with the event. * * Triggers * -------- * Events: * 1. channel: `global`, event: `navigate` * to navigate to a note page */ var Controller = Marionette.Object.extend({ initialize: function(options) { this.radio = Radio.channel; this.options = options; _.bindAll(this, 'show', 'filter', 'navigate'); // Get configs this.configs = Radio.request('configs', 'get:object'); // Listen to events this.listenTo(this.radio('appNote'), 'model:active', this.onModelActive, this); this.listenTo(this.radio('notes'), 'model:navigate', _.debounce(this.navigate, 420)); // Fetch notes and show them this.filter(options); }, onDestroy: function() { this.view.trigger('destroy'); this.view.collection.trigger('reset:all'); }, /** * Renders the sidebar view */ show: function(notes) { // Destroy old view if (this.view) { this.view.trigger('destroy'); this.view = null; } // Render the view this.view = new Sidebar({ collection: notes, args : this.options }); Radio.request('global', 'region:show', 'sidebar', this.view); }, /** * Fetches data from `Notes` collection. */ filter: _.debounce(function(options) { options.profile = options.profile || Radio.request('uri', 'profile'); var tOptions = this.view ? this.view.options.args : {}, isEqual = _.isEqual( _.pick((tOptions), 'filter', 'profile', 'query', 'page'), _.pick(options , 'filter', 'profile', 'query', 'page') ); // Do not fetch anything because nothing has changed if (isEqual) { return; } // Show the navbar Radio.request('navbar', 'start', options); options.pageSize = this.configs.pagination; this.options = options; // Fetch data Radio.request('notes', 'get:all', options) .then(this.show); }, 100), onModelActive: function(model) { // The view was not rendered or the model is already active if (!this.view || !model || model.id === this.view.options.args.id) { return; } // Trigger `focus` event to a model. this.view.options.args.id = model.id; model = this.view.collection.get(model.id); if (model) { model.trigger('focus'); } }, navigate: function(model) { var args = Radio.request('appNote', 'route:args'); /** * Before navigating to a note, change URI. * It is done because if a user navigates back to the same page * a note might not appear at all. */ Radio.request('uri', 'navigate', {options: args}, {trigger: false}); // Navigate to a note page Radio.request( 'uri', 'navigate', {options: args, model: model}, {trigger: true} ); } }); return Controller; }); ================================================ FILE: app/scripts/apps/notes/list/listApp.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'app', 'backbone.radio', 'apps/notes/list/controller' ], function(_, App, Radio, Controller) { 'use strict'; /** * List module - shows notes list in the sidebar. * * Listens to events on channel `appNote`: * 1. `filter` - filters notes */ var List = App.module('AppNote.List', { startWithParent: false }); List.on('before:start', function(options) { List.controller = new Controller(options); Radio.reply('appNote', 'filter', List.controller.filter, List.controller); }); List.on('before:stop', function() { Radio.channel('appNote').stopReplying('filter'); List.controller.destroy(); List.controller = null; }); return List; }); ================================================ FILE: app/scripts/apps/notes/list/templates/sidebarList.html ================================================
<% if (collection.state.totalPages > 1) { %> <% } %> ================================================ FILE: app/scripts/apps/notes/list/templates/sidebarListItem.html ================================================

{= cleanXSS(title, false, true) }

{{ getContent() }}

================================================ FILE: app/scripts/apps/notes/list/views/noteSidebar.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'marionette', 'backbone.radio', 'behaviors/sidebar', 'apps/notes/list/views/noteSidebarItem', 'text!apps/notes/list/templates/sidebarList.html', 'mousetrap' ], function(_, Marionette, Radio, Behavior, NoteSidebarItem, Tmpl, Mousetrap) { 'use strict'; /** * Sidebar composite view. * * Listens to * ----------- * Events: * 1. channel: `notes`, event: `model:navigate` * Makes the provided model active. */ var View = Marionette.CompositeView.extend({ template : _.template(Tmpl), childView : NoteSidebarItem, childViewContainer : '.list', childViewOptions : {}, behaviors: { SidebarBehavior: { behaviorClass: Behavior } }, ui: { pageNav : '#pageNav', prevPage : '#prevPage', nextPage : '#nextPage' }, events: { 'click @ui.nextPage': 'nextPage', 'click @ui.prevPage': 'previousPage' }, collectionEvents: { 'page:next' : 'nextPage', 'page:previous' : 'previousPage', 'reset' : 'updatePagination' }, childEvents: { 'scroll:top': 'changeScrollTop' }, initialize: function() { _.bindAll(this, 'toNextNote', 'toPreviousNote'); this.$scroll = $('#sidebar .-scroll'); this.configs = Radio.request('configs', 'get:object'); // Shortcuts Mousetrap.bind(this.configs.navigateBottom, this.toNextNote); Mousetrap.bind(this.configs.navigateTop, this.toPreviousNote); // Events this.listenTo(Radio.channel('notes'), 'model:navigate', this.modelFocus, this); // Pass options to childView this.childViewOptions.args = this.options.args; }, onDestroy: function() { Mousetrap.unbind([this.configs.navigateBottom, this.configs.navigateTop]); }, onBeforeRender: function() { this.options.args = Radio.request('appNote', 'route:args') || this.options.args; this.childViewOptions.args = this.options.args; }, /** * Makes the provided model active. */ modelFocus: _.debounce(function(model) { this.options.args.id = model.id; model.trigger('focus'); }, 10), toNextNote: function() { this.collection.getNextItem(this.options.args.id); return false; }, toPreviousNote: function() { this.collection.getPreviousItem(this.options.args.id); return false; }, /** * Updates pagination buttons */ updatePagination: function() { this.ui.pageNav.toggleClass('hidden', this.collection.state.totalPages <= 1); this.ui.prevPage.toggleClass('disabled', !this.collection.hasPreviousPage()); this.ui.nextPage.toggleClass('disabled', !this.collection.hasNextPage()); }, /** * Gets next page's models and resets the collection */ nextPage: function() { if (this.ui.nextPage.hasClass('disabled')) { return false; } this.navigatePage(1); this.collection.getNextPage(); }, /** * Gets previous page's models and resets the collection */ previousPage: function() { if (this.ui.prevPage.hasClass('disabled')) { return false; } this.navigatePage(-1); this.collection.getPreviousPage(); }, /** * Saves page status in window.location */ navigatePage: function(number) { this.options.args.page = this.collection.state.currentPage + number; Radio.request( 'uri', 'navigate', {options: this.options.args}, {trigger: false} ); }, /** * Changes scroll position. */ changeScrollTop: function(view, scrollTop) { this.$scroll.scrollTop( scrollTop - this.$scroll.offset().top + this.$scroll.scrollTop() - 100 ); }, serializeData: function() { var viewData = { collection : this.collection, args : this.options.args }; return viewData; } }); return View; }); ================================================ FILE: app/scripts/apps/notes/list/views/noteSidebarItem.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'backbone.radio', 'marionette', 'text!apps/notes/list/templates/sidebarListItem.html', ], function(_, Radio, Marionette, Tmpl) { 'use strict'; /** * Sidebar item view. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), className: 'list-group list--group', ui: { favorite : '.favorite', }, events: { 'click @ui.favorite': 'toggleFavorite' }, modelEvents: { 'change' : 'render', 'change:trash' : 'remove', 'focus' : 'onChangeFocus' }, initialize: function() { this.options.args.page = this.model.collection.state.currentPage; }, toggleFavorite: function() { Radio.request('notes', 'save', this.model, this.model.toggleFavorite()); return false; }, onChangeFocus: function() { var $listGroup = this.$('.list-group-item'); $('.list-group-item.active').removeClass('active'); $listGroup.addClass('active'); this.trigger('scroll:top', $listGroup.offset().top); }, serializeData: function() { // Decrypting return _.extend(this.model.toJSON(), { args : this.options.args }); }, templateHelpers: function() { return { // Show only first 50 characters of the content getContent: function() { return _.unescape(this.content).substring(0, 50); }, // Generate link link: function() { return Radio.request('uri', 'link', this.args, this); }, isActive: function() { return this.args.id === this.id ? 'active' : ''; } }; } }); return View; }); ================================================ FILE: app/scripts/apps/notes/remove/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'backbone.radio', 'marionette' ], function(_, Radio, Marionette) { 'use strict'; /** * A controller which removes a note. * * Requests: * 1. channel: `notes`, request: `get:model` * expects to receive a model with provided ID * 2. channel: `notes`, request: `remove` * 3. channel: `Confirm`, request: `start` */ var Controller = Marionette.Object.extend({ labels: [ 'notes.confirm trash', 'notes.confirm remove' ], initialize: function(options) { this.options = options; _.bindAll(this, 'showConfirm'); // Events this.listenTo(Radio.channel('notes'), 'destroy:model', this.destroy); this.listenTo(Radio.channel('Confirm'), 'cancel', this.destroy); this.listenTo(Radio.channel('Confirm'), 'confirm', this.remove); // Fetch the note by ID var self = this; Radio.request('notes', 'get:model', options) .then(function(model){ // Delete note without dialog when new note was canceled if(self.options.deleteDirect){ Radio.request('notes', 'remove',model); } // Or else show the dialog else{ self.showConfirm(model); } }); }, /** * Show a confirmation dialog before removing a note. */ showConfirm: function(model) { var content = this.labels[Number(model.get('trash'))]; this.model = model; Radio.request('Confirm', 'start', { content : $.t(content, model.toJSON()) }); }, remove: function() { Radio.request('notes', 'remove', this.model); } }); return Controller; }); ================================================ FILE: app/scripts/apps/notes/show/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'jquery', 'app', 'backbone.radio', 'marionette', 'apps/notes/show/controller' ], function(_, $, App, Radio, Marionette, Controller) { 'use strict'; /** * A module which instantiates the controller that shows a note. * * Listens to * ---------- * Events: * 1. channel: `notes`, event: `model:navigate` * stops itself after this event. */ var Show = App.module('AppNote.Show', { startWithParent: false }); Show.on('before:start', function(options) { Show.controller = new Controller(options); this.listenTo(Radio.channel('notes'), 'model:navigate', Show.stop, Show); }); Show.on('before:stop', function() { this.stopListening(Radio.channel('notes')); Show.controller.destroy(); Show.controller = null; }); return Show; }); ================================================ FILE: app/scripts/apps/notes/show/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'apps/notes/show/noteView' ], function(_, Marionette, Radio, View) { 'use strict'; /** * The controller that shows a note. * * Triggers the following: * Events: * 1. channel: `appNote`, event: `model:active` * Request: * 1. channel: editor, request: task:toggle * request: * 1. channel: notes, request: save * 2. channel: appNote, request: `remove:note` * in order to destroy a model * 3. channel: global, request: `set:title` * * Requests: * 1. channel: markdown, request: render * it expects to receive HTML. */ var Controller = Marionette.Object.extend({ initialize: function(options) { _.bindAll(this, '_show'); this.options = options; // Fetch the note by ID Radio.request('notes', 'get:model:full', options) .spread(this._show); this.listenTo(Radio.channel('notes'), 'destroy:model', this.onModelDestroy, this); }, onDestroy: function() { this.stopListening(this.view); this.stopListening(Radio.channel('notes')); Radio.request('global', 'region:empty', 'content'); }, _show: function(note, notebook) { var self = this; Radio.request('markdown', 'render', note) .then(function(content) { return self.render(note, content, notebook); }); }, render: function(note, content, notebook) { // Trigger an event that the model is active Radio.trigger('appNote', 'model:active', note); this.view = new View({ model : note, content : content, notebook : notebook, args : this.options, files : [], }); // Show the view in the `content` region Radio.request('global', 'region:show', 'content', this.view); // Set document title Radio.request('global', 'set:title', note.get('title')); // Events this.listenTo(Radio.channel('notes'), 'synced:' + note.id, this.onSync); this.listenTo(this.view, 'toggle:task', this.toggleTask); this.listenTo(this.view, 'model:remove', this.modelRemove); this.listenTo(this.view, 'model:restore', this.modelRestore); }, /** * After a model is synchronized, refetch the model again. */ onSync: function() { var self = this; Radio.request('notes', 'get:model:full', this.options) .spread(function(note, notebook) { Radio.request('markdown', 'render', note) .then(function(content) { self.view.options.notebook = notebook; self.view.options.content = content; self.view.model.set(note.attributes); self.view.model.trigger('synced'); }); }) .fail(function(e) { console.error('After sync error:', e); }); }, modelRestore: function() { Radio.request('notes', 'restore', this.view.model); }, /** * Triggers an event and expects that a model will be destroyed */ modelRemove: function() { Radio.request('appNote', 'remove:note', this.view.model.id); }, /** * Tries to get new content with toggled task. * It is expected that such editor modules as Pagedown * replies to the request `task:toggle` and returns an object with * counts of completed tasks and content with toggled task. */ toggleTask: function(taskId) { var model = this.view.model; return Radio.request('markdown', 'task:toggle', { content : model.get('content'), taskId : taskId }) .then(function(data) { if (!data) { return; } // Save the note Radio.request('notes', 'save', model, _.pick(data, 'content', 'taskCompleted', 'taskAll')); }); }, /** * Destroy this controller if the model is destroyed. */ onModelDestroy: function(model) { if (this.view.model.attributes.id === model.attributes.id) { this.destroy(); } } }); return Controller; }); ================================================ FILE: app/scripts/apps/notes/show/noteView.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'jquery', 'marionette', 'backbone.radio', 'behaviors/content', 'text!apps/notes/show/templates/item.html', 'mousetrap' ], function(_, $, Marionette, Radio, Behavior, Tmpl, Mousetrap) { 'use strict'; /** * Note view. * * Triggers the following * Events: * 1. channel: noteView, event: view:render * when the view is rendered and ready. * 2. channel: noteView, event: view:destroy * before the view is destroyed. * Requests: * 1. channel: global, request: configs */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), className: 'layout--body', behaviors: { ContentBehavior: { behaviorClass: Behavior } }, ui: { favorite : '.btn--favourite--icon', body : '.-scroll', // Tasks tasks : '.task [type="checkbox"]', progress : '.progress-bar', percent : '.progress-percent', // Action buttons editBtn : '.note--edit', rmBtn : '.note--remove' }, events: { 'click .btn--favourite' : 'favorite', 'click @ui.tasks' : 'onClickTask', 'click @ui.rmBtn' : 'rmNote' }, triggers: { 'click .note--restore' : 'model:restore' }, modelEvents: { 'synced' : 'render', 'change:trash' : 'render', 'change:isFavorite' : 'onChangeFavorite', 'change:taskCompleted' : 'onTaskCompleted' }, initialize: function() { _.bindAll(this, 'scrollTop', 'scrollDown', 'editNote', 'rmNote', 'favorite'); this.configs = Radio.request('configs', 'get:object'); Mousetrap.bind('up', this.scrollTop); Mousetrap.bind('down', this.scrollDown); Mousetrap.bind(this.configs.actionsEdit, this.editNote); Mousetrap.bind(this.configs.actionsRemove, this.rmNote); Mousetrap.bind(this.configs.actionsRotateStar, this.favorite); }, onRender: function() { Radio.trigger('noteView', 'view:render', this); }, onBeforeDestroy: function() { Mousetrap.unbind(['up', 'down', this.configs.actionsEdit, this.configs.actionsRemove, this.configs.actionsRotateStar]); Radio.trigger('noteView', 'view:destroy'); }, scrollTop: function() { this.ui.body.scrollTop(this.ui.body.scrollTop() - 50); return false; }, scrollDown: function() { this.ui.body.scrollTop(this.ui.body.scrollTop() + 50); return false; }, editNote: function() { Radio.request('uri', 'navigate', this.ui.editBtn.attr('href')); }, rmNote: function() { this.trigger('model:remove'); return false; }, /** * Changes favorite status of the note */ favorite: _.throttle(function() { Radio.request('notes', 'save', this.model, this.model.toggleFavorite()); return false; }, 300, {leading: false}), onChangeFavorite: function() { this.ui.favorite.toggleClass('icon-favorite', this.model.get('isFavorite')); }, onClickTask: function(e) { e.preventDefault(); this.toggleTask(e); }, /** * Toggle the status of a task */ toggleTask: _.debounce(function(e) { var $task = $(e.target), taskId = Number($task.attr('data-task')); $task.blur(); $task.prop('checked', $task.is(':checked') === false); this.trigger('toggle:task', taskId); }, 200), /** * Update progress bar when the status of a task was changed */ onTaskCompleted: function() { var percent = Math.floor( this.model.get('taskCompleted') * 100 / this.model.get('taskAll') ); this.ui.progress.css({width: percent + '%'}); this.ui.percent.html(percent + '%'); }, serializeData: function() { // var content = Radio.request('markdown', 'render', this.model); return _.extend(this.model.toJSON(), { content : this.options.content || this.model.get('content'), notebook : this.options.notebook.toJSON(), uri : Radio.request('uri', 'link:profile', '/') }); }, templateHelpers: function() { return { createdDate: function() { return new Date(this.created).toLocaleDateString(); }, getProgress: function() { return Math.floor(this.taskCompleted * 100 / this.taskAll); } }; } }); return View; }); ================================================ FILE: app/scripts/apps/notes/show/templates/item.html ================================================

{=cleanXSS(title)} {{createdDate()}}

<% if (taskAll !== 0) { %>
{{getProgress()}}%
<% } %>

<% if (notebook && notebook.id) { %> {=cleanXSS(notebook.name)} <% } %>

{=content}
================================================ FILE: app/scripts/apps/settings/appSettings.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requirejs */ define([ 'underscore', 'marionette', 'backbone.radio', 'app', 'apps/settings/sidebar/app' ], function(_, Marionette, Radio, App, SidebarApp) { 'use strict'; /** * Settings sub app. */ var Settings = App.module('AppSettings', {startWithParent: false}), controller; /** * The router */ Settings.Router = Marionette.AppRouter.extend({ appRoutes: { '(p/:profile/)settings(/:tab)' : 'showSettings', '(p/:profile/)settings/module/:tab' : 'showModule' }, // Starts itself onRoute: function() { if (!Settings._isInitialized) { App.startSubApp( 'AppSettings', controller.getOptions.apply(controller, arguments[2]) ); } } }); controller = { showSettings: function(profile, tab) { requirejs(['apps/settings/show/app'], function(Module) { // Stop previously started module if (Settings.currentApp && controller.args.tab !== tab) { Settings.currentApp.stop(); } Settings.currentApp = Module; Module.start(controller.getOptions(profile, tab)); }); }, showModule: function(profile, module) { requirejs(['apps/settings/module/app'], function(Module) { // Stop previously started module if (Settings.currentApp) { Settings.currentApp.stop(); } Settings.currentApp = Module; Module.start({profile: profile, module: module}); }); }, getOptions: function() { controller.args = { profile : arguments[0], tab : arguments[1] || 'general', }; return controller.args; } }; /** * Initializer and finalizer */ Settings.on('before:start', function(options) { SidebarApp.start(options); }); Settings.on('before:stop', function() { Settings.currentApp.stop(); Settings.currentApp = null; controller.args = null; SidebarApp.stop(); }); // Register the router App.on('before:start', function() { new Settings.Router({ controller: controller }); }); }); ================================================ FILE: app/scripts/apps/settings/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'q', 'underscore', 'marionette', 'backbone.radio', 'apps/settings/show/views/showView' ], function(Q, _, Marionette, Radio, View) { 'use strict'; /** * The main controller. * * Replies: * 1. channel: `AppSettings`, replies: `has:changes` * if there are some changes, show a confirm message * * requests: * 1. channel: `AppSettings`, request: `activate` * 2. channel: `global`, request: `region:empty` * 3. channel: `global`, request: `region:show` * 4. channel: `configs`, request: `create:profile` * 5. channel: `configs`, request: `remove:profile` * 6. channel: `configs`, request: `save:objects` * 7. channel: `Confirm`, request: `start` * 8. channel: `uri`, request: `navigate` * 9. channel: `navbar`, request: `stop` */ var Controller = Marionette.Object.extend({ initialize: function(options) { _.bindAll(this, 'show', 'onFetch', 'requireView', 'redirect'); options.tab = options.tab || 'modules'; this.options = options; this.changes = {}; this.saves = {}; // Fetch configs Q.all([ Radio.request('configs', 'get:all', options), Radio.request('configs', 'get:model', { name : 'useDefaultConfigs', profile : options.profile }), Radio.request('configs', 'get:model', 'appProfiles') ]) .spread(this.onFetch) .then(this.requireView) .fail(function(e) { console.error('Error:', e); }); // Events, replies Radio.reply('AppSettings', 'has:changes', this.hasChanges, this); }, onDestroy: function() { this.stopListening(); Radio.stopReplying('AppSettings', 'has:changes'); Radio.request('global', 'region:empty', 'content'); }, /** * Show the layout. */ onFetch: function(configs, useDefault, profiles) { this.configs = configs; this.profiles = profiles; this.useDefault = useDefault; // Instantiate layout view and show it this.layout = new View(this.options); Radio.request('global', 'region:show', 'content', this.layout); }, /** * Load a content view. */ requireView: function() { }, /** * Render the content view. */ show: function(ContentView) { this.view = new ContentView({ collection : this.configs, profiles : this.profiles, useDefault : this.useDefault }); this.layout.content.show(this.view); // Collection events this.listenTo(this.configs, 'new:value', this.onChange); // Layout events this.listenTo(this.layout, 'cancel', this.confirmRedirect); this.listenTo(this.layout, 'save', this.save); }, onChange: function(data) { this.changes[data.name] = data; }, /** * Reload the page when config 'useDefaultConfigs' is changed */ onChangeConfigs: function(changes) { if (changes.useDefaultConfigs) { window.location.reload(); } this.redirect(); }, save: function() { // Do nothing if there are not any changes if (_.isEmpty(this.changes)) { this.redirect(); return; } Radio.channel('configs') .request('save:objects', this.changes, this.useDefault); this.saves = _.union(this.saves, this.changes); this.changes = {}; }, /** * Before closing the page, show a confirm message */ confirmRedirect: function() { this.showConfirm(this.redirect); }, /** * If there are any changes, show a confirm message. */ hasChanges: function() { var defer = Q.defer(); this.showConfirm(defer.resolve, defer.reject); return defer.promise; }, showConfirm: function(onconfirm, onreject) { if (_.isEmpty(this.changes)) { return onconfirm(); } Radio.request('Confirm', 'start', { content : $.t('You have unsaved changes'), onconfirm : onconfirm, onreject : onreject }); }, }); return Controller; }); ================================================ FILE: app/scripts/apps/settings/module/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'app', 'apps/settings/module/controller' ], function(_, Marionette, Radio, App, Controller) { 'use strict'; var Module = App.module('AppSettings.Module', {startWithParent: false}); /** * Initializer & finalizer */ Module.on('before:start', function(options) { Module.controller = new Controller(options); }); Module.on('before:stop', function() { Module.controller.destroy(); Module.controller = null; }); return Module; }); ================================================ FILE: app/scripts/apps/settings/module/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requirejs */ define([ 'q', 'underscore', 'marionette', 'backbone.radio', 'apps/settings/controller', ], function(Q, _, Marionette, Radio, BasicController) { 'use strict'; /** * Module configs. */ var Controller = BasicController.extend({ /** * Show layout view and load a module view */ requireView: function() { requirejs(['modules/' + this.options.module + '/views/settings'], this.show); }, redirect: function() { Radio.request('uri', 'navigate', '/settings/modules', { trigger : false, includeProfile : true }); window.location.reload(); }, }); return Controller; }); ================================================ FILE: app/scripts/apps/settings/show/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'app', 'apps/settings/show/controller' ], function(_, Marionette, Radio, App, Controller) { 'use strict'; var Show = App.module('AppSettings.Show', {startWithParent: false}); /** * Initializer & finalizer */ Show.on('before:start', function(options) { Show.controller = new Controller(options); }); Show.on('before:stop', function() { Show.controller.destroy(); Show.controller = null; }); return Show; }); ================================================ FILE: app/scripts/apps/settings/show/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requirejs */ define([ 'q', 'underscore', 'marionette', 'backbone.radio', 'apps/settings/controller', 'helpers/fileSaver', ], function(Q, _, Marionette, Radio, BasicController, fileSaver) { 'use strict'; /** * Settings show controller. */ var Controller = BasicController.extend({ initialize: function(options) { // Execute parent initializer first _.bind(BasicController.prototype.initialize, this)(options); // Activate tab in sidebar Radio.request('AppSettings', 'activate:tab', this.options.tab); // Events, replies this.listenTo(Radio.channel('configs'), 'changed', this.onChangeConfigs); }, /** * Load the tab view. */ requireView: function() { requirejs(['apps/settings/show/views/' + this.options.tab], this.show); }, /** * Show settings. */ show: function(TabView) { // Execute parent function first _.bind(BasicController.prototype.show, this)(TabView); // View events this.listenTo(this.view, 'remove:profile', this.confirmRmProfile); this.listenTo(this.view, 'create:profile', this.createProfile); this.listenTo(this.view, 'import', this.import); this.listenTo(this.view, 'export', this.export); }, /** * Create a new profile. */ createProfile: function(name) { Radio.request('configs', 'create:profile', this.profiles, name); }, /** * Before removing a profile, show a confirm message. */ confirmRmProfile: function(name) { var self = this; Radio.request('Confirm', 'start', { content : $.t('profile.confirm remove', {profile: name}), onconfirm : function() { self.removeProfile(name); } }); }, /** * Remove a profile */ removeProfile: function(name) { Radio.request('configs', 'remove:profile', this.profiles, name); }, /** * Import settings from a JSON file */ import: function(data) { var reader = new FileReader(), self = this; reader.onload = function(evt) { try { self.changes = JSON.parse(evt.target.result); self.save(); } catch (e) { Radio.request('Confirm', 'start', { title : $.t('Wrong format'), content : $.t('File chould be in json format') }); } }; reader.readAsText(data.files[0]); }, /** * Export settings to a JSON file. */ export: function() { var blob = new Blob( [JSON.stringify(this.configs)], {type: 'text/plain;charset=utf8'} ); fileSaver(blob, 'laverna-settings.json'); }, /** * Before closing the page, show a confirm message */ confirmRedirect: function() { this.showConfirm(this.redirect); }, redirect: function() { Radio.request('uri', 'navigate', '/notes', { trigger : false, includeProfile : true }); window.location.reload(); } }); return Controller; }); ================================================ FILE: app/scripts/apps/settings/show/formBehavior.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'marionette', 'sjcl' ], function(Marionette, sjcl) { 'use strict'; /** * Default behaviour for settings views. */ var FormBehavior = Marionette.Behavior.extend({ events: { 'input input, select, textarea' : 'triggerChange', 'change input, select, textarea': 'triggerChange', 'change .show-onselect' : 'showOnSelect', 'click .showField' : 'showOnCheck', }, initialize: function() { this.view.on('render', this.popover, this); }, popover: function() { var pop = this.view.$('.popover-dropbox').html(); this.view.$('.popover-key').popover({ trigger: 'click', html : true, content: function() { return pop; } }); }, triggerChange: function(e) { var el = $(e.target), conf = { name: el.attr('name') }; if (el.attr('type') !== 'checkbox') { conf.value = el.val(); } else { conf.value = (el.is(':checked')) ? 1 : 0; } if (el.hasClass('hex') && typeof conf.value === 'string') { conf.value = sjcl.codec.hex.toBits(conf.value); } this.view.collection.trigger('new:value', conf); }, /** * Shows additional parameters when option is selected */ showOnSelect: function(e) { var $el = $(e.target), option = $el.find('option[value=' + $el.attr('data-option') + ']'); if (option.is(':selected')) { $( option.attr('data-show') ).removeClass('hidden'); } else { $( option.attr('data-show') ).addClass('hidden'); } }, /** * Shows fieldsets with aditional parameters when checkbox is checked */ showOnCheck: function(e) { var input = $(e.currentTarget), field = $(input.attr('data-field')); if ( input.is(':checked') ) { field.removeClass('hidden'); } else { field.addClass('hidden'); } } }); return FormBehavior; }); ================================================ FILE: app/scripts/apps/settings/show/module.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'marionette' ], function(Marionette) { 'use strict'; /** * Module settings. */ var Controller = Marionette.Object.extend({ initialize: function() { }, }); return Controller; }); ================================================ FILE: app/scripts/apps/settings/show/templates/editor.html ================================================
= 3){%>style="display: none;"<%}%>>{{ i18n('Recommended 3 or more, for indenting numbered lists') }}
================================================ FILE: app/scripts/apps/settings/show/templates/encryption.html ================================================
disabled="disabled"<% } %>>
================================================ FILE: app/scripts/apps/settings/show/templates/general.html ================================================ <% if (!isDefaultProfile()) { %>
<% } %>
================================================ FILE: app/scripts/apps/settings/show/templates/importExport.html ================================================

{{i18n('Transfer settings')}}

{{i18n('Transfer everything')}}

================================================ FILE: app/scripts/apps/settings/show/templates/keybindings.html ================================================
{{ i18n('Navigation') }} <% _.forEach(filter('navigate'), function (model) { %>
<% }); %>
{{ i18n('Jump') }} <% _.forEach(filter('jump'), function (model) { %>
<% }); %>
{{ i18n('Actions') }} <% _.forEach(filter('actions'), function (model) { %>
<% }); %>
{{ i18n('App') }} <% _.forEach(appShortcuts(), function (model) { %>
<% }); %>
================================================ FILE: app/scripts/apps/settings/show/templates/modules.html ================================================ <% _.each(modules, function(module) { %>

{{module.name}}

{{module.description}}

<% if (isEnabled(module)) {%> <% if (module.hasSettings) {%> {{i18n('Settings')}} <% } %> <% } else { %> <% } %>
<% }) %> ================================================ FILE: app/scripts/apps/settings/show/templates/profiles.html ================================================ <% _.forEach (appProfiles, function(profile) { %> <% }); %>
{{ i18n('profile.profile name') }} {{ i18n('Action') }}
{{ profile }} <% if (profile !== 'notes-db') { %> <% } %>
================================================ FILE: app/scripts/apps/settings/show/templates/showTemplate.html ================================================
================================================ FILE: app/scripts/apps/settings/show/templates/sync.html ================================================
================================================ FILE: app/scripts/apps/settings/show/views/editor.js ================================================ /** * Copyright (C) 2016 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'apps/settings/show/formBehavior', 'text!apps/settings/show/templates/editor.html' ], function(_, Marionette, FormBehavior, Tmpl) { 'use strict'; /** * Note editor settings */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), behaviors: { FormBehavior: { behaviorClass: FormBehavior } }, ui: { indentUnit: '#indentUnit', indentUnitLowWarning: '#indentUnit-low-warning' }, events: { 'change #indentUnit' : 'checkIndentUnit' }, serializeData: function() { return { models: this.collection.getConfigs() }; }, checkIndentUnit: function() { if (this.ui.indentUnit.val() < 3) { this.ui.indentUnitLowWarning.show(); } else { this.ui.indentUnitLowWarning.hide(); } } }); return View; }); ================================================ FILE: app/scripts/apps/settings/show/views/encryption.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'sjcl', 'apps/settings/show/formBehavior', 'text!apps/settings/show/templates/encryption.html' ], function(_, Marionette, Radio, sjcl, FormBehavior, Tmpl) { 'use strict'; /** * Encryption settings. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), behaviors: { FormBehavior: { behaviorClass: FormBehavior.extend({ /** * Prevent from saving an empty password. */ triggerChange: function(e) { var $e = $(e.currentTarget); if ($e.attr('name') !== 'encryptPass' || $e.val().length) { $e.parent().parent().removeClass('has-error'); FormBehavior.prototype.triggerChange.apply(this, arguments); return; } $e.parent().parent().addClass('has-error'); } }) } }, ui: { settings : '#encryptOpt', saltInput : 'input[name=encryptSalt]', password : 'input[name=encryptPass]' }, events: { 'click #useEncryption' : 'toggleSettings', 'click #randomize' : 'randomize', 'blur @ui.password' : 'randomizeOnPassword' }, serializeData: function() { return {models : this.collection.getConfigs()}; }, templateHelpers: function() { return { hex: function(str) { if (typeof str === 'string') { return str; } return sjcl.codec.hex.fromBits(str); }, passwordText: function() { if (this.models.encryptPass.length !== 0) { return $.t('encryption.change password'); } return $.t('encryption.provide password'); }, }; }, initialize: function() { sjcl.random.startCollectors(); }, onDestroy: function() { sjcl.random.stopCollectors(); }, /** * Toggle active status of encryption settings. */ toggleSettings: function() { var state = this.ui.settings.attr('disabled') !== 'disabled'; this.ui.settings.attr('disabled', state); }, /** * Automatically generate new encryption salt every time the password is * changed. */ randomizeOnPassword: function() { if (!this.ui.password.val().trim().length) { return; } this.randomize(); }, /** * Generate random salt. */ randomize: function() { var random = Radio.request('encrypt', 'randomize', 5, 0); this.ui.saltInput.val(random).trigger('change'); return false; } }); return View; }); ================================================ FILE: app/scripts/apps/settings/show/views/general.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'i18next', 'text!locales/locales.json', 'apps/settings/show/formBehavior', 'text!apps/settings/show/templates/general.html' ], function(_, Marionette, Radio, i18n, locales, FormBehavior, Tmpl) { 'use strict'; /** * General settings. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), behaviors: { FormBehavior: { behaviorClass: FormBehavior } }, serializeData: function() { var appLang = this.collection.get('appLang'); return { locales : JSON.parse(locales), models : this.collection.getConfigs(), appLang : (appLang.get('value') || i18n.language) || 'en', useDefault : this.options.useDefault.toJSON() }; }, templateHelpers: function() { return { isDefaultProfile: function() { var profile = Radio.request('uri', 'profile'); return _.indexOf([null, 'notes-db'], profile) > -1; }, isLocaleActive: function(locale) { if (this.appLang === locale || this.appLang.search(locale) >= 0) { return ' selected'; } } }; } }); return View; }); ================================================ FILE: app/scripts/apps/settings/show/views/importExport.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'text!apps/settings/show/templates/importExport.html' ], function (_, Marionette, Radio, Tmpl) { 'use strict'; /** * Import or export settings */ var ImportExport = Marionette.ItemView.extend({ template: _.template(Tmpl), ui: { importBtn : '#do-import', import : '#import-file', // Export / import buttons importData : '#import-data-file', exportData : '#export-data', }, events: { 'click .btn--import' : 'triggerClick', 'change @ui.import' : 'triggerImport', 'change @ui.importData' : 'triggerImportData', 'click @ui.exportData' : 'triggerExportData' }, triggers: { 'click #do-export' : 'export' }, triggerImport: function(e) { if (!e.target.files.length) { return; } this.trigger('import', e.target); }, triggerImportData: function(e) { if (!e.target.files.length) { return; } Radio.request('importExport', 'import', e.target.files); }, triggerExportData: function() { Radio.request('importExport', 'export'); }, triggerClick: function(e) { var file = $(e.currentTarget).attr('data-file'); $(file).click(); } }); return ImportExport; }); ================================================ FILE: app/scripts/apps/settings/show/views/keybindings.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'apps/settings/show/formBehavior', 'text!apps/settings/show/templates/keybindings.html' ], function(_, Marionette, FormBehavior, Tmpl) { 'use strict'; /** * Keybinding settings */ var Shortcuts = Marionette.ItemView.extend({ template: _.template(Tmpl), behaviors: { FormBehavior: { behaviorClass: FormBehavior } }, serializeData: function() { return { collection: this.collection }; }, templateHelpers: function() { return { filter: function(str) { return this.collection.filterName(str); }, appShortcuts: function() { return this.collection.appShortcuts(); } }; } }); return Shortcuts; }); ================================================ FILE: app/scripts/apps/settings/show/views/modules.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'text!apps/settings/show/templates/modules.html' ], function(_, Marionette, Radio, Tmpl) { 'use strict'; /** * Sync settings. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), events: { 'click .-enable' : 'toggle', 'click .-disable' : 'toggle', }, serializeData: function() { return { active : this.collection.get('modules').get('value'), modules : this.options.modules }; }, templateHelpers: function() { return { isEnabled: function(module) { return _.indexOf(this.active, module.id) > -1; } }; }, initialize: function() { this.options.modules = Radio.request('global', 'modules'); }, /** * Enable or disable a module. */ toggle: function(e) { var id = $(e.currentTarget).attr('data-id'), configs = this.collection.get('modules').get('value'), self = this; e.preventDefault(); if (_.indexOf(configs, id) === -1) { configs.push(id); } else { configs = _.without(configs, id); } // Save the list of modules Radio.request('configs', 'save', this.collection.get('modules'), {value: configs}) .then(function() { self.render(); }); } }); return View; }); ================================================ FILE: app/scripts/apps/settings/show/views/profiles.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'text!apps/settings/show/templates/profiles.html' ], function(_, Marionette, Radio, Tmpl) { 'use strict'; /** * Show, add, remove profiles */ var Profiles = Marionette.ItemView.extend({ template: _.template(Tmpl), ui: { profile : '#profileName' }, events: { 'keypress @ui.profile' : 'createProfile', 'click .removeProfile' : 'removeProfile' }, initialize: function() { this.listenTo(this.options.profiles, 'change', this.render); }, removeProfile: function(e) { this.trigger('remove:profile', $(e.currentTarget).attr('data-profile')); return false; }, createProfile: function(e) { if (e.which === 13) { e.preventDefault(); this.trigger('create:profile', this.ui.profile.val().trim()); this.ui.profile.val('').blur(); } }, serializeData: function() { return { appProfiles: this.options.profiles.getValueJSON() }; } }); return Profiles; }); ================================================ FILE: app/scripts/apps/settings/show/views/showView.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'jquery', 'marionette', 'behaviors/content', 'text!apps/settings/show/templates/showTemplate.html' ], function(_, $, Marionette, Behavior, Tmpl) { 'use strict'; /** * Settings layout view */ var View = Marionette.LayoutView.extend({ template: _.template(Tmpl), regions: { content: 'form' }, behaviors: { ContentBehavior: { behaviorClass: Behavior } }, events: { 'click .settings--save' : 'save', 'click .settings--cancel' : 'cancel' }, onRender: function() { this.$cancel = $('.settings--cancel'); this.$cancel.on('click', _.bind(this.cancel, this)); }, onBeforeDestroy: function() { this.$cancel.off('click'); }, serializeData: function() { return this.options; }, cancel: function(e) { e.preventDefault(); console.warn('cancel'); this.trigger('cancel'); }, save: function(e) { var view = this.content.currentView; e.preventDefault(); /* * If the password was autofilled by a user's browser, it usually will * not trigger `change` event. This will fix it. */ if (view.ui && view.ui.password) { this.content.currentView.ui.password.trigger('change'); } this.trigger('save'); } }); return View; }); ================================================ FILE: app/scripts/apps/settings/show/views/sync.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'constants', 'apps/settings/show/formBehavior', 'text!apps/settings/show/templates/sync.html' ], function(_, Marionette, constants, FormBehavior, Tmpl) { 'use strict'; /** * Sync settings. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), behaviors: { FormBehavior: { behaviorClass: FormBehavior } }, serializeData: function() { return { models : this.collection.getConfigs(), dropboxKeyNeed : constants.DROPBOXKEYNEED }; } }); return View; }); ================================================ FILE: app/scripts/apps/settings/sidebar/app.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'app', 'apps/settings/sidebar/controller' ], function(_, Marionette, Radio, App, Controller) { 'use strict'; var Sidebar = App.module('AppSettings.Sidebar', {startWithParent: false}); /** * Initializer & finalizer */ Sidebar.on('before:start', function(options) { Sidebar.controller = new Controller(options); }); Sidebar.on('before:stop', function() { Sidebar.controller.destroy(); Sidebar.controller = null; }); return Sidebar; }); ================================================ FILE: app/scripts/apps/settings/sidebar/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'apps/settings/sidebar/view', 'apps/settings/sidebar/views/navbar' ], function(_, Marionette, Radio, View, Navbar) { 'use strict'; /** * Sidebar controller for settings module */ var Controller = Marionette.Object.extend({ initialize: function(options) { this.options = options; this.show(); }, onDestroy: function() { this.stopListening(); this.view.trigger('destroy'); Radio.request('global', 'region:empty', 'sidebarNavbar'); }, show: function() { this.view = new View(this.options); Radio.request('global', 'region:show', 'sidebar', this.view); // Show a different Navbar Radio.request('navbar', 'stop'); Radio.request('global', 'region:show', 'sidebarNavbar', new Navbar()); } }); return Controller; }); ================================================ FILE: app/scripts/apps/settings/sidebar/template.html ================================================
================================================ FILE: app/scripts/apps/settings/sidebar/templates/navbar.html ================================================ ================================================ FILE: app/scripts/apps/settings/sidebar/view.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'marionette', 'backbone.radio', 'behaviors/sidebar', 'text!apps/settings/sidebar/template.html' ], function(_, Q, Marionette, Radio, Behavior, Tmpl) { 'use strict'; /** * Sidebar view for settings */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), behaviors: { SidebarBehavior: { behaviorClass: Behavior } }, events: { 'click a': 'confirm' }, serializeData: function() { return { uri: Radio.request('uri', 'link:profile', '') }; }, initialize: function() { Radio.channel('AppSettings') .reply('activate:tab', this.activateTab, this); }, onDestroy: function() { Radio.channel('AppSettings') .stopReplying('activate:tab'); }, onRender: function() { this.activateTab(this.options.tab); }, /** * Before navigating to another page, ask for confirmation. */ confirm: function(e) { e.preventDefault(); Radio.request('AppSettings', 'has:changes') .then(function() { Radio.request('uri', 'navigate', $(e.currentTarget).attr('href')); }); }, activateTab: function(tab) { this.$('.active').removeClass('active'); this.$('[href*=' + tab + ']').addClass('active'); } }); return View; }); ================================================ FILE: app/scripts/apps/settings/sidebar/views/navbar.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'text!apps/settings/sidebar/templates/navbar.html' ], function(_, Marionette, Radio, Tmpl) { 'use strict'; /** * Settings navbar */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl) }); return View; }); ================================================ FILE: app/scripts/backbone.noworker.sync.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'q', 'underscore', 'backbone.sync', 'helpers/db' ], function(Q, _, Sync, DB) { 'use strict'; var Adapter = _.extend({}, Sync, { promises: [], /** * Create a new worker and start listening to its events. * * @return function */ sync: function() { var self = this, sync; sync = function(method, model, options) { return self[method](model, options); }; return sync; }, /** * Send a message to the Webworker. * * @type string msg * @type object data */ _emit: function(msg, data) { return DB[msg](data); } }); return Adapter; }); ================================================ FILE: app/scripts/backbone.sync.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'q', 'underscore' ], function(Q, _) { 'use strict'; var Adapter = { promises: {}, /** * Create a new worker and start listening to its events. * * @return function */ sync: function() { _.bindAll(this, 'listenToWorker', 'backboneSync'); this.worker = new Worker('scripts/workers/localForage.js'); // Promise which signifies whether the worker is ready this.workerPromise = Q.defer(); // Start listening to WebWorker events this.worker.onmessage = this.listenToWorker; // A function for Backbone sync return this.backboneSync; }, /** * Resolve promises after receiving WebWorker messages. * * @type object data */ listenToWorker: function(data) { var msg = data.data; switch (msg.msg) { // Database webworker is ready case 'ready': this.workerPromise.resolve(); break; // Request was fullfilled case 'done': this.promises[msg.promiseId].resolve(msg.data); delete this.promises[msg.promiseId]; break; // Request failed with errors case 'fail': this.promises[msg.promiseId].reject(msg.data); delete this.promises[msg.promiseId]; break; default: } }, /** * With this method Backbone.sync will be overriden. * * @return promise */ backboneSync: function(method, model, options) { // First, make sure WebWorker is ready return this.workerPromise.promise .then(_.bind(function() { // Execute the method (read, create, update) return this[method](model, options); }, this)); }, /** * Find a model in the database by ID or models in a collection. * * @type object model * @type object options */ read: function(model, options) { // If it has an ID, it is a model if (model.id) { return this.find(model, options); } return this.findAll(model, options); }, create: function() { return this.save.apply(this, arguments); }, update: function() { return this.save.apply(this, arguments); }, /** * Generate four random hex digits. */ /* jshint ignore:start */ s4: function() { return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); }, /** * Generate a pseudo-GUID by concatenating random hexadecimal. */ guid: function() { return (this.s4() + this.s4() + "-" + this.s4() + "-" + this.s4() + "-" + this.s4() + "-" + this.s4() + this.s4() + this.s4()); }, /* jshint ignore:end */ /** * Save a model. * * @type object Backbone model * @type object options */ save: function(model, options) { // Generate an ID if it wasn't provided model.id = model.id || this.guid(); model.set(model.idAttribute, model.id); return this._emit('save', { id : model.id, data : model.toJSON(), options : { profile : options.profile || model.profileId, storeName : model.storeName, encryptKeys : model.encryptKeys } }); }, /** * Find a model by ID. * * @type object Backbone model * @type object options */ find: function(model, options) { return this._emit('find', { id : model.id, options : { profile : options.profile || model.profileId, storeName : model.storeName } }) .then(function(res) { model.set(res); return model; }); }, /** * Find models in a collection. * * @type object Backbone collection * @type object options */ findAll: function(collection, options) { return this._emit('findAll', { options: { conditions : options.conditions, storeName : collection.storeName, profile : options.profile || collection.profileId } }) .then(function(res) { if (res && res.length) { collection.add(res); } return collection; }); }, /** * Send a message to the Webworker. * * @type string msg * @type object data */ _emit: function(msg, data) { // Generate a unique ID for the worker's promise var promiseId = this.guid(); this.promises[promiseId] = Q.defer(); this.worker.postMessage({ msg : msg, promiseId : promiseId, data : data }); return this.promises[promiseId].promise; } }; return Adapter; }); ================================================ FILE: app/scripts/behaviors/content.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio' ], function(_, Marionette, Radio) { 'use strict'; /** * Content region behaviour */ var Content = Marionette.Behavior.extend({ events: { 'swiperight' : 'showSidebar', 'click #show--sidebar': 'showSidebar', }, initialize: function() { var channel = Radio.channel('region'); this.showContent(); this.listenActive(); this.listenTo(channel, 'sidebar:shown', this.listenActive); this.listenTo(channel, 'content:hidden', this.showSidebar); this.listenTo(channel, 'content:shown', this.showContent); }, onRender: function() { this.view.$el.hammer(); }, /** * When some active element is clicked, hide the sidebar. */ listenActive: function() { if (!this.$active || !this.$active.length) { this.$active = $('.list--item.active, .list--settings.active'); this.$active.on('click.toggle', this.showContent); } }, onDestroy: function() { if (this.$active) { this.showSidebar(); this.$active.off('click.toggle'); } }, /** * Show only the content */ showContent: function() { Radio.request('global', 'region:hide', 'sidebar', 'hidden-xs'); Radio.request('global', 'region:visible', 'content', 'hidden-xs'); }, /** * Show only the sidebar. */ showSidebar: function() { Radio.request('global', 'region:visible', 'sidebar', 'hidden-xs'); Radio.request('global', 'region:hide', 'content', 'hidden-xs'); } }); return Content; }); ================================================ FILE: app/scripts/behaviors/modal.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'marionette' ], function(Marionette) { 'use strict'; /** * Typical behaviors of modal views */ var ModalBehavior = Marionette.Behavior.extend({ initialize: function() { this.view.on('hidden.modal', this.redirect, this); }, /** * Triggers redirect event when it's closed */ redirect: function() { this.view.trigger('redirect'); } }); return ModalBehavior; }); ================================================ FILE: app/scripts/behaviors/modalForm.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'behaviors/modal' ], function(_, Marionette, ModalBehavior) { 'use strict'; var ModalForm = Marionette.Behavior.extend({ behaviors: { ModalBehavior: { behaviorClass: ModalBehavior } }, defaults: { uiFocus: 'name' }, events: {}, triggers: { 'submit form' : 'save', 'click .ok' : 'save', 'click .cancelBtn' : 'close' }, modelEvents: { 'invalid': 'showErrors' }, initialize: function() { this.events['keyup @ui.' + this.options.uiFocus] = 'closeOnEsc'; this.view.on('shown.modal', this.onFormShown, this); }, onFormShown: function() { this.view.ui[this.options.uiFocus].focus(); }, /** * Triggers 'close' event when user hits ESC */ closeOnEsc: function(e) { if (e.which === 27) { this.view.trigger('close'); } }, /** * Shows validation errors */ showErrors: function(model, errors) { _.forEach(errors, function(err) { this.view.ui[err].parent().addClass('has-error'); if (this.view.ui[err].attr('type') === 'text') { this.view.ui[err].attr('placeholder', $.t(model.storeName + '.' + err)); } }, this); } }); return ModalForm; }); ================================================ FILE: app/scripts/behaviors/sidebar.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'jquery', 'marionette', 'backbone.radio' ], function(_, $, Marionette, Radio) { 'use strict'; /** * Sidebar region behaviour */ var Sidebar = Marionette.Behavior.extend({ defaults: { events: { 'swipeleft' : 'onSwipeLeft', 'swiperight' : 'onSwipeRight', } }, onRender: function() { var hammer = $('#sidebar--content').hammer(); _.each(this.options.events, function(func, ev) { hammer.bind(ev, this.view[func] || this[func]); }, this); }, /** * Show sidemenu. */ onSwipeRight: function() { Radio.trigger('sidemenu', 'show'); }, /** * Switch to content region (hide sidebar). */ onSwipeLeft: function() { Radio.trigger('region', 'content:shown'); }, }); return Sidebar; }); ================================================ FILE: app/scripts/behaviors/sidemenu.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'jquery', 'marionette', 'backbone.radio', 'mousetrap' ], function(_, $, Marionette, Radio, Mousetrap) { 'use strict'; /** * This behaviour can be used for views which need to show * hamburger type of menu. */ var Sidemenu = Marionette.Behavior.extend({ ui: { sidemenu: '.sidemenu' }, defaults: { events: { 'swipeleft' : 'hideMenu', } }, events: { 'click .sidemenu--open' : 'showMenu', 'click .sidemenu--close' : 'hideMenu', 'click .sidemenu a' : 'hideMenu' }, initialize: function() { this.listenTo(Radio.channel('sidemenu'), 'show', this.showMenu); }, onRender: function() { // To avoid bugginess, add hammer events to the backdrop el too var hammer = $('.layout--backdrop').hammer(), hammer2 = this.$el.hammer(); _.each(this.options.events, function(func, ev) { hammer.bind(ev, _.bind(this.view[func] || this[func], this)); hammer2.bind(ev, _.bind(this.view[func] || this[func], this)); }, this); }, onShow: function() { _.bindAll(this, 'hideMenu'); this.$backdrop = $('#layout--backdrop'); // Hide when 'Esc' was pressed Mousetrap.bind('esc', this.hideMenu); }, hideMenu: function() { this.ui.sidemenu.removeClass('-show'); this.$backdrop.removeClass('-show'); }, showMenu: function() { var self = this; // Show the menu and backdrop this.ui.sidemenu.addClass('-show'); this.$backdrop.addClass('-show'); this.ui.sidemenu.scrollTop(0); // Hide the menu when a user clicks on the backdrop area this.$backdrop.on('click', function() { self.$backdrop.off('click'); self.hideMenu(); }); return false; } }); return Sidemenu; }); ================================================ FILE: app/scripts/classes/encryption.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'q', 'underscore', 'marionette', 'backbone.radio', 'classes/sjcl.worker', 'sjcl' ], function(Q, _, Marionette, Radio, Sjcl, sjcl) { 'use strict'; /** * Encryption class. * * Replies to requests on channel `encrypt`: * 1. `sha256` - generates and returns sha256 hash of provided string. * 2. `randomize` - generates and returns random data. * 3. `change:configs` - changes encryption configs. * 4. `delete:secureKey` - delete PBKDF2 from session storage. * * 3. `check:auth` - checks whether a user is authorized. * 4. `check:password` - validate provided password. * 5. `save:secureKey` - compute PBKDF2 and save it to session storage. * * 6. `encrypt` - encrypt a string * 7. `decrypt` - decrypt a string * 8. `encrypt:model` - encrypt a Backbone model * 9. `decrypt:model` - decrypt a Backbone model * 10. `encrypt:models` - encrypt a Backbone collection * 11. `decrypt:models` - decrypt a Backbone collection */ var Encrypt = Marionette.Object.extend({ initialize: function() { // Get configs this.configs = Radio.request('configs', 'get:object'); this.keys = {}; this.sjcl = new Sjcl(this.configs); // Pass requests directly to Sjcl class Radio.reply('encrypt', { 'sha256' : this.sjcl.sha256, }, this.sjcl); // Replies Radio.reply('encrypt', { 'randomize' : this.randomize, 'change:configs' : this.changeConfigs, // Check auth/password 'check:auth' : this.checkAuth, 'check:password' : this.checkPassword, 'save:secureKey' : this.saveSecureKey, 'delete:secureKey' : this.deleteSecureKey, // Encrypt/decrypt some string 'encrypt' : this.encrypt, 'decrypt' : this.decrypt, // Encrypt/decrypt a model 'encrypt:model' : this.encryptModel, 'decrypt:model' : this.decryptModel, // Encrypt/decrypt a collection of models 'encrypt:models' : this.encryptModels, 'decrypt:models' : this.decryptModels }, this); }, /** * Generate random words. * * @return string */ randomize: function(number, paranoia, noHex) { if (noHex) { return sjcl.random.randomWords(number, paranoia); } return sjcl.codec.hex.fromBits( sjcl.random.randomWords(number, paranoia) ); }, /** * Change encryption configs. It is useful when re-encrypting data. */ changeConfigs: function(configs) { configs = configs || Radio.request('configs', 'get:object'); this.configs = _.extend(this.configs, configs); }, /** * Check whether a user is already authorized * * @return bool */ checkAuth: function() { /** * If encryption backup is not empty, it means a user changed * encryption settings. */ if (!_.isEmpty(this.configs.encryptBackup)) { Radio.trigger('encrypt', 'changed'); return {isChanged: true}; } // Encryption is disabled if (!Number(this.configs.encrypt) || this.configs.encryptPass === '') { return true; } return !_.isEmpty(this.keys) || this._getSession() !== null; }, /** * Check the password with the password in the database which is saved * in there in sha256 hash format. Note, just the password is not used * for encrypting/decrypting data. We use instead PBKDF2. * * @return promise */ checkPassword: function(password) { var pwd = this.configs.encryptPass; return new Q(this.sjcl.sha256(password)) .then(function(hash) { return hash.toString() === pwd.toString(); }); }, /** * Generate PBKDF2 and save it. It will be used to encrypt/decrypt data. * * @return promise */ saveSecureKey: function(password) { var self = this; return new Q(this.sjcl.deriveKey({ configs : this.configs, password: password })) .then(function(keys) { self.keys.key = keys.key; self.keys.hexKey = keys.hexKey; self._saveSession(); }); }, /** * Delete current PBKDF2. */ deleteSecureKey: function() { this.keys = {}; if (window.sessionStorage) { window.sessionStorage.removeItem(this._getSessionKey()); } }, /** * Encrypt data. * * @return promise */ encrypt: function(str) { return new Q(this.sjcl.encrypt({ configs : this.configs, string : str, keys : this.keys, // Random initialization vector every time iv : sjcl.random.randomWords(4, 0), })); }, /** * Decrypt data. * * @return promise */ decrypt: function(str) { return new Q(this.sjcl.decrypt({ configs : this.configs, string : str, keys : this.keys, })); }, /** * Encrypt a model. * * @return promise */ encryptModel: function(model) { var data = _.pick(model.attributes, model.encryptKeys); return this.encrypt(data) .then(function(encrypted) { model.set('encryptedData', encrypted); return model; }); }, /** * Decrypt a model. * * @return promise */ decryptModel: function(model) { if (model.attributes.encryptedData) { return this._decryptModel(model); } return this._decryptModelKeys(model); }, /** * Encrypt a collection. * * @return promise */ encryptModels: function(collection) { // The collection is empty or PBKDF2 wasn't generated if (!collection.length || !Number(this.configs.encrypt) || !this.keys.key) { return new Q(); } var promises = [], self = this; Radio.trigger('encrypt', 'encrypting:models', collection); collection.each(function(model) { promises.push(function() { return new Q(self.encryptModel(model)); }); }, this); return _.reduce(promises, Q.when, new Q()) .fail(function(e) { console.error('EncryptModels Error:', e); }); }, /** * Decrypt a collection. * * @return promise */ decryptModels: function(collection) { // The collection is empty or encryption is disabled if (!collection.length || !Number(this.configs.encrypt)) { return new Q(); } // PBKDF2 wasn't generated if (!this.keys.key) { Radio.trigger('encrypt', 'decrypt:error', 'PBKDF2 is empty'); return new Q(); } var promises = [], self = this; Radio.trigger('encrypt', 'decrypting:models', collection); collection.each(function(model) { promises.push(function() { return new Q(self.decryptModel(model)); }); }, this); return _.reduce(promises, Q.when, new Q()) .fail(function(e) { console.error('DecryptModels Error:', e); }); }, /** * Decrypt a model by getting data from "encryptedData" attribute. * * @return promise */ _decryptModel: function(model) { return new Q(this.sjcl.decrypt({ configs : this.configs, string : model.get('encryptedData'), keys : this.keys, })) .then(function(data) { _.each(JSON.parse(data), function(val, key) { model.set(key, val); }); Radio.trigger('encrypt', 'decrypted:model', model); return model; }); }, /** * Deprecated decryption. * * @return promise */ _decryptModelKeys: function(model) { var promises = [], self = this; _.each(model.encryptKeys, function(key) { promises.push( new Q(self.sjcl.decryptLegacy({ configs : self.configs, string : model.get(key), keys : this.keys })) .then(function(data) { model.set(key, data); }) ); }, this); return Q.all(promises) .then(function() { Radio.trigger('encrypt', 'decrypted:model', model); return model; }); }, /** * Save PBKDF2 to sessionStorage. That way the user will not have to * type their passwords every time. */ _saveSession: function() { if (!window.sessionStorage || !this.keys) { return; } window.sessionStorage.setItem( this._getSessionKey(), JSON.stringify(this.keys) ); }, /** * Get PBKDF2 from sessionStorage. * * @return [object|null] */ _getSession: function() { if (!window.sessionStorage) { return null; } var keys = window.sessionStorage.getItem(this._getSessionKey()); try { keys = JSON.parse(keys); this.keys = keys || this.keys; } catch (e) { keys = null; } return keys; }, /** * Return session storage key which will be used to save PBKDF2. * * @return string */ _getSessionKey: function() { var profile = Radio.request('uri', 'profile') || 'default'; profile = (Number(this.configs.useDefaultConfigs) ? 'default' : profile); return 'secureKey.' + profile; } }); // Initialize Radio.request('init', 'add', 'app:before', function() { new Encrypt(); }); return Encrypt; }); ================================================ FILE: app/scripts/classes/sjcl.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'sjcl' ], function(_, Q, sjcl) { 'use strict'; /** * Sjcl adapter */ function Sjcl() { } _.extend(Sjcl.prototype, { keys: [], /** * Convert a string to a HEX. * * @type string str * @return string */ hex: function(str) { return sjcl.codec.hex.fromBits(str); }, /** * Replace every letter in a HEX string with uppercased letters. * * @type string str * @return string */ toUpperCase: function(str) { return str.toUpperCase().replace(/ /g,'').replace(/(.{8})/g, '$1 ').replace(/ $/, ''); }, /** * Hash a string. * * @type string str * @return promise */ sha256: function(str) { // Don't try to compute SHA256 if a string is empty if (!str || !str.length) { return new Q(str); } return new Q(sjcl.hash.sha256.hash(str)); }, /** * Return encryption configs. * * @type object configs * @return object */ getConfigs: function(configs) { return { mode : 'ccm', iter : Number(configs.encryptIter), ts : Number(configs.encryptTag), ks : Number(configs.encryptKeySize), salt : configs.encryptSalt, v : 1, adata : '', cipher : 'aes', }; }, /** * Compute PBKDF2 which will be used to encrypt/decrypt data * * @type object data * @return object */ deriveKey: function(data) { // If encryption is disabled, don't compute PBKDF2 if (!Number(data.configs.encrypt)) { return {}; } var pbkdf2 = {}, password; pbkdf2.iter = Number(data.configs.encryptIter); pbkdf2.salt = data.configs.encryptSalt; pbkdf2 = sjcl.misc.cachedPbkdf2(data.password, pbkdf2); password = pbkdf2.key.slice(0, Number(data.configs.encryptKeySize) / 32); return (this.keys = { key : password, hexKey : sjcl.codec.hex.fromBits(password) }); }, /** * Returns either locally cached keys or the keys from the provided object. * * @type object data * @return object */ getKeys: function(data) { return data.keys || this.keys; }, /** * Encrypt data. * * @type object data * @return string */ encrypt: function(data) { // Encryption is disabled if (!data.string || !Number(data.configs.encrypt)) { return data.string; } var p = this.getConfigs(data.configs); // Random initialization vector every time p.iv = data.iv; if (typeof data.string !== 'string') { data.string = JSON.stringify(data.string); } data.string = sjcl.encrypt(this.getKeys(data).key, data.string, p); return data.string; }, /** * Decrypt data. * * @type object data * @return [string|object] */ decrypt: function(data) { // Encryption is disabled if ((!data.string || !data.string.length) || !Number(data.configs.encrypt)) { return data.string; } try { data.string = sjcl.decrypt(this.getKeys(data).key, data.string); } catch (e) { return this.triggerDecryptError(e, data.string); } return data.string; }, /** * Deprecated decryption. * * @type object data * @return string */ decryptLegacy: function(data) { // Encryption is disabled if ((!data.string || !data.string.length) || !Number(data.configs.encrypt)) { return data.string; } var keys = this.getKeys(data), key = keys.key.toString(), str, object; try { str = _.unescape(data.string); object = JSON.parse(data.string); } catch (e) { return data.string; } // We need more encryption data if (_.size(object) < 9) { key = this.toUpperCase(keys.hexKey); str = _.extend(this.getConfigs(data.configs), object); str = JSON.stringify(str); } try { str = sjcl.decrypt(key, str); } catch (e) { return this.triggerDecryptError(e, str); } return str; }, triggerDecryptError: function(e, str) { // The text wasn't encrypted if (e.message.search('json decode') > -1 && !/"ct":"([^"]*)"./.test(str)) { return str; } console.error('Decryption error', e, str); throw new Error(e.message); } }); return Sjcl; }); ================================================ FILE: app/scripts/classes/sjcl.worker.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'classes/sjcl', 'backbone.radio' ], function(_, Q, Sjcl, Radio) { 'use strict'; // Use Sjcl without WebWorkers if (!Radio.request('global', 'use:webworkers')) { return Sjcl; } function SjclWorker() { var self = this; this.worker = new Worker('scripts/workers/sjcl.js'); // Promise which signifies whether the worker is ready this.workerPromise = Q.defer(); this.worker.onmessage = function(data) { var msg = data.data; switch (msg.msg) { // Webworker is ready case 'ready': self.workerPromise.resolve(); break; // Request was fullfilled case 'done': self.promises[msg.promiseId].resolve(msg.data); delete self.promises[msg.promiseId]; break; // Request failed with errors case 'fail': self.promises[msg.promiseId].reject(msg.data); delete self.promises[msg.promiseId]; break; default: } }; } _.extend(SjclWorker.prototype, { promises: [], /** * Send a message to the Webworker. * * @type string msg * @type object data */ _emit: function(msg, data) { var self = this; // Worker is ready if (!this.workerPromise.promise.isPending()) { return this._send(msg, data); } return this.workerPromise.promise .then(function() { return self._send(msg, data); }); }, _send: function(msg, data) { // Generate a unique ID for the worker's promise var promiseId = ((1 + Math.random()) * 0x10000); this.promises[promiseId] = Q.defer(); this.worker.postMessage({ msg : msg, promiseId : promiseId, data : data }); return this.promises[promiseId].promise; } }); /** * Automatically create "proxy" functions. Copy function names from * classes/sjcl.js. */ _.each(_.keys(Sjcl.prototype), function(key) { if (_.isFunction(Sjcl.prototype[key])) { SjclWorker.prototype[key] = function(data) { return this._emit(key, data); }; } }); return SjclWorker; }); ================================================ FILE: app/scripts/collections/configs.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'q', 'backbone', 'models/config', ], function(_, Q, Backbone, Config) { 'use strict'; var Configs = Backbone.Collection.extend({ model : Config, profileId : 'notes-db', storeName : 'configs', configNames: { 'appVersion' : '0.5.0', 'firstStart' : '1', 'appProfiles' : JSON.stringify(['notes-db']), 'appLang' : '', 'cloudStorage' : '0', 'dropboxKey' : '', 'dropboxAccessToken' : '', 'pagination' : '10', 'sortnotes' : 'created', 'sortnotebooks' : 'name', 'navbarNotebooksMax' : '5', 'useDefaultConfigs' : '1', // Editor settings 'editMode' : 'preview', 'indentUnit' : '4', // Encryption settings 'encrypt' : '0', 'encryptPass' : '', 'encryptSalt' : '', 'encryptIter' : '10000', 'encryptTag' : '128', 'encryptKeySize' : '256', 'encryptBackup' : {}, // Keybindings 'navigateTop' : 'k', 'navigateBottom' : 'j', 'jumpInbox' : 'g i', 'jumpNotebook' : 'g n', 'jumpFavorite' : 'g f', 'jumpRemoved' : 'g t', 'jumpOpenTasks' : 'g o', 'actionsEdit' : 'e', 'actionsOpen' : 'o', 'actionsRemove' : 'shift+3', 'actionsRotateStar' : 's', 'appCreateNote' : 'c', 'appSearch' : '/', 'appKeyboardHelp' : '?', 'textEditor' : 'default', 'modules' : [] }, /** * Check for new configs. */ hasNewConfigs: function() { return ( _.keys(this.configNames).length !== this.length ); }, /** * Switch to another profile */ changeDB: function(id) { this.profileId = id; this.model.prototype.profileId = this.profileId; }, /** * Migrate configs from localStorage. */ migrateFromLocal: function() { var val; _.each(this.configNames, function(value, name) { val = localStorage.getItem('vimarkable.configs-' + name); if (val) { this.configNames[name] = JSON.parse(val).value; } }, this); }, /** * Create default configs. */ createDefault: function() { var self = this, promises = []; _.each(this.configNames, function(value, name) { // Check whether a model already exists if (typeof this.get(name) !== 'undefined') { return; } promises.push( new Q(new self.model().save({name: name, value: value})) ); }, this); return Q.all(promises); }, /** * Transform the collection to key = value structure. */ getConfigs: function() { var data = {}; _.forEach(this.models, function(model) { data[model.get('name')] = model.get('value'); }); data.appProfiles = JSON.parse(data.appProfiles || this.configNames.appProfiles); return data; }, /** * Return a model with a default value */ getDefault: function(name) { var config = this.configNames[name]; return new this.model({name: name, value: config}); }, resetFromJSON: function(jsonSettings) { var newConfs = []; _.forEach(jsonSettings, function(val, key) { newConfs.push({name: key, value: val}); }); this.reset(newConfs); }, shortcuts: function() { var pattern = /(actions|navigate|jump|appCreateNote|appSearch|appKeyboardHelp)/; return this.filter(function(m) { pattern.lastIndex = 0; return pattern.test(m.get('name')); }); }, /** * Filter */ appShortcuts: function() { var names = ['appCreateNote', 'appSearch', 'appKeyboardHelp']; return this.filter(function(m) { return _.contains(names, m.get('name')); }); }, filterName: function(str) { return this.filter(function(m) { return m.get('name').search(str) >= 0; }); } }); return Configs; }); ================================================ FILE: app/scripts/collections/files.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'backbone', 'models/file' ], function(_, Backbone, File) { 'use strict'; /** * Files collection */ var Files = Backbone.Collection.extend({ model: File, profileId : 'notes-db', storeName : 'files', }); return Files; }); ================================================ FILE: app/scripts/collections/modules/configs.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'marionette', 'backbone.radio', 'sjcl', 'collections/modules/module', 'collections/configs' ], function(_, Q, Marionette, Radio, sjcl, ModuleObject, Configs) { 'use strict'; /** * Collection module for Configs. * * Apart from the replies and events in collections/modules/module.js, * it also has additional replies and events. * * Triggers events on channel `configs`: * 1. event: `collection:empty` - if the collection is empty. * 2. event: `removed:profile` - when some profile is removed. * 3. event: `changed` - after configs are changed * * Replies on channel `configs` to: * 1. request: `get:config` - returns a config. * 2. request: `get:object` - returns configs in key=value format. * 3. request: `get:profiles` - returns list of profiles * 4. request: `reset:encrypt` - resets encryption configs backup. * 5. request: `save:objects` - save several configs at once * 6. request: `create:profile` - create a new profile * 7. request: `remove:profile` - remove a profile * 8. request: `save:object` */ var Collection = ModuleObject.extend({ Collection: Configs, reply: function() { return { 'save:object' : this.saveObject, 'save:objects' : this.saveObjects, 'create:profile' : this.createProfile, 'remove:profile' : this.removeProfile, 'get:config' : this.getConfig, 'get:object' : this.getObject, 'get:profiles' : this.getProfiles, 'reset:encrypt' : this.resetEncrypt }; }, encryptionKeys: [ 'encrypt' , 'encryptPass', 'encryptSalt' , 'encryptIter', 'encryptTag' , 'encryptKeySize' ], /** * Reset encryptBackup */ resetEncrypt: function() { var model = this.collection.get('encryptBackup'); return this.saveModel(model, {value: {}}); }, /** * Create a new profile * * @type object Backbone.model - appProfiles model */ createProfile: function(model, name) { return model.createProfile(name); }, /** * Remove a profile */ removeProfile: function(model, name) { return new Q(model.removeProfile(name, model)) .then(function() { Radio.trigger('configs', 'removed:profile', name); }); }, /** * Save a config. * @type object Backbone model * @type object new value */ saveModel: function(model, data) { var saveFunc = _.bind(ModuleObject.prototype.saveModel, this); if (!model.isPassword(data)) { return saveFunc(model, data); } // Always save passwords as sha256 return new Q(Radio.request('encrypt', 'sha256', data.value)) .then(function(result) { data.value = result; return saveFunc(model, data); }); }, /** * Update several configs at once * @type array array of configs * @type object Backbone model */ saveObjects: function(objects, useDefault) { var promises = [], self = this; // Backup current encryption configs if (objects.useDefaultConfigs) { promises.push( this._backupEncrypt(useDefault.profileId) ); } // Convert configs to a key = value object. objects = (_.isArray(objects) ? _.indexBy(objects, 'name') : objects); // Backup encryption configs this._backupEncryption(objects); // return; _.forEach(objects, function(object) { promises.push( new Q(self.saveObject(object, useDefault, {profile: useDefault.profileId})) ); }, this); return Q.all(promises) .then(function() { Radio.trigger('configs', 'changed', objects); }); }, /** * Saves an object to the database. * @type object * @type object Backbone model */ saveObject: function(object, useDefault, options) { var self = this; return this.getModel(_.extend({}, options || {}, {name: object.name})) .then(function(model) { if (!model) { return; } if (object.name === 'useDefaultConfigs') { model = useDefault; } return self.saveModel(model, object); }); }, /** * Return the value of a specific config */ getConfig: function(name, defaultValue) { var config = this.getObject()[name]; return !_.isUndefined(config) ? config : defaultValue; }, /** * Return configs as key=value */ getObject: function() { return this.collection.getConfigs(); }, /** * Find a model by ID. * @type object options */ getModel: function(options) { var getFunc = _.bind(ModuleObject.prototype.getModel, this), self = this; options = (typeof options === 'string' ? {name: options} : options); return getFunc(options) .then(function(model) { if (model) { return model; } // If a model doesn't exist, return default values var collection = new (self.changeDatabase(options))(); return collection.getDefault(options.name); }); }, /** * Return all configs. * @type object options */ getAll: function(options) { if (this.collection && this.collection.length) { return new Q(this.collection); } var self = this, profile = options.profile || this.defaultDB, getFunc = _.bind(ModuleObject.prototype.getAll, this); /** * Before fetching configs collection, find out whether * we should use configs from the default profile. */ return this.useDefaultConfigs(options.profile) .then(function(profile) { options.profile = profile; return getFunc(options); }) .then(function() { return self._checkBackup(profile); }) .then(function() { return self._createDefault(options); }) .fail(function(e) { console.error('Error:', e); }); }, /** * Return null if configs from the default profile should be used. * * @type string profile */ useDefaultConfigs: function(profile) { return this.getModel({name: 'useDefaultConfigs', profile: profile}) .then(function(model) { return (!model || Number(model.get('value')) ? null : profile); }); }, /** * Returns profiles which use configs from default profile or * if current profile doesn't use configs from default profile, * returns only current profile. */ getProfiles: function() { var current = this.collection.profileId, backup = this.collection.get('encryptBackup'); // If it is not the default profile, return only current profile if (current !== this.defaultDB || backup.profileId !== this.defaultDB) { return new Q([backup.profileId]); } /* * If it is the default profile, return all profiles which * use configs from default profile. */ return this.getModel({name: 'appProfiles'}) .then(_.bind(this._getDefaultProfiles, this)); }, /** * Return profiles which use configs from default profile. * @type object Backbone model */ _getDefaultProfiles: function(model) { var profiles = model.getValueJSON(), self = this, promises = []; // Fetch `useDefaultConfigs` model of every profile _.each(profiles, function(profile) { promises.push( self.getModel({ name : 'useDefaultConfigs', profile : profile }) ); }); return Q.all(promises) .then(function(profiles) { profiles = _.filter(profiles, function(profile) { return ( Number(profile.get('value')) === 1 || profile.profileId === self.defaultDB ); }); return _.pluck(profiles, 'profileId'); }); }, /** * Check encryption backup */ _checkBackup: function(profile) { var self = this; return this.getModel({name: 'encryptBackup'}) .then(function(backup) { /** * If it is the default profile or default backup is not empty, * do nothing. */ if (profile === self.defaultDB || (!backup || !_.isEmpty(backup.get('value')))) { return; } // Fetch current profile's encryption backup configs return self.getModel({ name : 'encryptBackup', profile : profile }) .then(function(model) { // If profile's backup is not empty, change backup model if (!_.isEmpty(model.get('value'))) { backup.set(model.toJSON()); backup.changeDB(profile); } return model; }); }); }, /** * If collection is empty, create configs with default values. * @type object options */ _createDefault: function(options) { if (!this.collection.hasNewConfigs()) { return new Q(this.collection); } var self = this; // Trigger an event if the collection is empty if (this.collection.length === 0) { this.vent.trigger('collection:empty'); } // If the collection is empty, create default set of configs. return new Q(this.collection.migrateFromLocal()) .then(_.bind(this.collection.createDefault, this.collection)) .then(function() { var func = _.bind(ModuleObject.prototype.getAll, self); self.collection.trigger('reset:all'); return func(options); }) .thenResolve(self.collection); }, /** * Check whether there are any changes in encryption configs. */ _getEncryption: function(collection) { // Don't create a backup if encryption is not used in both new and old configs if ((!collection.encrypt || !Number(collection.encrypt.value)) && !Number(this.getConfig('encrypt'))) { return []; } // Disable encryption if password is empty in both configs if ((!collection.encryptPass || !collection.encryptPass.value.length) && !this.getConfig('encryptPass').length) { collection.encrypt = {value : '0', name: 'encrypt'}; return []; } return _.filter(collection, function(value) { // Compare values if (typeof value === 'object') { return ( _.indexOf(this.encryptionKeys, value.name) > -1 && this.getConfig(value.name) !== value.value && this._checkPassChanged(value) ); } return (_.indexOf(this.encryptionKeys, value) > -1); }, this); }, _checkPassChanged: function(object) { if (object.name !== 'encryptPass') { return true; } var pass = this.getConfig('encryptPass'); pass = pass ? pass.toString() : pass; // Password salt was saved if (pass === object.value) { return false; } // Additional check to make sure it's not the same password var salt = sjcl.hash.sha256.hash(object.value); return (salt.toString() !== pass); }, /** * Backup current encryption configs to current profile. */ _backupEncrypt: function(profile) { var encrypt = _.pluck(this.collection.filter(function(model) { return (_.indexOf(this.encryptionKeys, model.get('name')) > -1); }, this), 'id'), model = this.collection.get('encryptBackup'); model.changeDB(profile); return new Q(this.saveModel(model, { value: _.pick(this.collection.getConfigs(), encrypt) })); }, /** * Backup encryption configs if there are any changes in them. */ _backupEncryption: function(objects) { var changed = _.pluck(this._getEncryption(objects), 'name'); /* * Don't create encryption backup if: * Encryption configs have not changed * or * there is already a backup. */ if (_.isEmpty(changed) || _.keys(this.getConfig('encryptBackup')).length) { return; } // Backup configs that are changed var configs = this.getObject(); changed = _.pick(configs, changed); // Password hasn't changed if (objects.encryptPass && configs.encryptPass.toString() === objects.encryptPass.value.toString()) { delete changed.encryptPass; } if (!_.keys(changed).length) { return; } /** * Extend old backup from new. * That way we ensure that only the oldest configs will be saved. */ objects.encryptBackup = { name : 'encryptBackup', value : _.extend({}, changed, configs.encryptBackup) }; return objects; }, }); /** * Initialize it automaticaly because everything depends on configs * collection and it should be available as soon as possible. */ return new Collection(); }); ================================================ FILE: app/scripts/collections/modules/files.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'q', 'underscore', 'backbone.radio', 'collections/modules/module', 'collections/files', 'models/file', 'toBlob', 'blobjs' ], function(Q, _, Radio, ModuleObject, Files, File, toBlob) { 'use strict'; /** * Collection module for Files. * * Apart from the replies in collections/modules/module.js, * it also has additional replies: * * 1. `get:files` - fetches all files with specified IDs. * * Triggers events on channel `files`: * 1. `saved:all` - after changes to collection are saved. */ var Collection = ModuleObject.extend({ Collection: Files, reply: function() { return { 'get:files' : this.getFiles, 'save:all' : this.saveAll }; }, /** * Fetch files with specific ids * @type array ids of files * @type object options */ getFiles: function(ids, options) { var promises = []; _.each(ids, function(id) { promises.push( this.getModel(_.extend({id: id}, options)) ); }, this); return Q.all(promises) .then(function() { return arguments[0]; }); }, /** * Save a file. * @type object Backbone model * @type object new values */ saveModel: function(model, data) { var saveFunc = _.bind(ModuleObject.prototype.saveModel, this); data.src = toBlob(data.src); model.setEscape(data); return saveFunc(model, model.attributes); }, /** * Save several files. * @type array * @type object */ saveAll: function(data, options) { var promises = [], self = this, files = [], model; _.each(data, function(imgData) { promises.push(function() { model = new (self.changeDatabase(options)).prototype.model(); return self.saveModel(model, imgData) .then(function(file) { files.push(file); }); }); }); return _.reduce(promises, Q.when, new Q()) .then(function() { Radio.trigger('files', 'saved:all'); return files; }); } }); // Initialize it automaticaly Radio.request('init', 'add', 'app:before', function() { new Collection(); }); return Collection; }); ================================================ FILE: app/scripts/collections/modules/module.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'marionette', 'backbone.radio' ], function(_, Q, Marionette, Radio) { 'use strict'; /** * Collection object from which other collection objects extend. * * For default it * * replies to the following requests: * 1. save - save model changes * 2. save:collection - save all collection changes * 3. save:all:raw - saves several objects * 4. fetch - fetches models from the database * 5. get:model - returns a specific model * 6. get:all - returns a collection * 7. remove - removes a model * * and triggers the following events: * 1. model:update - after a model is updated or created * 2. destroy:model - after a model is removed */ var Module = Marionette.Object.extend({ /** * @type object Backbone collection */ Collection: null, /** * @type string default profile */ defaultDB: 'notes-db', /** * Requests to which every collection module * replies for default. * @return object */ reply: function() { return { 'save' : this.saveModel, 'save:collection' : this.saveCollection, 'save:raw' : this.saveRaw, 'save:all:raw' : this.saveAllRaw, 'fetch' : this.fetch, 'get:model' : this.getModel, 'get:all' : this.getAll, 'remove' : this.remove, }; }, initialize: function() { // Default replies var defReply = _.bind(Module.prototype.reply, this); this.vent = Radio.channel(this.Collection.prototype.storeName); _.bindAll(this, 'encryptModel', 'decryptModel', 'decryptModels'); // Register replies this.vent.reply(_.extend(defReply(), this.reply()), this); // Listen to events this.listenTo(this.vent, 'destroy:collection', this.onReset, this); }, /** * Switch to another database (e.g. profile) * @type object */ changeDatabase: function(options) { var profile = options && options.profile ? options.profile : this.defaultDB, model, collection; model = this.Collection.prototype.model.extend({ profileId : profile }); collection = this.Collection.extend({ profileId : profile, model : model }); return collection; }, /** * Stop listening to current collection's events. */ onReset: function() { if (!this.collection) { return; } this.stopListening(this.collection); if (this.collection.removeEvents) { this.collection.removeEvents(); } this.collection.reset([]); this.collection = null; }, /** * Save changes to a model. * @type object Backbone model * @type object new values */ save: function(model, data) { var self = this, setF = model.setEscape ? 'setEscape' : 'set', errors = model.validate(data); if (errors) { model.trigger('invalid', model, errors); return Q.reject('Validation error:' + model.storeName, errors); } // Set new values model[setF](data); return new Q(self.encryptModel(model)) .then(function(model) { return new Q(model.save(model.attributes, {validate: false})) .thenResolve(model); }); }, /** * @type object Backbone model * @type object new values */ saveModel: function(model, data) { var self = this; data.updated = Date.now(); if (!model.attributes.created) { data.created = Date.now(); } return this.save(model, data) .then(function(model) { self.vent.trigger('sync:model', model); return self.decryptModel(model); }) .then(function(model) { self.vent.trigger('update:model', model); return model; }); }, /** * Save all changes in the collection. * @type object Backbone collection */ saveCollection: function(collection) { var promises = [], self = this; collection = collection || this.collection; collection.each(function(model) { model.attributes.updated = Date.now(); promises.push( Q.invoke(model, 'save', model.attributes) ); }); return Q.all(promises) .then(function() { self.vent.trigger('saved:collection'); return collection; }); }, /** * Saves raw object to the database. * @type object JSON object * @type object options */ saveRaw: function(data, options) { var self = this, model = new (this.changeDatabase(options)).prototype.model(data), errors; return this.decryptModel(model) .then(function() { errors = model.validate(model.attributes); // Don't save data which can't be validated if (errors) { console.error('Validation failed:' + model.storeName, errors); return; } return self.save(model, data) .then(self.decryptModel) .then(function(model) { self.vent.trigger('update:model', model); self.vent.trigger('synced:' + model.id, model); return model; }); }); }, /** * Saves all changes. * @type array */ saveAllRaw: function(arData, options) { var promises = [], self = this; _.each(arData, function(data) { promises.push(function() { return self.saveRaw(data, options); }); }); return _.reduce(promises, Q.when, new Q()); }, /** * Remove a model. * @type object Backbone model or ID * @type object options */ remove: function(model, options) { var self = this; // Change model's attributes to default values (empty values) model = typeof model === 'string' ? model : model.id; model = new (this.changeDatabase(options)).prototype.model({id: model}); model.set({'trash': 2, updated: Date.now()}); return this.save(model, model.attributes) .then(function() { self.vent.trigger('destroy:model', model); }); }, /** * Find a model by id. * @type object options */ getModel: function(options) { var Model = (this.changeDatabase(options)).prototype.model, idAttr = Model.prototype.idAttribute, data = {}, model; data[idAttr] = options[idAttr]; model = new Model(data); // If id was not provided, return a model with default values if (!options[idAttr] || options[idAttr] === '0') { model.set(idAttr, undefined); return new Q(model); } // In case if the collection isn't empty, get the model from there. if (this.collection && this.collection.profileId === model.profileId && this.collection.get(options[idAttr])) { return new Q(this.collection.get(options[idAttr])); } var self = this; return new Q(model.fetch()) .then(function() { return self.decryptModel(model) .thenResolve(model); }) .fail(function(e) { if (typeof e === 'string' && e.search('not found') > -1) { return null; } throw new Error(e); }); }, /** * Fetch data and create a new collection. * @type object options */ getAll: function(options) { var self = this; this.vent.trigger('destroy:collection'); // Add filter conditions if (options.filter) { var cond = this.Collection.prototype.conditions[options.filter]; cond = (typeof cond === 'function' ? cond(options) : cond); options.conditions = cond; } // this.onReset(); return this.fetch(options || {}) .then(function(collection) { self.collection = collection; self.collection.conditionFilter = options.filter; self.collection.conditionCurrent = options.conditions; // Register events if (self.collection.registerEvents) { self.collection.registerEvents(); } // Events self.listenTo(self.collection, 'reset:all', self.onReset); return self.collection; }); }, /** * Fetch data. * @type object options */ fetch: function(options) { var collection = new (this.changeDatabase(options))(), self = this; return new Q(collection.fetch(options)) .then(function() { // Return in decrypted format if (!options.encrypt) { return self.decryptModels(collection.fullCollection || collection) .then(function() { collection.trigger('decrypted'); return; }) .thenResolve(collection); } return collection; }); }, /** * @return boolean */ _isEncryptEnabled: function(model) { // Don't use encryption on configs if (this.Collection.prototype.storeName === 'configs') { return false; } var configs = Radio.request('configs', 'get:object'), backup = {encrypt: configs.encryptBackup.encrypt || 0}; model = model || this.Collection.prototype.model.prototype; return ( !_.isUndefined(model.encryptKeys) && (Number(configs.encrypt) || Number(backup.encrypt)) === 1 ); }, /** * @type object Backbone model */ encryptModel: function(model) { if (!this._isEncryptEnabled(model)) { return new Q(model); } return Radio.request('encrypt', 'encrypt:model', model); }, /** * @type object Backbone model */ decryptModel: function(model) { if (!this._isEncryptEnabled(model)) { return new Q(model); } return new Q( Radio.request('encrypt', 'decrypt:model', model) ); }, /** * Decrypt every model in the collection * @type object Backbone collection */ decryptModels: function(collection) { collection = collection || this.collection; if (!this._isEncryptEnabled(collection.model.prototype)) { return new Q(collection); } collection = collection.fullCollection || collection; return Radio.request('encrypt', 'decrypt:models', collection); } }); return Module; }); ================================================ FILE: app/scripts/collections/modules/notebooks.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'marionette', 'backbone.radio', 'collections/modules/module', 'collections/notebooks' ], function(_, Q, Marionette, Radio, ModuleObject, Notebooks) { 'use strict'; /** * Collection module for Notebooks. * A convenience object that handles operations to Notebooks collection. * * It listens to all events and replies registered in collections/modules/module.js */ var Collection = ModuleObject.extend({ Collection: Notebooks, /** * Remove an existing notebook. * @type object Backbone model * @type object options * @type boolean true if all attached notes should be removed */ remove: function(model, options, remove) { var self = this; if (typeof model === 'string') { model = this.getModel(_.extend({id: model}, options)); } /** * Move child models to a higher level. * Then, remove notes attached to the notebook or change their notebookId. * And finally, remove the notebook. */ return new Q(model) .then(function(model) { return self.updateChildren(model).thenResolve(model); }) .then(function(model) { return Radio.request('notes', 'change:notebookId', model, remove) .thenResolve(model); }) .then(function(model) { var removeFunc = _.bind(ModuleObject.prototype.remove, self); return removeFunc(model, options); }); }, /** * Move child models to a higher level. * @type object Backbone model */ updateChildren: function(model) { var self = this; return this.getChildren(model.id, {profile: model.profileId}) .then(function(collection) { var promises = []; // Change parentId of each children collection.each(function(child) { promises.push(new Q( self.saveModel(child, {parentId: model.get('parentId')}) )); }); return Q.all(promises); }); }, /** * Returns models with the specified parent ID. * @type string */ getChildren: function(parentId, options) { // Just filter an existing collection if (this.collection) { var collection = this.collection.clone(); collection.reset(collection.getChildren(parentId)); return Q.resolve(collection); } return this.fetch(_.extend( {conditions: {parentId: parentId}}, options || {} )); }, /** * Get all notebooks. * @type object options */ getAll: function(options) { var self = this, sortField = Radio.request('configs', 'get:config', 'sortnotebooks'); options.profile = options.profile || this.defaultDB; options.filter = options.filter || 'active'; // Do not fetch twice if (this.collection && this.collection.profileId === options.profile) { this.collection.models = this.collection.getTree(); return new Q( this.collection ); } var getFunc = _.bind(ModuleObject.prototype.getAll, this); this.Collection.prototype.sortField = sortField; return getFunc(options) .then(function() { self.collection.models = self.collection.getTree(); return self.collection; }); }, }); // Initialize it automaticaly Radio.request('init', 'add', 'app:before', function() { new Collection(); }); return Collection; }); ================================================ FILE: app/scripts/collections/modules/notes.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'marionette', 'backbone.radio', 'collections/modules/module', 'collections/notes' ], function(_, Q, Marionette, Radio, ModuleObject, Notes) { 'use strict'; /** * Collection module for Notes. * * Apart from the replies in collections/modules/module.js, * it also has additional replies: * 1. `get:model:full` - returns a note with its notebook. * 2. `restore` - restores a note from trash * 3. `change:notebookId` - when a notebook is removed, either move all attached * notes to trash or change notebook ID. */ var Collection = ModuleObject.extend({ Collection: Notes, reply: function() { return { 'get:model:full' : this.getModelFull, 'restore' : this.restore, 'change:notebookId' : this.onNotebookRemove }; }, /** * Save a note. * @type object Backbone model * @type object new values */ saveModel: function(model, data, saveTags) { if(saveTags === undefined || saveTags === null){ saveTags = true; } var saveFunc = _.bind(ModuleObject.prototype.saveModel, this); if(saveTags){ // Before saving the model, add tags return new Q(Radio.request('tags', 'add', data.tags || [], { profile: model.profileId })) .then(function() { return saveFunc(model, data); }); } // Save model without tags return saveFunc(model,data); }, /** * Remove a note. * @type object Backbone model * @type object options */ remove: function(model, options) { var self = this; model = (typeof model === 'string' ? this.getModel(model, options) : model); return new Q(model) .then(function(model) { // If the model is already in trash, destroy it if (Number(model.get('trash')) === 1) { var removeFunc = _.bind(ModuleObject.prototype.remove, self); return removeFunc(model, options); } // Otherwise, just change 'trash' status return self.save(model, {trash: 1, updated: Date.now()}) .then(function(model) { self.vent.trigger('destroy:model', model); return model; }); }); }, /** * Restore a model from trash. * @type object Backbone model * @type object options */ restore: function(model, options) { var self = this; model = (typeof model === 'string' ? this.getModel(model, options) : model); return new Q(model) .then(function(model) { return self.save(model, {trash: 0, updated: Date.now()}) .then(function(model) { self.vent.trigger('restore:model', model); return model; }); }); }, /** * When a notebook is removed, either move all attached * notes to trash or change notebook ID. * @type object Backbone model * @type boolean true if all attached notes should be removed */ onNotebookRemove: function(notebook, remove) { var self = this, data = {notebookId: 0}; if (remove) { data.trash = 1; } return this.fetch({ conditions: { notebookId: notebook.id }, profile : notebook.profileId }) .then(function(notes) { if (notes.length === 0) { return; } var coll = notes.fullCollection || notes, promises = []; coll.each(function(note) { promises.push(self.saveModel(note, data)); }); return Q.all(promises); }); }, /** * Get all notes. * @type object options */ getAll: function(options) { var getAll = _.bind(ModuleObject.prototype.getAll, this), self = this, sortField = Radio.request('configs', 'get:config', 'sortnotes'); options.filter = options.filter || 'active'; this.Collection.prototype.sortField = sortField; return getAll(options) .then(function(collection) { self._filterOnFetch(collection, options); return collection; }); }, /** * Use Backbone's filters when IndexedDB is not available */ _filterOnFetch: function(collection, options) { collection.filterList(options.filter, options); }, /** * Return a note with its notebook. * @type object options */ getModelFull: function(options) { return this.getModel(options) .then(function(note) { return Q.all([ Radio.request('notebooks', 'get:model', { profile : options.profile, id : note.get('notebookId') }), Radio.request('files', 'get:files', note.get('files'), { profile: options.profile }) ]) .spread(function(notebook, files) { note.notebook = notebook; note.files = files; return [note, notebook]; }); }); }, }); // Initialize it automaticaly Radio.request('init', 'add', 'app:before', function() { new Collection(); }); return Collection; }); ================================================ FILE: app/scripts/collections/modules/tags.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'marionette', 'backbone.radio', 'collections/modules/module', 'collections/tags' ], function(_, Q, Marionette, Radio, ModuleObject, Tags) { 'use strict'; /** * Collection module for Tags. * * Apart from the replies in collections/modules/module.js, * it also has an additional reply `add` which inserts several * tags into database. */ var Collection = ModuleObject.extend({ Collection: Tags, reply: function() { return { 'add': this.addTags, }; }, /** * Add a bunch of tags. * @type array array of tags * @type object options */ addTags: function(tags, options) { var self = this, promises = []; if (!tags.length) { return new Q(); } options = options || {}; _.each(tags, function(tag) { promises.push(function() { return self.addTag(tag, options); }); }); return _.reduce(promises, Q.when, new Q()) .fail(function(e) { console.error('Error:', e); }); }, /** * Add a tag. * @type string name of a tag * @type object options */ addTag: function(tag, options) { var self = this; return new Q(Radio.request('encrypt', 'sha256', tag)) .then(function(id) { options.id = id; return self.getModel({id: id.join(''), profile: options.profile}); }) .then(function(model) { if (!model || !model.get('name').length) { model = new (self.changeDatabase(options)).prototype.model(); return self.saveModel(model, {name: tag}); } return model; }); }, /** * Save or create a tag. * @type object Backbone model * @type object new values */ saveModel: function(model, data) { var self = this, errors = model.validate(data); if (errors) { model.trigger('invalid', model, errors); return Q.reject('Validation error: tags', errors); } // First, make sure that a model won't duplicate itself. return new Q(Radio.request('encrypt', 'sha256', data.name)) .then(function(id) { id = id.join(''); if (!model.id) { return id; } return self.remove(model, {profile: model.profileId}) .thenResolve(id); }) .then(function(id) { var saveFunc = _.bind(ModuleObject.prototype.saveModel, self); model.set(data); model.set('id', id); return saveFunc(model, model.attributes); }); }, }); // Initialize the collection automaticaly Radio.request('init', 'add', 'app:before', function() { new Collection(); }); return Collection; }); ================================================ FILE: app/scripts/collections/notebooks.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'backbone', 'collections/pageable', 'backbone.radio', 'models/notebook', ], function(_, Backbone, PageableCollection, Radio, Notebook) { 'use strict'; /** * Notebooks collection */ var Notebooks = PageableCollection.extend({ model: Notebook, profileId : 'notes-db', storeName : 'notebooks', state: { pageSize : 0, firstPage : 1, totalPages : 1, currentPage : 0 }, conditions: { active: {trash: 0} }, sortField: 'name', comparator: function(model) { if (this.sortField === 'name') { return model.get(this.sortField); } else { return -model.get(this.sortField); } }, sortItOut: function() { this.models = this.getTree(); }, sortFullCollection: function() { this.sortItOut(); this.reset(this.models); }, _onAddItem: function(model) { /** * Remove a model from the collection if it doesn't meet * the current filter condition. */ if (!model.matches(this.conditionCurrent || {trash: 0})) { return this._navigateOnRemove(model); } var colModel = this.get(model.id); if (colModel) { colModel.set(model.attributes); } else { this.add(model); } this.sortFullCollection(); Radio.trigger('notebooks', 'model:navigate', model); }, /** * Return only notebooks that are not related to a specified notebook. */ rejectTree: function(id) { var ids = [id]; return this.filter(function(model) { if (_.indexOf(ids, model.id) > -1 || _.indexOf(ids, model.get('parentId')) > -1) { ids.push(model.id); return false; } return true; }); }, /** * Build a tree structure */ getTree: function(parents, tree) { var self = this, children; parents = (parents || this.getRoots()); tree = (tree || []); _.forEach(parents, function(model) { tree.push(model); children = self.getChildren(model.get('id')); // Every child model can have its own children if (children.length > 0) { children = self.getTree(children, tree); } }); return tree; }, /** * Finds notebooks children */ getChildren: function(parentId) { return this.filter(function(model) { return model.get('parentId') === parentId; }); }, /** * Only root notebooks */ getRoots: function() { return this.filter(function(notebook) { return notebook.get('parentId') === '0'; }); }, }); return Notebooks; }); ================================================ FILE: app/scripts/collections/notes.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'backbone', 'backbone.radio', 'collections/pageable', 'models/note', 'fuse', ], function(_, Backbone, Radio, PageableCollection, Note, Fuse) { 'use strict'; var Notes = PageableCollection.extend({ model: Note, profileId : 'notes-db', storeName : 'notes', state: { pageSize : 10, firstPage : 0, currentPage : 0, totalRecords : 0 }, conditions: { active : {trash : 0}, favorite : {isFavorite : 1, trash : 0}, trashed : {trash : 1}, notebook : function(args) { return {notebookId: args.query, trash: 0}; } }, sortField: 'created', initialize: function() { this.state.comparator = {}; this.state.comparator[this.sortField] = this.sortField === 'title' ? 'asc' : 'desc'; this.state.comparator.isFavorite = 'desc'; }, filterList: function(filter, options) { if (!filter || !this[filter + 'Filter']) { return; } var res = this[filter + 'Filter'](options.query); return this.reset(res); }, /** * Show notes with unfinished tasks */ taskFilter: function() { return this.filter(function(note) { return note.get('taskCompleted') < note.get('taskAll'); }); }, /** * Show only tag's notes * Returns notes to which a specified tag was attached. */ tagFilter: function(tagName) { return this.filter(function(note) { if (note.get('tags').length > 0) { return ( (_.indexOf(note.get('tags'), tagName) !== -1) && note.get('trash') === 0 ); } }); }, /** * Search */ searchFilter: function(letters) { if (!letters || letters === '') { return this; } var pattern = new RegExp(letters, 'gim'); return this.filter(function(model) { pattern.lastIndex = 0; return pattern.test(model.get('title')) || pattern.test(model.get('content')); }); }, fuzzySearch: function(text) { var fuse = new Fuse(this.fullCollection.models, { keys : ['title'], getFn : function(obj, path) { return obj.get(path); } }); return fuse.search(text); }, }); return Notes; }); ================================================ FILE: app/scripts/collections/pageable.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'backbone', 'backbone.radio' ], function(_, Backbone, Radio) { 'use strict'; /** * Pagination support for Backbone collections. * Some code was borrowed from the plugin Backbone.paginator. * * Triggers: * --------- * Events to channel `notes`: * 1. `model:navigate` - when the next or previous model was requested * or a model was removed. * * Events to itself (e.g. collection): * 1. `page:next` - when the next model was requested but a user * has reached the last model on the page. * 2. `page:previous` - when the previous model was requested but a user * has reached the first model on the page. */ var PageableCollection = Backbone.Collection.extend({ // Default pagination settings state: { pageSize : 4, firstPage : 0, currentPage : 0, totalRecords : 0, comparator : {'isFavorite' : 'desc', 'created' : 'desc'} }, /** * Overrite `fetch` method. */ fetch: function(options) { options = options || {}; options.options = options.options || {}; if (!_.isUndefined(options.pageSize)) { this.state.pageSize = Number(options.pageSize); } // Do not use pagination if (this.state.pageSize === 0) { return Backbone.Collection.prototype.fetch.call(this, options); } var success = options.success, self = this; options.success = function(resp) { // Keep full collection in memory self.fullCollection = self.clone(); // Sort the collection self.fullCollection.sortItOut(); // Pagination self._updateTotalPages(); self.getPage(options.page || self.state.firstPage); if (success) { success(self, resp); } }; return Backbone.Collection.prototype.fetch.call(this, options) .then(function(resp) { options.success(resp); return resp; }); }, /** * Handles events. * It needs to be called after a collection was instantiated. */ registerEvents: function() { this.vent = Radio.channel(this.storeName); // Sort the collection again when favorite status is changed this.listenTo(this, 'change:isFavorite', this.sortItOut); this.listenTo(this, 'reset', this.sortItOut); // Listen to events this.listenTo(this.vent, 'update:model' , this._onAddItem, this); this.listenTo(this.vent, 'destroy:model', this._navigateOnRemove, this); this.listenTo(this.vent, 'restore:model', this._onRestore, this); return this; }, /** * It makes some "garbage collection" * by destroying full collection and event listeners. * If a collection is no longer in use, this method should be called. */ removeEvents: function() { // Destroy a full collection if (this.fullCollection) { this.fullCollection.reset(); this.fullCollection = null; } // Remove all the event listeners this.stopListening(); this.stopListening(this.vent); return this; }, getNextPage: function() { var models = this.getPage(this.state.currentPage + 1); this.reset(models); }, getPreviousPage: function() { var models = this.getPage(this.state.currentPage - 1); this.reset(models); }, /** * Sets state.currentPage to the given number. * Then, it overwrites models of the current collection. */ getPage: function(number) { // Calculate page number var pageStart = this.getOffset(number); // Save where we currently are this.state.currentPage = number; // Slice an array of models this.models = this.fullCollection.models.slice(pageStart, pageStart + this.state.pageSize); return this.models; }, getOffset: function(number) { return ( (this.state.firstPage === 0 ? number : number - 1) * this.state.pageSize ); }, hasPreviousPage: function() { return this.state.currentPage !== this.state.firstPage; }, hasNextPage: function() { return this.state.currentPage !== this.state.totalPages - 1; }, /** * It is used to sort models in full collection. */ sortFullCollection: function() { if (!this.fullCollection) { return; } // Sort the full collection again this.fullCollection.sortItOut(); // Update pagination state this._updateTotalPages(); this.getPage(this.state.currentPage); // Reset the collection so the view could re-render itself this.reset(this.models); }, /** * Useful when sorting models in a collection by multiple keys. */ sortItOut: function() { var comparator = this.comparator, self = this; _.each(this.state.comparator, function(value, key) { self.comparator = function(model) { return (value === 'desc' ? (-model.get(key)) : model.get(key)); }; self.sort(); }); this.comparator = comparator; return this.models; }, getNextItem: function(id) { // The collection is empty if (this.length === 0) { return false; } var model = this.get(id), index = model ? this.indexOf(model) + 1 : 0; // It is the last model on this page if (index >= this.models.length) { return this.trigger( this.hasNextPage() ? 'page:next' : 'page:end' ); } Radio.trigger(this.storeName, 'model:navigate', this.at(index)); }, getPreviousItem: function(id) { // The collection is empty if (this.length === 0) { return false; } var model = this.get(id), index = model ? this.indexOf(model) - 1 : this.models.length - 1; // It is the first model on this page if (index < 0) { return this.trigger( this.hasPreviousPage() ? 'page:previous' : 'page:start' ); } Radio.trigger(this.storeName, 'model:navigate', this.at(index)); }, /** * When some model was removed, trigger `model:navigate` event * passing a model which has the same index as the removed model. * @type object Backbone model */ _navigateOnRemove: function(model) { model = this.get(model.id); if (!model) { return false; } var coll = this.fullCollection || this, index = this.indexOf(model); coll.remove(model); this.sortFullCollection(); if (!this.at(index)) { index--; } if (!this.at(index)) { return this.hasPreviousPage() ? this.trigger('page:previous') : null; } Radio.trigger(this.storeName, 'model:navigate', this.at(index)); }, /** * When a model was restored from trash. */ _onRestore: function(model) { if (this.conditionFilter !== 'trashed') { return this._onAddItem(model); } if (this.length > 1) { return this._navigateOnRemove(model); } }, /** * Update pagination when a model is added */ _onAddItem: function(model) { // Don't add models from other profiles if (this.profileId !== model.profileId) { return; } /** * Remove a model from the collection if it doesn't meet * the current filter condition. */ if (!model.matches(this.conditionCurrent || {trash: 0})) { return this._navigateOnRemove(model); } // If the model already exists, update it var coll = this.fullCollection || this, colModel = coll.get(model.id); if (colModel) { return colModel.set(model.toJSON()); } // Or add it to fullCollection and sort the collection again coll.add(model, {at: 0}); this.sortFullCollection(); }, /** * Update pagination when a model is removed */ _onRemoveItem: function(model) { this.fullCollection.remove(model); this.sortFullCollection(); }, /** * Updates the number of available pages */ _updateTotalPages: function() { this.state.totalPages = Math.ceil( this.fullCollection.length / this.state.pageSize ); } }); return PageableCollection; }); ================================================ FILE: app/scripts/collections/tags.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'backbone', 'collections/pageable', 'backbone.radio', 'models/tag' ], function(_, Backbone, PageableCollection, Radio, Tag) { 'use strict'; /** * Tags collection */ var Tags = PageableCollection.extend({ model: Tag, profileId : 'notes-db', storeName : 'tags', state: { pageSize : 20, firstPage : 0, currentPage : 0, totalRecords : 0, comparator : {'updated': 'desc'} }, conditions: { active: {trash: 0} }, _onAddItem: function(model) { PageableCollection.prototype._onAddItem.apply(this, arguments); Radio.trigger('tags', 'model:navigate', model); }, sortFullCollection: function() { if (!this.fullCollection) { return; } // Sort the full collection again this.fullCollection.sortItOut(); // Update pagination state this._updateTotalPages(); var models = this.fullCollection.models.slice( 0, this.state.pageSize * (this.state.currentPage + 1) ); // Reset the collection so the view could re-render itself this.reset(models); return true; }, /** * Sets state.currentPage to the given number. * Then, it overwrites models of the current collection. */ getPage: function(number) { // Calculate page number var pageStart = this.getOffset(number), models; // Save where we currently are this.state.currentPage = number; // Slice an array of models models = this.fullCollection.models.slice(pageStart, pageStart + this.state.pageSize); if (number === 0) { this.reset(models); } else { this.add(models); } return this.models; }, /** * This collection is never going to have a previous page * because it uses inifinite pagination. */ hasPreviousPage: function() { return false; } }); return Tags; }); ================================================ FILE: app/scripts/constants.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define(['underscore'], function (_) { 'use strict'; var constants = {}; constants.VERSION = '0.7.51'; constants.URL = location.origin + location.pathname.replace('index.html', ''); // List of hosts and urls where default dropbox API will work constants.DEFAULTHOSTS = ['laverna.cc', 'laverna.github.io', 'localhost', 'localhost:9000', 'localhost:9100']; constants.DROPBOX_KEY = '10iirspliqts95d'; constants.DROPBOX_SECRET = null; // Default Dropbox API key will not work if ( !_.contains(constants.DEFAULTHOSTS, location.host) ) { constants.DROPBOXKEYNEED = true; } return constants; }); ================================================ FILE: app/scripts/helpers/db.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'localforage' ], function(_, Q, localForage) { 'use strict'; /** * LocalForage adapter. */ var db = { dbs: {}, /** * Create a new localforage instance if it doesn't exist for * current profile or store. * * @type object options */ getDb: function(options) { var dbId = options.profile + '/' + options.storeName; this.dbs[dbId] = this.dbs[dbId] || localForage.createInstance({ name : options.profile || 'notes-db', storeName : options.storeName }); return this.dbs[dbId]; }, /** * Find an item by ID. * * @type object data */ find: function(data) { var defer = Q.defer(); this.getDb(data.options).getItem(data.id, function(err, data) { if (err) { return defer.reject(err); } if (!data) { defer.reject('not found'); } return defer.resolve(data); }); return defer.promise; }, /** * Find all items. * * @type object data */ findAll: function(data) { var defer = Q.defer(), self = this; // Find all keys of objects this.getDb(data.options).keys(function(err, keys) { if (!keys || !keys.length) { return defer.resolve([]); } // Return all found objects return self.findByKeys(keys, data) .then(function(res) { defer.resolve(res); }) .fail(function(e) { defer.reject(e); }); }); return defer.promise; }, /** * Find all models with specified keys. * * @type array keys * @type object data */ findByKeys: function(keys, data) { var promises = [], self = this, models = []; _.each(keys, function(key) { promises.push( self.find({id: key, options: data.options}) .then(function(item) { // If conditions are provided, filter items with them if (item && (!data.options.conditions || _.isMatch(item, data.options.conditions))) { models.push(item); return item; } return; }) ); }); return Q.all(promises) .then(function() { return models; }); }, /** * Save an item. * * @type object data */ save: function(data) { var defer = Q.defer(), sData = data.data; if (sData.encryptedData) { sData = _.omit(sData, data.options.encryptKeys); } this.getDb(data.options).setItem(data.id, sData, function(err, val) { if (err) { return defer.reject(err); } return defer.resolve(val); }); return defer.promise; }, }; return db; }); ================================================ FILE: app/scripts/helpers/fileSaver.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ define([ 'q', 'fileSaver' ], function(Q, fileSaver) { 'use strict'; return function(content, fileName) { // If it is not Cordova app, use HTML5's saveAs function if (!window.cordova) { return new Q(fileSaver(content, fileName)); } var defer = Q.defer(); // Use file plugin API window.resolveLocalFileSystemURL(window.cordova.file.externalDataDirectory, function(dir) { dir.getFile(fileName, {create: true}, function(file) { file.createWriter(function(writer) { writer.write(content); defer.resolve(); }); }); }); return defer.promise; }; }); ================================================ FILE: app/scripts/helpers/i18next.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'jquery', 'q', 'backbone.radio', 'i18next', 'i18nextXHRBackend', 'text!locales/locales.json' ], function(_, $, Q, Radio, i18n, XHR, locales) { 'use strict'; var __ = { /** * Initialize i18next */ init: function() { var defer = Q.defer(); $.t = i18n.t.bind(i18n); i18n .use(XHR) .init( { lng : __.getLang(), fallbackLng : ['en'], ns : [''], defaultNS : '', backend : { loadPath : 'locales/{{lng}}/translation.json' }, }, function() { defer.resolve(); } ); return defer.promise; }, /** * Get language either from configs or * autodetect it from browser settings. */ getLang: function() { var lng = Radio.request('configs', 'get:config', 'appLang'); if (lng || typeof window.navigator === 'undefined') { return lng; } // Language keys in navigator lng = ['languages', 'language', 'userLanguage', 'browserLanguage']; // Available locales locales = _.keys(JSON.parse(locales)); return _.chain(window.navigator) .pick(lng) .values() .flatten() .compact() .map(function(key) { return key.replace('-', '_').toLowerCase(); }) .find(function(key) { return _.contains(locales, key); }) .value(); }, }; /** * Init i18next on `app:before` initialize request. */ Radio.request('init', 'add', 'app:before', __.init); return __; }); ================================================ FILE: app/scripts/helpers/keybindings.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'mousetrap', 'mousetrap.pause' ], function(_, Marionette, Radio, Mousetrap) { 'use strict'; /** * Keybindings helper. * * Replies to requests on `global` channel: * 1. `mousetrap:toggle` - pause or unpause Mousetrap. * 2. `mousetrap:restart` - rebind the keys. * 3. `mousetrap:reset` - reset the keys. */ var Controller = Marionette.Object.extend({ initialize: function() { // Fetch configs and bind the keys this.configs = Radio.request('configs', 'get:object'); this.bind(); Radio.reply('global', { 'mousetrap:toggle' : this.toggle, 'mousetrap:restart' : this.restart, 'mousetrap:reset' : Mousetrap.reset }, this); }, /** * Reset Mousetrap keys and bind them again. */ restart: function() { Mousetrap.reset(); this.bind(); }, /** * Pause or unpause Mousetrap */ toggle: function() { Mousetrap[(this.paused ? 'unpause' : 'pause')](); this.paused = (this.paused ? false : true); }, navigate: function(uri) { Radio.request('uri', 'navigate', uri, { includeProfile : true, trigger : true }); }, /** * Register keybindings. */ bind: function() { var self = this; // Help Mousetrap.bind(this.configs.appKeyboardHelp, function(e) { e.preventDefault(); Radio.request('Help', 'show:keybindings'); }); // Focus on search form Mousetrap.bind(this.configs.appSearch, function(e) { e.preventDefault(); Radio.trigger('global', 'show:search'); }); // Add or edit notes or notebooks Mousetrap.bind(this.configs.appCreateNote, function() { Radio.trigger('global', 'form:show'); }); // Redirect to notes list Mousetrap.bind(this.configs.jumpInbox, function() { self.navigate('/notes'); }); // Redirect to favorite notes Mousetrap.bind(this.configs.jumpFavorite, function() { self.navigate('/notes/f/favorite'); }); // Redirect to removed list of notes Mousetrap.bind(this.configs.jumpRemoved, function() { self.navigate('/notes/f/trashed'); }); // Redirect to list of notes with open tasks Mousetrap.bind(this.configs.jumpOpenTasks, function() { self.navigate('/notes/f/task'); }); // Redirect to notebooks list Mousetrap.bind(this.configs.jumpNotebook, function() { self.navigate('/notebooks'); }); } }); /** * Initializer */ Radio.request('init', 'add', 'app:before', function() { new Controller(); }); return Controller; }); ================================================ FILE: app/scripts/helpers/migrate.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, Modernizr */ define([ 'q', 'underscore', 'localforage', 'sjcl' ], function(Q, _, localForage, sjcl) { 'use strict'; /** * Migrate data from IndexedDB to localForage. */ function Migrate() { } _.extend(Migrate.prototype, { /** * Initialize migration proccess. * * @return promise */ init: function() { if (!Modernizr.indexeddb) { return new Q(); } var defer = Q.defer(), self = this; self.start() .then(function() { setTimeout(function() { return defer.resolve(); }, 100); }); return defer.promise; }, /** * Start migration. * * @return promise */ start: function() { var self = this; return this.openDb('notes-db') .then(function(db) { if (_.isNull(db)) { console.log('no migration is needed'); return; } self.db = db; return self.migrate(db); }) .fail(function(e) { console.error('Migration:initialize', e); }); }, /** * Open an indexedDB database. * * @type string name * @return promise */ openDb: function(name) { var req = window.indexedDB.open(name), defer = Q.defer(); req.onerror = function(e) { console.error('Migration:openDb', e); defer.reject(e); }; // If DB is empty, resolve with null req.onupgradeneeded = function() { if (req.result) { req.result.close(); } defer.resolve(null); }; req.onsuccess = function(e) { defer.resolve(e.target.result); }; return defer.promise; }, /** * Migrate data from all stores to localForage. * * @type object db * @return promise */ migrate: function(db) { var stores = ['notes', 'notebooks', 'tags', 'files'], self = this, promises = []; _.each(stores, function(store) { promises.push(function() { var tr; try { tr = db.transaction([store]); } catch (e) { return; } return self.getData(tr, store) .then(function(data) { return self.migrateStore(store, data); }); }); }); return _.reduce(promises, Q.when, new Q()) .then(function() { db.close(); return; }) .fail(function(e) { console.error('Migration:migrate', e); }); }, /** * Call it after fetching data. It saves data to localForage. * * @type string store * @type object data * @return promise */ migrateStore: function(store, data) { var self = this, promises = [], db = localForage.createInstance({ name : 'notes-db', storeName : store }); _.each(data, function(item) { promises.push(function() { return self.removeItem(store, db, item.id) .then(function() { return self.saveForageItem(store, db, item); }); }); }); return _.reduce(promises, Q.when, new Q()) .then(function() { return; }) .fail(function(e) { console.error('Migration:migrateStore', e); }); }, /** * Save an item to localForage. * * @type string storeName * @type object db * @type string id * @type object data * @return promise */ saveForageItem: function(storeName, db, data) { var defer = Q.defer(); // Convert data if (!_.isUndefined(data.notebookId)) { data.notebookId = data.notebookId.toString(); } if (!_.isUndefined(data.parentId)) { data.parentId = data.parentId.toString(); } if (storeName === 'files') { data.fileType = data.type; data.type = 'files'; } if (storeName === 'tags') { data.id = sjcl.hash.sha256.hash(data.name.toString()).join(''); } data = _.extend( { type : storeName, created : Date.now(), updated : Date.now(), trash : 0 }, data, { id: data.id.toString() } ); db.setItem(data.id, data, function(err, val) { if (err) { return defer.reject(err); } return defer.resolve(val); }); return defer.promise; }, /** * Remove an item from IndexedDB. * * @type string storeName * @type number id */ removeItem: function(storeName, db, id) { if (storeName === 'tags') { return this.putToTrash.apply(this, arguments); } if (storeName !== 'notebooks' || typeof id === 'string') { return new Q(); } var defer = Q.defer(), req = this.db.transaction([storeName], 'readwrite').objectStore(storeName).delete(id); req.onsuccess = function() { defer.resolve(); }; req.onerror = function(e) { defer.reject(e); }; return defer.promise; }, putToTrash: function(storeName, db, id) { var defer = Q.defer(), data = {id: id, type: 'tags', name: id, trash: 2, created: 0, updated: 0}; db.setItem(data.id, data, function(err, val) { if (err) { return defer.reject(err); } return defer.resolve(val); }); return defer.promise; }, /** * Fetch data from indexedDB. * * @type object transaction * @type string storeName * @return promise */ getData: function(transaction, storeName) { var defer = Q.defer(), req = transaction.objectStore(storeName).openCursor(), data = {}; req.onerror = function(e) { console.error('Migration:getData', e); defer.reject(e); }; req.onsuccess = function(e) { var cursor = e.target.result; if (cursor) { data[cursor.key] = cursor.value; cursor.continue(); } else { return defer.resolve(data); } }; return defer.promise; }, }); return Migrate; }); ================================================ FILE: app/scripts/helpers/radio.shim.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ (function(root, factory) { 'use strict'; /* global define */ if (typeof define === 'function' && define.amd) { define(['marionette', 'backbone.radio', 'underscore'], function(Marionette, Radio, _) { return factory(Marionette, Radio, _); }); } else if (typeof exports !== 'undefined') { var Marionette = require('marionette'); var Radio = require('backbone.radio'); var _ = require('underscore'); module.exports = factory(Marionette, Radio, _); } else { factory(root.Backbone.Marionette, root.Backbone.Radio, root._); } }(this, function(Marionette, Radio, _) { 'use strict'; Marionette.Application.prototype._initChannel = function() { this.channelName = _.result(this, 'channelName') || 'global'; this.channel = _.result(this, 'channel') || Radio.channel(this.channelName); this.reqres = this.channel; }; })); ================================================ FILE: app/scripts/helpers/storage.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requirejs, Modernizr */ define([ 'underscore', 'q', 'backbone', 'backbone.radio' ], function(_, Q, Backbone, Radio) { 'use strict'; /** * Used for checking indexedDB support in a browser. */ var Storage = { /** * If indexeddb isn't available use sync adapter without workers. * @return promise */ check: function() { // Browser doesn't support indexeddb at all if (!Modernizr.indexeddb || !Radio.request('global', 'use:webworkers')) { return this.switchDb('backbone.noworker.sync'); } var self = this; return this.testDb() .then(function() { return self.switchDb('backbone.sync'); }) .fail(function() { return self.switchDb('backbone.noworker.sync'); }); }, /** * Test if indexeddb can be used by opening a database. * @return promise */ testDb: function() { var defer = Q.defer(), request = window.indexedDB.open('isPrivateMode'); request.onerror = function() { defer.reject(); }; request.onsuccess = function() { defer.resolve(); }; return defer.promise; }, /** * Override Backbone.sync with our own adapter. * @return promise */ switchDb: function(syncFile) { var defer = Q.defer(); requirejs([syncFile], function(Adapter) { // Override Backbone's sync adapter Backbone.ajaxSync = Backbone.sync; Backbone.sync = Adapter.sync(); defer.resolve(); }); return defer.promise; }, }; return Storage; }); ================================================ FILE: app/scripts/helpers/title.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'jquery', 'q', 'helpers/underscore-util', 'marionette', 'backbone.radio' ], function($, Q, _, Marionette, Radio) { 'use strict'; /** * Title helper. It is used to build title from provided arguments and * to change document title. * * Replies to: * 1. channel: `global`, request: `get:title` * * Replies to: * 1. channel: `global`, request: `set:title` */ var Controller = Marionette.Object.extend({ title: { page : '', main : '', db : '', app : 'Laverna' }, initialize: function() { _.bindAll(this, '_makeTitle'); this.title.db = Radio.request('uri', 'profile'); this.title.db = this.title.db === 'notes-db' ? '' : this.title.db; this.vent = Radio.channel('global'); this.vent.reply('get:title', this.getTitle, this); this.vent.reply('set:title', this.setTitle, this); }, onDestroy: function() { this.vent .stopReplying('get:title set:title'); }, /** * Updates document title. */ setTitle: function(title, type) { /* * If main title needs to be changed, it probably means * that a user is not browsing a note. And that means we * need to reset page title. */ if (type === 'main' && this.title.main !== '') { this.title.page = ''; } type = type || 'page'; this.title[type] = title; // Prepare an array of titles and remove empty ones title = _.compact(_.values(this.title)); document.title = _.cleanXSS(title.join(' - ')); }, getTitle: function(args) { // Filter has additional logic if (args.query && this['_' + args.filter + 'Title']) { return this['_' + args.filter + 'Title'](args) .then(this._makeTitle); } else { return new Q(this._makeTitle(args)); } }, _makeTitle: function(args) { // Translate the title to other languages var title = args.title || (args.filter && args.filter !== 'active' ? args.filter : 'All notes'); title = $.t(title.substr(0, 1).toUpperCase() + title.substr(1)); if (!args.title && args.query && args.filter !== 'search') { title = args.query; } // Change document.title and return the title this.vent.request('set:title', title, 'main'); return title; }, /** * Use notebook name as a title instead of ID. */ _notebookTitle: function(args) { args.id = args.query; return Radio.request('notebooks', 'get:model', args) .then(function(model) { args.title = model.get('name'); return args; }); } }); Radio.request('init', 'add', 'app:before', function() { new Controller(); }); return Controller; }); ================================================ FILE: app/scripts/helpers/underscore-util.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'xss' ], function(_, cleanXSS) { 'use strict'; /** * Add some helper functions to Underscore. */ _.mixin({ /** * Sanitize HTML to prevent XSS. * * @type string str * @type boolean unescape * @type boolean stripTags */ cleanXSS: function(str, unescape, stripTags) { /* Unescape the string 2 times because * data from Dropbox is always escaped + we escape them too. */ if (unescape === true) { str = _.runTimes(_.unescape, 2, str); } // Remove all HTML tags if (stripTags === true) { str = _.stripTags(str); } return cleanXSS(str); }, /** * Invokes the given function n times and returns the last result. * Example: * _.runTimes(_.unescape, 2, 'String'); * * @type function func * @type number n */ runTimes: function(func, n) { var args = Array.prototype.slice.call(arguments, 2), res; res = _.times(n, function() { return func.apply(null, args); }); return res[res.length - 1]; }, /** * Remove all HTML from string. * * @type string str */ stripTags: function(str) { return str.replace(/<\/?[^>]+>/g, ''); }, }); return _; }); ================================================ FILE: app/scripts/helpers/uri.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'backbone', 'backbone.radio', 'marionette', ], function(_, Backbone, Radio, Marionette) { 'use strict'; /** * Uri helper. It is a convenient module that we use to navigate * or do some URI related stuff. * It listens to requests on `uri` channel. * * Responds to: * ----------- * * Requests: * 1. request: `profile` * returns current profile name. * 2. request: `link:profile` * returns a link to a profile. * 3. request: `link` * generates and returns a link to notes list or to a note. * 4. request: `link:file` * generate file URL. * 5. request: `navigate` * navigate to provided URL. * 6. request: `back` * it navigates to the previous page. */ var Uri = Marionette.Object.extend({ initialize: function() { this.vent = Radio.channel('uri'); this.profile = this.getProfile(); _.bindAll(this, 'checkProfile'); $(window).on('hashchange', this.checkProfile); // Replies this.vent.reply({ 'navigate' : this.navigate, 'back' : this.navigateBack, 'profile' : this.getProfile, 'link:profile' : this.getProfileLink, 'link:file' : this.getFileLink, 'link' : this.getLink, 'get:current' : this.getRoute, }, this); }, checkProfile: function() { if (this.getProfile() !== this.profile) { window.location.reload(); } }, /** * Navigate to url */ navigate: function(uri, options) { options = options || {}; if (typeof options.trigger === 'undefined') { options.trigger = true; } // Build URL to notes list or a note if (_.isObject(uri)) { uri = (uri.model || uri.options) ? uri : {options: uri}; uri = this.getLink(uri.options, uri.model); } // Include profile link if (options.includeProfile) { uri = this.getProfileLink(uri); options.includeProfile = null; } Backbone.history.navigate(uri, options); }, navigateBack: function(url) { var history = window.history; if (history.length === 0) { return this.navigate(url || '/notes', arguments[1]); } history.back(); }, /** * Generate file URL. */ getFileLink: function(model, blob) { // Just generate pseudo URL if (!blob) { return '#file:' + model.id; } var url = window.URL || window.webkitURL, src = (model.src || model.get('src')); return url.createObjectURL(src); }, /** * Generates a link to a profile */ getProfileLink: function(uri, profile) { profile = profile || this.getProfile(); uri = (uri[0] !== '/' ? '/' + uri : uri); return !profile ? uri : '/p/' + profile + uri.replace(/\/?p\/[^/]*\//, '/'); }, /** * Returns current profile's name */ getProfile: function() { var profile = document.location.hash.match(/\/?p\/([^/]*)\//); return (!profile ? profile : profile[profile.index]); }, /** * Returns current route */ getRoute: function() { return Backbone.history.fragment; }, /** * Generates a link from provided options */ getLink: function(options, model) { options = _.extend({}, options || {}); var url = '/notes', filters = { filter : '/f/', query : '/q/', page : '/p' }; options.page = isNaN(options.page) ? 0 : options.page; _.each(filters, function(value, filter) { if (_.has(options, filter) && options[filter]) { url += value + options[filter]; } }); url += model ? '/show/' + model.id : ''; return this.getProfileLink(url, options.profile); } }); /** * Add a new initializer */ Radio.request('init', 'add', 'app:before', function() { new Uri(); }); return Uri; }); ================================================ FILE: app/scripts/init.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requirejs */ define([ 'jquery', 'q', 'fastclick', 'hammerjs', 'helpers/radio.shim', 'backbone.radio', 'app', 'initializers', 'bootstrap', 'jHammer' ], function($, Q, FastClick, Hammer, shim, Radio, App) { 'use strict'; var hash = document.location.hash; Radio.reply('global', 'hash:original', function() { return hash; }); console.time('App'); // Remove 300ms delay FastClick.attach(document.body); // Enable text selection delete Hammer.defaults.cssProps.userSelect; // Load all modules then start an application requirejs([ // Helpers 'helpers/storage', 'helpers/uri', 'helpers/title', 'helpers/i18next', 'helpers/keybindings', // Classes 'moduleLoader', 'classes/encryption', // Collection modules 'collections/modules/notes', 'collections/modules/notebooks', 'collections/modules/tags', 'collections/modules/files', 'collections/modules/configs', // Apps 'apps/confirm/appConfirm', 'apps/encryption/appEncrypt', 'apps/navbar/appNavbar', 'apps/notes/appNote', 'apps/notebooks/appNotebooks', 'apps/settings/appSettings', 'apps/help/appHelp', // Modules 'modules/markdown/module', 'modules/codemirror/module', 'modules/linkDialog/module', 'modules/fileDialog/module', 'modules/importExport/module' ], function(storage) { // Get profile name from location hash var profile = document.location.hash.match(/\/?p\/([^/]*)\//); profile = (!profile ? profile : profile[profile.index]); console.warn('prof', profile); return storage.check() .then(function() { return Radio.request('configs', 'get:all', {profile: profile}); }) // Load optional modules .then(function() { return Radio.request('init', 'start', 'load:modules')(); }) .then(Radio.request('init', 'start', 'app:before app auth module')) .then(function() { console.log('modules are loaded'); App.start(); }) .fail(function(e) { console.error('Error', e); }); }); }); ================================================ FILE: app/scripts/initializers.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'backbone.radio', 'marionette' ], function(_, Q, Radio, Marionette) { 'use strict'; /** * This class is used to add and execute asynchronous initializers. * It is very convenient when writing modules or writing sub apps. * Also it ensures that every module or sub app is ready * before the app starts. * * App initializers will be executed first. Then, module initializers. * * Adding new initializers: * Radio.request('init', 'add', '[app|app:before|module]', function() {}); * * Executing initializers * Radio.request('init', 'start', '[app|app:before|module]', args); */ var Initializers = Marionette.Object.extend({ initialize: function() { this._inits = {}; Radio.channel('init') .reply('add', this.addInit, this) .reply('start', this.executeInits, this); }, addInit: function(name, initializer) { this._inits[name] = this._inits[name] || []; this._inits[name].push(initializer); }, /** * Executes all the initializers */ executeInits: function(types, args) { var self = this; types = types.split(' '); args = Array.prototype.slice.call(arguments, 1); return function() { var promises = []; // Execute every init one after another _.each(types, function(type) { promises.push(function() { return self._executeInit(type, args); }); }); return _.reduce(promises, Q.when, new Q()); }; }, /** * Executes an initializer */ _executeInit: function(type, args) { var self = this, promises = []; // Execute all the functions asynchronously _.each(self._inits[type], function(fnc) { promises.push(function() { return new Q(fnc.apply(null, args)); }); }); return _.reduce(promises, Q.when, new Q()); } }); return new Initializers(); }); ================================================ FILE: app/scripts/main.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global requirejs, requireNode */ requirejs.config({ // Find all nested dependencies findNestedDependencies: true, waitSeconds: 10, nodeRequire: (typeof requireNode === 'undefined' ? null : requireNode), packages: [ // Codemirror editor { name : 'codemirror', location : '../bower_components/codemirror', main : 'lib/codemirror' }, // Prismjs { name : 'prism', location : '../bower_components/prism', main : 'bundle' }, // Xregexp { name : 'xregexp', location : '../bower_components/xregexp/src', main : 'xregexp' } ], paths: { sjcl : '../bower_components/sjcl/sjcl', text : '../bower_components/requirejs-text/text', jquery : '../bower_components/jquery/dist/jquery', q : '../bower_components/q/q', bootstrap : '../bower_components/bootstrap/dist/js/bootstrap.min', i18next : '../bower_components/i18next/i18next', i18nextXHRBackend : '../bower_components/i18next-xhr-backend/i18nextXHRBackend', // Backbone underscore : '../bower_components/underscore/underscore', backbone : '../bower_components/backbone/backbone', marionette : '../bower_components/backbone.marionette/lib/core/backbone.marionette', 'backbone.radio' : '../bower_components/backbone.radio/build/backbone.radio.min', 'backbone.babysitter' : '../bower_components/backbone.babysitter/lib/backbone.babysitter', fuse : '../bower_components/fuse/src/fuse', // Mousetrap 'mousetrap' : '../bower_components/mousetrap/mousetrap', 'mousetrap.pause' : '../bower_components/mousetrap/plugins/pause/mousetrap-pause', 'mousetrap.global' : '../bower_components/mousetrap/plugins/global-bind/mousetrap-global-bind', // Storage adapters localforage : '../bower_components/localforage/dist/localforage', remotestorage : '../bower_components/remotestorage.js/release/stable/remotestorage', bluebird : '../bower_components/bluebird/js/browser/bluebird.min', tv4 : '../bower_components/tv4/tv4', dropbox : 'helpers/Dropbox-sdk.min', // Markdown 'markdown-it' : '../bower_components/markdown-it/dist/markdown-it.min', 'markdown-it-san' : '../bower_components/markdown-it-sanitizer/dist/markdown-it-sanitizer.min', 'markdown-it-hash' : '../bower_components/markdown-it-hashtag/dist/markdown-it-hashtag.min', 'markdown-it-math' : '../bower_components/markdown-it-math/dist/markdown-it-math.min', 'markdown-it-imsize' : '../bower_components/markdown-it-imsize/dist/markdown-it-imsize.min', 'to-markdown' : '../bower_components/to-markdown/src/to-markdown', // Others xss : '../bower_components/xss/dist/xss', mathjax : '../bower_components/MathJax/MathJax.js?config=TeX-AMS-MML_HTMLorMML', prettify : '../bower_components/google-code-prettify/src/prettify', dropzone : '../bower_components/dropzone/dist/dropzone-amd-module', toBlob : '../bower_components/blueimp-canvas-to-blob/js/canvas-to-blob', blobjs : '../bower_components/Blob/Blob', fileSaver : '../bower_components/FileSaver/FileSaver', enquire : '../bower_components/enquire/dist/enquire.min', hammerjs : '../bower_components/hammerjs/hammer', jHammer : '../bower_components/jquery-hammerjs/jquery.hammer', fastclick : '../bower_components/fastclick/lib/fastclick', devicejs : '../bower_components/device.js/lib/device.min', jszip : '../bower_components/jszip/dist/jszip', // Aliases 'modalRegion' : 'views/modal', 'brandRegion' : 'views/brand', 'apps' : 'apps', 'locales' : '../locales' }, map: { '*': { 'backbone.wreqr' : 'backbone.radio' } }, shim: { // Backbone underscore: { exports: '_' }, fileSaver: { exports: 'saveAs', }, backbone: { deps: ['underscore', 'jquery'], exports: 'Backbone' }, // Storage adapters dropbox: { exports: 'Dropbox' }, 'remotestorage': { exports: 'RemoteStorage', deps: [ 'tv4', 'bluebird', ] }, tv4: { exports: 'tv4' }, // Markdown 'to-markdown': { exports: 'toMarkdown' }, // Xregexp 'xregexp/xregexp': { exports: 'XRegExp' }, 'xregexp/addons/unicode/unicode-base': { deps: ['xregexp/xregexp'], exports: 'XRegExp' }, 'xregexp/addons/unicode/unicode-categories': { deps: [ 'xregexp/addons/unicode/unicode-base' ], exports: 'XRegExp' }, // Others sjcl: { exports: 'sjcl' }, 'prism/bundle': { exports: 'Prism' }, xss: { exports: 'filterXSS' }, bootstrap: { deps: ['jquery'] }, 'mathjax': { exports: 'MathJax' }, devicejs: { exports: 'device' }, prettify: { exports: 'PR' } } }); // Starting point requirejs(['init']); ================================================ FILE: app/scripts/migrate.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global requirejs */ requirejs.config({ paths: { q : '../bower_components/q/q', underscore : '../bower_components/underscore/underscore', localforage : '../bower_components/localforage/dist/localforage', sjcl : '../bower_components/sjcl/sjcl', } }); requirejs(['helpers/migrate'], function(Migrate) { 'use strict'; new Migrate().init() .then(function() { document.location.href = document.location.href.toString().replace('migrate.html', ''); }) .fail(function(e) { console.error('Migrate Error:', e); }); }); ================================================ FILE: app/scripts/models/config.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'backbone' ], function(_, Backbone) { 'use strict'; var Config = Backbone.Model.extend({ idAttribute: 'name', profileId : 'notes-db', storeName : 'configs', defaults: { 'name' : '', 'value' : '' }, validate: function(attrs) { var errors = []; if (attrs.name === '') { errors.push('name'); } if (errors.length > 0) { return errors; } }, /** * Switch to another profile */ changeDB: function(id) { this.profileId = id; }, /** * Parse the value of a model */ getValueJSON: function() { return JSON.parse(this.get('value')); }, createProfile: function(name) { if (!name) { return; } var value = JSON.parse(this.get('value')); if (_.contains(value, name) === false) { value.push(name); return this.save({value: JSON.stringify(value)}); } }, removeProfile: function(name) { if (!name) { return; } var value = JSON.parse(this.get('value')); if (_.contains(value, name) === true) { value = _.without(value, name); window.indexedDB.deleteDatabase(name); return this.save({value: JSON.stringify(value)}); } }, /** * @return bool */ isPassword: function(data) { return ( ( this.get('name') === 'encryptPass' || data.name === 'encryptPass' ) && ( typeof data.value !== 'object' && data.value !== this.get('value').toString() ) ); } }); return Config; }); ================================================ FILE: app/scripts/models/file.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'backbone' ], function(_, Backbone) { 'use strict'; /** * Files model */ var File = Backbone.Model.extend({ idAttribute: 'id', profileId : 'notes-db', storeName : 'files', defaults: { type : 'files', id : undefined, name : '', src : '', fileType : '', trash : 0, created : 0, updated : 0 }, validate: function(attrs) { var errors = []; if (attrs.src === '') { errors.push('src'); } if (attrs.fileType === '') { errors.push('fileType'); } if (errors.length > 0) { return errors; } }, setEscape: function(data) { if (data.name) { data.name = _.cleanXSS(data.name, true); } this.set(data); return this; } }); return File; }); ================================================ FILE: app/scripts/models/note.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'backbone' ], function(_, Backbone) { 'use strict'; /** * Notes model */ var Model = Backbone.Model.extend({ idAttribute: 'id', profileId : 'notes-db', storeName : 'notes', defaults: { 'type' : 'notes', 'id' : undefined, 'title' : '', 'content' : '', 'taskAll' : 0, 'taskCompleted' : 0, 'created' : 0, 'updated' : 0, 'notebookId' : '0', 'tags' : [], 'isFavorite' : 0, 'trash' : 0, 'files' : [] }, encryptKeys: [ 'title', 'content', 'tags', 'tasks' ], validate: function(attrs) { // It's not neccessary to validate when a model is about to be removed if (attrs.trash && Number(attrs.trash) === 2) { return; } var errors = []; if (!_.isUndefined(attrs.title) && !attrs.title.trim().length) { errors.push('title'); } if (errors.length > 0) { return errors; } }, toggleFavorite: function() { return {isFavorite: (this.get('isFavorite') === 1) ? 0 : 1}; }, /** * Purify user inputs */ setEscape: function(data) { if (data.title) { data.title = _.cleanXSS(data.title, true); } if (data.content) { data.content = _.cleanXSS(data.content, true); } this.set(data); return this; } }); return Model; }); ================================================ FILE: app/scripts/models/notebook.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'underscore', 'backbone' ], function(_, Backbone) { 'use strict'; var Model = Backbone.Model.extend({ idAttribute: 'id', profileId : 'notes-db', storeName : 'notebooks', defaults: { 'type' : 'notebooks', 'id' : undefined, 'parentId' : '0', 'name' : '', 'count' : 0, 'trash' : 0, 'created' : 0, 'updated' : 0 }, encryptKeys: ['name'], validate: function(attrs) { // It's not neccessary to validate when a model is about to be removed if (attrs.trash && Number(attrs.trash) === 2) { return; } var errors = []; if (attrs.name === '') { errors.push('name'); } if (attrs.parentId === attrs.id) { errors.push('parentId'); } if (errors.length > 0) { return errors; } }, initialize: function() { if (typeof this.id === 'number') { this.set('id', this.id.toString()); this.set('parentId', this.get('parentId').toString()); } }, setEscape: function(data) { if (data.name) { data.name = _.cleanXSS(data.name, true); } this.set(data); return this; }, }); return Model; }); ================================================ FILE: app/scripts/models/tag.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /*global define*/ define([ 'jquery', 'underscore', 'backbone' ], function($, _, Backbone) { 'use strict'; /** * Tags model */ var Tag = Backbone.Model.extend({ idAttribute: 'id', profileId : 'notes-db', storeName : 'tags', defaults: { 'type' : 'tags', 'id' : undefined, 'name' : '', 'count' : '', 'trash' : 0, 'created' : 0, 'updated' : 0 }, encryptKeys: ['name'], setEscape: function(data) { if (data.name) { data.name = _.cleanXSS(data.name, true); } this.set(data); return this; }, /** * Validates a tag. * @type array */ validate: function(attrs) { // It's not neccessary to validate when a model is about to be removed if (attrs.trash && Number(attrs.trash) === 2) { return; } var errors = []; if (!_.isUndefined(attrs.name) && !attrs.name.trim().length) { errors.push('name'); } if (errors.length > 0) { return errors; } }, }); return Tag; }); ================================================ FILE: app/scripts/moduleLoader.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requirejs */ define([ 'underscore', 'q', 'backbone.radio', 'text!modules/modules.json' ], function(_, Q, Radio, modules) { 'use strict'; /** * Module loader. */ var ModuleLoader = { init: function() { var platform = Radio.request('global', 'platform'); // List of available modules modules = _.filter(JSON.parse(modules), function(m) { return _.indexOf(m.platforms, platform) > -1; }); Radio.reply('global', 'modules', modules); return this.load(); }, /** * Load modules. */ load: function() { var defer = Q.defer(); requirejs(this.get(), function() { defer.resolve(); }); return defer.promise; }, /** * Return a list of modules which need to be loaded. * * @return array */ get: function() { var modules2Load = Radio.request('configs', 'get:config', 'modules'); modules2Load = _.map(modules2Load, function(name) { if (!name || !_.findWhere(modules, {id: name})) { return ''; } return 'modules/' + name + '/module'; }); switch (Radio.request('configs', 'get:config', 'cloudStorage')) { case 'remotestorage': modules2Load.push('modules/remotestorage/module'); break; case 'dropbox': modules2Load.push('modules/dropbox/module'); break; default: break; } return _.compact(modules2Load); }, }; // Register an initializer Radio.request('init', 'add', 'load:modules', function() { return ModuleLoader.init(); }); return ModuleLoader; }); ================================================ FILE: app/scripts/modules/codemirror/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'jquery', 'marionette', 'backbone.radio', 'codemirror/lib/codemirror', 'modules/codemirror/views/editor', 'codemirror/mode/gfm/gfm', 'codemirror/mode/markdown/markdown', 'codemirror/addon/edit/continuelist', 'codemirror/addon/mode/overlay', 'codemirror/keymap/vim', 'codemirror/keymap/emacs', 'codemirror/keymap/sublime' ], function(_, $, Marionette, Radio, CodeMirror, View) { 'use strict'; /** * Codemirror module. * Regex and WYSIWG button functions are based on simplemde-markdown-editor: * https://github.com/NextStepWebs/simplemde-markdown-editor */ var Controller = Marionette.Object.extend({ marks: { strong: { tag : ['**', '__'], start : /(\*\*|__)(?![\s\S]*(\*\*|__))/, end : /(\*\*|__)/, }, em: { tag : ['*', '_'], start : /(\*|_)(?![\s\S]*(\*|_))/, end : /(\*|_)/, }, strikethrough: { tag : ['~~'], start : /(\*\*|~~)(?![\s\S]*(\*\*|~~))/, end : /(\*\*|~~)/, }, 'code': { tag : '```\r\n', tagEnd : '\r\n```', }, 'unordered-list': { replace : /^(\s*)(\*|\-|\+)\s+/, tag : '* ', }, 'ordered-list': { replace : /^(\s*)\d+\.\s+/, tag : '1. ', }, }, initialize: function() { _.bindAll(this, 'onChange', 'onScroll', 'onCursor', 'boldAction', 'italicAction', 'linkAction', 'headingAction', 'attachmentAction', 'codeAction', 'hrAction', 'listAction', 'numberedListAction'); // Get configs this.configs = Radio.request('configs', 'get:object'); // Initialize the view this.view = new View({ model : Radio.request('notesForm', 'model'), configs : this.configs }); this.view.once('dom:refresh', this.initEditor, this); // Events this.listenTo(this.view, 'editor:action', this.onViewAction); this.listenTo(Radio.channel('notesForm'), 'set:mode', this.changeMode); this.listenTo(Radio.channel('editor'), 'focus', this.focus); // Show the view and render Pagedown editor Radio.request('notesForm', 'show:editor', this.view); Radio.reply('editor', { 'get:data' : this.getData, 'generate:link' : this.generateLink, 'generate:image': this.generateImage }, this); // Init footer to show current line numbers // but first of hide it, because when you open/add a note // the title is focused, not the editor this.footer = $('#editor--footer'); this.footer.hide(); }, onDestroy: function() { Radio.stopReplying('editor', 'get:data'); }, initEditor: function() { this.editor = CodeMirror.fromTextArea(document.getElementById('editor--input'), { mode : { name : 'gfm', gitHubSpice : false }, keyMap: this.configs.textEditor || 'default', lineNumbers : false, matchBrackets : true, lineWrapping : true, indentUnit : parseInt(this.configs.indentUnit, 10), extraKeys : { 'Cmd-B' : this.boldAction, 'Ctrl-B' : this.boldAction, 'Cmd-I' : this.italicAction, 'Ctrl-I' : this.italicAction, 'Cmd-H' : this.headingAction, 'Ctrl-H' : this.headingAction, 'Cmd-L' : this.linkAction, 'Ctrl-L' : this.linkAction, 'Cmd-K' : this.codeAction, 'Ctrl-K' : this.codeAction, 'Cmd-O' : this.numberedListAction, 'Ctrl-O' : this.numberedListAction, 'Cmd-U' : this.listAction, 'Ctrl-U' : this.listAction, // Ctrl+G - attach file 'Cmd-G' : this.attachmentAction, 'Ctrl-G' : this.attachmentAction, // Shift+Ctrl+- - divider 'Shift-Cmd--' : this.hrAction, 'Shift-Ctrl--' : this.hrAction, // Ctrl+. - indent line 'Ctrl-.' : 'indentMore', 'Shift-Ctrl-.' : 'indentLess', 'Cmd-.' : 'indentMore', 'Shift-Cmd-.' : 'indentLess', 'Enter' : 'newlineAndIndentContinueMarkdownList', } }); window.dispatchEvent(new Event('resize')); this.editor.on('change', this.onChange); this.editor.on('scroll', this.onScroll); this.editor.on('cursorActivity', this.onCursor); // Show the preview this.updatePreview(); }, changeMode: function(mode) { window.dispatchEvent(new Event('resize')); this.view.trigger('change:mode', mode); }, /** * Update the preview. */ updatePreview: function() { var self = this, data = _.pick(this.view.model, 'attributes', 'files'); return Radio.request('markdown', 'render', _.extend({}, data, { attributes: { content: this.editor.getValue() } })) .then(function(content) { self.view.trigger('editor:change', content); }); }, /** * Text in the editor changed. */ onChange: function() { // Update the preview this.updatePreview(); // Trigger autosave this.autoSave(); }, /** * Editor's cursor position changed. */ onCursor: function() { var state = this.getState(); this.$btns = this.$btns || $('.editor--btns .btn'); // Make a specific button active depending on the type of the element under cursor this.$btns.removeClass('btn-primary'); for (var i = 0; i < state.length; i++) { this['$btn' + state[i]] = this['$btn' + state[i]] || $('.editor--btns [data-state="' + state[i] + '"]'); this['$btn' + state[i]].addClass('btn-primary'); } // Update lines in footer this.footer.show(); var currentLine = this.editor.getCursor('start').line + 1; var numberOfLines = this.editor.lineCount(); this.footer.html($.t('Line of', {currentLine: currentLine, numberOfLines: numberOfLines})); }, /** * Trigger 'save:auto' event. */ autoSave: _.debounce(function() { Radio.trigger('notesForm', 'save:auto'); }, 1000), /** * Synchronize the editor's scroll position with the preview's. */ onScroll: _.debounce(function(e) { // Don't do any computations if (!e.doc.scrollTop) { this.view.ui.previewScroll.scrollTop(0); return; } var info = this.editor.getScrollInfo(), lineNumber = this.editor.lineAtHeight(info.top, 'local'), range = this.editor.getRange({line: 0, ch: null}, {line: lineNumber, ch: null}), self = this, fragment, temp, lines, els; Radio.request('markdown', 'render', range) .then(function(html) { // Create a fragment and attach rendered HTML fragment = document.createDocumentFragment(); temp = document.createElement('div'); temp.innerHTML = html; fragment.appendChild(temp); // Get all elements in both the fragment and the preview lines = temp.children; els = self.view.ui.preview[0].children; // Get from the preview the last visible element of the editor var newPos = els[lines.length].offsetTop; /** * If the scroll position is on the same element, * change it according to the difference of scroll positions in the editor. */ if (self.scrollTop && self.scrollPos === newPos) { self.view.ui.previewScroll.scrollTop(self.view.ui.previewScroll.scrollTop() + (e.doc.scrollTop - self.scrollTop)); self.scrollTop = e.doc.scrollTop; return; } // Scroll to the last visible element's position self.view.ui.previewScroll.animate({ scrollTop: newPos }, 70, 'swing'); self.scrollPos = newPos; self.scrollTop = e.doc.scrollTop; }); }, 10), /** * If the view triggered some action event, call a suitable function. * For instance, when action='bold', call boldAction method. */ onViewAction: function(action) { action = action + 'Action'; if (this[action]) { this[action](); } }, /** * Return data from the editor. */ getData: function() { var content = this.editor.getValue(); return Radio.request('markdown', 'parse', content) .then(function(env) { return _.extend( _.pick(env, 'tags', 'tasks', 'taskCompleted', 'taskAll', 'files'), {content: content} ); }); }, /** * Return state of the element under the cursor. */ getState: function(pos) { pos = pos || this.editor.getCursor('start'); var stat = this.editor.getTokenAt(pos); if (!stat.type) { return []; } stat.type = stat.type.split(' '); if (_.indexOf(stat.type, 'variable-2') !== -1) { if (/^\s*\d+\.\s/.test(this.editor.getLine(pos.line))) { stat.type.push('ordered-list'); } else { stat.type.push('unordered-list'); } } return stat.type; }, /** * Toggle Markdown block. */ toggleBlock: function(type) { var stat = this.getState(), start = this.editor.getCursor('start'), end = this.editor.getCursor('end'), text, startText, endText; // Text is already [strong|italic|etc] if (_.indexOf(stat, type) !== -1) { text = this.editor.getLine(start.line); startText = text.slice(0, start.ch); endText = text.slice(start.ch); // Remove Markdown tags from the text startText = startText.replace(this.marks[type].start, ''); endText = endText.replace(this.marks[type].end, ''); this.replaceRange(startText + endText, start.line); start.ch -= this.marks[type].tag[0].length; end.ch -= this.marks[type].tag[0].length; } else { text = this.editor.getSelection(); for (var i = 0; i < this.marks[type].tag.length - 1; i++) { text = text.split(this.marks[type].tag[i]).join(''); } this.editor.replaceSelection(this.marks[type].tag[0] + text + this.marks[type].tag[0]); start.ch += this.marks[type].tag[0].length; end.ch = start.ch + text.length; } this.editor.setSelection(start, end); this.editor.focus(); }, /** * Make selected text strong. */ boldAction: function() { this.toggleBlock('strong'); }, /** * Make selected text italicized. */ italicAction: function() { this.toggleBlock('em'); }, /** * Create headings. */ headingAction: function() { var start = this.editor.getCursor('start'), end = this.editor.getCursor('end'); for (var i = start.line; i <= end.line; i++) { this.toggleHeading(i); } }, /** * Show a dialog to attach images or files. */ attachmentAction: function() { var self = this, dialog = Radio.request('editor', 'show:attachment', this.view.model); if (!dialog) { return; } dialog.then(function(text) { if (!text || !text.length) { return; } self.editor.replaceSelection(text, true); self.editor.focus(); }); }, /** * Show a link dialog. */ linkAction: function() { var self = this, dialog = Radio.request('editor', 'show:link'); if (!dialog) { return; } dialog.then(function(link) { if (!link || !link.length) { return; } var cursor = self.editor.getCursor('start'), text = self.editor.getSelection() || 'Link'; self.editor.replaceSelection('[' + text + '](' + link + ')'); self.editor.setSelection( {line: cursor.line, ch: cursor.ch + 1}, {line: cursor.line, ch: cursor.ch + text.length + 1} ); self.editor.focus(); }); }, /** * Create a divider. */ hrAction: function() { var start = this.editor.getCursor('start'); this.editor.replaceSelection('\r\r-----\r\r'); start.line += 4; start.ch = 0; this.editor.setSelection( start, start ); this.editor.focus(); }, /** * Create a code block. */ codeAction: function() { var state = this.getState(), start = this.editor.getCursor('start'), end = this.editor.getCursor('end'), text; if (_.indexOf(state, 'code') !== -1) { return; } else { text = this.editor.getSelection(); this.editor.replaceSelection(this.marks.code.tag + text + this.marks.code.tagEnd); } this.editor.setSelection({line: start.line + 1, ch: start.ch}, {line: end.line + 1, ch: end.ch}); this.editor.focus(); }, replaceRange: function(text, line) { this.editor.replaceRange(text, { line : line, ch : 0 }, { line : line, ch : 99999999999999 }); this.editor.focus(); }, /** * Convert a line to a headline. */ toggleHeading: function(i) { var text = this.editor.getLine(i), headingLvl = text.search(/[^#]/); // Create a default headline if (headingLvl === -1) { text = '# Heading'; this.replaceRange(text, i); return this.editor.setSelection( {line: i, ch: 2}, {line: i, ch: 9} ); } // Increase headline level up to 6th if (headingLvl < 6) { text = headingLvl > 0 ? text.substr(headingLvl + 1) : text; text = new Array(headingLvl + 2).join('#') + ' ' + text; } else { text = text.substr(headingLvl + 1); } this.replaceRange(text, i); }, /** * Convert selected text to unordered list. */ listAction: function() { this.toggleLists('unordered-list'); }, /** * Convert selected text to ordered list. */ numberedListAction: function() { this.toggleLists('ordered-list', 1); }, /** * Convert several selected lines to ordered or unordered lists. */ toggleLists: function(type, order) { var state = this.getState(), start = this.editor.getCursor('start'), end = this.editor.getCursor('end'); // Convert each line to list _.each(new Array(end.line - start.line + 1), function(val, i) { this.toggleList(type, start.line + i, state, order); if (order) { order++; } }, this); }, /** * Convert selected text to an ordered or unordered list. */ toggleList: function(name, line, state, order) { var text = this.editor.getLine(line); // If it is a list, convert it to normal text if (_.indexOf(state, name) !== -1) { text = text.replace(this.marks[name].replace, '$1'); } else if (order) { text = order + '. ' + text; } else { text = this.marks[name].tag + text; } this.replaceRange(text, line); }, /** * Redo the last action in Codemirror. */ redoAction: function() { this.editor.redo(); }, /** * Undo the last action in Codemirror. */ undoAction: function() { this.editor.undo(); }, /** * Focus on the editor. */ focus: function() { this.editor.focus(); }, generateLink: function(data) { return '[' + data.text + ']' + '(' + data.url + ')'; }, generateImage: function(data) { return '!' + this.generateLink(data); }, }); return Controller; }); ================================================ FILE: app/scripts/modules/codemirror/module.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'backbone.radio', 'marionette', 'modules', 'modules/codemirror/controller' ], function(_, Radio, Marionette, Modules, Controller) { 'use strict'; /** * Codemirror module. */ var Module = Modules.module('Codemirror', {}); /** * Initializers & finalizers of the module */ Module.on('start', function() { console.info('Codemirror module has started'); Module.controller = new Controller(); }); Module.on('stop', function() { console.info('Codemirror module has stoped'); Module.controller.destroy(); Module.controller = null; }); // Add a global module initializer Radio.request('init', 'add', 'module', function() { console.info('Codemirror module has been initialized'); Radio.channel('notesForm') .on('view:ready', Module.start, Module) .on('view:destroy', Module.stop, Module); }); return Module; }); ================================================ FILE: app/scripts/modules/codemirror/templates/editor.html ================================================
================================================ FILE: app/scripts/modules/codemirror/views/editor.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'text!modules/codemirror/templates/editor.html' ], function(_, Marionette, Radio, Tmpl) { 'use strict'; /** * Codemirror view. */ var View = Marionette.ItemView.extend({ template: _.template(Tmpl), className: 'layout--body container-fluid', ui: { preview : '#wmd-preview', previewScroll : '.editor--preview', bar : '.editor--bar' }, events: { 'click .editor--btns .btn' : 'triggerAction', 'click .editor--col--btn' : 'showColumn' }, initialize: function() { this.options.mode = Radio.request('configs', 'get:config', 'editMode'); this.listenTo(this, 'editor:change', this.onEditorChange); this.listenTo(this, 'change:mode', this.onChangeMode); this.$layoutBody = $('.layout--body.-scroll.-form'); this.$layoutBody.on('scroll', _.bind(this.onScroll, this)); }, onDestroy: function() { this.$layoutBody.off('scroll'); Radio.trigger('editor', 'view:destroy'); }, onChangeMode: function(mode) { this.options.mode = mode; if (mode !== 'normal') { // Make the editor visible by scrolling back this.$layoutBody.scrollTop(0); // Change WYSIWYG bar width this.ui.bar.css('width', 'initial'); return this.ui.bar.removeClass('-fixed'); } }, onScroll: function() { // If editor mode is not 'normal' mode, don't do anything if (this.options.mode !== 'normal') { return; } // Fix WYSIWYG bar on top if (this.$layoutBody.scrollTop() > this.ui.bar.offset().top) { this.ui.bar.css('width', this.$layoutBody.width()); return this.ui.bar.addClass('-fixed'); } this.ui.bar.css('width', 'initial'); return this.ui.bar.removeClass('-fixed'); }, onEditorChange: function(content) { this.ui.preview.html(content); if (!this.isFirst) { this.isFirst = true; return Radio.trigger('editor', 'view:render', this); } Radio.trigger('editor', 'preview:refresh'); }, triggerAction: function(e) { e.preventDefault(); var action = $(e.currentTarget).attr('data-action'); if (action) { this.trigger('editor:action', action); } }, /** * Shows either the preview or the editor. */ showColumn: function(e) { var $btn = $(e.currentTarget), col = $btn.attr('data-col'), hideCol = (col === 'left' ? 'right' : 'left'); // Add 'active' class to the button this.$('.editor--col--btn.active').removeClass('active'); $btn.addClass('active'); // Show only one column this.$('.-' + hideCol).removeClass('-show'); this.$('.-' + col).addClass('-show'); }, }); return View; }); ================================================ FILE: app/scripts/modules/dropbox/classes/adapter.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q' ], function(_, Q) { 'use strict'; var Adapter = { init: function(client, profile) { this.client = client; this.profile = profile; }, /** * Save a model to Dropbox. * * @type string type [notes|notebooks|tags] * @type object model * @type array encryptKeys */ save: function(type, model, encryptKeys) { if (!model.id) { return new Q(); } if (model.encryptedData) { model = _.omit(model, encryptKeys); } var path = '/' + this.profile + '/' + type + '/' + model.id + '.json'; return this.client.filesUpload({ path: path, autorename: false, mode: {'.tag': 'overwrite'}, contents: JSON.stringify(model), }); }, /** * Get all models from Dropbox. * * @type string type [notes|notebooks|tags] */ getAll: function(type) { var self = this; return this.readdir(type) .then(function(resp) { var promises = []; _.each(resp.entries, function(file) { if (file.name.search('.json') !== -1) { promises.push(self.getFile(type, file.path_lower)); } }); return Q.all(promises); }); }, /** * Get a JSON object by ID from Dropbox. * * @type string type [notes|notebooks|tags] * @type string path */ getFile: function(type, path) { return this.client.filesDownload({path: path}) .then(function(resp) { var defer = Q.defer(); var reader = new FileReader(); reader.addEventListener('loadend', function() { defer.resolve(JSON.parse(reader.result)); }); reader.readAsText(resp.fileBlob); return defer.promise; }); }, /** * Get a folder stat from Dropbox. * * @type string type [notes|notebooks|tags] * @type object options */ readdir: function(type, options) { return this.client.filesListFolder({ path : '/' + this.profile + '/' + type, include_deleted : false, }) .catch(function(err) { if (err.status === 409) { return []; } return Promise.reject(err); }); }, }; return Adapter; }); ================================================ FILE: app/scripts/modules/dropbox/classes/sync.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'jquery', 'q', 'backbone', 'marionette', 'backbone.radio', 'dropbox', 'modules/dropbox/classes/adapter' ], function(_, $, Q, Backbone, Marionette, Radio, Dropbox, adapter) { 'use strict'; /** * Dropbox synchronizer. * * Triggers: * 1. `auth:success` on `dropbox` channel * - after authentication is completed successfully. * 2. `start` on `sync` channel * when synchronizing starts * 3. `stop` on `sync` channel * when synchronizing stops * * Replies: * 1. `start` on `sync` channel * starts synchronizing. */ var Sync = Marionette.Object.extend({ configs : { // Dropbox app key key : '10iirspliqts95d', // Interval configs interval : 2000, intervalMax : 15000, intervalMin : 2000, // A state which shows if something is changed remotely statRemote : false }, initialize: function() { var key = Radio.request('configs', 'get:config', 'dropboxKey'); this.configs.key = key || this.configs.key; this.configs.accessToken = Radio.request('configs', 'get:config', 'dropboxAccessToken'); this.vent = Radio.channel('dropbox'); this.client = new Dropbox({ clientId: this.configs.key }); // Replies Radio.reply('sync', 'start', this.startSync, this); // Listen to Laverna events this.listenTo(Radio.channel('notes'), 'sync:model destroy:model restore:model', this.onSave); this.listenTo(Radio.channel('notebooks'), 'sync:model destroy:model restore:model', this.onSave); this.listenTo(Radio.channel('tags'), 'sync:model destroy:model restore:model', this.onSave); // Authorize the app var self = this; this.checkAuth() .then(function(authenticated) { if (authenticated) { return self.onReady(); } console.error('Dropbox authentication failed.'); }) .catch(function(err) { console.log('Dropbox error', err); }); }, /** * Start synchronizing immediately. */ startSync: function() { if (this.timeout) { clearTimeout(this.timeout); } this.timeout = setTimeout(_.bind(function() { this.checkChanges(); }, this), 0); }, /** * Check if Dropbox was authenticated. */ checkAuth: function() { var hash = this.parseHash(); if (this.configs.accessToken && this.configs.accessToken.length) { this.client.setAccessToken(this.configs.accessToken); return Promise.resolve(true); } else if (hash.access_token && hash.access_token.length) { return this.saveAccessToken(hash.access_token); } else { if (hash.error) { Radio.request('uri', 'navigate', '/'); } return this.authenticate(); } }, /** * Parse location hash. * * @returns {Object} */ parseHash: function() { var hash = window.location.hash.replace('#', '').split('&'); var ret = {}; if (!hash.length) { return ret; } _.each(hash, function(str) { var parts = str.replace(/\+/g, ' ').split('='); if (parts.length > 1) { var key = parts.shift(); var val = parts.length > 0 ? parts.join('=') : undefined; val = undefined ? null : decodeURIComponent(val.trim()); ret[key] = val; } }); return ret; }, authenticate: function() { var defer = Q.defer(); var authUrl = this.client.getAuthenticationUrl(document.location); Radio.once('Confirm', 'cancel', _.bind(defer.reject, defer)); Radio.once('Confirm', 'confirm', function() { window.location = authUrl; }); Radio.request('Confirm', 'start', { title : $.t('dropbox.auth title'), content: $.t('dropbox.auth confirm') }); return defer.promise; }, /** * Save the access token in configs. * * @param {String} accessToken * @returns {Promise} */ saveAccessToken: function(accessToken) { var self = this; return Radio.request('configs', 'save:object', { name : 'dropboxAccessToken', value : accessToken, }) .then(function() { Radio.request('uri', 'navigate', '/'); self.configs.accessToken.accessToken; return true; }); }, /** * Start synchronizing all data after Dropbox client is ready. */ onReady: function() { var profile = Radio.request('uri', 'profile') || 'notes-db'; var self = this; adapter.init(this.client, profile); this.timeout = window.setTimeout(function() { self.checkChanges(); }, 500); }, /** * Check for changes. */ checkChanges: function() { var promises = [], self = this; this.configs.statRemote = false; Radio.trigger('sync', 'start', 'dropbox'); // Synchronize all collections _.each(['notes', 'notebooks', 'tags'], function(module) { promises.push(function() { return Q.all([ Radio.request(module, 'fetch', {encrypt: true}), adapter.getAll(module) ]) .spread(function(localData, remoteData) { return self.syncAll(localData, remoteData, module); }); }); }); // After synchronizing, start watching for changes return _.reduce(promises, Q.when, new Q()) .then(function() { Radio.trigger('sync', 'stop', 'dropbox'); self.startWatch(); }) .fail(function(err) { if (err) { switch (err.status) { // If access was revoked, try to ask for it again case 401: self.checkAuth(); break; // On connection error, increase watch interval case 0: self.configs.interval = self.configs.intervalMax; self.startWatch(); break; } } Radio.trigger('sync', 'stop', 'dropbox'); Radio.trigger('sync', 'error', {cloud: 'dropbox', error: err}); console.error('Error', arguments[0], arguments); }); }, /** * Synchronize a collection. * * @type array localData * @type array remoteData * @type string module * @return promise */ syncAll: function(localData, remoteData, module) { var promises, encryptKeys = localData.model.prototype.encryptKeys; localData = (localData.fullCollection || localData).toJSON(); promises = this.checkRemoteChanges(localData, remoteData, module); promises.push.apply( promises, this.checkLocalChanges(localData, remoteData, module, encryptKeys) ); return _.reduce(promises, Q.when, new Q()) .then(function() { return Radio.request(module, 'fetch', {encrypt: true}); }); }, /** * Save only models which don't exist locally or which were updated * remotely. */ checkRemoteChanges: function(localData, remoteData, module) { var promises = [], newData = _.filter(remoteData, function(rModel) { var model = _.findWhere(localData, {id: rModel.id}); return !model || model.updated < rModel.updated; }); if (newData.length) { console.log('Dropbox changes:', newData); this.configs.statRemote = true; promises.push(function() { return Radio.request(module, 'save:all:raw', newData, {profile: adapter.profile}); }); } return promises; }, /** * Save only models which don't exist on Dropbox or * which were updated locally. */ checkLocalChanges: function(localData, remoteData, module, encryptKeys) { var promises = []; _.each(localData, function(lModel) { var model = _.findWhere(remoteData, {id: lModel.id}); if (model && model.updated >= lModel.updated) { return; } console.log('Dropbox local changes:', lModel); promises.push(function() { return adapter.save(module, lModel, encryptKeys); }); }); return promises; }, startWatch: function() { if (this.timeout) { clearTimeout(this.timeout); } this.calcInterval(); console.log('interval is', this.configs.interval); this.timeout = setTimeout(_.bind(function() { this.checkChanges(); }, this), this.configs.interval); }, /** * Increase or descrease watch interval depending on * whether changes appear on Dropbox. */ calcInterval: function() { var range = this.configs.intervalMax - this.configs.intervalMin; if (this.configs.statRemote) { this.configs.interval -= (range * 0.4); } else { this.configs.interval += (range * 0.2); } this.configs.interval = Math.max(this.configs.intervalMin, this.configs.interval); this.configs.interval = Math.min(this.configs.intervalMax, this.configs.interval); }, /** * Immediately after a model is changed locally, synchronize it with * Dropbox. */ onSave: function(model) { return adapter.save(model.storeName, model.attributes, model.encryptKeys); } }); return Sync; }); ================================================ FILE: app/scripts/modules/dropbox/module.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'backbone.radio', 'marionette', 'modules', 'modules/dropbox/classes/sync' ], function(_, Radio, Marionette, Modules, Sync) { 'use strict'; var Dropbox = Modules.module('Dropbox', {}); /** * Initializers & finalizers of the module */ Dropbox.on('start', function() { console.info('Dropbox started'); new Sync(); }); Dropbox.on('stop', function() { }); // Add a global module initializer Radio.request('init', 'add', 'module', function() { Dropbox.start(); }); return Dropbox; }); ================================================ FILE: app/scripts/modules/electronSearch/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'modules/electronSearch/view' ], function(_, Marionette, Radio, View) { 'use strict'; return Marionette.Object.extend({ initialize: function() { // Create a new region Radio.request('global', 'region:add', 'module--electronSearch'); // Render the view this.view = new View({}); Radio.request('global', 'region:show', 'module--electronSearch', this.view); this.view.trigger('rendered'); // Destroy the controller this.view.on('destroy', this.destroy, this); } }); }); ================================================ FILE: app/scripts/modules/electronSearch/module.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'backbone.radio', 'modules', 'mousetrap', 'modules/electronSearch/controller', 'mousetrap.global' ], function(Radio, Modules, Mousetrap, Controller) { 'use strict'; /** * Adds page search functionality into electron app. */ var Search = Modules.module('ElectronSearch', {}); Search.on('start', function() { this.controller = new Controller(); this.controller.on('destroy', Search.stop, Search); }); Search.on('stop', function() { this.controller.destroy(); }); // Add a global module initializer Radio.request('init', 'add', 'module', function() { // Start the module Mousetrap.bind(['ctrl+f', 'command+f'], function(e) { e.preventDefault(); Search.start(); }); }); return Search; }); ================================================ FILE: app/scripts/modules/electronSearch/template.html ================================================
================================================ FILE: app/scripts/modules/electronSearch/view.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requireNode */ define([ 'underscore', 'marionette', 'text!modules/electronSearch/template.html' ], function(_, Marionette, Tmpl) { 'use strict'; var remote = requireNode('electron').remote, View; View = Marionette.ItemView.extend({ template: _.template(Tmpl), className: 'electron--search', ui: { 'search': '[name="text"]' }, events: { 'input @ui.search' : 'onInput', 'keyup @ui.search' : 'destroyOnEsc', 'submit form' : 'next', 'click #search--next' : 'next', 'click #search--prev' : 'previous', 'click .search--close' : 'destroy' }, initialize: function() { _.bindAll(this, 'onFind'); this.listenTo(this, 'rendered', this.onRendered); }, onRendered: function() { this.ui.search.focus(); }, onDestroy: function() { remote.getCurrentWindow().webContents.stopFindInPage('clearSelection'); }, destroyOnEsc: function(e) { if (e.keyCode === 27) { this.destroy(); } }, onFind: function() { this.ui.search.focus(); }, onInput: function() { this.search(); // Prevent it from losing focus remote.getCurrentWindow().webContents.once('found-in-page', this.onFind); return true; }, next: function() { this.search(); return false; }, previous: function() { this.search(true); return false; }, search: function(backSearch) { var text = this.ui.search.val(); if (text) { if (backSearch) { return remote.getCurrentWindow().webContents.findInPage(text, {forward: false}); } return remote.getCurrentWindow().webContents.findInPage(text); } }, }); return View; }); ================================================ FILE: app/scripts/modules/fileDialog/controller.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'marionette', 'backbone.radio', 'modules/fileDialog/views/dialog' ], function(_, Marionette, Radio, View) { 'use strict'; /** * File dialog controller. * * Requests: * 1. channel: `files`, request: `save:all` * in order to save uploaded files. * 2. channel: `uri`, request: `link:file` * expects to receive a link to a file. * 3. channel: `editor`, request: `generate:link` * expects to receive link code. For example, Markdown code for a link. * 4. channel: `editor`, request: `generate:image` * expects to receive image code. For example, Markdown code for an image. * * requests: * 1. channel: `editor`, request: `insert` * in order to insert some text to the editor. */ var Controller = Marionette.Object.extend({ initialize: function(options) { this.options = options; // Instantiate and show the view this.view = new View(); Radio.request('global', 'region:show', 'modal', this.view); // Events this.listenTo(this.view, 'save', this.link); this.listenTo(this.view, 'redirect', this.destroy); }, onDestroy: function() { if (this.options.callback) { this.options.callback(null); } this.stopListening(); Radio.request('global', 'region:empty', 'modal'); }, /** * Provide the url to editor callback */ link: function(isFile) { var self = this, url = this.view.ui.url.val().trim(); if (url !== '') { var method = (isFile === true ? 'attachFiles' : 'attachImage'); return this[method](url); } Radio.request('files', 'save:all', this.view.files, { profile: Radio.request('uri', 'profile') }) .then(function(files) { self.options.model.files = _.union(self.options.model.files, files); // If there is only 1 file and its type is image if (files.length === 1 && self.isImage(files[0])) { return self.attachText(self.generateCode(files[0])); } // Otherwise, we will generate custom Markdown code self.attachFiles(files); }) .fail(function() { console.error('Error while uploading file:', arguments); }); }, /** * Attach files to the editor. */ attachFiles: function(files) { var str = ''; // It is just a link if (typeof files === 'string') { str = Radio.request('editor', 'generate:link', { text : 'Alt description', url : files }); } else { _.each(files, function(model) { str += this.generateCode(model) + '\n'; }, this); } this.attachText(str); }, attachImage: function(url) { var text = Radio.request('editor', 'generate:image', { text : 'Alt description', url : url }); this.attachText(text); }, attachText: function(text) { this.options.callback(text !== '' ? text : null); // Close the dialog this.options.callback = null; this.destroy(); }, isImage: function(model) { return (model.get('fileType').indexOf('image') > -1); }, /** * Generate Markdown code. */ generateCode: function(model) { var url = Radio.request('uri', 'link:file', model), request = 'link'; // If file type is an image type, generate image MD code if (this.isImage(model)) { request = 'image'; } return Radio.request('editor', 'generate:' + request, { text : model.get('name'), url : url }); } }); return Controller; }); ================================================ FILE: app/scripts/modules/fileDialog/helper.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'backbone.radio' ], function(_, Radio) { 'use strict'; var Helper = { data: {urls: []}, /** * Replace all file links with generated URIs * * @var string text * @var array files an array of IDs */ toHtml: function(text, model) { // If the note doesn't have attached files, revoke URLs and return text if (!model.files.length) { this.revokeUrls(); return text; } // If it is not the same note model, revoke URLs if (model.id !== this.data.id) { this.revokeUrls(); } var url, pattern; this.data.id = model.id; _.each(model.files, function(fModel) { url = this._generateUrl(fModel); /* * Replace colons in the URL to prevent Pagedown from converting it * to a link. */ url = url.replace(/:(?!http)/, ':'); // Replace fake URLs with real ones pattern = new RegExp('#file:' + fModel.id, 'g'); text = text.replace(pattern, url); }, this); return text; }, /** * Parse the text for file IDs * @var string text */ getFileIds: function(text) { if (text === '') { return text; } var ids = [], results = text.match(/#file:([a-z0-9\-])+/g); // Remove duplicate IDs results = _.uniq(results); _.each(results, function(res) { ids.push( res.replace('#file:', '') ); }); return ids; }, /** * Revoke object URLs */ revokeUrls: function() { var url = window.URL || window.webkitURL; _.each(this.data.urls, function(link) { url.revokeObjectURL(link); }); this.data = {urls: []}; }, /** * Generate object URL to a file. */ _generateUrl: function(fModel) { // Do not generate URLs every time var url = _.findWhere(this.data.urls, {id: fModel.id}); if (url) { return url.url; } url = Radio.request('uri', 'link:file', fModel, true); this.data.urls.push({ id : fModel.id, url : url }); return url; } }; return Helper; }); ================================================ FILE: app/scripts/modules/fileDialog/module.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'modules', 'backbone.radio', 'modules/fileDialog/controller', 'modules/fileDialog/helper' ], function(_, Q, Modules, Radio, Controller, Helper) { 'use strict'; /** * File dialog. * It shows a dialog where a user can upload files. * * Listens for events: * 1. channel: `editor`, event: `destroy` * stops itself * 2. channel: `editor`, event: `converter:init` * adds hooks. * 3. channel: `noteView`, event: `view:destroy` * revokes all generated URLs * * Replies to requests on channel `editor`: * 1. `get:files` - parses the text for file links and returns * an array of IDs. * * Adds the following hooks to Pagedown editor: * 1. `insertImageDialog` * 2. `postConversion` */ var FileDialog = Modules.module('FileDialog', {}); /** * Initializers & finalizers of the module */ FileDialog.on('before:start', function(options) { FileDialog.controller = new Controller(options); this.listenTo(FileDialog.controller, 'destroy', FileDialog.stop); }); FileDialog.on('before:stop', function() { console.info('FileDialog stopped'); Helper.revokeUrls(); this.stopListening(); FileDialog.controller = null; }); /** * Add converter hooks */ function addHook(converter, model) { // Do not add hooks if a model wasn't provided. if (!model) { return; } converter.hooks.chain('preConversion', function(text) { return Helper.toHtml(text, model); }); // Make colons normal again converter.hooks.chain('postConversion', function(text) { return text.replace(/(blob:http):/g, '$1:'); }); } // Radio.request('init', 'add', 'editor:before', function(editor, model) { // editor.hooks.set('insertImageDialog', function(fnc) { // return true; // }); // }); Radio.request('init', 'add', 'module', function() { // Stop the module when editor is closed. Radio.on('editor', 'destroy', FileDialog.stop, FileDialog); // Show custom dialog on `insertImageDialog` hook. Radio.reply('editor', 'show:attachment', function(model) { var defer = Q.defer(); FileDialog.start({model: model, callback: function(link) { defer.resolve(link); }}); return defer.promise; }); Radio.reply('editor', 'get:files', Helper.getFileIds, Helper); // When editor converter is initialized, add hooks Radio.on('editor', 'converter:init', addHook); // Revoke all URLs when a note is closed. Radio.on('noteView', 'view:destroy', Helper.revokeUrls, Helper); }); return FileDialog; }); ================================================ FILE: app/scripts/modules/fileDialog/templates/dialog.html ================================================ ================================================ FILE: app/scripts/modules/fileDialog/templates/dropzone.html ================================================

================================================ FILE: app/scripts/modules/fileDialog/views/dialog.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, Modernizr */ define([ 'underscore', 'i18next', 'marionette', 'backbone.radio', 'behaviors/modalForm', 'dropzone', 'text!modules/fileDialog/templates/dialog.html', 'text!modules/fileDialog/templates/dropzone.html' ], function(_, i18n, Marionette, Radio, ModalForm, Dropzone, Tmpl, dropzoneTmpl) { 'use strict'; /** * File dialog view. */ var View = Marionette.ItemView.extend({ template : _.template(Tmpl), className : 'modal fade', behaviors: { ModalForm: { behaviorClass : ModalForm, uiFocus : 'url' } }, ui: { url : '[name=url]', okBtn : '#ok-btn', attach : '#btn-attach' }, events: { 'keyup @ui.url' : 'toggleAttachBtn', 'click .attach-file': 'attachFile' }, initialize: function() { this.files = []; this.listenTo(this, 'shown.modal', this.onShown); }, attachFile: function(e) { e.preventDefault(); this.trigger('save', true); }, toggleAttachBtn: _.debounce(function() { this.ui.okBtn.toggleClass('hidden', this.ui.url.val().trim() !== ''); this.ui.attach.toggleClass('hidden', this.ui.url.val().trim() === ''); }, 250), onShown: function() { // File uploading is allowed only if either Indexeddb or WebSQL is supported if (Modernizr.indexeddb || Modernizr.websqldatabase) { new Dropzone('.dropzone', { url : '/#notes', clickable : true, accept : _.bind(this.getImage, this), thumbnailWidth : 100, thumbnailHeight : 100, previewTemplate: dropzoneTmpl, dictDefaultMessage: i18n.t('Drop files') }); } }, /** * Save file data to a variable. */ getImage: function(file) { var reader = new FileReader(); this.ui.url.val('').trigger('keyup'); reader.onload = _.bind(function(evt) { this.files.push({ name : file.name, src : evt.target.result, fileType : file.type }); }, this); reader.readAsDataURL(file); } }); return View; }); ================================================ FILE: app/scripts/modules/fs/classes/adapter.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requireNode */ define([ 'underscore', 'q', 'backbone.radio' ], function(_, Q, Radio) { 'use strict'; // A hack to trick Nodejs modules not to register as RequireJS modules var def = _.clone(define.amd); define.amd = false; var glob = requireNode(window.nodeDir + 'glob'), fs = requireNode(window.nodeDir + 'graceful-fs'), chokidar = requireNode(window.nodeDir + 'chokidar'), Adapter; define.amd = _.clone(def); def = undefined; Adapter = { /** * Since module isn't ready yet, sync to /tmp folder (for now) * (/tmp/laverna folder must exist) */ path: '/tmp/laverna/', /** * Check if directories exist. If they don't, create them. */ checkDirs: function() { if (!this.isDirSync(this.path)) { fs.mkdirSync(this.path); } _.each(['notes', 'notebooks', 'tags', 'files'], function(dir) { if (!this.isDirSync(this.path + dir)) { fs.mkdirSync(this.path + dir); } }, this); }, /** * Check if a dir exists. */ isDirSync: function(path) { try { return fs.statSync(path).isDirectory(); } catch (e) { if (e.code === 'ENOENT') { return false; } else { throw e; } } }, /** * Overwrite a file with data. */ _write: function(name, data) { var defer = Q.defer(); fs.writeFile(this.path + name, data, function(err) { if (err) { return defer.reject(err); } defer.resolve(); }); return defer.promise; }, /** * Read a file. * * @type string name */ _read: function(name) { var defer = Q.defer(); fs.readFile(name, 'utf8', function(err, data) { if (err) { return defer.reject(err); } defer.resolve(data); }); return defer.promise; }, /** * Save model data to the FS. */ writeFile: function(module, model) { var name = module + '/' + model.id, data = JSON.stringify(_.omit(model, 'content')), promises = [ this._write(name + '.json', data) ]; // Save content into a separate Markdown file if (model.content) { promises.push( this._write(name + '.md', model.content) ); } // Save all files return Q.all(promises); }, /** * Watch for changes. */ startWatch: function() { var watcher = chokidar.watch(Adapter.path, { persistent: true }); watcher.on('change', function(file) { Adapter._read(file) .then(function(data) { var obj = Adapter.getFileInfo(file); // The file has Markdown extension if (obj.ext === 'md') { data = {content: data, id: obj.id}; } else if (obj.ext === 'json') { data = JSON.parse(data); } // Trigger an event Radio.trigger('fs', 'change', { storeName : obj.storeName, data : data }); }); }); }, /** * Get the content of all files. */ getList: function(type) { var defer = Q.defer(), dir = Adapter.path + (type ? type + '/' : '') + '*.*'; // First, read get the list of all files in a folder glob(dir, {}, function(err, files) { Adapter.getFiles(files) .then(function(data) { defer.resolve(type ? data[type] : data); }); }); return defer.promise; }, /** * Read the content of each file in a list * * @type array files */ getFiles: function(files) { var promises = []; _.each(files, function(file) { // Add the path to a place where the file is located if (file.search(Adapter.path) === -1) { file = Adapter.path + file; } // Read the file promises.push(Adapter._read(file)); }); return Q.all(promises) .then(function(data) { return _.object(files, data); }) .then(function(data) { return Adapter.filesToObject(data); }); }, /** * Get extension, type, and ID from file name. */ getFileInfo: function(fileName) { var key = fileName.split('/'); // Get a model ID fileName = _.last(key).split('.'); return { ext : fileName[1], // Get store name (notes|notebooks|tags) storeName: key[key.length - 2], id : fileName[0] }; }, /** * Convert data to model structure. */ filesToObject: function(data) { var obj = {}; _.each(data, function(value, key) { key = Adapter.getFileInfo(key); // Separate models by their store names obj[key.storeName] = obj[key.storeName] || {}; obj[key.storeName][key.id] = obj[key.storeName][key.id] || {}; // The file has Markdown extension if (key.ext === 'md') { obj[key.storeName][key.id].content = value; } else if (key.ext === 'json') { obj[key.storeName][key.id] = _.extend( obj[key.storeName][key.id], JSON.parse(value) ); } }); return obj; }, }; return Adapter; }); ================================================ FILE: app/scripts/modules/fs/classes/sync.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'marionette', 'backbone.radio', 'modules/fs/classes/adapter' ], function(_, Q, Marionette, Radio, FS) { 'use strict'; /** * File system synchronizer. */ var Controller = Marionette.Object.extend({ initialize: function() { FS.path = Radio.request('configs', 'get:config', 'module:fs:folder'); /** * @todo Show a message or something. * For now disable synchronizing. */ if (!FS.path) { return; } // Create current profile's folder FS.path = FS.path + '/' + (Radio.request('uri', 'profile') || 'notes-db') + '/'; FS.checkDirs(); // Check for changes on file system this.checkChanges(); // Listen to Laverna events this.listenTo(Radio.channel('notes'), 'sync:model destroy:model restore:model', this.onSave); this.listenTo(Radio.channel('notebooks'), 'sync:model destroy:model restore:model', this.onSave); this.listenTo(Radio.channel('tags'), 'sync:model destroy:model restore:model', this.onSave); // Listen to FS events this.listenTo(Radio.channel('fs'), 'change', this.onFsChange); }, /** * Check for changes on start. */ checkChanges: function() { var promises = [], self = this; _.each(['notes', 'notebooks', 'tags'], function(module) { return Q.all([ Radio.request(module, 'fetch', {encrypt: true}), FS.getList(module) ]) .spread(function(localData, remoteData) { return self.syncAll(localData, remoteData, module); }); }); return _.reduce(promises, Q.when, new Q()) .then(function() { self.startWatch(); }) .fail(function(e) { console.error('Error:', e); }); }, /** * Start watching for FS changes. */ startWatch: function() { FS.startWatch(); }, /** * Synchronize FS and IndexedDB. */ syncAll: function(localData, remoteData, module) { var promises = []; localData = (localData.fullCollection || localData).toJSON(); // First, check if there are any changes in IndexedDB promises.push.apply( promises, this.checkLocalChanges(localData, remoteData, module) ); // Then, check if there are any changes on file system promises.push.apply( promises, this.checkRemoteChanges(localData, remoteData, module) ); return _.reduce(promises, Q.when, new Q()) .fail(function(e) { console.error('Error:', e); }); }, /** * Synchronize models from IndexedDB to file system. */ checkLocalChanges: function(localData, remoteData, module) { var promises = []; _.each(localData, function(lModel) { var model = _.findWhere(remoteData, {id: lModel.id}); if (model && model.updated >= lModel.updated) { return; } promises.push(function() { return FS.writeFile(module, lModel); }); }); return promises; }, /** * Synchronize models from file system to IndexedDB. */ checkRemoteChanges: function(localData, remoteData, module) { var newData = _.filter(remoteData, function(rModel) { var model = _.findWhere(localData, {id: rModel.id}); rModel.content = rModel.content || ''; if (model && model.updated >= rModel.updated && _.isEqual(rModel, model)) { return false; } return true; }); return Radio.request(module, 'save:all:raw', newData); }, /** * Laverna triggered `change` event. */ onSave: function(model) { FS.writeFile(model.storeName, model.attributes) .fail(function(e) { console.error('onSave error:', e); }); }, /** * File system triggered `change` event. */ onFsChange: function(data) { return Radio.request(data.storeName, 'get:model', { id: data.data.id }) .then(function(model) { data.data = _.extend({}, model.attributes, data.data); // Don't parse content if (!data.data.content) { return [data, model]; } // Parse tasks and tags return Radio.request('markdown', 'parse', data.data.content) .then(function(env) { data.data = _.extend( data.data, _.pick(env, 'tags', 'tasks', 'taskCompleted', 'taskAll', 'files') ); return [data, model]; }); }) .spread(function(data, model) { // Nothing's changed if (_.isEqual(data.data, model.attributes)) { return; } return Radio.request(data.storeName, 'save:raw', data.data); }) .fail(function(e) { console.error('onFsChange error:', e); }); }, }); return Controller; }); ================================================ FILE: app/scripts/modules/fs/module.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'backbone.radio', 'marionette', 'modules', 'modules/fs/classes/sync' ], function(_, Radio, Marionette, Modules, Sync) { 'use strict'; /** * Module which synchronizes all models to a file system. * (Works only on Electron app) */ var FS = Modules.module('FS', {}); /** * Initializers & finalizers of the module */ FS.on('start', function() { console.info('FS started'); new Sync(); }); FS.on('stop', function() { }); // Add a global module initializer Radio.request('init', 'add', 'module', function() { FS.start(); }); return FS; }); ================================================ FILE: app/scripts/modules/fs/templates/settings.html ================================================
================================================ FILE: app/scripts/modules/fs/views/settings.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define, requireNode */ define([ 'underscore', 'marionette', 'backbone.radio', 'text!modules/fs/templates/settings.html' ], function(_, Marionette, Radio, Tmpl) { 'use strict'; /** * Shows FS module's configs. */ var dialog = requireNode('electron').remote.dialog, View; View = Marionette.ItemView.extend({ template: _.template(Tmpl), ui: { input: '.input--fs' }, events : { 'click .btn--fs': 'showFolderDialog', }, initialize: function() { }, serializeData: function() { return { models: this.collection.getConfigs() }; }, showFolderDialog: function(e) { e.preventDefault(); var folder = dialog.showOpenDialog({properties: ['openDirectory']}); if (!folder) { return; } this.ui.input.val(folder[0]); this.collection.trigger('new:value', { name : 'module:fs:folder', value : folder[0] }); }, }); return View; }); ================================================ FILE: app/scripts/modules/fuzzySearch/controllers/main.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'q', 'marionette', 'backbone.radio', 'modules/fuzzySearch/views/composite' ], function(_, Q, Marionette, Radio, View) { 'use strict'; /** * Fuzzy search controller * * Listens to * ------------ * Events: * 1. channel: `global`, event: `search:change` * makes fuzzy search and shows the result. * * Triggers * -------- * 1. channel: `global`, event: `search:hidden` * when a model in search results was selected. * 2. channel: `appNote`, request: `filter` * in order to filter notes in sidebar. * 3. channel: `fuzzySearch`, request: `region:show` * in order to render the view and show search results */ var Controller = Marionette.Object.extend({ initialize: function() { _.bindAll(this, 'search', 'onFetch'); // Fetch data this.wait = Radio.request('notes', 'fetch', { profile : Radio.request('uri', 'profile') }).then(this.onFetch); // Listen to events this.listenTo(Radio.channel('global'), 'search:change', _.debounce(this.search, 150)); }, onDestroy: function() { Radio.request('fuzzySearch', 'region:empty'); }, /** * Searches and shows the result. */ search: function(text) { // Wait until everything is fetched if (this.wait) { var self = this; return this.wait.then(function() { self.search(text); }); } var result = this.notes.fuzzySearch(text); this.notes.reset(result); if (!this.view.isRendered) { Radio.request('fuzzySearch', 'region:show', this.view); } }, onFetch: function(collection) { this.wait = null; this.notes = collection; // Instantiate notes collection and a view this.view = new View({collection: this.notes}); // Events this.listenTo(this.view, 'childview:navigate:search', this.filter, this); }, /** * It triggers "filter" event and stops this module. */ filter: function(model) { model = model.model; Radio.request('appNote', 'filter', { filter : 'search', query : model.get('title') }); Radio.trigger('global', 'search:hidden'); } }); return Controller; }); ================================================ FILE: app/scripts/modules/fuzzySearch/module.js ================================================ /** * Copyright (C) 2015 Laverna project Authors. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global define */ define([ 'underscore', 'backbone.radio', 'marionette', 'modules', 'modules/fuzzySearch/regions/sidebar', 'modules/fuzzySearch/controllers/main' ], function(_, Radio, Marionette, Modules, Region, Controller) { 'use strict'; /** * Fuzzy search module. * * It creates a new region on `init` event in order to show search results * there. * * Listens to * ---------- * Events: * 1. channel: `global`, event: `search:shown` * starts itself * 2. channel: `global`, event: `search:hidden` * stops itself * * Requests: * 1. channel: `fuzzySearch`, request: `region:show` * renders the provided view in fuzzy search region * 2. channel: `fuzzySearch`, request: `region:empty` */ var Fuzzy = Modules.module('FuzzySearch', {}); /** * Initializers & finalizers of the module */ Fuzzy.on('start', function() { console.info('FuzzySearch module has started'); Fuzzy.controller = new Controller(); }); Fuzzy.on('stop', function() { console.info('FuzzySearch module has stoped'); Fuzzy.controller.destroy(); Fuzzy.controller = null; }); // Add a global module initializer Radio.request('init', 'add', 'module', function() { console.info('FuzzySearch module has been initialized'); // Create a new region $('#sidebar').append( $('