├── .babelrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .gitmodules ├── .jshintrc ├── .modernizrrc ├── .travis.yml ├── CONTRIBUTE.md ├── LICENSE ├── README.md ├── app ├── .htaccess ├── 404.html ├── config.xml ├── docs │ └── howto.md ├── favicon.ico ├── images │ ├── icon │ │ ├── IconMenubarTemplate.png │ │ ├── IconMenubarTemplate@2x.png │ │ ├── icon-120x120.png │ │ ├── icon-128x128.png │ │ ├── icon-150x150.png │ │ ├── icon-152x152.png │ │ ├── icon-16x16.png │ │ ├── icon-310x150.png │ │ ├── icon-310x310.png │ │ ├── icon-36x36.png │ │ ├── icon-48x48.png │ │ ├── icon-512x512.icns │ │ ├── icon-512x512.png │ │ ├── icon-70x70.png │ │ ├── icon-72x72.png │ │ ├── icon-76x76.png │ │ ├── icon-96x96.png │ │ └── icon.png │ └── 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 ├── robots.txt ├── scripts │ ├── App.js │ ├── behaviors │ │ ├── Content.js │ │ ├── ModalForm.js │ │ ├── ModelFocus.js │ │ ├── Navigate.js │ │ ├── Pagination.js │ │ ├── Sidebar.js │ │ └── Sidemenu.js │ ├── collections │ │ ├── Collection.js │ │ ├── Configs.js │ │ ├── DiffCollection.js │ │ ├── Edits.js │ │ ├── Files.js │ │ ├── Notebooks.js │ │ ├── Notes.js │ │ ├── Pageable.js │ │ ├── Profiles.js │ │ ├── Shadows.js │ │ ├── Tags.js │ │ ├── Users.js │ │ ├── collections-new │ │ │ ├── Collection.js │ │ │ ├── Configs.js │ │ │ └── configNames.js │ │ ├── configNames.js │ │ └── modules │ │ │ ├── Configs.js │ │ │ ├── Edits.js │ │ │ ├── Files.js │ │ │ ├── Module.js │ │ │ ├── Notebooks.js │ │ │ ├── Notes.js │ │ │ ├── Profiles.js │ │ │ ├── Shadows.js │ │ │ ├── Tags.js │ │ │ ├── Users.js │ │ │ └── main.js │ ├── components │ │ ├── codemirror │ │ │ ├── Controller.js │ │ │ ├── Editor.js │ │ │ ├── View.js │ │ │ ├── main.js │ │ │ └── template.html │ │ ├── confirm │ │ │ ├── Controller.js │ │ │ ├── View.js │ │ │ └── template.html │ │ ├── dropbox │ │ │ ├── Adapter.js │ │ │ ├── Sync.js │ │ │ ├── main.js │ │ │ └── settings │ │ │ │ ├── View.js │ │ │ │ └── template.html │ │ ├── electronSearch │ │ │ ├── Controller.js │ │ │ ├── View.js │ │ │ └── template.html │ │ ├── encryption │ │ │ ├── auth │ │ │ │ ├── Controller.js │ │ │ │ ├── View.js │ │ │ │ └── template.html │ │ │ ├── encrypt │ │ │ │ ├── Controller.js │ │ │ │ ├── View.js │ │ │ │ └── template.html │ │ │ └── main.js │ │ ├── fileDialog │ │ │ ├── Controller.js │ │ │ ├── View.js │ │ │ ├── main.js │ │ │ └── templates │ │ │ │ ├── dialog.html │ │ │ │ └── dropzone.html │ │ ├── fuzzySearch │ │ │ ├── Controller.js │ │ │ ├── main.js │ │ │ ├── template.html │ │ │ └── views │ │ │ │ ├── Child.js │ │ │ │ ├── Region.js │ │ │ │ └── View.js │ │ ├── help │ │ │ ├── about │ │ │ │ ├── Controller.js │ │ │ │ ├── View.js │ │ │ │ └── template.html │ │ │ ├── controller.js │ │ │ └── keybindings │ │ │ │ ├── Controller.js │ │ │ │ ├── View.js │ │ │ │ └── template.html │ │ ├── importExport │ │ │ ├── Export.js │ │ │ ├── Import.js │ │ │ ├── ImportEvernote.js │ │ │ ├── main.js │ │ │ └── migrate │ │ │ │ ├── Controller.js │ │ │ │ ├── Encryption.js │ │ │ │ ├── View.js │ │ │ │ └── template.html │ │ ├── linkDialog │ │ │ ├── Controller.js │ │ │ ├── main.js │ │ │ ├── templates │ │ │ │ ├── item.html │ │ │ │ └── template.html │ │ │ └── views │ │ │ │ ├── Collection.js │ │ │ │ ├── Item.js │ │ │ │ └── View.js │ │ ├── markdown │ │ │ ├── Markdown.js │ │ │ ├── file.js │ │ │ ├── main.js │ │ │ └── task.js │ │ ├── navbar │ │ │ ├── Controller.js │ │ │ ├── View.js │ │ │ └── template.html │ │ ├── notebooks │ │ │ ├── Router.js │ │ │ ├── controller.js │ │ │ ├── form │ │ │ │ ├── notebook │ │ │ │ │ ├── Controller.js │ │ │ │ │ ├── View.js │ │ │ │ │ └── template.html │ │ │ │ └── tag │ │ │ │ │ ├── Controller.js │ │ │ │ │ ├── View.js │ │ │ │ │ └── template.html │ │ │ ├── list │ │ │ │ ├── Controller.js │ │ │ │ ├── templates │ │ │ │ │ ├── layout.html │ │ │ │ │ ├── notebook.html │ │ │ │ │ └── tag.html │ │ │ │ └── views │ │ │ │ │ ├── ItemView.js │ │ │ │ │ ├── Layout.js │ │ │ │ │ ├── Notebook.js │ │ │ │ │ ├── Notebooks.js │ │ │ │ │ ├── Tag.js │ │ │ │ │ └── Tags.js │ │ │ └── remove │ │ │ │ └── Controller.js │ │ ├── notes │ │ │ ├── Router.js │ │ │ ├── controller.js │ │ │ ├── form │ │ │ │ ├── Controller.js │ │ │ │ ├── templates │ │ │ │ │ ├── form.html │ │ │ │ │ └── notebooks.html │ │ │ │ └── views │ │ │ │ │ ├── Form.js │ │ │ │ │ ├── Notebook.js │ │ │ │ │ ├── Notebooks.js │ │ │ │ │ └── NotebooksCollection.js │ │ │ ├── list │ │ │ │ ├── Controller.js │ │ │ │ ├── templates │ │ │ │ │ ├── layout.html │ │ │ │ │ └── noteView.html │ │ │ │ └── views │ │ │ │ │ ├── Layout.js │ │ │ │ │ ├── NoteView.js │ │ │ │ │ └── NotesView.js │ │ │ ├── remove │ │ │ │ └── Controller.js │ │ │ └── show │ │ │ │ ├── Controller.js │ │ │ │ ├── View.js │ │ │ │ └── template.html │ │ ├── settings │ │ │ ├── Router.js │ │ │ ├── controller.js │ │ │ ├── show │ │ │ │ ├── Behavior.js │ │ │ │ ├── Controller.js │ │ │ │ ├── View.js │ │ │ │ ├── editor │ │ │ │ │ ├── View.js │ │ │ │ │ └── template.html │ │ │ │ ├── encryption │ │ │ │ │ ├── Key.js │ │ │ │ │ ├── Passphrase.js │ │ │ │ │ ├── View.js │ │ │ │ │ ├── key.html │ │ │ │ │ ├── passphrase.html │ │ │ │ │ └── template.html │ │ │ │ ├── general │ │ │ │ │ ├── View.js │ │ │ │ │ └── template.html │ │ │ │ ├── importExport │ │ │ │ │ ├── View.js │ │ │ │ │ └── template.html │ │ │ │ ├── keybindings │ │ │ │ │ ├── View.js │ │ │ │ │ └── template.html │ │ │ │ ├── sync │ │ │ │ │ ├── Users.js │ │ │ │ │ ├── View.js │ │ │ │ │ ├── template.html │ │ │ │ │ └── users.html │ │ │ │ ├── template.html │ │ │ │ └── wipe │ │ │ │ │ ├── View.js │ │ │ │ │ ├── confirm.html │ │ │ │ │ └── template.html │ │ │ └── sidebar │ │ │ │ ├── Controller.js │ │ │ │ ├── templates │ │ │ │ ├── navbar.html │ │ │ │ └── template.html │ │ │ │ └── views │ │ │ │ ├── Navbar.js │ │ │ │ └── View.js │ │ ├── setup │ │ │ ├── ContentView.js │ │ │ ├── Controller.js │ │ │ ├── View.js │ │ │ ├── export │ │ │ │ ├── View.js │ │ │ │ └── template.html │ │ │ ├── main.js │ │ │ ├── register │ │ │ │ ├── View.js │ │ │ │ └── template.html │ │ │ ├── template.html │ │ │ └── username │ │ │ │ ├── View.js │ │ │ │ └── template.html │ │ └── share │ │ │ ├── Controller.js │ │ │ ├── View.js │ │ │ ├── info │ │ │ ├── View.js │ │ │ └── template.html │ │ │ ├── template.html │ │ │ └── users │ │ │ ├── View.js │ │ │ └── template.html │ ├── constants.js │ ├── main.js │ ├── models │ │ ├── Config.js │ │ ├── Db.js │ │ ├── Edit.js │ │ ├── Encryption.js │ │ ├── File.js │ │ ├── Model.js │ │ ├── Note.js │ │ ├── Notebook.js │ │ ├── Peer.js │ │ ├── Profile.js │ │ ├── Shadow.js │ │ ├── Signal.js │ │ ├── Sync.js │ │ ├── Tag.js │ │ ├── User.js │ │ └── diffsync │ │ │ ├── Core.js │ │ │ ├── Diff.js │ │ │ ├── Model.js │ │ │ ├── Patch.js │ │ │ └── main.js │ ├── modules │ │ ├── fs │ │ │ ├── classes │ │ │ │ ├── adapter.js │ │ │ │ └── sync.js │ │ │ ├── module.js │ │ │ ├── templates │ │ │ │ └── settings.html │ │ │ └── views │ │ │ │ └── settings.js │ │ ├── mathjax │ │ │ ├── libs │ │ │ │ └── mathjax.js │ │ │ └── module.js │ │ ├── modules.json │ │ └── remotestorage │ │ │ ├── classes │ │ │ ├── module.js │ │ │ ├── rs.js │ │ │ └── sync.js │ │ │ └── module.js │ ├── templates │ │ ├── layout.html │ │ └── loader.html │ ├── utils │ │ ├── Env.js │ │ ├── I18n.js │ │ ├── Initializer.js │ │ ├── Keybindings.js │ │ ├── Notify.js │ │ ├── Title.js │ │ ├── Url.js │ │ ├── electronListener.js │ │ ├── fileSaver.js │ │ ├── theme.js │ │ └── underscore.js │ ├── views │ │ ├── Brand.js │ │ ├── Layout.js │ │ ├── Loader.js │ │ └── Modal.js │ └── workers │ │ ├── Delegator.js │ │ ├── Module.js │ │ └── worker.js └── styles │ ├── core │ ├── bootstrap.less │ ├── codemirror.less │ ├── codemirror │ │ ├── core.less │ │ └── theme.less │ ├── editor.less │ ├── fontello.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 │ │ └── font │ │ │ ├── fontello.eot │ │ │ ├── fontello.svg │ │ │ ├── fontello.ttf │ │ │ └── fontello.woff │ ├── fuzzy.less │ ├── header.less │ ├── layout.less │ ├── list.less │ ├── main.less │ ├── responsive.less │ ├── sidemenu.less │ ├── utils.less │ └── variables.less │ ├── theme-dark │ ├── main.less │ ├── prism.less │ └── theme.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 │ └── themes.json ├── config.xml ├── electron ├── electron.js ├── package-lock.json ├── package.json └── server.js ├── gulpfile.js ├── gulps ├── bundle.js ├── clean.js ├── copy.js ├── copyDist.js ├── copyRelease.js ├── css.js ├── electron.js ├── html.js ├── lav-server.js ├── lint.js ├── mobile.js ├── nightwatch.js ├── npm.js ├── serve-hosted.js ├── serve.js └── test.js ├── package.json ├── test ├── nightwatch.json ├── 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 │ │ └── tags │ │ │ ├── form.js │ │ │ ├── formEdit.js │ │ │ ├── list.js │ │ │ └── remove.js │ │ └── modules │ │ └── fuzzySearch │ │ └── fuzzySearch.js └── tape │ ├── app.js │ ├── behaviors │ ├── content.js │ ├── modalForm.js │ ├── modelFocus.js │ ├── navigate.js │ ├── pagination.js │ ├── sidebar.js │ └── sidemenu.js │ ├── collections │ ├── collection.js │ ├── configs.js │ ├── edits.js │ ├── files.js │ ├── modules │ │ ├── configs.js │ │ ├── edits.js │ │ ├── files.js │ │ ├── module.js │ │ ├── notebooks.js │ │ ├── notes.js │ │ ├── profiles.js │ │ ├── shadows.js │ │ ├── tags.js │ │ └── users.js │ ├── notebooks.js │ ├── notes.js │ ├── pageable.js │ ├── profiles.js │ ├── shadows.js │ ├── tags.js │ └── users.js │ ├── components │ ├── codemirror │ │ ├── controller.js │ │ ├── editor.js │ │ └── view.js │ ├── confirm │ │ ├── controller.js │ │ └── view.js │ ├── dropbox │ │ ├── adapter.js │ │ ├── main.js │ │ ├── settings │ │ │ └── view.js │ │ └── sync.js │ ├── electronSearch │ │ ├── controller.js │ │ └── view.js │ ├── encryption │ │ ├── auth │ │ │ ├── controller.js │ │ │ └── view.js │ │ ├── encrypt │ │ │ ├── controller.js │ │ │ └── view.js │ │ └── main.js │ ├── fileDialog │ │ ├── controller.js │ │ ├── main.js │ │ └── view.js │ ├── fuzzySearch │ │ ├── child.js │ │ ├── controller.js │ │ ├── main.js │ │ ├── region.js │ │ └── view.js │ ├── help │ │ ├── about │ │ │ ├── controller.js │ │ │ └── view.js │ │ ├── controller.js │ │ └── keybindings │ │ │ ├── controller.js │ │ │ └── view.js │ ├── importExport │ │ ├── backup.enex │ │ ├── export.js │ │ ├── import.js │ │ ├── importEvernote.js │ │ ├── main.js │ │ └── migrate │ │ │ ├── controller.js │ │ │ ├── encryption.js │ │ │ └── view.js │ ├── linkDialog │ │ ├── collection.js │ │ ├── controller.js │ │ ├── item.js │ │ ├── main.js │ │ └── view.js │ ├── markdown │ │ ├── file.js │ │ ├── markdown.js │ │ └── task.js │ ├── navbar │ │ ├── controller.js │ │ └── view.js │ ├── notebooks │ │ ├── controller.js │ │ ├── form │ │ │ ├── notebook │ │ │ │ ├── controller.js │ │ │ │ └── view.js │ │ │ └── tag │ │ │ │ ├── controller.js │ │ │ │ └── view.js │ │ ├── list │ │ │ ├── controller.js │ │ │ ├── itemView.js │ │ │ ├── layout.js │ │ │ ├── notebook.js │ │ │ ├── notebooks.js │ │ │ ├── tag.js │ │ │ └── tags.js │ │ ├── remove │ │ │ └── controller.js │ │ └── router.js │ ├── notes │ │ ├── controller.js │ │ ├── form │ │ │ ├── controller.js │ │ │ └── views │ │ │ │ ├── form.js │ │ │ │ ├── notebook.js │ │ │ │ ├── notebooks.js │ │ │ │ └── notebooksCollection.js │ │ ├── list │ │ │ ├── controller.js │ │ │ ├── layout.js │ │ │ ├── noteView.js │ │ │ └── notesView.js │ │ ├── remove │ │ │ └── controller.js │ │ ├── router.js │ │ └── show │ │ │ ├── controller.js │ │ │ └── view.js │ ├── settings │ │ ├── controller.js │ │ ├── router.js │ │ ├── show │ │ │ ├── behavior.js │ │ │ ├── controller.js │ │ │ ├── editor.js │ │ │ ├── encryption.js │ │ │ ├── general.js │ │ │ ├── importExport.js │ │ │ ├── key.js │ │ │ ├── keybindings.js │ │ │ ├── passphrase.js │ │ │ ├── sync.js │ │ │ ├── users.js │ │ │ └── view.js │ │ └── sidebar │ │ │ ├── controller.js │ │ │ └── view.js │ ├── setup │ │ ├── contentView.js │ │ ├── controller.js │ │ ├── export.js │ │ ├── main.js │ │ ├── register.js │ │ ├── username.js │ │ └── view.js │ └── share │ │ ├── controller.js │ │ ├── info.js │ │ ├── users.js │ │ └── view.js │ ├── index.js │ ├── models │ ├── config.js │ ├── db.js │ ├── diffsync │ │ ├── core.js │ │ ├── diff.js │ │ ├── main.js │ │ ├── model.js │ │ ├── patch-diff.js │ │ └── patch.js │ ├── edit.js │ ├── encryption.js │ ├── file.js │ ├── model.js │ ├── note.js │ ├── notebook.js │ ├── peer.js │ ├── profile.js │ ├── shadow.js │ ├── signal.js │ ├── sync.js │ ├── tag.js │ └── user.js │ ├── overrideTemplate.js │ ├── utils │ ├── electronListener.js │ ├── env.js │ ├── i18n.js │ ├── initializer.js │ ├── keybindings.js │ ├── notify.js │ ├── theme.js │ ├── title.js │ ├── underscore.js │ └── url.js │ ├── views │ ├── brand.js │ ├── layout.js │ └── modal.js │ └── workers │ ├── delegator.js │ ├── module.js │ └── worker.js ├── webpack.config.js ├── webpack.production.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { 3 | "targets": { 4 | "browsers": [ 5 | "last 2 Chrome versions" 6 | ] 7 | } 8 | }]] 9 | } 10 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | eslint: 3 | enabled: true 4 | duplication: 5 | enabled: true 6 | config : 7 | languages: 8 | - javascript 9 | 10 | languages: 11 | JavaScript: true 12 | 13 | ratings: 14 | paths: 15 | - app/scripts/** 16 | 17 | exclude_paths: 18 | - test/** 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/.gitmodules -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "jquery": true, 22 | "mocha": true, 23 | "globals": { 24 | "expect": false, 25 | "define": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.modernizrrc: -------------------------------------------------------------------------------- 1 | { 2 | "minify": true, 3 | "options": [ 4 | "setClasses" 5 | ], 6 | "feature-detects": [ 7 | "blob", 8 | "crypto", 9 | "crypto/getrandomvalues", 10 | "es6/promises", 11 | "fullscreen-api", 12 | "mathml", 13 | "notification", 14 | "storage/localstorage", 15 | "storage/sessionstorage", 16 | "storage/websqldatabase", 17 | "websockets", 18 | "websockets/binary", 19 | "workers/blobworkers", 20 | "workers/dataworkers", 21 | "workers/sharedworkers", 22 | "workers/transferables", 23 | "workers/webworkers", 24 | "indexeddb", 25 | "storage/localstorage", 26 | "storage/sessionstorage", 27 | "storage/websqldatabase" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | Contribution 2 | ================ 3 | Note, all contributions should be done on `dev` branch. 4 | 5 | 6 | ### Localizations 7 | ---------------- 8 | 1. Copy ./app/locales/en and rename it to locale name - ./app/locales/[localeName] 9 | 2. Open ./app/locales/[localeName]/translation.json 10 | 3. Replace the values 11 | 4. Make a *pull request* 12 | -------------------------------------------------------------------------------- /app/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | Laverna 5 | 6 | Open source note taking application 7 | 8 | 9 | Laverna project 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/docs/howto.md: -------------------------------------------------------------------------------- 1 | ### How to use tags in Laverna 2 | 3 | When you are editing or creating notes, write "@" before any word to create a tag. 4 | 5 | ### How to use tasks in Laverna 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/favicon.ico -------------------------------------------------------------------------------- /app/images/icon/IconMenubarTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/IconMenubarTemplate.png -------------------------------------------------------------------------------- /app/images/icon/IconMenubarTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/IconMenubarTemplate@2x.png -------------------------------------------------------------------------------- /app/images/icon/icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-120x120.png -------------------------------------------------------------------------------- /app/images/icon/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-128x128.png -------------------------------------------------------------------------------- /app/images/icon/icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-150x150.png -------------------------------------------------------------------------------- /app/images/icon/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-152x152.png -------------------------------------------------------------------------------- /app/images/icon/icon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-16x16.png -------------------------------------------------------------------------------- /app/images/icon/icon-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-310x150.png -------------------------------------------------------------------------------- /app/images/icon/icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-310x310.png -------------------------------------------------------------------------------- /app/images/icon/icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-36x36.png -------------------------------------------------------------------------------- /app/images/icon/icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-48x48.png -------------------------------------------------------------------------------- /app/images/icon/icon-512x512.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-512x512.icns -------------------------------------------------------------------------------- /app/images/icon/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-512x512.png -------------------------------------------------------------------------------- /app/images/icon/icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-70x70.png -------------------------------------------------------------------------------- /app/images/icon/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-72x72.png -------------------------------------------------------------------------------- /app/images/icon/icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-76x76.png -------------------------------------------------------------------------------- /app/images/icon/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon-96x96.png -------------------------------------------------------------------------------- /app/images/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/icon/icon.png -------------------------------------------------------------------------------- /app/images/laverna.icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/images/laverna.icon.xcf -------------------------------------------------------------------------------- /app/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laverna", 3 | "description": "Open source note taking web application", 4 | "version": "0.6.2", 5 | "launch_path": "/index.html", 6 | "icons": { 7 | "512": "/images/icon/icon.png", 8 | "128": "/images/icon/icon-128x128.png", 9 | "16": "/images/icon/icon-16x16.png", 10 | "36": "/images/icon/icon-36x36.png", 11 | "48": "/images/icon/icon-48x48.png", 12 | "72": "/images/icon/icon-72x72.png" 13 | }, 14 | "developer": { 15 | "name": "Laverna project", 16 | "url": "https://github.com/Laverna/laverna/" 17 | }, 18 | "default_locale": "en", 19 | "installs_allowed_from": ["*"] 20 | } 21 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /app/scripts/behaviors/ModalForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module behavior/ModalForm 3 | */ 4 | import Mn from 'backbone.marionette'; 5 | import _ from 'underscore'; 6 | 7 | /** 8 | * Modal form behavior. 9 | * 10 | * @class 11 | * @extends Marionette.Behavior 12 | * @license MPL-2.0 13 | */ 14 | export default class ModalForm extends Mn.Behavior { 15 | 16 | get uiFocus() { 17 | return this.view.uiFocus || 'name'; 18 | } 19 | 20 | triggers() { 21 | return { 22 | 'submit form' : 'save', 23 | 'click .ok' : 'save', 24 | 'click .cancelBtn' : 'cancel', 25 | }; 26 | } 27 | 28 | modelEvents() { 29 | return { 30 | invalid: 'showErrors', 31 | }; 32 | } 33 | 34 | constructor(...args) { 35 | super(...args); 36 | 37 | this.events = { 38 | [`keyup @ui.${this.uiFocus}`]: 'closeOnEsc', 39 | }; 40 | } 41 | 42 | /** 43 | * Focus on an ui element. 44 | */ 45 | onShownModal() { 46 | this.view.ui[this.uiFocus].focus(); 47 | } 48 | 49 | /** 50 | * Trigger "cancel" event if user hits Escape. 51 | */ 52 | closeOnEsc(e) { 53 | if (e.which === 27) { 54 | this.view.trigger('cancel'); 55 | } 56 | } 57 | 58 | /** 59 | * Indicate that validation errors occured by highlighting 60 | * form elements with red color. 61 | * 62 | * @param {Object} data 63 | * @param {Object} data.errors 64 | */ 65 | showErrors(data) { 66 | _.each(data.errors, err => { 67 | this.view.ui[err].parent().addClass('has-error'); 68 | }); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /app/scripts/behaviors/ModelFocus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module behaviors/ModelFocus 3 | */ 4 | import Mn from 'backbone.marionette'; 5 | import $ from 'jquery'; 6 | 7 | /** 8 | * Handle navigation between models. 9 | * 10 | * @class 11 | * @extends Marionette.Behavior 12 | * @license MPL-2.0 13 | */ 14 | export default class ModelFocus extends Mn.Behavior { 15 | 16 | ui() { 17 | return { 18 | listGroup: '.list-group-item:first', 19 | }; 20 | } 21 | 22 | modelEvents() { 23 | return { 24 | focus: 'onFocus', 25 | }; 26 | } 27 | 28 | /** 29 | * The model is active (under focus). 30 | */ 31 | onFocus() { 32 | $('.list-group-item.active').removeClass('active'); 33 | this.ui.listGroup.addClass('active'); 34 | this.view.trigger('scroll:top', {offset: this.ui.listGroup.offset()}); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/scripts/behaviors/Sidebar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module behaviors/Sidebar 3 | */ 4 | import Mn from 'backbone.marionette'; 5 | import $ from 'jquery'; 6 | import Hammer from 'hammerjs'; 7 | import Radio from 'backbone.radio'; 8 | 9 | /** 10 | * Sidebar region behavior. 11 | * 12 | * @class 13 | * @extends Marionette.Behavior 14 | * @license MPL-2.0 15 | */ 16 | export default class Sidebar extends Mn.Behavior { 17 | 18 | /** 19 | * Stop listening to touch events. 20 | */ 21 | onDestroy() { 22 | if (this.hammer) { 23 | this.hammer.destroy(); 24 | } 25 | } 26 | 27 | /** 28 | * Start listening to touch events. 29 | */ 30 | onRender() { 31 | this.hammer = new Hammer($('#sidebar--content')[0]); 32 | 33 | this.hammer.on('swiperight', () => this.onSwipeRight()); 34 | this.hammer.on('swipeleft', () => this.onSwipeLeft()); 35 | } 36 | 37 | /** 38 | * Show sidebar menu on swiperight. 39 | */ 40 | onSwipeRight() { 41 | Radio.trigger('components/navbar', 'show:sidemenu'); 42 | } 43 | 44 | /** 45 | * Switch to content region (hide sidebar) on swipeleft. 46 | */ 47 | onSwipeLeft() { 48 | // Notebooks component does not use content region 49 | if (!this.view.noSwipeLeft) { 50 | Radio.request('Layout', 'toggleContent', {visible: true}); 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/scripts/collections/DiffCollection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module collections/DiffCollection 3 | */ 4 | import _ from 'underscore'; 5 | import Collection from './Collection'; 6 | 7 | /** 8 | * Core collection for diffsync. 9 | * 10 | * @class 11 | * @extends Marionette.Object 12 | * @license MPL-2.0 13 | */ 14 | export default class DiffCollection extends Collection { 15 | 16 | /** 17 | * Find a shadow/edit for a particular document. 18 | * 19 | * @param {Object} peer - peer information (username, deviceId) 20 | * @param {Object} doc - document model 21 | */ 22 | findForDoc(peer, doc) { 23 | return this.findOrCreate({ 24 | username : peer.username, 25 | deviceId : peer.deviceId, 26 | docId : doc.id, 27 | docType : doc.storeName, 28 | }); 29 | } 30 | 31 | /** 32 | * Find a model or create a new one. 33 | * 34 | * @param {Object} data 35 | * @param {String} data.docId 36 | * @param {String} data.docType 37 | * @param {String} data.username 38 | * @param {String} data.deviceId 39 | * @returns {Object} 40 | */ 41 | findOrCreate(data) { 42 | const mData = _.pick(data, 'docId', 'docType', 'username', 'deviceId'); 43 | 44 | let model = this.findWhere(mData); 45 | if (model) { 46 | return model; 47 | } 48 | 49 | model = new this.model(mData); 50 | this.add(model); 51 | return model; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/scripts/collections/Edits.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module collections/Edits 3 | */ 4 | import DiffCollection from './DiffCollection'; 5 | import Edit from '../models/Edit'; 6 | 7 | /** 8 | * Edits collection. 9 | * 10 | * @class 11 | * @extends module:collections/DiffCollection 12 | * @license MPL-2.0 13 | */ 14 | export default class Edits extends DiffCollection { 15 | 16 | /** 17 | * Edit model. 18 | * 19 | * @see module:models/Edit 20 | * @prop {Object} 21 | */ 22 | get model() { 23 | return Edit; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/scripts/collections/Files.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module collections/Files 3 | */ 4 | import File from '../models/File'; 5 | import Collection from './Collection'; 6 | 7 | /** 8 | * File collection. 9 | * 10 | * @class 11 | * @extends module:collections/Collection 12 | * @license MPL-2.0 13 | */ 14 | export default class Files extends Collection { 15 | 16 | /** 17 | * File model. 18 | * 19 | * @returns {Object} 20 | */ 21 | get model() { 22 | return File; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/scripts/collections/Profiles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module collections/Profiles 3 | */ 4 | import Collection from './Collection'; 5 | import Profile from '../models/Profile'; 6 | 7 | /** 8 | * Profiles collection. 9 | * 10 | * @class 11 | * @extends module:collections/Collection 12 | * @license MPL-2.0 13 | */ 14 | export default class Profiles extends Collection { 15 | 16 | /** 17 | * Profile model. 18 | * 19 | * @returns {Object} 20 | */ 21 | get model() { 22 | return Profile; 23 | } 24 | 25 | destroyUser(opt) { 26 | console.log('destroyUser(): calling destroyDb()'); 27 | this.sync('destroyDb', opt, {}); 28 | } 29 | 30 | constructor(models) { 31 | // Change the profileId to "default" 32 | super(models, {profileId: 'default'}); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/scripts/collections/Shadows.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module collections/Shadows 3 | */ 4 | import DiffCollection from './DiffCollection'; 5 | import Shadow from '../models/Shadow'; 6 | 7 | /** 8 | * Shadows collection. 9 | * 10 | * @class 11 | * @extends module:collections/DiffCollection 12 | * @license MPL-2.0 13 | */ 14 | export default class Shadows extends DiffCollection { 15 | 16 | get model() { 17 | return Shadow; 18 | } 19 | 20 | findOrCreate(...args) { 21 | const model = super.findOrCreate(...args); 22 | 23 | // Create a backup if it's a completely new model 24 | if (!model.id && !model.get('m')) { 25 | model.createBackup(); 26 | } 27 | 28 | return model; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/scripts/collections/Users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module collections/Users 3 | */ 4 | import User from '../models/User'; 5 | import Collection from './Collection'; 6 | 7 | /** 8 | * User collection. 9 | * 10 | * @class 11 | * @extends module:collections/Collection 12 | * @license MPL-2.0 13 | */ 14 | export default class Users extends Collection { 15 | 16 | /** 17 | * User model. 18 | * 19 | * @returns {Object} 20 | */ 21 | get model() { 22 | return User; 23 | } 24 | 25 | /** 26 | * Return an array of users who are waiting for your approval. 27 | * 28 | * @returns {Array} 29 | */ 30 | getPending() { 31 | return this.filter(model => model.get('pendingAccept')); 32 | } 33 | 34 | /** 35 | * Return an array of users whom you trust. 36 | * 37 | * @returns {Array} 38 | */ 39 | getTrusted() { 40 | return this.filter(model => !model.get('pendingAccept')); 41 | } 42 | 43 | /** 44 | * Return an array of users whom you trust and who accepted your invite. 45 | * Those are the users with whom it is safe to try to establish connection. 46 | * 47 | * @returns {Array} 48 | */ 49 | getActive() { 50 | return this.filter(model => { 51 | return !model.get('pendingAccept') && !model.get('pendingInvite'); 52 | }); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/scripts/collections/modules/Edits.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module collections/modules/Edits 3 | */ 4 | import Module from './Module'; 5 | import Collection from '../Edits'; 6 | // import _ from 'underscore'; 7 | // import deb from 'debug'; 8 | 9 | // const log = deb('lav:collections/modules/Edits'); 10 | 11 | /** 12 | * Edits collection module. 13 | * 14 | * @class 15 | * @extends module:collections/modules/Module 16 | * @license MPL-2.0 17 | */ 18 | export default class Edits extends Module { 19 | 20 | /** 21 | * Shadow collection. 22 | * 23 | * @see module:collections/Edits 24 | * @returns {Object} 25 | */ 26 | get Collection() { 27 | return Collection; 28 | } 29 | 30 | constructor() { 31 | super(); 32 | 33 | this.channel.reply({ 34 | }, this); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/scripts/collections/modules/Shadows.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module collections/modules/Shadows 3 | */ 4 | import Module from './Module'; 5 | import Collection from '../Shadows'; 6 | 7 | /** 8 | * Shadows collection module. 9 | * 10 | * @class 11 | * @extends module:collections/modules/Module 12 | * @license MPL-2.0 13 | */ 14 | export default class Shadows extends Module { 15 | 16 | /** 17 | * Shadow collection. 18 | * 19 | * @see module:collections/Shadows 20 | * @returns {Object} 21 | */ 22 | get Collection() { 23 | return Collection; 24 | } 25 | 26 | constructor() { 27 | super(); 28 | 29 | this.channel.reply({ 30 | }, this); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/scripts/collections/modules/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file instantiate all collection modules 3 | * @license MPL-2.0 4 | */ 5 | import Radio from 'backbone.radio'; 6 | 7 | import Profiles from './Profiles'; 8 | import Configs from './Configs'; 9 | import Users from './Users'; 10 | import Files from './Files'; 11 | import Notebooks from './Notebooks'; 12 | import Notes from './Notes'; 13 | import Tags from './Tags'; 14 | import Shadows from './Shadows'; 15 | import Edits from './Edits'; 16 | 17 | /** 18 | * Instantiate all collection modules 19 | * 20 | * @returns {Promise} 21 | */ 22 | function initializer() { 23 | // Instantiate all collection modules to start listening to requests 24 | new Profiles(); 25 | new Configs(); 26 | new Users(); 27 | new Files(); 28 | new Notebooks(); 29 | new Notes(); 30 | new Tags(); 31 | new Shadows(); 32 | new Edits(); 33 | 34 | // Find or create configs 35 | return Radio.request('collections/Profiles', 'find'); 36 | } 37 | 38 | // Add a new initializer 39 | Radio.once('App', 'init', () => { 40 | Radio.request('utils/Initializer', 'add', { 41 | name : 'App:core', 42 | callback: initializer, 43 | }); 44 | }); 45 | 46 | export default initializer; 47 | -------------------------------------------------------------------------------- /app/scripts/components/codemirror/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/codemirror/main 3 | * @license MPL-2.0 4 | */ 5 | import Radio from 'backbone.radio'; 6 | import Controller from './Controller'; 7 | 8 | Radio.once('App', 'init', () => { 9 | Radio.on('components/notes/form', 'ready', opt => new Controller(opt).init()); 10 | }); 11 | -------------------------------------------------------------------------------- /app/scripts/components/confirm/template.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /app/scripts/components/dropbox/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/dropbox/main 3 | * @license MPL-2.0 4 | */ 5 | import Radio from 'backbone.radio'; 6 | import Sync from './Sync'; 7 | import View from './settings/View'; 8 | 9 | export default function initialize() { 10 | const sync = Radio.request('collections/Configs', 'findConfig', { 11 | name: 'cloudStorage', 12 | }); 13 | 14 | // Reply with the settings view 15 | Radio.channel('components/dropbox').reply({getSettingsView: () => View}); 16 | 17 | if (sync === 'dropbox') { 18 | return new Sync().init(); 19 | } 20 | } 21 | 22 | Radio.once('App', 'start', initialize); 23 | -------------------------------------------------------------------------------- /app/scripts/components/dropbox/settings/View.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/dropbox/show/settings/View 3 | */ 4 | import _ from 'underscore'; 5 | import Mn from 'backbone.marionette'; 6 | import constants from '../../../constants'; 7 | 8 | /** 9 | * Show a list of users whom you trust or invited. 10 | * 11 | * @class 12 | * @extends Marionette.View 13 | * @license MPL-2.0 14 | */ 15 | export default class Users extends Mn.View { 16 | 17 | get template() { 18 | const tmpl = require('./template.html'); 19 | return _.template(tmpl); 20 | } 21 | 22 | get dropboxKeyNeed() { 23 | return constants.dropboxKeyNeed; 24 | } 25 | 26 | serializeData() { 27 | return { 28 | dropboxKey : this.collection.get('dropboxKey').get('value'), 29 | placeholder : this.dropboxKeyNeed ? 'Required' : 'Optional', 30 | }; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/scripts/components/dropbox/settings/template.html: -------------------------------------------------------------------------------- 1 |
2 |

{{_.i18n('Dropbox API key')}}

3 | 4 |
5 | 6 |
7 |

{{_.i18n('dropbox.api info 1')}} Dropbox

8 |

{{_.i18n('dropbox.api info 2')}}

9 |
    10 |
  1. {{_.i18n('dropbox.api info li 1')}}
  2. 11 |
  3. {{_.i18n('dropbox.api info li 2')}}
  4. 12 |
13 |
14 | -------------------------------------------------------------------------------- /app/scripts/components/electronSearch/template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 | 9 | 12 |
13 |
14 | 15 |
16 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /app/scripts/components/encryption/auth/template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 |

Laverna

6 |
7 | 8 |
9 | 11 | 16 |
17 | 18 |
19 | 21 |
22 | 23 |
24 | 27 |
28 | 29 |
30 | 33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /app/scripts/components/encryption/encrypt/template.html: -------------------------------------------------------------------------------- 1 |
2 |

3 |

{{title}}

4 |
5 | 6 |
7 |
8 |
9 | <% if (!encrypt) { %> 10 |

{{_.i18n('Data decryption after disabling encryption')}}

11 | <% } else { %> 12 |

{{_.i18n('Data encryption after enabling encryption')}}

13 | <% } %> 14 |
15 | 16 |
17 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |
29 |
30 | 31 |

{{ _.i18n('Please wait') }}...

32 |
33 | -------------------------------------------------------------------------------- /app/scripts/components/encryption/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/encryption/main 3 | */ 4 | import Encryption from '../../models/Encryption'; 5 | import Radio from 'backbone.radio'; 6 | import Auth from './auth/Controller'; 7 | import Encrypt from './encrypt/Controller'; 8 | 9 | export default function initialize() { 10 | // Instantiate encryption model 11 | new Encryption(); 12 | 13 | // Don't start the app until auth is successful 14 | Radio.request('utils/Initializer', 'add', { 15 | name : 'App:auth', 16 | callback: () => new Auth().init(), 17 | }); 18 | 19 | // In case if a user enabled/disabled encryption, encrypt/decrypt data 20 | Radio.request('utils/Initializer', 'add', { 21 | name : 'App:auth', 22 | callback: () => new Encrypt().init(), 23 | }); 24 | } 25 | 26 | Radio.once('App', 'init', initialize); 27 | -------------------------------------------------------------------------------- /app/scripts/components/fileDialog/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/fileDialog/main 3 | * @license MPL-2.0 4 | */ 5 | import Radio from 'backbone.radio'; 6 | import Controller from './Controller'; 7 | 8 | export default function initialize() { 9 | const channel = Radio.channel('components/fileDialog'); 10 | channel.reply('show', (...args) => new Controller(...args).init()); 11 | } 12 | 13 | Radio.once('App', 'init', initialize); 14 | -------------------------------------------------------------------------------- /app/scripts/components/fileDialog/templates/dropzone.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |

7 |

8 |
9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /app/scripts/components/fuzzySearch/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/fuzzySearch/main 3 | * @license MPL-2.0 4 | */ 5 | import $ from 'jquery'; 6 | import Radio from 'backbone.radio'; 7 | import Controller from './Controller'; 8 | import regionClass from './views/Region'; 9 | 10 | const main = { 11 | 12 | initialize() { 13 | main.createRegion(); 14 | 15 | // Instantiate fuzzy search controller on shown:search event 16 | Radio.channel('components/navbar') 17 | .on('shown:search', () => new Controller().init()); 18 | }, 19 | 20 | /** 21 | * Create fuzzySearch region. 22 | * 23 | * @returns {Object} region instance 24 | */ 25 | createRegion() { 26 | $('#sidebar').append( 27 | ' 24 | -------------------------------------------------------------------------------- /app/scripts/components/settings/show/wipe/template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{_.i18n('Erasure options')}}

4 |
5 | 6 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /app/scripts/components/settings/sidebar/templates/navbar.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/scripts/components/settings/sidebar/templates/template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | {{_.i18n('General')}} 6 | 7 | 8 | 9 | {{_.i18n('Editor')}} 10 | 11 | 12 | 13 | {{_.i18n('Keybindings')}} 14 | 15 | 16 | 17 | {{_.i18n('Encryption')}} 18 | 19 | 20 | 21 | {{_.i18n('Sync')}} 22 | 23 | 24 | 25 | {{_.i18n('Wipe')}} 26 | 27 | 28 |
  • {{_.i18n('Other')}}
  • 29 | 30 | 31 | {{_.i18n('Backup')}} 32 | 33 |
    34 |
    35 | -------------------------------------------------------------------------------- /app/scripts/components/settings/sidebar/views/Navbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/settings/sidebar/Navbar 3 | */ 4 | import Mn from 'backbone.marionette'; 5 | import _ from 'underscore'; 6 | 7 | /** 8 | * Settings navbar view for the sidebar. 9 | * 10 | * @class 11 | * @extends Marionette.View 12 | * @license MPL-2.0 13 | */ 14 | export default class Navbar extends Mn.View { 15 | 16 | get template() { 17 | const tmpl = require('../templates/navbar.html'); 18 | return _.template(tmpl); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/scripts/components/setup/export/View.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/setup/export/View 3 | */ 4 | import Mn from 'backbone.marionette'; 5 | import _ from 'underscore'; 6 | 7 | /** 8 | * Export the generated key. 9 | * 10 | * @class 11 | * @extends Marionette.View 12 | * @license MPL-2.0 13 | */ 14 | export default class Export extends Mn.View { 15 | 16 | get template() { 17 | const tmpl = require('./template.html'); 18 | return _.template(tmpl); 19 | } 20 | 21 | ui() { 22 | return { 23 | sync: '[name=sync]', 24 | }; 25 | } 26 | 27 | className() { 28 | return 'row'; 29 | } 30 | 31 | serializeData() { 32 | return this.options; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/scripts/components/setup/export/template.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    {{_.i18n('You successfuly claimed username')}} {{username}}.

    4 |

    {{_.i18n('To keep your notes available on every device, choose one of the synchronization options')}}:

    5 |
    6 | 7 |
    8 | 14 |
    15 | 16 |
    17 |

    {{_.i18n('Please download your private key and keep it in a safe place. If you lose your private key, you will lose your data and your username.')}}

    18 |
    19 | 20 |
    21 | 25 |
    26 |
    27 | -------------------------------------------------------------------------------- /app/scripts/components/setup/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/setup/main 3 | * @license MPL-2.0 4 | */ 5 | import Radio from 'backbone.radio'; 6 | import Controller from './Controller'; 7 | 8 | export default function initialize() { 9 | // On "start" request show the "setup" form 10 | Radio.reply('components/setup', 'start', opt => new Controller(opt).init()); 11 | 12 | // Add a new initializer to show the "setup" form on start 13 | Radio.request('utils/Initializer', 'add', { 14 | name : 'App:components', 15 | callback: () => new Controller().init(), 16 | }); 17 | } 18 | 19 | Radio.once('App', 'init', initialize); 20 | -------------------------------------------------------------------------------- /app/scripts/components/setup/template.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{_.i18n('Laverna')}}

    3 |
    4 |
    5 | -------------------------------------------------------------------------------- /app/scripts/components/share/info/View.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/share/Info 3 | */ 4 | import _ from 'underscore'; 5 | import Mn from 'backbone.marionette'; 6 | 7 | /** 8 | * Show information about a user. 9 | * 10 | * @class 11 | * @extends Marionette.View 12 | * @license MPL-2.0 13 | */ 14 | export default class Info extends Mn.View { 15 | 16 | get template() { 17 | const tmpl = require('./template.html'); 18 | return _.template(tmpl); 19 | } 20 | 21 | triggers() { 22 | return { 23 | 'click .share--trust': 'add:trust', 24 | }; 25 | } 26 | 27 | serializeData() { 28 | return this.options; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/scripts/components/share/info/template.html: -------------------------------------------------------------------------------- 1 |
    2 | Before adding a user to trust, check their key fingerprint. 3 | You can ask the user to share their fingerprint by email or phone. 4 |
    5 | 6 |
    7 |
    8 | 11 |
    12 |
    13 | 14 |

    {{user.username}}

    15 |

    {{_.i18n('Key fingerprint')}}: {{_.splitBy4(user.fingerprint)}}

    16 |
    17 |
    18 | -------------------------------------------------------------------------------- /app/scripts/components/share/template.html: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /app/scripts/components/share/users/View.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module components/share/Users 3 | */ 4 | import _ from 'underscore'; 5 | import Mn from 'backbone.marionette'; 6 | 7 | /** 8 | * Show trusted users. 9 | * 10 | * @class 11 | * @extends Marionette.View 12 | * @license MPL-2.0 13 | */ 14 | export default class Users extends Mn.View { 15 | 16 | get template() { 17 | const tmpl = require('./template.html'); 18 | return _.template(tmpl); 19 | } 20 | 21 | events() { 22 | return { 23 | 'click .share--toggle' : 'share', 24 | }; 25 | } 26 | 27 | modelEvents() { 28 | return { 29 | 'change:sharedWith': 'render', 30 | }; 31 | } 32 | 33 | collectionEvents() { 34 | return { 35 | 'add change update': 'render', 36 | }; 37 | } 38 | 39 | /** 40 | * Trigger "share" method. 41 | * 42 | * @param {Object} e 43 | */ 44 | share(e) { 45 | const username = this.$(e.currentTarget).attr('data-user'); 46 | this.triggerMethod('share', {username}); 47 | } 48 | 49 | serializeData() { 50 | return this.options; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /app/scripts/components/share/users/template.html: -------------------------------------------------------------------------------- 1 | <% if (!_.isEmpty(collection.models)) { %> 2 |

    {{_.i18n('Users you trust')}}:

    3 |
    4 | <% _.each(collection.getTrusted(), function(user) { %> 5 |
    6 |
    7 | <% if (model.isSharedWith(user.get('username'))) { %> 8 | 11 | <% } else { %> 12 | 15 | <% } %> 16 |
    17 |
    18 |

    19 | {{user.get('username')}} 20 | <% if (user.get('pendingInvite')) { %> 21 | {{_.i18n('pending invite')}} 22 | <% } %> 23 |

    24 |

    {{_.i18n('Key fingerprint')}}: {{_.splitBy4(user.get('fingerprint'))}}

    25 |
    26 |
    27 | <% }) %> 28 |
    29 | <% } %> 30 | 31 |
    32 | {{_.i18n('Please note that when you share a document with someone, you give them read and write access.')}} 33 |
    34 | -------------------------------------------------------------------------------- /app/scripts/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configs that don't change. 3 | * 4 | * @module constants 5 | * @license MPL-2.0 6 | */ 7 | import _ from 'underscore'; 8 | 9 | /** 10 | * @namespace constants 11 | * @prop {String} version - current version of the app 12 | * @prop {String} url - the URL where the app is accessed from 13 | * @prop {Array} defaultHosts - hosts that Dropbox auth server accepts 14 | * @prop {String} dropboxKey - dropbox API key 15 | * @prop {String} dropboxSecret - dropbox secret key (not necessary) 16 | * @prop {Boolean} dropboxKeyNeed - true if the default Dropbox API key will 17 | * not work and a user needs to provid their own 18 | */ 19 | const constants = { 20 | version : '1.0.3-beta', 21 | url : location.origin + location.pathname.replace('index.html', ''), 22 | defaultHosts : [ 23 | 'privatenote-online.com', 24 | 'laverna.github.io', 25 | 'localhost', 26 | 'localhost:9000', 27 | 'localhost:9100', 28 | ], 29 | dropboxKey : 'hlicys9cs8rj3ep', 30 | dropboxSecret : null, 31 | dropboxKeyNeed: false, 32 | }; 33 | 34 | // The default Dropbox API key will not work 35 | if (!_.contains(constants.defaultHosts, location.host) && !window.electron) { 36 | constants.dropboxKeyNeed = true; 37 | } 38 | 39 | export default constants; 40 | -------------------------------------------------------------------------------- /app/scripts/models/Config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module models/Config 3 | */ 4 | import Model from './Model'; 5 | 6 | /** 7 | * Config model. 8 | * 9 | * @class 10 | * @extends module:models/Model 11 | * @license MPL-2.0 12 | */ 13 | export default class Config extends Model { 14 | 15 | /** 16 | * Use name as ID. 17 | * 18 | * @returns {String} 19 | */ 20 | get idAttribute() { 21 | return 'name'; 22 | } 23 | 24 | /** 25 | * Default values. 26 | * 27 | * @returns {Object} 28 | */ 29 | get defaults() { 30 | return { 31 | name : '', 32 | value : '', 33 | }; 34 | } 35 | 36 | /** 37 | * Store name. 38 | * 39 | * @returns {String} 40 | */ 41 | get storeName() { 42 | return 'configs'; 43 | } 44 | 45 | get validateAttributes() { 46 | return ['name']; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/scripts/models/File.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module models/File 3 | */ 4 | import Model from './Model'; 5 | 6 | /** 7 | * File model. 8 | * 9 | * @class 10 | * @extends module:models/Model 11 | * @license MPL-2.0 12 | */ 13 | export default class File extends Model { 14 | 15 | /** 16 | * Store name. 17 | * 18 | * @returns {String} 19 | */ 20 | get storeName() { 21 | return 'files'; 22 | } 23 | 24 | /** 25 | * Default values. 26 | * 27 | * @property {String} type - type of data stored in this model 28 | * @property {(String|Undefined)} id - undefined for default 29 | * @property {String} name - name of the stored file 30 | * @property {String} src - file data (blob?) 31 | * @property {String} fileType - (jpg|png|pdf|...) 32 | * @property {Number} trash - 1 if the model is in trash 33 | * @property {Date.now()} created - the date when the model was created 34 | * @property {Date.now()} update - the date when the model was updated 35 | * the last time 36 | */ 37 | get defaults() { 38 | return { 39 | type : 'files', 40 | id : undefined, 41 | name : '', 42 | src : '', 43 | fileType : '', 44 | trash : 0, 45 | created : 0, 46 | updated : 0, 47 | }; 48 | } 49 | 50 | get validateAttributes() { 51 | return ['src', 'fileType']; 52 | } 53 | 54 | get escapeAttributes() { 55 | return ['name']; 56 | } 57 | 58 | /** 59 | * @todo implement a proper check 60 | */ 61 | isSharedWith() { 62 | return true; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/scripts/models/Profile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module models/Profile 3 | */ 4 | import Model from './Model'; 5 | 6 | /** 7 | * Profile model. 8 | * 9 | * @class 10 | * @extends module:models/Model 11 | * @license MPL-2.0 12 | */ 13 | export default class Profile extends Model { 14 | 15 | get storeName() { 16 | return 'profiles'; 17 | } 18 | 19 | /** 20 | * Use username as ID. 21 | * 22 | * @returns {String} 23 | */ 24 | get idAttribute() { 25 | return 'username'; 26 | } 27 | 28 | /** 29 | * Default values. 30 | * 31 | * @prop {String} username 32 | * @prop {String} privateKey - private key 33 | * @prop {String} publicKey - public key 34 | * @returns {Object} 35 | */ 36 | get defaults() { 37 | return { 38 | username : '', 39 | privateKey : '', 40 | publicKey : '', 41 | }; 42 | } 43 | 44 | get validateAttributes() { 45 | return ['username']; 46 | } 47 | 48 | get escapeAttributes() { 49 | return ['username']; 50 | } 51 | 52 | constructor(options) { 53 | super(options, {profileId: 'default'}); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/scripts/models/Tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module models/Tag 3 | */ 4 | import Model from './Model'; 5 | 6 | /** 7 | * Tag model. 8 | * 9 | * @class 10 | * @extends module:models/Model 11 | * @license MPL-2.0 12 | */ 13 | export default class Tag extends Model { 14 | 15 | get storeName() { 16 | return 'tags'; 17 | } 18 | 19 | /** 20 | * Default values. 21 | * 22 | * @property {String} type - type of data stored in this model 23 | * @property {(String|Undefined)} id - undefined for default 24 | * @property {String} encryptedData 25 | * @property {String} name - the name of a tag 26 | * @property {Number} count - the number of notes attached to a tag 27 | * @property {Number} trash 28 | * @property {Date.now()} created 29 | * @property {Date.now()} updated 30 | * @returns {Object} 31 | */ 32 | get defaults() { 33 | return { 34 | type : 'tags', 35 | id : undefined, 36 | encryptedData : '', 37 | name : '', 38 | count : 0, 39 | trash : 0, 40 | created : 0, 41 | updated : 0, 42 | }; 43 | } 44 | 45 | /** 46 | * Attributes that need to be encrypted. 47 | * 48 | * @returns {Array} 49 | */ 50 | get encryptKeys() { 51 | return ['name']; 52 | } 53 | 54 | get validateAttributes() { 55 | return ['name']; 56 | } 57 | 58 | get escapeAttributes() { 59 | return ['name']; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/scripts/models/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module models/User 3 | */ 4 | import Model from './Model'; 5 | 6 | /** 7 | * User model. 8 | * Stores information about users with whom you want to connect. 9 | * 10 | * @class 11 | * @extends module:models/Model 12 | * @license MPL-2.0 13 | */ 14 | export default class User extends Model { 15 | 16 | get storeName() { 17 | return 'users'; 18 | } 19 | 20 | /** 21 | * Use username as ID attribute. 22 | * 23 | * @prop {String} 24 | */ 25 | get idAttribute() { 26 | return 'username'; 27 | } 28 | 29 | /** 30 | * Default values. 31 | * 32 | * @prop {String} type - equal to users 33 | * @prop {String} username 34 | * @prop {String} fingerprint 35 | * @prop {String} publicKey 36 | * @prop {Boolean} pendingInvite - true if the user was invited by you 37 | * and you're waiting for their answer 38 | * @prop {Boolean} pendingAccept - true if you have been invited by this 39 | * user and they're waiting for your answer 40 | * @returns {Object} 41 | */ 42 | get defaults() { 43 | return { 44 | type : 'users', 45 | username : '', 46 | fingerprint : '', 47 | publicKey : '', 48 | pendingAccept : false, 49 | pendingInvite : false, 50 | }; 51 | } 52 | 53 | get validateAttributes() { 54 | return ['username', 'publicKey', 'fingerprint']; 55 | } 56 | 57 | get escapeAttributes() { 58 | return ['username', 'publicKey', 'fingerprint']; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /app/scripts/models/diffsync/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module models/diffsync/main 3 | * @license MPL-2.0 4 | */ 5 | import Radio from 'backbone.radio'; 6 | import Signal from '../Signal'; 7 | import Peer from '../Peer'; 8 | import Core from './Core'; 9 | 10 | export default function initializer() { 11 | // Instantiate the class that connects a user to the signaling server 12 | const signal = new Signal(); 13 | 14 | function start() { 15 | const sync = Radio.request('collections/Configs', 'findConfig', { 16 | name: 'cloudStorage', 17 | }); 18 | 19 | // intentionally breaking signal server for now. no p2p until it works. 20 | sync == 'none'; 21 | if (sync === 'p2p') { 22 | signal.changeServer(); 23 | new Peer().init(); 24 | new Core().init(); 25 | } 26 | else { 27 | signal.destroy(); 28 | } 29 | } 30 | 31 | // Initialize peer class and differential synchronization core 32 | Radio.once('App', 'start', start); 33 | return start; 34 | } 35 | 36 | // Add a new initializer 37 | Radio.once('App', 'init', () => { 38 | Radio.request('utils/Initializer', 'add', { 39 | name : 'App:utils', 40 | callback: initializer, 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /app/scripts/modules/fs/module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Laverna project Authors. 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | */ 8 | /* global define */ 9 | define([ 10 | 'underscore', 11 | 'backbone.radio', 12 | 'marionette', 13 | 'modules', 14 | 'modules/fs/classes/sync', 15 | ], (_, Radio, Marionette, Modules, Sync) => { 16 | 'use strict'; 17 | 18 | /** 19 | * Module which synchronizes all models to a file system. 20 | * (Works only on Electron app) 21 | */ 22 | const FS = Modules.module('FS', {}); 23 | 24 | /** 25 | * Initializers & finalizers of the module 26 | */ 27 | FS.on('start', () => { 28 | console.info('FS started'); 29 | new Sync(); 30 | }); 31 | 32 | FS.on('stop', () => { 33 | }); 34 | 35 | // Add a global module initializer 36 | Radio.request('init', 'add', 'module', () => { 37 | FS.start(); 38 | }); 39 | 40 | return FS; 41 | }); 42 | -------------------------------------------------------------------------------- /app/scripts/modules/fs/templates/settings.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 6 | 7 | 8 |
    9 | -------------------------------------------------------------------------------- /app/scripts/modules/fs/views/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Laverna project Authors. 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | */ 8 | /* global define, requireNode */ 9 | define([ 10 | 'underscore', 11 | 'marionette', 12 | 'backbone.radio', 13 | 'text!modules/fs/templates/settings.html', 14 | ], (_, Marionette, Radio, Tmpl) => { 15 | 'use strict'; 16 | 17 | /** 18 | * Shows FS module's configs. 19 | */ 20 | const dialog = requireNode('electron').remote.dialog; 21 | 22 | const View = Marionette.ItemView.extend({ 23 | template: _.template(Tmpl), 24 | 25 | ui: { 26 | input: '.input--fs', 27 | }, 28 | 29 | events : { 30 | 'click .btn--fs': 'showFolderDialog', 31 | }, 32 | 33 | initialize() { 34 | }, 35 | 36 | serializeData() { 37 | return { 38 | models: this.collection.getConfigs(), 39 | }; 40 | }, 41 | 42 | showFolderDialog(e) { 43 | e.preventDefault(); 44 | 45 | const folder = dialog.showOpenDialog({properties: ['openDirectory']}); 46 | 47 | if (!folder) { 48 | return; 49 | } 50 | 51 | this.ui.input.val(folder[0]); 52 | 53 | this.collection.trigger('new:value', { 54 | name : 'module:fs:folder', 55 | value : folder[0], 56 | }); 57 | }, 58 | 59 | }); 60 | 61 | return View; 62 | }); 63 | -------------------------------------------------------------------------------- /app/scripts/modules/modules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id" : "mathjax", 4 | "name" : "MathJax", 5 | "description" : "MathJax", 6 | "minVersion" : "", 7 | "maxVersion" : "", 8 | "platforms" : [ 9 | "electron", 10 | "browser", 11 | "mobile" 12 | ] 13 | }, 14 | { 15 | "id" : "fuzzySearch", 16 | "name" : "fuzzySearch", 17 | "description" : "fuzzySearch module", 18 | "minVersion" : "", 19 | "maxVersion" : "", 20 | "platforms" : [ 21 | "electron", 22 | "browser", 23 | "mobile" 24 | ] 25 | }, 26 | { 27 | "id" : "fs", 28 | "name" : "FS", 29 | "description" : "Synchronizes everything to the file system", 30 | "hasSettings" : true, 31 | "minVersion" : "", 32 | "maxVersion" : "", 33 | "platforms" : [ 34 | "electron" 35 | ] 36 | }, 37 | { 38 | "id" : "electronSearch", 39 | "name" : "Electron Search", 40 | "description" : "", 41 | "hasSettings" : false, 42 | "minVersion" : "", 43 | "maxVersion" : "", 44 | "platforms" : [ 45 | "electron" 46 | ] 47 | } 48 | ] 49 | -------------------------------------------------------------------------------- /app/scripts/modules/remotestorage/classes/rs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Laverna project Authors. 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | */ 8 | /* global define, RemoteStorage */ 9 | define([ 10 | 'tv4', 11 | 'backbone.radio', 12 | 'remotestorage', 13 | ], (tv4, Radio) => { 14 | 'use strict'; 15 | 16 | // Make TV4 globally available because RemoteStorage needs it. 17 | window.tv4 = tv4; 18 | const RS = new RemoteStorage({ 19 | logging : false, 20 | cordovaRedirectUri : 'https://laverna.cc', 21 | changeEvents : { 22 | local : false, 23 | window : false, 24 | remote : true, 25 | conflict : true, 26 | }, 27 | }); 28 | 29 | 30 | /** 31 | * Sometimes hash is not saved automatically after starting Backbone router. 32 | */ 33 | let md = Radio.request('global', 'hash:original'); 34 | md = md.match(/access_token=([^&]+)/); 35 | if (md && !RS.remote.token) { 36 | RS.remote.configure({ 37 | token: md[1], 38 | }); 39 | } 40 | 41 | // Make remoteStorage globally available 42 | window.remoteStorage = RS; 43 | return window.remoteStorage; 44 | }); 45 | -------------------------------------------------------------------------------- /app/scripts/modules/remotestorage/module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 Laverna project Authors. 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | */ 8 | /* global define */ 9 | define([ 10 | 'underscore', 11 | 'backbone.radio', 12 | 'marionette', 13 | 'modules', 14 | 'modules/remotestorage/classes/sync', 15 | ], (_, Radio, Marionette, Modules, Sync) => { 16 | 'use strict'; 17 | 18 | const RemoteStorage = Modules.module('RemoteStorage', {}); 19 | 20 | /** 21 | * Initializers & finalizers of the module 22 | */ 23 | RemoteStorage.on('start', () => { 24 | console.info('RemoteStorage started'); 25 | new Sync(); 26 | }); 27 | 28 | RemoteStorage.on('stop', () => { 29 | }); 30 | 31 | // Add a global module initializer 32 | Radio.request('init', 'add', 'module', () => { 33 | RemoteStorage.start(); 34 | }); 35 | 36 | return RemoteStorage; 37 | }); 38 | -------------------------------------------------------------------------------- /app/scripts/templates/layout.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 |
    9 | 10 |
    11 |
    12 |
    13 |
    14 |
    15 | 16 |
    17 | -------------------------------------------------------------------------------- /app/scripts/templates/loader.html: -------------------------------------------------------------------------------- 1 |

    2 | 3 |

    4 | -------------------------------------------------------------------------------- /app/scripts/utils/electronListener.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module utils/electronListener 3 | * @license MPL-2.0 4 | */ 5 | import Radio from 'backbone.radio'; 6 | 7 | /** 8 | * Listen to events sent by the main Electron process. 9 | */ 10 | function electronListener() { 11 | // Do nothing if it isn't Electron environment 12 | if (!window.electron) { 13 | return false; 14 | } 15 | 16 | const urlChannel = Radio.channel('utils/Url'); 17 | const {ipcRenderer} = window.electron; 18 | 19 | ipcRenderer.on('lav:settings', () => { 20 | urlChannel.request('navigate', {url: '/settings'}); 21 | }); 22 | 23 | ipcRenderer.on('lav:newNote', () => { 24 | urlChannel.request('navigate', {url: '/notes/add'}); 25 | }); 26 | 27 | ipcRenderer.on('lav:about', () => { 28 | Radio.request('components/help', 'showAbout'); 29 | }); 30 | 31 | ipcRenderer.on('lav:import:evernote', (e, data) => { 32 | Radio.request('components/importExport', 'importEvernote', data); 33 | }); 34 | 35 | ipcRenderer.on('lav:backup:key', () => { 36 | Radio.request('components/importExport', 'export', {exportKey: true}); 37 | }); 38 | 39 | ipcRenderer.on('lav:backup:data', () => { 40 | Radio.request('components/importExport', 'export'); 41 | }); 42 | 43 | return true; 44 | } 45 | 46 | Radio.once('App', 'init', () => { 47 | Radio.request('utils/Initializer', 'add', { 48 | name : 'App:utils', 49 | callback: electronListener, 50 | }); 51 | }); 52 | 53 | export default electronListener; 54 | -------------------------------------------------------------------------------- /app/scripts/utils/fileSaver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module utils/fileSaver 3 | * @license MPL-2.0 4 | */ 5 | import {saveAs} from 'file-saver'; 6 | 7 | /** 8 | * Save a file in Cordova environment. 9 | * 10 | * @param {String} content 11 | * @param {String} fileName 12 | * @param {Function} resolve 13 | */ 14 | function cordovaSave(content, fileName, resolve) { 15 | const externalDir = window.cordova.file.externalDataDirectory; 16 | 17 | window.resolveLocalFileSystemURL(externalDir, dir => { 18 | dir.getFile(fileName, {create: true}, file => { 19 | file.createWriter(writer => { 20 | writer.write(content); 21 | resolve(); 22 | }); 23 | }); 24 | }); 25 | } 26 | 27 | /** 28 | * Save a file. 29 | * 30 | * @param {String} content 31 | * @param {String} fileName 32 | * @returns {Promise} 33 | */ 34 | function fileSaver(content, fileName) { 35 | if (window.cordova) { 36 | return new Promise(resolve => cordovaSave(content, fileName, resolve)); 37 | } 38 | 39 | // Use HTML5 saveAs 40 | return Promise.resolve(saveAs(content, fileName)); 41 | } 42 | 43 | export {fileSaver as default, cordovaSave}; 44 | -------------------------------------------------------------------------------- /app/scripts/utils/theme.js: -------------------------------------------------------------------------------- 1 | import Radio from 'backbone.radio'; 2 | 3 | const theme = { 4 | /** 5 | * Apply a theme. 6 | * 7 | * @param {Object} data 8 | * @param {String} data.name - theme name 9 | */ 10 | applyTheme(data = {}) { 11 | const theme = data.name || Radio.request('collections/Configs', 'findConfig', { 12 | name: 'theme', 13 | }); 14 | $('#lav--theme').attr('href', `styles/theme-${theme || 'default'}.css`); 15 | }, 16 | 17 | /** 18 | * Initializer. 19 | */ 20 | initializer() { 21 | Radio.on('components/settings', 'changeTheme', theme.applyTheme); 22 | theme.applyTheme(); 23 | }, 24 | }; 25 | 26 | Radio.once('App', 'init', () => { 27 | Radio.request('utils/Initializer', 'add', { 28 | name : 'App:utils', 29 | callback: theme.initializer, 30 | }); 31 | }); 32 | 33 | export default theme; 34 | -------------------------------------------------------------------------------- /app/scripts/views/Brand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module views/Brand 3 | */ 4 | import Mn from 'backbone.marionette'; 5 | 6 | /** 7 | * The region that shows its content on fullscreen and on green background. 8 | * 9 | * @class 10 | * @extends Marionette.Region 11 | * @license MPL-2.0 12 | */ 13 | export default class Brand extends Mn.Region { 14 | 15 | /** 16 | * Show the region with animation. 17 | */ 18 | onShow() { 19 | this.$el.slideDown('fast'); 20 | } 21 | 22 | /** 23 | * Hide the region with animation. 24 | */ 25 | onEmpty() { 26 | this.$el.slideUp('fast'); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/scripts/views/Loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module views/Loader 3 | */ 4 | import Mn from 'backbone.marionette'; 5 | import _ from 'underscore'; 6 | 7 | /** 8 | * A view that shows a spining icon. 9 | * 10 | * @class 11 | * @extends Marionette.View 12 | * @license MPL-2.0 13 | */ 14 | export default class Loader extends Mn.View { 15 | 16 | get template() { 17 | const tmpl = require('../templates/loader.html'); 18 | return _.template(tmpl); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/styles/core/codemirror.less: -------------------------------------------------------------------------------- 1 | @import 'codemirror/core.less'; 2 | @import 'codemirror/theme.less'; 3 | -------------------------------------------------------------------------------- /app/styles/core/codemirror/theme.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Default theme for Codemirror. 3 | */ 4 | 5 | /** 6 | * Headers 7 | */ 8 | .cm-s-default .cm-header { 9 | color: #000; 10 | } 11 | .cm-s-default .cm-header-1 { 12 | &:extend(h1); 13 | line-height: floor((@font-size-h1 * @line-height-base)); 14 | } 15 | .cm-s-default .cm-header-2 { 16 | &:extend(h2); 17 | line-height: floor((@font-size-h2 * @line-height-base)); 18 | } 19 | .cm-s-default .cm-header-3 { 20 | &:extend(h3); 21 | line-height: floor((@font-size-h3 * @line-height-base)); 22 | } 23 | .cm-s-default .cm-header-4 { 24 | &:extend(h4); 25 | line-height: floor((@font-size-h4 * @line-height-base)); 26 | } 27 | .cm-s-default .cm-header-5 { 28 | &:extend(h5); 29 | line-height: floor((@font-size-h5 * @line-height-base)); 30 | } 31 | .cm-s-default .cm-header-6 { 32 | &:extend(h6); 33 | line-height: floor((@font-size-h6 * @line-height-base)); 34 | } 35 | 36 | /** 37 | * Other styles 38 | */ 39 | .CodeMirror-composing { 40 | border-bottom: 2px solid; 41 | } 42 | 43 | .cm-header, 44 | .cm-strong:extend(strong) {} 45 | 46 | .cm-em { 47 | font-style: italic; 48 | } 49 | 50 | .cm-strikethrough { 51 | text-decoration: line-through; 52 | } 53 | 54 | .cm-link { 55 | text-decoration: underline; 56 | } 57 | 58 | /** 59 | * Default styles for common addons 60 | */ 61 | div.CodeMirror span.CodeMirror-matchingbracket { 62 | color: #0f0; 63 | } 64 | div.CodeMirror span.CodeMirror-nonmatchingbracket { 65 | color: #f22; 66 | } 67 | .CodeMirror-matchingtag { 68 | background: rgba(255, 150, 0, .3); 69 | } 70 | .CodeMirror-activeline-background { 71 | background: #e8f2ff; 72 | } 73 | -------------------------------------------------------------------------------- /app/styles/core/fontello.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Fontello 3 | */ 4 | @import 'fontello/css/fontello.less'; 5 | @import 'fontello/css/fontello-codes.less'; 6 | // @import 'fontello/css/fontello-ie7-codes.less'; 7 | @import 'fontello/css/fontello-embedded.less'; 8 | // @import 'fontello/css/fontello-ie7.less'; 9 | @import 'fontello/css/animation.less'; 10 | 11 | .icons { 12 | font-family: "fontello"; 13 | } 14 | .icon--block { 15 | display: block; 16 | } 17 | -------------------------------------------------------------------------------- /app/styles/core/fontello/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font license info 2 | 3 | 4 | ## Font Awesome 5 | 6 | Copyright (C) 2012 by Dave Gandy 7 | 8 | Author: Dave Gandy 9 | License: SIL () 10 | Homepage: http://fortawesome.github.com/Font-Awesome/ 11 | 12 | 13 | ## Entypo 14 | 15 | Copyright (C) 2012 by Daniel Bruce 16 | 17 | Author: Daniel Bruce 18 | License: SIL (http://scripts.sil.org/OFL) 19 | Homepage: http://www.entypo.com 20 | 21 | 22 | ## Elusive 23 | 24 | Copyright (C) 2013 by Aristeides Stathopoulos 25 | 26 | Author: Aristeides Stathopoulos 27 | License: SIL (http://scripts.sil.org/OFL) 28 | Homepage: http://aristeides.com/ 29 | 30 | 31 | ## Modern Pictograms 32 | 33 | Copyright (c) 2012 by John Caserta. All rights reserved. 34 | 35 | Author: John Caserta 36 | License: SIL (http://scripts.sil.org/OFL) 37 | Homepage: http://thedesignoffice.org/project/modern-pictograms/ 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/styles/core/fontello/font/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/styles/core/fontello/font/fontello.eot -------------------------------------------------------------------------------- /app/styles/core/fontello/font/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/styles/core/fontello/font/fontello.ttf -------------------------------------------------------------------------------- /app/styles/core/fontello/font/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daed/laverna/592e89d32120200e33cde5bc04a9a7fcff400001/app/styles/core/fontello/font/fontello.woff -------------------------------------------------------------------------------- /app/styles/core/fuzzy.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Fuzzy search 3 | */ 4 | // Hide other content in sidebar while Fuzzy search is active 5 | .-fuzzy #sidebar--content { 6 | display: none; 7 | } 8 | -------------------------------------------------------------------------------- /app/styles/core/list.less: -------------------------------------------------------------------------------- 1 | /** 2 | * List module 3 | */ 4 | .list { 5 | } 6 | 7 | .list--group { 8 | margin-bottom: 0; 9 | position: relative; 10 | 11 | &:last-child .list--item { 12 | border-bottom-width: 1px; 13 | } 14 | } 15 | 16 | .list--item { 17 | border-right-width : 0; 18 | border-bottom-width : 0; 19 | } 20 | 21 | .list--item.-note { 22 | min-height: 60px; 23 | max-height: 60px; 24 | } 25 | 26 | .list--item--title.-note { 27 | overflow : hidden; 28 | white-space : nowrap; 29 | width : 90%; 30 | &:extend(.pull-left); 31 | } 32 | 33 | .list--item--favorite { 34 | width: 7%; 35 | &:extend(.pull-right); 36 | } 37 | 38 | .list--item--text { 39 | max-width: 80%; 40 | &:extend(.-text-elipsis); 41 | } 42 | 43 | // Button groups 44 | .list--buttons { 45 | position : absolute; 46 | top : 0; 47 | right : 4px; 48 | padding : 8px 4px 8px 0; 49 | } 50 | 51 | // Navbars 52 | .list--navbar { 53 | z-index : inherit; 54 | margin-bottom : 0; 55 | padding-top : 20px; 56 | } 57 | .list--nav { 58 | margin-right: 2px; 59 | } 60 | 61 | // Pagination bar 62 | .list--pager { 63 | margin-top : 15px; 64 | margin-left : 10px; 65 | margin-right : 10px; 66 | } 67 | -------------------------------------------------------------------------------- /app/styles/core/main.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Core styles. 3 | */ 4 | 5 | // Fontello 6 | @import 'fontello.less'; 7 | 8 | // Import Bootstrap 9 | @import 'bootstrap.less'; 10 | @import 'variables.less'; 11 | 12 | // Navbars 13 | @import 'header.less'; 14 | @import 'sidemenu.less'; 15 | 16 | // Sidebar 17 | @import 'list.less'; 18 | @import 'fuzzy.less'; 19 | 20 | // Editor 21 | @import 'codemirror.less'; 22 | @import 'editor.less'; 23 | 24 | // Core 25 | @import 'layout.less'; 26 | @import 'utils.less'; 27 | @import 'responsive.less'; 28 | 29 | // RemoteStorage styles 30 | #remotestorage-widget { 31 | top : auto !important; 32 | right : 15px !important; 33 | bottom : 80px !important; 34 | 35 | &.remotestorage-state-connected { 36 | bottom: 40px !important; 37 | } 38 | } 39 | 40 | .container--white { 41 | background-color: #FFF; 42 | border-radius: 4px; 43 | color: darken(#FFF, 70%); 44 | > .row { 45 | padding: 30px 10px; 46 | } 47 | .input--brand { 48 | color: darken(#FFF, 60%); 49 | &::placeholder { 50 | color: darken(#FFF, 40%); 51 | } 52 | &:hover { 53 | border-color: darken(#FFF, 10%); 54 | } 55 | &:focus, 56 | &:active { 57 | border-color: darken(#FFF, 50%); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/styles/core/responsive.less: -------------------------------------------------------------------------------- 1 | .make-visible(@resolution) { 2 | @media (max-width: @resolution) { 3 | .responsive-visibility(); 4 | } 5 | } 6 | 7 | .visible-500, 8 | .visible-400 { 9 | .responsive-invisibility(); 10 | } 11 | 12 | .visible-500 { 13 | .make-visible(500px); 14 | } 15 | .visible-400 { 16 | .make-visible(400px); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /app/styles/core/sidemenu.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Side menu 3 | */ 4 | .sidemenu { 5 | .make-xs-column(8); 6 | .make-md-column(3); 7 | .make-sm-column(3); 8 | .make-lg-column(3); 9 | } 10 | .sidemenu { 11 | background-color: #fff; 12 | 13 | // Position and z-index 14 | position : fixed; 15 | top : 0; 16 | bottom : 0; 17 | z-index : @zindex--sidemenu; 18 | 19 | // Size 20 | height : 100%; 21 | padding : 0; 22 | 23 | // Show scrollbars 24 | overflow-y : auto; 25 | overflow-x : hidden; 26 | 27 | // Slide open from left 28 | visibility : hidden; 29 | transition : all 0.3s ease; 30 | .make-xs-column-push(-8); 31 | .make-md-column-push(-3); 32 | .make-sm-column-push(-3); 33 | .make-lg-column-push(-3); 34 | 35 | &.-show { 36 | visibility: visible; 37 | left: 0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/styles/core/utils.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility classes 3 | */ 4 | // Show scrollbars 5 | .-noscroll { 6 | overflow: hidden; 7 | } 8 | .-scroll { 9 | overflow: hidden; 10 | overflow-y: scroll; 11 | } 12 | .-scroll-x { 13 | overflow-x: scroll; 14 | } 15 | .-relative { 16 | // position: relative; 17 | } 18 | 19 | // Hide text that doesn't fit 20 | .-text-elipsis { 21 | overflow: hidden; 22 | white-space: nowrap; 23 | text-overflow: ellipsis; 24 | } 25 | 26 | // Default view for all tables inside of notes 27 | .layout--body.-note, 28 | .layout--body.-form { 29 | table { 30 | &:extend(.table all); 31 | &:extend(.table-bordered all); 32 | &:extend(.table-striped all); 33 | &:extend(.table-hover all); 34 | width: auto; 35 | } 36 | img { 37 | &:extend(.img-responsive); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/styles/core/variables.less: -------------------------------------------------------------------------------- 1 | /** 2 | */ 3 | // Basic variables 4 | @grid-float-breakpoint: 0; 5 | @border-radius-base: 3px; 6 | 7 | // List group 8 | @list-group-border-radius: 0; 9 | 10 | // Z-indexes 11 | @zindex--backdrop: @zindex-navbar-fixed + 1; 12 | @zindex--sidemenu: @zindex-navbar-fixed + 2; 13 | -------------------------------------------------------------------------------- /app/styles/theme-dark/main.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Default theme 3 | */ 4 | @import "bootstrap/less/variables.less"; 5 | @import "bootstrap/less/mixins.less"; 6 | 7 | // Variables 8 | @import '../theme-default/variables.less'; 9 | 10 | // Core 11 | @import '../theme-default/layout.less'; 12 | @import '../theme-default/utils.less'; 13 | @import '../theme-default/forms.less'; 14 | @import '../theme-default/modal.less'; 15 | @import '../theme-default/buttons.less'; 16 | @import '../theme-default/checkbox.less'; 17 | 18 | // Navbars 19 | @import '../theme-default/header.less'; 20 | @import '../theme-default/sidemenu.less'; 21 | 22 | // List of notes, notebooks, etc. 23 | @import '../theme-default/list.less'; 24 | 25 | // Settings 26 | @import '../theme-default/settings.less'; 27 | 28 | // Codemirror editor 29 | @import '../theme-default/editor.less'; 30 | @import '../theme-default/codemirror.less'; 31 | @import '../theme-default/dropzone.less'; 32 | 33 | // Loading animation 34 | @import '../theme-default/loading-animation.less'; 35 | 36 | @import 'prism.less'; 37 | @import 'theme.less'; 38 | -------------------------------------------------------------------------------- /app/styles/theme-default/buttons.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Buttons 3 | */ 4 | .btn-default { 5 | border-color: @btn--border--color; 6 | } 7 | 8 | // Success button 9 | .btn.btn-success { 10 | background-color : @btn--success; 11 | border-color : @btn--success--border; 12 | &:hover, 13 | &:focus { 14 | background-color: @btn--success--hover; 15 | } 16 | } 17 | 18 | /** 19 | * Change button styles in note view 20 | */ 21 | .btn.-note { 22 | background : @btn--note--bg; 23 | border : none; 24 | box-shadow : none; 25 | padding : 7px 15px; 26 | margin-right : 5px; 27 | &:hover { 28 | background: @btn--note--hover; 29 | } 30 | } 31 | 32 | /** 33 | * Favorite button 34 | */ 35 | .btn--favorite { 36 | color : @btn--favorite; 37 | font-size : 1em; 38 | padding : 0; 39 | &:hover, 40 | &:focus { 41 | outline : none; 42 | color : @btn--favorite--hover; 43 | } 44 | } 45 | 46 | /** 47 | * WYSIWYG buttons 48 | */ 49 | .btn--wysiwyg { 50 | border-radius : 0; 51 | transition : background .3s ease-in-out; 52 | 53 | &:hover { 54 | background : @pagedown--btn--hover--bg; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/styles/theme-default/checkbox.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Checkbox module 3 | */ 4 | .task--checkbox { 5 | position : relative; 6 | cursor : pointer; 7 | &:hover { 8 | color: lighten(@text-color, 15%); 9 | } 10 | } 11 | 12 | // Text 13 | .checkbox--text { 14 | margin-left: 10px; 15 | font-weight: 300; 16 | 17 | // Create checkbox border block 18 | &::before { 19 | content : ' '; 20 | width : @check--size; 21 | height : @check--size; 22 | 23 | // Borders 24 | border : @check--border; 25 | border-radius : @check--border-radius; 26 | 27 | // Position 28 | position : absolute; 29 | left : 0; 30 | top : 3px; 31 | } 32 | } 33 | 34 | .checkbox--svg { 35 | width : @check--icon--size; 36 | height : @check--icon--size; 37 | position : absolute; 38 | top : 6px; 39 | left : 3px; 40 | } 41 | 42 | .checkbox--path { 43 | fill : none; 44 | stroke : @check--color; 45 | stroke-width : 20px; 46 | stroke-linecap : round; 47 | 48 | stroke-dasharray : 126.366, 126.366; 49 | stroke-dashoffset : 126.6; 50 | transition : stroke-dashoffset 0.2s ease-in-out 0s; 51 | stroke-linejoin : round; 52 | } 53 | 54 | .checkbox--input { 55 | opacity: 0; 56 | 57 | &:checked { 58 | & ~ .checkbox--text { 59 | text-decoration: line-through; 60 | } 61 | & + svg path { 62 | stroke-dashoffset: 0; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/styles/theme-default/codemirror.less: -------------------------------------------------------------------------------- 1 | .CodeMirror { 2 | background : white; 3 | } 4 | 5 | .cm-link { 6 | color: @gray; 7 | } 8 | .cm-url { 9 | color: @gray-light; 10 | } 11 | 12 | .cm-comment { 13 | background-color: lighten(@gray-lighter, 2); 14 | } 15 | 16 | .cm-quote { 17 | color: @gray-light; 18 | } 19 | -------------------------------------------------------------------------------- /app/styles/theme-default/dropzone.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Dropzone styles. 3 | */ 4 | .dropzone--container { 5 | margin: 20px auto; 6 | border: 10px dashed lighten(#ccc, 8%); 7 | } 8 | 9 | .dropzone { 10 | min-height : 100px; 11 | cursor : pointer; 12 | padding : 20px; 13 | 14 | .dz-default.dz-message { 15 | margin-bottom : 16px; 16 | text-align : center; 17 | } 18 | 19 | .dz-details { 20 | text-align: center; 21 | } 22 | 23 | .dz-preview { 24 | margin-right : 5px; 25 | margin-bottom : 5px; 26 | padding : 4px; 27 | } 28 | 29 | .dz-image img { 30 | border-radius : 5px; 31 | cursor : auto; 32 | } 33 | 34 | .dz-filename { 35 | word-break: break-all; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/styles/theme-default/header.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Header module 3 | */ 4 | .header--bg(@bg, @shadow, @border, @color) { 5 | background : @bg; 6 | box-shadow : @shadow; 7 | border : @border; 8 | color : @color; 9 | 10 | .header--title { 11 | &:hover, 12 | &:focus { 13 | background-color: darken(@bg, 5%); 14 | } 15 | } 16 | 17 | .navbar-nav > li > a, 18 | .navbar-btn, 19 | .navbar-link, 20 | .navbar-text, 21 | .btn-link { 22 | color : @color; 23 | &:hover, 24 | &:focus { 25 | color : @color; 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * Left Navbar 32 | */ 33 | .header.-left { 34 | .header--bg(@header--left--bg, @header--left--shadow, @header--left--border, @header--left--color); 35 | } 36 | .header--container.-left { 37 | padding-left: 0; 38 | } 39 | .header--search { 40 | padding-right: 0; 41 | } 42 | 43 | /** 44 | * Right Navbar 45 | */ 46 | .header.-right, 47 | .header.-note { 48 | .header--bg(@header--right--bg, @header--right--shadow, @header--right--border, @header--right--color); 49 | } 50 | 51 | /** 52 | * The main navbar in note form, pagedown 53 | */ 54 | .header.-form { 55 | .header--bg(@header--right--bg, @header--right--shadow, @header--right--border, @header--right--color); 56 | } 57 | -------------------------------------------------------------------------------- /app/styles/theme-default/list.less: -------------------------------------------------------------------------------- 1 | /** 2 | * List module 3 | */ 4 | 5 | .list--item { 6 | border-color: @list--border--color; 7 | 8 | &.active, 9 | &.active:hover, 10 | &.active:focus { 11 | background-color : @list--active--bg; 12 | border-color : @list--active--border; 13 | box-shadow : none; 14 | } 15 | } 16 | 17 | // List of notes 18 | .list--item.-note { 19 | padding-top : 25px; 20 | padding-bottom : 15px; 21 | max-height : 80px; 22 | min-height : 80px; 23 | } 24 | 25 | // Pagination buttons 26 | .list--pager--btn { 27 | padding : 15px; 28 | background : transparent; 29 | } 30 | 31 | // List of notebooks 32 | .list--nested { 33 | padding-left: 30px; 34 | 35 | .list--item { 36 | border-left-color: transparent; 37 | margin-left: -30px * 4; 38 | padding-left: 30px * 4; 39 | &:last-child { 40 | border-bottom-color: transparent; 41 | } 42 | } 43 | } 44 | 45 | // Navbars 46 | .list--navbar { 47 | color : @list--navbar--color; 48 | background : @list--navbar--bg; 49 | } 50 | .list--brand { 51 | font-variant : small-caps; 52 | color : @list--navbar--brand; 53 | } 54 | -------------------------------------------------------------------------------- /app/styles/theme-default/main.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Default theme 3 | */ 4 | @import "bootstrap/less/variables.less"; 5 | @import "bootstrap/less/mixins.less"; 6 | 7 | // Variables 8 | @import 'variables.less'; 9 | 10 | // Core 11 | @import 'layout.less'; 12 | @import 'utils.less'; 13 | @import 'forms.less'; 14 | @import 'modal.less'; 15 | @import 'buttons.less'; 16 | @import 'checkbox.less'; 17 | 18 | // Navbars 19 | @import 'header.less'; 20 | @import 'sidemenu.less'; 21 | 22 | // List of notes, notebooks, etc. 23 | @import 'list.less'; 24 | 25 | // Settings 26 | @import 'settings.less'; 27 | 28 | // Codemirror editor 29 | @import 'editor.less'; 30 | @import 'codemirror.less'; 31 | @import 'dropzone.less'; 32 | 33 | @import 'prism.less'; 34 | 35 | // Loading animation 36 | @import 'loading-animation.less'; 37 | -------------------------------------------------------------------------------- /app/styles/theme-default/modal.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Modal windows 3 | */ 4 | .modal--body.modal-body { 5 | padding: 0 15px; 6 | } 7 | 8 | .modal--group.form-group { 9 | border-bottom: 1px solid #e5e5e5; 10 | overflow: hidden; 11 | margin-left : -30px; 12 | margin-right : -30px; 13 | margin-bottom : 0; 14 | padding-top : 5px; 15 | padding-bottom : 5px; 16 | &:last-child { 17 | border-bottom-width: 0; 18 | } 19 | } 20 | 21 | .modal--input { 22 | border-width: 0; 23 | 24 | &, &:hover, &:focus { 25 | box-shadow: none; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/styles/theme-default/settings.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Settings 3 | */ 4 | // List of settings 5 | .list--settings { 6 | &:extend(.sidemenu--item all); 7 | 8 | &.list-group-item.active { 9 | background : lighten(@sidemenu--item--hover, 2%); 10 | } 11 | } 12 | 13 | // Back button 14 | .header--settings--back:hover { 15 | background: #f8f8f8; 16 | } 17 | 18 | // Content 19 | .container-fluid.-settings { 20 | padding: 40px 30px; 21 | } 22 | -------------------------------------------------------------------------------- /app/styles/theme-default/sidemenu.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Sidemenu 3 | */ 4 | .sidemenu { 5 | background: @sidemenu--bg; 6 | } 7 | 8 | .sidemenu--item.list-group-item { 9 | // Sizing 10 | min-height : 50px; 11 | max-height : 50px; 12 | padding-left : 20px; 13 | padding-top : 15px; 14 | 15 | // Colors 16 | background : @sidemenu--item; 17 | color : @sidemenu--item--color; 18 | border : @sidemenu--item--border; 19 | 20 | &.active, 21 | &:hover { 22 | background : @sidemenu--item--hover; 23 | color : @sidemenu--item--hover--color; 24 | } 25 | } 26 | 27 | .sidemenu--item.-disabled, 28 | .sidemenu--item.-disabled:hover { 29 | background : @sidemenu--disabled; 30 | color : @sidemenu--disabled--color; 31 | font-size : 1.3em; 32 | } 33 | 34 | .sidemenu--item.list-child-item { 35 | padding-top : 0px; 36 | padding-bottom : 0px; 37 | min-height : 0px; 38 | } 39 | 40 | .label-branch { 41 | letter-spacing: -0.11em; 42 | } 43 | .label-branch-last { 44 | .label-branch; 45 | } 46 | 47 | /* Center right pipe symbol (verticle box middle right) */ 48 | .label-branch:before { 49 | content: "\251C"; 50 | /* line up with dashes */ 51 | font-size: 10pt; 52 | } 53 | 54 | /* Bottom right pipe symbol (verticle box right) */ 55 | .label-branch-last:before { 56 | content: "\2514"; 57 | /* line up with dashes */ 58 | font-size: 10pt; 59 | position: relative; 60 | top: 0.04em; 61 | } 62 | 63 | .label-branch-indent { 64 | display: inline-block; 65 | } 66 | 67 | 68 | .sidemenu--close { 69 | cursor: pointer; 70 | } 71 | -------------------------------------------------------------------------------- /app/styles/theme-default/utils.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Utils 3 | */ 4 | a, 5 | .btn-link { 6 | color: darken(@color--brand, 5%); 7 | &:hover { 8 | color: lighten(@color--brand, 5%); 9 | } 10 | } 11 | 12 | .-text-big { 13 | font-size : 1.3em; 14 | line-height : 2em; 15 | } 16 | 17 | // Progress bars 18 | .progress-bar-success { 19 | background-color: @color--brand; 20 | } 21 | .progress-bar-info { 22 | background-color: @color--info; 23 | } 24 | -------------------------------------------------------------------------------- /app/styles/themes.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": "Default theme", 3 | "dark": "Dark" 4 | } 5 | -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Laverna 4 | 5 | Open source note taking application 6 | 7 | 8 | Laverna project 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /electron/electron.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | 3 | // TODO: reference the config file for this. 4 | port = 9000; 5 | 6 | const {app, BrowserWindow} = require('electron'); 7 | const laverna = require('./server.js')(port); 8 | //const {sigApp, sigServer} = require('./laverna-server/server.js'); 9 | 10 | //laverna.on("listening", function () { laverna.close(); }); 11 | 12 | function createWindow () { 13 | let win = new BrowserWindow({ 14 | width: 925, 15 | height: 800, 16 | backgroundColor: '#259D7A', 17 | minWidth: 925, 18 | // everything was working fine without the below line. Suddenly we needed it. 19 | // I cannot for the life of me figure out why. 20 | webPreferences: { nodeIntegration: false } 21 | }); 22 | //win.loadFile('dist/index.html'); 23 | win.loadURL("http://localhost:" + port); 24 | win.webContents.openDevTools(); 25 | // Emitted when the window is closed. 26 | win.on('closed', () => { 27 | // Dereference the window object, usually you would store windows 28 | // in an array if your app supports multi windows, this is the time 29 | // when you should delete the corresponding element. 30 | laverna.close(); 31 | //sigServer.close(); 32 | //sigApp.quit(); 33 | win = null; 34 | }); 35 | } 36 | 37 | app.on('ready', createWindow); 38 | app.on('window-all-closed', () => { 39 | app.quit(); 40 | }); 41 | -------------------------------------------------------------------------------- /electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laverna", 3 | "version": "1.0.0-beta.3", 4 | "description": "", 5 | "main": "electron.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "finalhandler": "^1.1.1", 14 | "serve-static": "^1.13.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /electron/server.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var finalhandler = require('finalhandler'), 5 | http = require('http'), 6 | serveStatic = require('serve-static'), 7 | serve, 8 | server; 9 | 10 | serve = serveStatic(__dirname + '/dist', {index: ['index.html']}); 11 | 12 | server = http.createServer(function(req, res) { 13 | var done = finalhandler(req, res); 14 | serve(req, res, done); 15 | }); 16 | 17 | module.exports = function(port) { 18 | console.log('Server is running on port: ' + port); 19 | return server.listen(port); 20 | }; -------------------------------------------------------------------------------- /gulps/bundle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpackStream = require('webpack-stream'); 4 | const webpack = require('webpack'); 5 | 6 | /** 7 | * @file Bundle JS files. 8 | * @example gulp bundle // Bundle JS files 9 | * @example gulp bundle --prod // Bundle JS for production (minify, uglify) 10 | * @returns {Function} 11 | */ 12 | module.exports = (gulp, $) => { 13 | 14 | // Use a different config file for production 15 | let webpackConfig = ($.util.env.prod ? 'production.config' : 'config.js'); 16 | webpackConfig = require(`../webpack.${webpackConfig}`); 17 | 18 | return cb => { 19 | const isBuild = process.argv[2] === 'build'; 20 | let called = false; 21 | 22 | // Watch for changes unless we're building the project 23 | if (!isBuild) { 24 | webpackConfig.watch = true; 25 | } 26 | 27 | return gulp.src('app/scripts/main.js') 28 | .pipe(webpackStream(webpackConfig, webpack, () => { 29 | $.browserSync.stream(); 30 | 31 | if (!called && !isBuild) { 32 | called = true; 33 | cb(); 34 | } 35 | })) 36 | .pipe(gulp.dest(`${$.distDir}/scripts/`)) 37 | .pipe($.browserSync.stream()); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /gulps/clean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @file Tasks for cleaning up. 5 | * @example gulp clean:dist // Clean dist directory 6 | * @example gulp clean:release // Clean release directory 7 | */ 8 | module.exports = function(gulp, $) { 9 | 10 | gulp.task('clean:dist', () => $.del([`${$.distDir}/**/*`])); 11 | 12 | gulp.task('clean:release', () => $.del(['./release/*'])); 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /gulps/copy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const merge = require('merge-stream'); 3 | 4 | /** 5 | * @file Tasks for copying static files to dist directory. 6 | * @example gulp copy // Copy static files 7 | */ 8 | module.exports = (gulp, $) => { 9 | return () => { 10 | return merge.apply(merge, [ 11 | gulp.src([ 12 | './LICENSE', 13 | ], {base: './'}) 14 | .pipe(gulp.dest($.distDir)), 15 | 16 | // Copy static files like images, locales, etc... 17 | gulp.src([ 18 | './app/images/**/*.+(png|jpg|gif|ico|icns)', 19 | './app/docs/**', 20 | './app/locales/**/*.json', 21 | './app/.htaccess', 22 | './app/*.+(xml|ico|txt|webapp)', 23 | './app/styles/**/*.+(eot|svg|ttf|woff)', 24 | ], {base: './app'}) 25 | .pipe(gulp.dest($.distDir)), 26 | 27 | gulp.src([ 28 | './node_modules/openpgp/dist/openpgp.worker.js', 29 | './node_modules/openpgp/dist/openpgp.js', 30 | ]).pipe(gulp.dest(`${$.distDir}/scripts`)), 31 | ]); 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /gulps/copyDist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(gulp) { 4 | return function() { 5 | return gulp.src([ 6 | './dist/**', 7 | ], {base: './'}) 8 | .pipe(gulp.dest('./release/laverna/')); 9 | }; 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /gulps/copyRelease.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(gulp, plugins) { 4 | return function() { 5 | /*return gulp.src([ 6 | './preload.js', 7 | './server.js', 8 | './electron.js', 9 | './package-lock.json', 10 | './npm-shrinkwrap.json', 11 | './package.json', 12 | ], {base: './'}) 13 | .pipe(gulp.dest('./release/laverna'));*/ 14 | return gulp.src([ 15 | './release', 16 | ]) 17 | .pipe(plugins.shell( 18 | 'cp ./electron/electron.js ./electron/package-lock.json ./electron/package.json ./electron/server.js release/laverna' 19 | )); 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /gulps/css.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | /** 6 | * @file Create CSS related Gulp tasks. 7 | * 8 | * @example gulp css // bundle CSS files 9 | * @example gulp cssmin // minify CSS for production 10 | */ 11 | module.exports = (gulp, $) => { 12 | 13 | gulp.task('css', () => { 14 | return gulp.src('./app/styles/*/main.less') 15 | .pipe($.less({ 16 | paths: [ 17 | path.join(__dirname, 'less', 'includes'), 18 | './node_modules/', 19 | ], 20 | })) 21 | // Unique names for theme files 22 | .pipe($.rename(pathF => { 23 | const pathNew = pathF; 24 | pathNew.basename = pathNew.dirname; 25 | pathNew.dirname = ''; 26 | return pathNew; 27 | })) 28 | // Autoprefixer 29 | .pipe($.autoprefixer({ 30 | browsers : ['> 5%'], 31 | cascade : false, 32 | remove : false, 33 | add : true, 34 | })) 35 | .pipe(gulp.dest(`${$.distDir}/styles/`)) 36 | .pipe($.browserSync.stream()); 37 | }); 38 | 39 | gulp.task('cssmin', () => { 40 | return gulp.src(`${$.distDir}/styles/*.css`) 41 | .pipe($.cleanCss({ 42 | compatibility: 'ie8', 43 | })) 44 | .pipe(gulp.dest(`${$.distDir}/styles`)); 45 | }); 46 | 47 | }; 48 | -------------------------------------------------------------------------------- /gulps/lav-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(gulp, plugins) { 4 | 5 | gulp.task('lav-server', function() { 6 | return gulp.src('./release') 7 | .pipe(plugins.shell( 8 | 'cd ./release/laverna && curl -o laverna-server.tar.gz https://codeload.github.com/daed/laverna-server/tar.gz/server-1.0.0 && gzip -d laverna-server.tar.gz && tar xvf laverna-server.tar && rm -rf laverna-server.tar && mv laverna-server-server-1.0.0 laverna-server && cd ../' 9 | )); 10 | }); 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /gulps/lint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @file Lint tasks. 5 | * 6 | * @example gulp eslint // JavaScript linter 7 | * @example gulp jsonlint // JSON linter 8 | * @example gulp lint // Run all linters 9 | */ 10 | module.exports = (gulp, $) => { 11 | gulp.task('eslint', $.shell.task(['npm run eslint'])); 12 | 13 | gulp.task('jsonlint', () => { 14 | return gulp.src([ 15 | 'app/manifest.webapp', 16 | 'bower.json', 17 | 'package.json', 18 | 'app/**/*.json', 19 | ]) 20 | .pipe($.jsonlint()) 21 | .pipe($.jsonlint.failAfterError()) 22 | .pipe($.jsonlint.reporter()); 23 | }); 24 | 25 | gulp.task('lint', ['jsonlint', 'eslint']); 26 | }; 27 | -------------------------------------------------------------------------------- /gulps/nightwatch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Run UI tests. 5 | * Example of running tests: 6 | * gulp nightwatch --env [test_settings profile] 7 | */ 8 | module.exports = function(gulp, plugins) { 9 | return function() { 10 | gulp.task('nightwatch', function() { 11 | return gulp.src('./test/spec-ui/test.js', {read: false}) 12 | .pipe(plugins.nightwatch({ 13 | configFile : './test/nightwatch.json', 14 | cliArgs : [ 15 | // '--test ' + './test/spec-ui/tests/apps/notes/show.js', 16 | '--env ' + (plugins.util.env.env || 'default') 17 | ] 18 | })) 19 | .once('error', function(err) { 20 | console.log('Error', err.toString()); 21 | process.exit(1); 22 | }); 23 | }); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /gulps/npm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(gulp, plugins) { 4 | 5 | gulp.task('npm:install', function() { 6 | return gulp.src('./release/laverna/package.json') 7 | .pipe(plugins.shell( 8 | 'cd ./release/laverna && npm install --production && cd ../../' 9 | )); 10 | }); 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /gulps/serve-hosted.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @file Live reload server. 5 | * 6 | * @example gulp serve // Starts live-reload server 7 | */ 8 | module.exports = (gulp, $) => { 9 | return () => { 10 | $.browserSync.init({ 11 | notify : false, 12 | open : !$.util.env.dev, 13 | server : $.util.env.root || $.distDir, 14 | port : $.util.env.port || 9001, 15 | ghostMode : $.util.env.ghostMode !== undefined, 16 | }); 17 | 18 | // Watch for changes in SASS and HTML 19 | gulp.watch('app/styles/**/*.less', ['css']); 20 | gulp.watch('app/*.html', ['html']); 21 | 22 | // Re-bundle if some new packages were installed 23 | gulp.watch('package.json', ['bundle']); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /gulps/serve.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @file Live reload server. 5 | * 6 | * @example gulp serve // Starts live-reload server 7 | */ 8 | module.exports = (gulp, $) => { 9 | return () => { 10 | $.browserSync.init({ 11 | notify : false, 12 | open : !$.util.env.dev, 13 | server : $.util.env.root || $.distDir, 14 | port : $.util.env.port || 9000, 15 | ghostMode : $.util.env.ghostMode !== undefined, 16 | }); 17 | 18 | // Watch for changes in SASS and HTML 19 | gulp.watch('app/styles/**/*.less', ['css']); 20 | gulp.watch('app/*.html', ['html']); 21 | 22 | // Re-bundle if some new packages were installed 23 | gulp.watch('package.json', ['bundle']); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /gulps/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @file Test related tasks. 5 | * @example gulp tape // Run Unit tests once 6 | * @example gulp test:run // Run Unit tests and linters once 7 | * @example gulp test // Run Unit tests and watch for changes 8 | * @example gulp cover:run // Generate coverage report once 9 | * @example gulp cover // Generate coverage report and watch for changes 10 | * @todo enable lint tests 11 | */ 12 | module.exports = function(gulp, $) { 13 | 14 | gulp.task('tape', $.shell.task([ 15 | 'babel-node ./test/tape/index.js | faucet', 16 | ])); 17 | 18 | gulp.task('tape:debug', $.shell.task([ 19 | 'set TAP_DIAG=1 && babel-node ./test/tape/index.js', 20 | ])); 21 | 22 | gulp.task('test:run', ['lint', 'tape']); 23 | 24 | gulp.task('test', ['test:run'], () => { 25 | gulp.watch(['app/scripts/**/*.js', 'test/tape/**/*.js'], ['tape']); 26 | }); 27 | 28 | gulp.task('cover:run', $.shell.task(['npm run cover'])); 29 | gulp.task('cover', ['cover:run'], () => { 30 | gulp.watch(['app/scripts/**/*.js', 'test/tape/**/*.js'], ['cover:run']); 31 | }); 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /test/spec-ui/commands/addNote.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | exports.command = function(item) { 3 | this 4 | .urlHash('notes/add') 5 | .expect.element('#editor--input--title').to.be.visible.before(50000); 6 | 7 | this 8 | .setValue('#editor--input--title', item.title) 9 | .expect.element('#editor--input--title').value.to.contain(item.title).before(2000); 10 | 11 | this 12 | .click('.CodeMirror-lines') 13 | .keys(item.content); 14 | 15 | if (item.notebook) { 16 | this 17 | .click('.addNotebook') 18 | .expect.element('.modal--input[name=name]').to.be.visible.before(1000); 19 | 20 | this 21 | .setValue('.modal--input[name=name]', [item.notebook, this.Keys.ENTER]) 22 | .pause(1000); 23 | } 24 | 25 | this 26 | .click('.editor--save') 27 | .expect.element('.layout--body.-form').not.to.be.present.before(2000); 28 | 29 | return this; 30 | }; 31 | -------------------------------------------------------------------------------- /test/spec-ui/commands/addNotebook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.command = function(item) { 4 | this 5 | .urlHash('notebooks/add') 6 | .expect.element('#modal input[name="name"]').to.be.present.before(5000); 7 | 8 | this.expect.element('#modal input[name="name"]').to.be.visible.before(5000); 9 | // this.expect.element('#modal .form-group').to.be.visible.before(5000); 10 | 11 | this.setValue('#modal input[name="name"]', item.name); 12 | 13 | this.perform(function(client, done) { 14 | client.execute(function(filter) { 15 | var ops = document.querySelectorAll('#modal select[name="parentId"] option'); 16 | for (var i = 0, len = ops.length; i < len; i++) { 17 | if (filter && ops[i].text.indexOf(filter) > -1) { 18 | document 19 | .querySelector('#modal select[name="parentId"]') 20 | .selectedIndex = ops[i].index; 21 | } 22 | } 23 | }, [item.parentId], function() { 24 | done(); 25 | }); 26 | }); 27 | 28 | this.keys(this.Keys.ENTER); 29 | this.expect.element('#modal input[name="name"]').to.be.not.present.before(5000); 30 | 31 | return this; 32 | }; 33 | -------------------------------------------------------------------------------- /test/spec-ui/commands/addTag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.command = function(item) { 4 | this 5 | .urlHash('notebooks') 6 | .pause(100) 7 | .urlHash('tags/add') 8 | .expect.element('#modal .form-group').to.be.present.before(5000); 9 | 10 | // this.expect.element('#modal .form-group').to.be.visible.before(5000); 11 | 12 | this.setValue('#modal input[name="name"]', [item.name, this.Keys.ENTER]); 13 | 14 | return this; 15 | }; 16 | -------------------------------------------------------------------------------- /test/spec-ui/commands/changeEncryption.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * This Nightwatch command changes encryption settings. 4 | */ 5 | exports.command = function(data) { 6 | this 7 | .urlHash('settings/encryption') 8 | .expect.element('.-tab-encryption').to.be.present.before(5000); 9 | 10 | this.getAttribute('input[name=encrypt]', 'checked', function(res) { 11 | if (data.use && res.value === null) { 12 | this.click('input[name="encrypt"]'); 13 | } 14 | }); 15 | 16 | this 17 | .clearValue('input[name="encryptPass"]') 18 | .setValue('input[name="encryptPass"]', data.password) 19 | .click('#randomize') 20 | .click('.settings--save') 21 | .pause(1000) 22 | .click('.settings--cancel'); 23 | 24 | return this; 25 | }; 26 | -------------------------------------------------------------------------------- /test/spec-ui/commands/closeWelcome.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.command = function() { 4 | this 5 | .urlHash('/notes') 6 | .expect.element('#welcome--page').to.be.present.before(100000); 7 | 8 | this 9 | .expect.element('.modal-header .close').to.be.present.before(5000); 10 | 11 | this 12 | .click('.modal-header .close') 13 | .keys(this.Keys.ESCAPE) 14 | .expect.element('#welcome--page').not.to.be.present.before(10000); 15 | 16 | return this; 17 | }; 18 | -------------------------------------------------------------------------------- /test/spec-ui/commands/findAll.js: -------------------------------------------------------------------------------- 1 | exports.command = function(selector, attr, callback) { 2 | this.execute(function(selector, attr) { 3 | var els = document.querySelectorAll(selector), 4 | param = []; 5 | 6 | for (var i = 0, len = els.length; i < len; i++) { 7 | if (attr) { 8 | param.push(els[i].getAttribute(attr)); 9 | } else { 10 | param.push(els[i]); 11 | } 12 | } 13 | 14 | return param; 15 | }, [selector, attr], function(res) { 16 | callback(res.value); 17 | }); 18 | 19 | return this; 20 | }; 21 | -------------------------------------------------------------------------------- /test/spec-ui/modules/remotestorage/auth.js: -------------------------------------------------------------------------------- 1 | /* global it */ 2 | 'use strict'; 3 | 4 | /** 5 | * It tests if it's possible to login to a RemoteStorage server. 6 | */ 7 | it('creates a new user on RemoteStorage server', function(client) { 8 | client 9 | .url('http://localhost:9100/signup') 10 | .expect.element('input[type="password"]').to.be.present.before(50000); 11 | 12 | client 13 | .setValue('#username', 'test') 14 | .setValue('#email', 'test@example.com') 15 | .setValue('#password', ['1', client.Keys.ENTER]) 16 | .pause(300); 17 | }); 18 | 19 | it('shows RemoteStorage widget', function(client) { 20 | client 21 | .urlHash('notes') 22 | .expect.element('.remotestorage-initial').to.be.present.before(50000); 23 | }); 24 | 25 | it('can login to a RemoteStorage server', function(client) { 26 | client 27 | .click('.rs-bubble') 28 | .setValue('.remotestorage-initial input[name="userAddress"]', 'test@localhost:9100') 29 | .click('.remotestorage-initial .connect') 30 | .expect.element('input[type="password"]').to.be.present.before(10000); 31 | 32 | client.assert.urlContains('localhost:9100'); 33 | 34 | client 35 | .setValue('#password', ['1', client.Keys.ENTER]) 36 | .expect.element('.remotestorage-connected').to.be.present.before(50000); 37 | }); 38 | -------------------------------------------------------------------------------- /test/spec-ui/modules/remotestorage/client2.js: -------------------------------------------------------------------------------- 1 | /* global describe, before, after, it */ 2 | 'use strict'; 3 | 4 | describe('RemoteStorage: client 2', function() { 5 | 6 | before(function(client, done) { 7 | done(); 8 | }); 9 | 10 | after(function(client, done) { 11 | client.end(function() { 12 | done(); 13 | }); 14 | }); 15 | 16 | it('wait', function(client) { 17 | client 18 | .urlHash('notes') 19 | .expect.element('.list').to.be.present.before(50000); 20 | }); 21 | 22 | it('creates new data', function(client) { 23 | client 24 | .addNote({title: 'Note from client 2'}) 25 | .pause(500) 26 | .addNotebook({name: 'Notebook from client 2'}) 27 | .pause(500) 28 | .addTag({name: 'Tag from client 2'}); 29 | }); 30 | 31 | // Try to login to a RemoteStorage first 32 | require('./auth.js'); 33 | 34 | it('fetches notes from Remotestorage', function(client) { 35 | client.urlHash('notes'); 36 | client.expect.element('#header--add').to.be.present.before(50000); 37 | 38 | client.expect 39 | .element('#sidebar--content').to.have.text.that.contains('Note from client 1') 40 | .before(50000); 41 | }); 42 | 43 | it('fetches notebooks & tags from Remotestorage', function(client) { 44 | client.urlHash('notebooks'); 45 | 46 | client.expect 47 | .element('#notebooks').text.to.contain('Notebook from client 1') 48 | .before(50000); 49 | 50 | client.expect 51 | .element('#tags').text.to.contain('Tag from client 1') 52 | .that.contains('Tag from client 1') 53 | .before(50000); 54 | }); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /test/spec-ui/tests/apps/settings/import.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Notebook list test 5 | // |)}># 6 | module.exports = { 7 | 8 | before: function(client) { 9 | client.closeWelcome(); 10 | 11 | client.urlHash('settings/importExport'); 12 | client.expect.element('.header--title').to.have.text.that.contains('Settings').before(50000); 13 | }, 14 | 15 | after: function(client) { 16 | client.end(); 17 | }, 18 | 19 | '"Import & Export" tab is active in #/settings/importExports': function(client) { 20 | client 21 | .expect.element('.list--settings.active').to.have.text.that.contains('Import & Export'); 22 | }, 23 | 24 | // @TODO Test importing & exporting json config files 25 | }; 26 | */ 27 | -------------------------------------------------------------------------------- /test/spec-ui/tests/apps/tags/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Tag removal test 5 | */ 6 | module.exports = { 7 | 8 | before: function(client) { 9 | client.closeWelcome(); 10 | 11 | client.urlHash('notes'); 12 | client.expect.element('#header--add').to.be.visible.before(50000); 13 | client.urlHash('notebooks'); 14 | }, 15 | 16 | after: function(client) { 17 | client.end(); 18 | }, 19 | 20 | 'can remove a tag': function(client) { 21 | // Prepare a tag to delete 22 | client.addTag({name: '1.ToRemove'}); 23 | 24 | client 25 | .urlHash('notes') 26 | .pause(100) 27 | .urlHash('notebooks'); 28 | 29 | client.expect.element('#tags').text.to.contain('1.ToRemove').before(5000); 30 | 31 | // Delete the tag 32 | client 33 | .click('#tags .list--buttons .drop-edit') 34 | .click('#tags .list--buttons .remove-link'); 35 | 36 | client.expect.element('#modal .modal-title').to.be.present.before(5000); 37 | client.expect.element('#modal .modal-title').to.be.visible.before(5000); 38 | client.click('#modal .btn[data-event="confirm"]'); 39 | client.pause(500); 40 | 41 | client.expect.element('#tags').text.not.to.contain('1.ToRemove').before(5000); 42 | }, 43 | 44 | 'deleted tags don\'t re-appear after url change': function(client) { 45 | client 46 | .urlHash('notes') 47 | .pause(1000) 48 | .urlHash('notebooks'); 49 | 50 | client 51 | .expect.element('#tags').text.not.to.contain('1.ToRemove') 52 | .before(5000); 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /test/tape/behaviors/modelFocus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test behaviors/ModelFocus.js 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | 8 | import ModelFocus from '../../../app/scripts/behaviors/ModelFocus'; 9 | 10 | let sand; 11 | test('behaviors/ModelFocus: before()', t => { 12 | sand = sinon.sandbox.create(); 13 | t.end(); 14 | }); 15 | 16 | test('behaviors/ModelFocus: ui()', t => { 17 | const ui = ModelFocus.prototype.ui(); 18 | t.equal(typeof ui, 'object', 'returns an object'); 19 | t.equal(ui.listGroup, '.list-group-item:first'); 20 | 21 | t.end(); 22 | }); 23 | 24 | test('behaviors/ModelFocus: modelEvents()', t => { 25 | const events = ModelFocus.prototype.modelEvents(); 26 | t.equal(typeof events, 'object', 'returns an object'); 27 | t.equal(events.focus, 'onFocus'); 28 | 29 | t.end(); 30 | }); 31 | 32 | test('behaviors/ModelFocus: onFocus()', t => { 33 | const focus = new ModelFocus(); 34 | focus.view = {trigger: sand.stub()}; 35 | focus.oldUi = focus.ui; 36 | focus.ui.listGroup = { 37 | addClass : sand.stub(), 38 | offset : sand.stub().returns(1), 39 | }; 40 | 41 | focus.onFocus(); 42 | t.equal(focus.ui.listGroup.addClass.calledWith('active'), true, 43 | 'adds "active" class to listGroup element'); 44 | t.equal(focus.view.trigger.calledWith('scroll:top', {offset: 1}), true, 45 | 'triggers "scroll:top" event'); 46 | 47 | focus.view = null; 48 | focus.ui = focus.oldUi; 49 | sand.restore(); 50 | t.end(); 51 | }); 52 | -------------------------------------------------------------------------------- /test/tape/behaviors/sidebar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test behaviors/Sidebar 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import Radio from 'backbone.radio'; 8 | 9 | import Sidebar from '../../../app/scripts/behaviors/Sidebar'; 10 | 11 | let sand; 12 | test('behaviors/Sidebar: before()', t => { 13 | sand = sinon.sandbox.create(); 14 | t.end(); 15 | }); 16 | 17 | test('behaviors/Sidebar: onDestroy()', t => { 18 | const side = new Sidebar(); 19 | side.hammer = {destroy: sand.stub()}; 20 | 21 | side.onDestroy(); 22 | t.equal(side.hammer.destroy.called, true, 'destroyes the hammer instance'); 23 | 24 | sand.restore(); 25 | t.end(); 26 | }); 27 | 28 | test('behaviors/Sidebar: onSwipeRight()', t => { 29 | const side = new Sidebar(); 30 | const trig = sand.stub(Radio, 'trigger'); 31 | 32 | side.onSwipeRight(); 33 | t.equal(trig.calledWith('components/navbar', 'show:sidemenu'), true, 34 | 'shows the sidebar menu'); 35 | 36 | sand.restore(); 37 | t.end(); 38 | }); 39 | 40 | test('behaviors/Sidebar: onSwipeLeft()', t => { 41 | const side = new Sidebar(); 42 | const req = sand.stub(Radio, 'request'); 43 | side.view = {noSwipeLeft: true}; 44 | 45 | side.onSwipeLeft(); 46 | t.equal(req.notCalled, true, 47 | 'does nothing if the noSwipeRight property is equal to true'); 48 | 49 | side.view.noSwipeLeft = false; 50 | side.onSwipeLeft(); 51 | t.equal(req.calledWith('Layout', 'toggleContent', { 52 | visible: true, 53 | }), true, 'shows content region and hides the sidebar'); 54 | 55 | sand.restore(); 56 | t.end(); 57 | }); 58 | -------------------------------------------------------------------------------- /test/tape/collections/files.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Test collections/Files 3 | */ 4 | import test from 'tape'; 5 | import Files from '../../../app/scripts/collections/Files'; 6 | import File from '../../../app/scripts/models/File'; 7 | 8 | test('collections/Files: sync', t => { 9 | const coll = new Files(); 10 | t.equal(typeof coll.sync, 'function', 'has sync method'); 11 | t.end(); 12 | }); 13 | 14 | test('collections/Files: profileId', t => { 15 | t.equal(new Files().profileId, undefined, 'is undefined by default'); 16 | t.equal(new Files(null, {profileId: 'test'}).profileId, 'test'); 17 | t.end(); 18 | }); 19 | 20 | test('collections/Files: model', t => { 21 | const coll = new Files(); 22 | t.equal(coll.model, File, 'uses file model'); 23 | t.end(); 24 | }); 25 | -------------------------------------------------------------------------------- /test/tape/collections/modules/edits.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Test collections/modules/Edits 3 | */ 4 | import test from 'tape'; 5 | import sinon from 'sinon'; 6 | 7 | import '../../../../app/scripts/utils/underscore'; 8 | import Module from '../../../../app/scripts/collections/modules/Edits'; 9 | import Edits from '../../../../app/scripts/collections/Edits'; 10 | 11 | import Edit from '../../../../app/scripts/models/Edit'; 12 | import Shadow from '../../../../app/scripts/models/Shadow'; 13 | 14 | let sand; 15 | test('collections/modules/Edits: before()', t => { 16 | sand = sinon.sandbox.create(); 17 | t.end(); 18 | }); 19 | 20 | test('collections/modules/Edits: Collection', t => { 21 | const mod = new Module(); 22 | t.equal(mod.Collection, Edits); 23 | t.end(); 24 | }); 25 | 26 | test('collections/modules/Edits: constructor()', t => { 27 | const reply = sand.stub(Module.prototype.channel, 'reply'); 28 | const mod = new Module(); 29 | 30 | t.equal(reply.calledWith({ 31 | }), true, 'replies to additional requests'); 32 | 33 | sand.restore(); 34 | t.end(); 35 | }); 36 | -------------------------------------------------------------------------------- /test/tape/collections/modules/shadows.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Test collections/modules/Shadows 3 | */ 4 | import test from 'tape'; 5 | import proxyquire from 'proxyquire'; 6 | import sinon from 'sinon'; 7 | 8 | import Module from '../../../../app/scripts/collections/modules/Shadows'; 9 | import Shadows from '../../../../app/scripts/collections/Shadows'; 10 | 11 | let sand; 12 | test('collections/modules/Shadows: before()', t => { 13 | sand = sinon.sandbox.create(); 14 | t.end(); 15 | }); 16 | 17 | test('collections/modules/Shadows: Collection', t => { 18 | const mod = new Module(); 19 | t.equal(mod.Collection, Shadows); 20 | t.end(); 21 | }); 22 | -------------------------------------------------------------------------------- /test/tape/collections/profiles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Test collections/Profiles 3 | */ 4 | import test from 'tape'; 5 | import '../../../app/scripts/utils/underscore'; 6 | import Profiles from '../../../app/scripts/collections/Profiles'; 7 | import Profile from '../../../app/scripts/models/Profile'; 8 | 9 | test('collections/Profiles: model', t => { 10 | t.equal(Profiles.prototype.model, Profile); 11 | t.end(); 12 | }); 13 | 14 | test('collections/Profiles: constructor()', t => { 15 | const prof = new Profiles(); 16 | t.equal(prof.profileId, 'default', 'profileId is always equal to "default"'); 17 | t.end(); 18 | }); 19 | -------------------------------------------------------------------------------- /test/tape/components/dropbox/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Test components/dropbox/main 3 | */ 4 | import test from 'tape'; 5 | import sinon from 'sinon'; 6 | import Radio from 'backbone.radio'; 7 | 8 | import '../../../../app/scripts/utils/underscore'; 9 | import initialize from '../../../../app/scripts/components/dropbox/main'; 10 | import Sync from '../../../../app/scripts/components/dropbox/Sync'; 11 | import View from '../../../../app/scripts/components/dropbox/settings/View'; 12 | 13 | let sand; 14 | test('components/dropbox/main: before()', t => { 15 | sand = sinon.sandbox.create(); 16 | t.end(); 17 | }); 18 | 19 | test('components/dropbox/main', t => { 20 | const stub = sand.stub(Sync.prototype, 'init').resolves(); 21 | 22 | initialize(); 23 | t.equal(stub.notCalled, true, 'does not initialize Sync.js'); 24 | 25 | sand.stub(Radio, 'request') 26 | .withArgs('collections/Configs', 'findConfigs') 27 | .returns({dropboxKey: ''}) 28 | .withArgs('collections/Configs', 'findConfig') 29 | .returns('dropbox'); 30 | 31 | initialize(); 32 | t.equal(stub.called, true, 'msg'); 33 | 34 | sand.restore(); 35 | t.equal(Radio.request('components/dropbox', 'getSettingsView'), View, 36 | 'replies with the settings view'); 37 | 38 | sand.restore(); 39 | t.end(); 40 | }); 41 | -------------------------------------------------------------------------------- /test/tape/components/dropbox/settings/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Test components/dropbox/settings/View 3 | */ 4 | import test from 'tape'; 5 | import sinon from 'sinon'; 6 | 7 | import _ from '../../../../../app/scripts/utils/underscore'; 8 | import '../../../../../app/scripts/utils/underscore'; 9 | import Configs from '../../../../../app/scripts/collections/Configs'; 10 | import {configNames} from '../../../../../app/scripts/collections/configNames'; 11 | import View from '../../../../../app/scripts/components/dropbox/settings/View'; 12 | 13 | let sand; 14 | test('components/dropbox/settings/View: before()', t => { 15 | sand = sinon.sandbox.create(); 16 | t.end(); 17 | }); 18 | 19 | test('components/dropbox/settings/View: serializeData()', t => { 20 | const collection = new Configs(); 21 | 22 | //collection.resetFromObject(collection.configNames); 23 | 24 | const models = []; 25 | _.each(configNames, (value, name) => models.push({name, value})); 26 | collection.reset(models); 27 | 28 | const view = new View({collection}); 29 | 30 | const data = view.serializeData(); 31 | t.equal(data.dropboxKey, '', 'is empty string'); 32 | t.equal(data.placeholder, 'Optional', 'custom Dropbox API key is not needed'); 33 | 34 | Object.defineProperty(view, 'dropboxKeyNeed', {get: () => true}); 35 | collection.get('dropboxKey').set('value', '1234'); 36 | const data2 = view.serializeData(); 37 | t.equal(data2.dropboxKey, '1234', 'is equal to the current value'); 38 | t.equal(data2.placeholder, 'Required', 'custom Dropbox API key is required'); 39 | 40 | sand.restore(); 41 | t.end(); 42 | }); 43 | -------------------------------------------------------------------------------- /test/tape/components/encryption/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/encryption/main 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import Radio from 'backbone.radio'; 8 | import '../../../../app/scripts/utils/underscore'; 9 | 10 | import initialize from '../../../../app/scripts/components/encryption/main'; 11 | import Auth from '../../../../app/scripts/components/encryption/auth/Controller'; 12 | import Encrypt from '../../../../app/scripts/components/encryption/encrypt/Controller'; 13 | 14 | let sand; 15 | test('encryption/main: before()', t => { 16 | sand = sinon.sandbox.create(); 17 | t.end(); 18 | }); 19 | 20 | test('encryption/Main', t => { 21 | const authInit = sand.stub(Auth.prototype, 'init'); 22 | const encryptInit = sand.stub(Encrypt.prototype, 'init'); 23 | const req = sand.stub(Radio, 'request').callsFake((n, m, data) => data.callback()); 24 | 25 | initialize(); 26 | t.equal(req.callCount, 2, 'adds two initializer'); 27 | t.equal(req.calledWithMatch('utils/Initializer', 'add', {name: 'App:auth'}), true, 28 | 'adds App:auth initializer'); 29 | t.equal(authInit.called, true, 'instantiates "auth" controller'); 30 | t.equal(encryptInit.called, true, 'instantiates "auth" controller'); 31 | 32 | sand.restore(); 33 | t.end(); 34 | }); 35 | -------------------------------------------------------------------------------- /test/tape/components/fileDialog/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/fileDialog/main 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import Radio from 'backbone.radio'; 8 | import '../../../../app/scripts/utils/underscore'; 9 | 10 | import initialize from '../../../../app/scripts/components/fileDialog/main'; 11 | 12 | test('fileDialog/main', t => { 13 | const reply = sinon.stub(); 14 | sinon.stub(Radio, 'channel').returns({reply}); 15 | 16 | initialize(); 17 | t.equal(reply.calledWith('show'), true, 'replies to "show" request'); 18 | 19 | Radio.channel.restore(); 20 | t.end(); 21 | }); 22 | -------------------------------------------------------------------------------- /test/tape/components/fuzzySearch/child.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/fuzzySearch/views/Child 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import '../../../../app/scripts/utils/underscore'; 7 | 8 | import Child from '../../../../app/scripts/components/fuzzySearch/views/Child'; 9 | 10 | test('fuzzySearch/views/Child: className', t => { 11 | t.equal(Child.prototype.className, 'list-group list--group'); 12 | t.end(); 13 | }); 14 | 15 | test('fuzzySearch/views/Child: triggers()', t => { 16 | const view = new Child(); 17 | const trig = view.triggers(); 18 | 19 | t.equal(typeof trig, 'object', 'returns an object'); 20 | t.equal(trig['click .list-group-item'], 'navigate:search', 21 | 'triggers navigate:search event'); 22 | 23 | t.end(); 24 | }); 25 | -------------------------------------------------------------------------------- /test/tape/components/fuzzySearch/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/fuzzySearch/main 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import Radio from 'backbone.radio'; 8 | import '../../../../app/scripts/utils/underscore'; 9 | 10 | import main from '../../../../app/scripts/components/fuzzySearch/main'; 11 | import regionClass from '../../../../app/scripts/components/fuzzySearch/views/Region'; 12 | 13 | let sand; 14 | test('fuzzySearch/main: before()', t => { 15 | sand = sinon.sandbox.create(); 16 | t.end(); 17 | }); 18 | 19 | test('fuzzySearch/main: initialize()', t => { 20 | const on = sand.stub(); 21 | sand.stub(Radio, 'channel').withArgs('components/navbar') 22 | .returns({on}); 23 | sand.stub(main, 'createRegion'); 24 | 25 | main.initialize(); 26 | t.equal(main.createRegion.called, true, 'creates fuzzySearch region'); 27 | t.equal(on.calledWith('shown:search'), true, 'listens to shown:search event'); 28 | 29 | sand.restore(); 30 | t.end(); 31 | }); 32 | 33 | test('fuzzySearch/main: createRegion()', t => { 34 | const req = sand.stub(Radio, 'request'); 35 | 36 | main.createRegion(); 37 | t.equal(req.calledWith('Layout', 'add', { 38 | region : 'fuzzySearch', 39 | regionOptions : {regionClass, el: '#sidebar--fuzzy'}, 40 | }), true, 'creates fuzzySearch region'); 41 | 42 | sand.restore(); 43 | t.end(); 44 | }); 45 | -------------------------------------------------------------------------------- /test/tape/components/fuzzySearch/region.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/fuzzySearch/views/Region 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import '../../../../app/scripts/utils/underscore'; 8 | 9 | import Region from '../../../../app/scripts/components/fuzzySearch/views/Region'; 10 | 11 | let sand; 12 | test('fuzzySearch/views/Region: before()', t => { 13 | sand = sinon.sandbox.create(); 14 | t.end(); 15 | }); 16 | 17 | test('fuzzySearch/views/Region: onShow()', t => { 18 | const prot = Region.prototype; 19 | prot.$body = {addClass : sand.stub()}; 20 | prot.$el = {removeClass: sand.stub()}; 21 | 22 | prot.onShow(); 23 | t.equal(prot.$body.addClass.calledWith('-fuzzy'), true, 24 | 'adds -fuzzy class to the body'); 25 | t.equal(prot.$el.removeClass.calledWith('hidden'), true, 26 | 'shows the region element'); 27 | 28 | prot.$body = null; 29 | prot.$el = null; 30 | sand.restore(); 31 | t.end(); 32 | }); 33 | 34 | test('fuzzySearch/views/Region: onEmpty()', t => { 35 | const prot = Region.prototype; 36 | prot.$body = {removeClass: sand.stub()}; 37 | prot.$el = {addClass : sand.stub()}; 38 | 39 | prot.onEmpty(); 40 | t.equal(prot.$el.addClass.calledWith('hidden'), true, 41 | 'hides the region element'); 42 | t.equal(prot.$body.removeClass.calledWith('-fuzzy'), true, 43 | 'removes -fuzzy class from the body'); 44 | 45 | prot.$body = null; 46 | prot.$el = null; 47 | sand.restore(); 48 | t.end(); 49 | }); 50 | -------------------------------------------------------------------------------- /test/tape/components/fuzzySearch/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/fuzzySearch/views/View 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import '../../../../app/scripts/utils/underscore'; 7 | 8 | import View from '../../../../app/scripts/components/fuzzySearch/views/View'; 9 | import Child from '../../../../app/scripts/components/fuzzySearch/views/Child'; 10 | 11 | test('View: className', t => { 12 | t.equal(View.prototype.className, 'main notes-list'); 13 | t.end(); 14 | }); 15 | 16 | test('View: childViewContainer', t => { 17 | t.equal(View.prototype.childViewContainer, '.notes-list'); 18 | t.end(); 19 | }); 20 | 21 | test('View: childView', t => { 22 | t.equal(View.prototype.childView, Child, 'uses the child view'); 23 | t.end(); 24 | }); 25 | -------------------------------------------------------------------------------- /test/tape/components/help/about/controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/help/about/Controller 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import Radio from 'backbone.radio'; 8 | import Controller from '../../../../../app/scripts/components/help/about/Controller'; 9 | 10 | let sand; 11 | test('help/about/Controller: before()', t => { 12 | sand = sinon.sandbox.create(); 13 | t.end(); 14 | }); 15 | 16 | test('help/about/Controller: init()', t => { 17 | const con = new Controller(); 18 | sand.stub(con, 'show'); 19 | con.init(); 20 | t.equal(con.show.called, true, 'calls show method'); 21 | 22 | sand.restore(); 23 | t.end(); 24 | }); 25 | 26 | test('help/about/Controller: show()', t => { 27 | const con = new Controller(); 28 | const req = sand.stub(Radio, 'request'); 29 | const listen = sand.stub(con, 'listenTo'); 30 | 31 | con.show(); 32 | t.equal(req.calledWith('Layout', 'show', { 33 | region : 'modal', 34 | view : con.view, 35 | }), true, 'renders the view in modal region'); 36 | t.equal(listen.calledWith(con.view, 'destroy', con.destroy), true, 37 | 'destroyes itself if the view is destroyed'); 38 | 39 | sand.restore(); 40 | t.end(); 41 | }); 42 | -------------------------------------------------------------------------------- /test/tape/components/help/about/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/help/about/View 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import View from '../../../../../app/scripts/components/help/about/View'; 7 | 8 | test('View: className', t => { 9 | t.equal(View.prototype.className, 'modal fade'); 10 | t.end(); 11 | }); 12 | 13 | test('View: serializeData()', t => { 14 | const view = new View({constants: 'test'}); 15 | t.deepEqual(view.serializeData(), {constants: 'test'}); 16 | t.end(); 17 | }); 18 | -------------------------------------------------------------------------------- /test/tape/components/help/keybindings/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/help/keybindings/View 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | 7 | /* eslint-disable */ 8 | import View from '../../../../../app/scripts/components/help/keybindings/View'; 9 | /* eslint-enable */ 10 | 11 | test('help/keybindings/View: className', t => { 12 | t.equal(View.prototype.className, 'modal fade'); 13 | t.end(); 14 | }); 15 | -------------------------------------------------------------------------------- /test/tape/components/importExport/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/importExport/main 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import Radio from 'backbone.radio'; 8 | import '../../../../app/scripts/utils/underscore'; 9 | 10 | import main from '../../../../app/scripts/components/importExport/main'; 11 | import Import from '../../../../app/scripts/components/importExport/Import'; 12 | import Export from '../../../../app/scripts/components/importExport/Export'; 13 | 14 | let sand; 15 | test('importExport/Main: before()', t => { 16 | sand = sinon.sandbox.create(); 17 | main(); 18 | t.end(); 19 | }); 20 | 21 | test('importExport/Main: responds to "import" request', t => { 22 | const init = sand.stub(Import.prototype, 'init'); 23 | 24 | Radio.request('components/importExport', 'import'); 25 | t.equal(init.called, true); 26 | 27 | sand.restore(); 28 | t.end(); 29 | }); 30 | 31 | test('importExport/Main: responds to "export" request', t => { 32 | const init = sand.stub(Export.prototype, 'init'); 33 | 34 | Radio.request('components/importExport', 'export'); 35 | t.equal(init.called, true); 36 | 37 | sand.restore(); 38 | t.end(); 39 | }); 40 | 41 | test('importExport/Main: adds "App:checks" initializer', t => { 42 | const req = sand.stub(Radio, 'request'); 43 | 44 | main(); 45 | t.equal(req.calledWithMatch('utils/Initializer', 'add', { 46 | name: 'App:last', 47 | }), true, 'msg'); 48 | 49 | sand.restore(); 50 | t.end(); 51 | }); 52 | -------------------------------------------------------------------------------- /test/tape/components/linkDialog/item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/linkDialog/views/Item 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import '../../../../app/scripts/utils/underscore'; 7 | 8 | import View from '../../../../app/scripts/components/linkDialog/views/Item'; 9 | 10 | test('linkDialog/views/Item: tagName()', t => { 11 | t.equal(View.prototype.tagName, 'li'); 12 | t.end(); 13 | }); 14 | -------------------------------------------------------------------------------- /test/tape/components/linkDialog/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/linkDialog/main 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import Radio from 'backbone.radio'; 8 | import '../../../../app/scripts/utils/underscore'; 9 | 10 | import initialize from '../../../../app/scripts/components/linkDialog/main'; 11 | 12 | test('linkDialog/main', t => { 13 | const reply = sinon.stub(); 14 | sinon.stub(Radio, 'channel').returns({reply}); 15 | 16 | initialize(); 17 | t.equal(reply.calledWith('show'), true, 'replies to "show" request'); 18 | 19 | Radio.channel.restore(); 20 | t.end(); 21 | }); 22 | -------------------------------------------------------------------------------- /test/tape/components/notebooks/form/tag/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/notebooks/form/tag/View 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | 7 | /* eslint-disable */ 8 | import _ from '../../../../../../app/scripts/utils/underscore'; 9 | import View from '../../../../../../app/scripts/components/notebooks/form/tag/View'; 10 | import ModalForm from '../../../../../../app/scripts/behaviors/ModalForm'; 11 | /* eslint-enable */ 12 | 13 | test('notebooks/form/tag/View: className', t => { 14 | t.equal(View.prototype.className, 'modal fade'); 15 | t.end(); 16 | }); 17 | 18 | test('notebooks/form/tag/View: ui()', t => { 19 | const ui = View.prototype.ui(); 20 | t.equal(typeof ui, 'object', 'returns an object'); 21 | t.equal(ui.name, 'input[name="name"]'); 22 | t.end(); 23 | }); 24 | 25 | test('notebooks/form/tag/View: behaviors()', t => { 26 | const behaviors = View.prototype.behaviors(); 27 | t.equal(Array.isArray(behaviors), true, 'returns an array'); 28 | t.equal(behaviors.indexOf(ModalForm) !== -1, true, 'uses ModalForm behavior'); 29 | 30 | t.end(); 31 | }); 32 | -------------------------------------------------------------------------------- /test/tape/components/notebooks/list/notebook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/notebooks/list/views/Notebook 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | 7 | /* eslint-disable */ 8 | import _ from '../../../../../app/scripts/utils/underscore'; 9 | import View from '../../../../../app/scripts/components/notebooks/list/views/Notebook'; 10 | import ItemView from '../../../../../app/scripts/components/notebooks/list/views/ItemView'; 11 | /* eslint-enable */ 12 | 13 | test('notebooks/list/views/Tag: extends from ItemView', t => { 14 | t.equal(View.prototype instanceof ItemView, true); 15 | t.end(); 16 | }); 17 | 18 | test('Notebook: modelEvents()', t => { 19 | const modelEvents = View.prototype.modelEvents(); 20 | t.equal(typeof modelEvents, 'object', 'returns an object'); 21 | t.equal(modelEvents.change, 'render', 're-renders itself if the model has changed'); 22 | t.end(); 23 | }); 24 | 25 | test('notebooks/list/views/Notebook: templateContext()', t => { 26 | const context = View.prototype.templateContext(); 27 | 28 | context.level = 1; 29 | t.equal(context.getPadding(), '', 'returns empty string if level is equal to 1'); 30 | 31 | context.level = 2; 32 | t.equal(context.getPadding(), 'padding-left:40px', 33 | 'padding-left should be equal to level*20 px'); 34 | 35 | t.end(); 36 | }); 37 | -------------------------------------------------------------------------------- /test/tape/components/notebooks/list/notebooks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/notebooks/list/views/Notebooks 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | 7 | /* eslint-disable */ 8 | import _ from '../../../../../app/scripts/utils/underscore'; 9 | import View from '../../../../../app/scripts/components/notebooks/list/views/Notebooks'; 10 | import Notebook from '../../../../../app/scripts/components/notebooks/list/views/Notebook'; 11 | import Navigate from '../../../../../app/scripts/behaviors/Navigate'; 12 | /* eslint-enable */ 13 | 14 | test('notebooks/list/views/Notebooks: className', t => { 15 | t.equal(View.prototype.className, 'list--notebooks'); 16 | t.end(); 17 | }); 18 | 19 | test('notebooks/list/views/Notebooks: behaviors()', t => { 20 | const behaviors = View.prototype.behaviors(); 21 | t.equal(Array.isArray(behaviors), true, 'returns an array'); 22 | t.equal(behaviors.indexOf(Navigate) !== -1, true, 'uses navigate behavior'); 23 | t.end(); 24 | }); 25 | 26 | test('notebooks/list/views/Notebooks: childView()', t => { 27 | t.equal(View.prototype.childView(), Notebook, 'uses notebook item view'); 28 | t.end(); 29 | }); 30 | 31 | test('notebooks/list/views/Notebooks: childViewOptions()', t => { 32 | const view = new View({profileId: 'test', collection: 'test'}); 33 | t.deepEqual(view.childViewOptions(), {profileId: 'test', filterArgs: {}}, 'msg'); 34 | t.end(); 35 | }); 36 | 37 | test('notebooks/list/views/Notebooks: initialize()', t => { 38 | const view = new View({collection: 'test'}); 39 | t.equal(_.isEmpty(view.options.filterArgs), true, 40 | 'creates options.filterArgs property'); 41 | t.end(); 42 | }); 43 | -------------------------------------------------------------------------------- /test/tape/components/notebooks/list/tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/notebooks/list/views/Tag 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | 7 | /* eslint-disable */ 8 | import _ from '../../../../../app/scripts/utils/underscore'; 9 | import View from '../../../../../app/scripts/components/notebooks/list/views/Tag'; 10 | import ItemView from '../../../../../app/scripts/components/notebooks/list/views/ItemView'; 11 | /* eslint-enable */ 12 | 13 | test('notebooks/list/views/Tag: extends from ItemView', t => { 14 | t.equal(View.prototype instanceof ItemView, true); 15 | t.end(); 16 | }); 17 | -------------------------------------------------------------------------------- /test/tape/components/notebooks/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/notebooks/Router.js 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | 7 | import Router from '../../../../app/scripts/components/notebooks/Router.js'; 8 | import controller from '../../../../app/scripts/components/notebooks/controller.js'; 9 | 10 | test('notebooks/Router: controller', t => { 11 | t.equal(typeof Router.prototype.controller, 'object', 12 | 'is an object'); 13 | t.equal(Router.prototype.controller, controller, 'uses the correct controller'); 14 | t.end(); 15 | }); 16 | 17 | test('notebooks/Router: appRoutes', t => { 18 | const routes = Router.prototype.appRoutes; 19 | t.equal(typeof routes, 'object', 'is an object'); 20 | 21 | t.equal(routes['notebooks'], 'showList', 22 | 'shows a list of notebooks and tags'); 23 | t.equal(routes['notebooks/add'], 'notebookForm', 24 | 'shows notebook add form'); 25 | t.equal(routes['notebooks/edit/:id'], 'notebookForm', 26 | 'shows notebook edit form'); 27 | t.equal(routes['tags/add'], 'tagForm', 28 | 'shows tag add form'); 29 | t.equal(routes['tags/edit/:id'], 'tagForm', 30 | 'shows tag edit form'); 31 | 32 | t.end(); 33 | }); 34 | -------------------------------------------------------------------------------- /test/tape/components/notes/form/views/notebook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test: components/notes/form/views/Notebook 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | 8 | import '../../../../../../app/scripts/utils/underscore'; 9 | 10 | /* eslint-disable */ 11 | import View from '../../../../../../app/scripts/components/notes/form/views/Notebook'; 12 | import Notebook from '../../../../../../app/scripts/models/Notebook'; 13 | /* eslint-enable */ 14 | 15 | test('notes/form/Notebook: tagName', t => { 16 | t.equal(View.prototype.tagName, 'option'); 17 | t.end(); 18 | }); 19 | 20 | test('notes/form/Notebook: onRender()', t => { 21 | const view = new View({model: new Notebook({id: '1'})}); 22 | view.$el = {attr: sinon.stub()}; 23 | 24 | view.onRender(); 25 | t.equal(view.$el.attr.calledWith('value', '1'), true, 26 | 'changes the "value" attribute'); 27 | 28 | t.end(); 29 | }); 30 | -------------------------------------------------------------------------------- /test/tape/components/notes/form/views/notebooksCollection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test: components/notes/form/views/NotebooksCollection.js 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | 8 | import _ from '../../../../../../app/scripts/utils/underscore'; 9 | 10 | /* eslint-disable */ 11 | import View from '../../../../../../app/scripts/components/notes/form/views/NotebooksCollection'; 12 | import NotebookView from '../../../../../../app/scripts/components/notes/form/views/Notebook'; 13 | /* eslint-enable */ 14 | 15 | test('notes/form/views/NotebooksCollection: tagName', t => { 16 | t.equal(View.prototype.tagName, 'optgroup'); 17 | t.end(); 18 | }); 19 | 20 | test('notes/form/views/NotebooksCollection: tagName', t => { 21 | t.equal(View.prototype.className, 'editor--notebooks--select'); 22 | t.end(); 23 | }); 24 | 25 | test('notes/form/views/NotebooksCollection: childView()', t => { 26 | const childView = View.prototype.childView(); 27 | t.equal(childView, NotebookView, 'uses Notebook view as the child view'); 28 | 29 | t.end(); 30 | }); 31 | 32 | test('notes/form/views/NotebooksCollection: onRender()', t => { 33 | const view = View.prototype; 34 | view.$el = {attr: sinon.stub()}; 35 | sinon.stub(_, 'i18n').callsFake(str => str); 36 | 37 | view.onRender(); 38 | t.equal(view.$el.attr.calledWith('label', 'Notebooks'), true, 39 | 'changes view element\'s label'); 40 | 41 | _.i18n.restore(); 42 | t.end(); 43 | }); 44 | -------------------------------------------------------------------------------- /test/tape/components/notes/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test the core app: components/notes/Router.js 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | 7 | import Router from '../../../../app/scripts/components/notes/Router.js'; 8 | import controller from '../../../../app/scripts/components/notes/controller.js'; 9 | 10 | test('notes/Router: controller()', t => { 11 | t.equal(typeof Router.prototype.controller, 'object', 12 | 'is an object'); 13 | t.equal(Router.prototype.controller, controller, 'uses the correct controller'); 14 | t.end(); 15 | }); 16 | 17 | test('notes/Router: appRoutes', t => { 18 | const routes = Router.prototype.appRoutes; 19 | t.equal(typeof routes, 'object', 'is an object'); 20 | 21 | t.equal(routes[''], 'showNotes', 'the index page shows a list of notes'); 22 | t.equal(routes['notes(/f/:filter)(/q/:query)(/p:page)'], 'showNotes', 23 | 'properly computes routes for showNotes()'); 24 | 25 | t.equal(routes['notes(/f/:filter)(/q/:query)(/p:page)/show/:id'], 'showNote', 26 | 'properly computes routes for showNote()'); 27 | 28 | t.equal(routes['notes/add'], 'showForm', 'shows the add form'); 29 | t.equal(routes['notes/edit/:id'], 'showForm', 'shows the edit form'); 30 | t.end(); 31 | }); 32 | -------------------------------------------------------------------------------- /test/tape/components/settings/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test: components/settings/Router.js 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | 7 | import Router from '../../../../app/scripts/components/settings/Router.js'; 8 | import controller from '../../../../app/scripts/components/settings/controller.js'; 9 | 10 | test('settings/Router: controller()', t => { 11 | t.equal(typeof Router.prototype.controller, 'object', 'is an object'); 12 | t.equal(Router.prototype.controller, controller, 'uses the correct controller'); 13 | t.end(); 14 | }); 15 | 16 | test('settings/Router: appRoutes', t => { 17 | const routes = Router.prototype.appRoutes; 18 | t.equal(typeof routes, 'object', 'is an object'); 19 | t.equal(routes['settings(/:tab)'], 'showContent'); 20 | t.end(); 21 | }); 22 | -------------------------------------------------------------------------------- /test/tape/components/setup/export.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/setup/export/View 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | 8 | import '../../../../app/scripts/utils/underscore'; 9 | import View from '../../../../app/scripts/components/setup/export/View'; 10 | 11 | let sand; 12 | test('setup/export/View: before()', t => { 13 | sand = sinon.sandbox.create(); 14 | t.end(); 15 | }); 16 | 17 | test('setup/export/View: serializeData()', t => { 18 | const opt = {el: 'test'}; 19 | View.prototype.options = {el: 'test'}; 20 | 21 | t.deepEqual(View.prototype.serializeData(), opt); 22 | 23 | View.prototype.options = null; 24 | t.end(); 25 | }); 26 | -------------------------------------------------------------------------------- /test/tape/components/setup/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/setup/main 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import Radio from 'backbone.radio'; 8 | 9 | import initialize from '../../../../app/scripts/components/setup/main'; 10 | import Controller from '../../../../app/scripts/components/setup/Controller'; 11 | 12 | let sand; 13 | test('setup/main: before()', t => { 14 | sand = sinon.sandbox.create(); 15 | t.end(); 16 | }); 17 | 18 | test('setup/main: initialize()', t => { 19 | const init = sand.stub(Controller.prototype, 'init'); 20 | const reply = sand.stub(Radio, 'reply'); 21 | const req = sand.stub(Radio, 'request').callsFake((...args) => { 22 | args[2].callback(); 23 | }); 24 | 25 | initialize(); 26 | t.equal(reply.calledWith('components/setup', 'start'), true, 27 | 'replies to "start" request'); 28 | 29 | t.equal(req.calledWithMatch('utils/Initializer', 'add', { 30 | name: 'App:components', 31 | }), true, 'adds App:components initializer'); 32 | 33 | t.equal(init.called, true, 'calls Controller.init method'); 34 | 35 | sand.restore(); 36 | t.end(); 37 | }); 38 | -------------------------------------------------------------------------------- /test/tape/components/share/info.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test components/share/info/View 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import Radio from 'backbone.radio'; 8 | import Mousetrap from 'mousetrap'; 9 | 10 | import _ from '../../../../app/scripts/utils/underscore'; 11 | import View from '../../../../app/scripts/components/share/info/View'; 12 | 13 | let sand; 14 | test('share/info/View: before()', t => { 15 | sand = sinon.sandbox.create(); 16 | t.end(); 17 | }); 18 | 19 | test('share/info/View: triggers()', t => { 20 | const triggers = View.prototype.triggers(); 21 | t.equal(typeof triggers, 'object'); 22 | t.equal(triggers['click .share--trust'], 'add:trust', 23 | 'triggers "add:trust" event'); 24 | 25 | t.end(); 26 | }); 27 | 28 | test('share/info/Users: serializeData()', t => { 29 | const view = new View({test: '1'}); 30 | t.equal(view.serializeData(), view.options); 31 | t.end(); 32 | }); 33 | -------------------------------------------------------------------------------- /test/tape/index.js: -------------------------------------------------------------------------------- 1 | import {JSDOM} from 'jsdom'; 2 | import {mkdirSync} from 'fs'; 3 | import glob from 'glob'; 4 | import {LocalStorage} from 'node-localstorage'; 5 | import overrideTemplate from './overrideTemplate'; 6 | import raf from 'raf'; 7 | 8 | try { 9 | mkdirSync(`${__dirname}/../../_dev`); 10 | mkdirSync(`${__dirname}/../../_dev/scratch`); 11 | } 12 | // eslint-disable-next-line 13 | catch (e) { 14 | } 15 | 16 | global.localStorage = new LocalStorage(`${__dirname}/../../_dev/scratch`); 17 | global.overrideTemplate = overrideTemplate; 18 | global.requestAnimationFrame = raf; 19 | 20 | /** 21 | * Create DOM environment. 22 | */ 23 | JSDOM.fromFile(`${__dirname}/../../app/index.html`, { 24 | url : 'http://localhost/#', 25 | contentType : 'text/html', 26 | }) 27 | .then(doc => { 28 | global.document = doc.window.document; 29 | global.window = doc.window; 30 | global.navigator = global.window.navigator; 31 | global.location = global.window.location; 32 | global.HTMLElement = doc.window.HTMLElement; 33 | 34 | Object.defineProperty(global.window, 'localStorage', { 35 | get: function() { 36 | return global.localStorage; 37 | } 38 | }); 39 | //global.window.localStorage = global.localStorage; 40 | global.window.setTimeout = setTimeout; 41 | global.window.clearTimeout = clearTimeout; 42 | 43 | // Automatically require all test files 44 | glob.sync(`${__dirname}/**/*.js`) 45 | .filter(file => file.indexOf('index.js') === -1) 46 | .forEach(file => require(file)); 47 | }); 48 | -------------------------------------------------------------------------------- /test/tape/models/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test models/Config.js 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | 7 | import Config from '../../../app/scripts/models/Config'; 8 | 9 | test('Config: idAttribute()', t => { 10 | const config = new Config(); 11 | t.equal(config.idAttribute, 'name', 'uses "name" as ID attribute'); 12 | t.end(); 13 | }); 14 | 15 | test('Config: defaults()', t => { 16 | const config = new Config(); 17 | 18 | t.equal(typeof config.defaults, 'object', 'has "defaults" property'); 19 | t.equal(config.defaults.name, '', 'for default name is equal to empty string'); 20 | t.equal(config.defaults.value, '', 'for default value is equal to empty string'); 21 | 22 | t.end(); 23 | }); 24 | 25 | test('Config: storeName()', t => { 26 | const config = new Config(); 27 | t.equal(config.storeName, 'configs'); 28 | t.end(); 29 | }); 30 | 31 | test('Config: validate()', t => { 32 | const config = new Config(); 33 | 34 | t.equal(config.validate({name: ''}).length, 1, 35 | 'returns array of errors if name is empty'); 36 | 37 | t.equal(config.validate({name: 'test'}), undefined, 38 | 'returns nothing if there are no validation errors'); 39 | 40 | t.end(); 41 | }); 42 | -------------------------------------------------------------------------------- /test/tape/models/diffsync/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test models/diffsync/main 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import Radio from 'backbone.radio'; 8 | 9 | import initializer from '../../../../app/scripts/models/diffsync/main'; 10 | import Peer from '../../../../app/scripts/models/Peer'; 11 | import Core from '../../../../app/scripts/models/diffsync/Core'; 12 | 13 | let sand; 14 | test('models/diffsync/main: before()', t => { 15 | sand = sinon.sandbox.create(); 16 | Radio.channel('App').stopListening(); 17 | t.end(); 18 | }); 19 | 20 | test('models/diffsync/main: initializer()', t => { 21 | const req = sand.stub(Radio, 'request') 22 | .withArgs('collections/Configs', 'findConfig', {name: 'cloudStorage'}) 23 | .returns('p2p') 24 | .withArgs('collections/Configs', 'findConfigs') 25 | .returns({}); 26 | 27 | const init = sand.stub(Peer.prototype, 'init'); 28 | const init2 = sand.stub(Core.prototype, 'init'); 29 | 30 | const start = initializer(); 31 | t.equal(typeof start, 'function', 'returns the callback'); 32 | 33 | start(); 34 | t.equal(init.called, true, 'initializes the peer class'); 35 | t.equal(init2.called, true, 'initializes the differential synchronization core'); 36 | 37 | req.withArgs('collections/Configs', 'findConfig', {name: 'cloudStorage'}) 38 | .returns('dropbox'); 39 | 40 | start(); 41 | t.equal(init.callCount, 1, 'does nothing if p2p is not used'); 42 | 43 | sand.restore(); 44 | t.end(); 45 | }); 46 | -------------------------------------------------------------------------------- /test/tape/models/file.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test models/Config.js 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | 7 | import File from '../../../app/scripts/models/File'; 8 | 9 | test('File: storeName', t => { 10 | const file = new File(); 11 | t.equal(file.storeName, 'files', 'storeName is equal to "files"'); 12 | t.end(); 13 | }); 14 | 15 | test('File: defaults', t => { 16 | const defaults = new File().defaults; 17 | 18 | t.equal(defaults.type, 'files'); 19 | t.equal(defaults.id, undefined); 20 | t.equal(defaults.name, ''); 21 | t.equal(defaults.fileType, ''); 22 | t.equal(defaults.trash, 0); 23 | t.equal(defaults.created, 0); 24 | t.equal(defaults.updated, 0); 25 | 26 | t.end(); 27 | }); 28 | 29 | test('File: validateAttributes', t => { 30 | const validate = new File().validateAttributes; 31 | 32 | t.equal(Array.isArray(validate), true, 'is an array'); 33 | t.equal(validate.indexOf('src') > -1, true, 34 | 'validates "src" attribute'); 35 | t.equal(validate.indexOf('fileType') > -1, true, 36 | 'validates "fileType" attribute'); 37 | 38 | t.end(); 39 | }); 40 | 41 | test('File: escapeAttributes', t => { 42 | const escape = new File().escapeAttributes; 43 | 44 | t.equal(Array.isArray(escape), true, 'is an array'); 45 | t.equal(escape.indexOf('name') > -1, true, 46 | 'filters "name" attribute'); 47 | 48 | t.end(); 49 | }); 50 | -------------------------------------------------------------------------------- /test/tape/models/profile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test models/Profile 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import Profile from '../../../app/scripts/models/Profile'; 7 | 8 | test('models/Profile: storeName', t => { 9 | t.equal(new Profile().storeName, 'profiles'); 10 | t.end(); 11 | }); 12 | 13 | test('models/Profile: idAttribute', t => { 14 | t.equal(new Profile().idAttribute, 'username'); 15 | t.end(); 16 | }); 17 | 18 | test('models/Profile: defaults', t => { 19 | const def = new Profile().defaults; 20 | t.equal(def.username, ''); 21 | t.equal(def.privateKey, ''); 22 | t.equal(def.publicKey, ''); 23 | 24 | t.end(); 25 | }); 26 | 27 | test('models/Profile: validateAttributes', t => { 28 | t.deepEqual(new Profile().validateAttributes, ['username']); 29 | t.end(); 30 | }); 31 | 32 | test('models/Profile: constructor()', t => { 33 | const prof = new Profile(); 34 | t.equal(prof.profileId, 'default', 'profileId is always equal to "default"'); 35 | t.end(); 36 | }); 37 | -------------------------------------------------------------------------------- /test/tape/models/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test models/User 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import User from '../../../app/scripts/models/User'; 7 | 8 | test('models/User: storeName', t => { 9 | const tag = new User(); 10 | t.equal(tag.storeName, 'users'); 11 | t.end(); 12 | }); 13 | 14 | test('models/User: idAttribute', t => { 15 | t.equal(User.prototype.idAttribute, 'username'); 16 | t.end(); 17 | }); 18 | 19 | test('models/User: defaults', t => { 20 | const defaults = new User().defaults; 21 | 22 | t.equal(defaults.type, 'users'); 23 | t.equal(defaults.username, ''); 24 | t.equal(defaults.fingerprint, ''); 25 | t.equal(defaults.publicKey, ''); 26 | t.equal(defaults.pendingAccept, false); 27 | t.equal(defaults.pendingInvite, false); 28 | 29 | t.end(); 30 | }); 31 | 32 | test('models/User: validateAttributes()', t => { 33 | const validate = new User().validateAttributes; 34 | t.equal(Array.isArray(validate), true, 'is an array'); 35 | t.equal(validate.indexOf('username') > -1, true, 'validates username'); 36 | t.equal(validate.indexOf('publicKey') > -1, true, 'validates publicKey'); 37 | t.equal(validate.indexOf('fingerprint') > -1, true, 'validates fingerprint'); 38 | t.end(); 39 | }); 40 | 41 | test('models/User: escapeAttributes()', t => { 42 | const escape = new User().escapeAttributes; 43 | t.equal(Array.isArray(escape), true, 'is an array'); 44 | t.equal(escape.indexOf('username') > -1, true, 'validates username'); 45 | t.equal(escape.indexOf('publicKey') > -1, true, 'validates publicKey'); 46 | t.equal(escape.indexOf('fingerprint') > -1, true, 'validates fingerprint'); 47 | t.end(); 48 | }); 49 | -------------------------------------------------------------------------------- /test/tape/overrideTemplate.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore'; 2 | import {readFileSync as read} from 'fs'; 3 | 4 | /** 5 | * Override the template property of a View. 6 | * This file exists because both Babel and commonjs cannot handle underscore 7 | * template files. 8 | * 9 | * @param {Object} View 10 | * @param {String} path - the relative path to a template 11 | */ 12 | export default function overrideTemplate(View, path) { 13 | const tmpl = read(`${__dirname}/../../app/scripts/${path}`); 14 | Object.defineProperty(View.prototype, 'template', { 15 | get: () => _.template(tmpl), 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test/tape/utils/electronListener.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test utils/electronListener 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | 8 | import listener from '../../../app/scripts/utils/electronListener'; 9 | 10 | let sand; 11 | test('utils/electronListener: before()', t => { 12 | sand = sinon.sandbox.create(); 13 | t.end(); 14 | }); 15 | 16 | test('utils/electronlistener: does nothing if it is not Electron environment', t => { 17 | t.equal(listener(), false); 18 | t.end(); 19 | }); 20 | 21 | test('utils/electronlistener: starts listening to ipcRenderer events', t => { 22 | const on = sand.stub(); 23 | window.electron = {ipcRenderer: {on}}; 24 | 25 | t.equal(listener(), true, 'returns true'); 26 | 27 | t.equal(on.calledWith('lav:settings'), true, 'listens to lav:settings'); 28 | t.equal(on.calledWith('lav:newNote'), true, 'listens to lav:newNote'); 29 | t.equal(on.calledWith('lav:about'), true, 'listens to lav:about'); 30 | 31 | t.equal(on.calledWith('lav:import:evernote'), true, 'listens to lav:import:evernote'); 32 | t.equal(on.calledWith('lav:backup:key'), true, 'listens to lav:backup:key'); 33 | t.equal(on.calledWith('lav:backup:data'), true, 'listens to lav:backup:data'); 34 | 35 | window.electron = null; 36 | sand.restore(); 37 | t.end(); 38 | }); 39 | -------------------------------------------------------------------------------- /test/tape/utils/theme.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test: utils/theme.js 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | import Radio from 'backbone.radio'; 8 | import theme from '../../../app/scripts/utils/theme'; 9 | 10 | let sand; 11 | test('utils/theme: before()', t => { 12 | sand = sinon.sandbox.create(); 13 | t.end(); 14 | }); 15 | 16 | test('utils/theme: applyTheme()', t => { 17 | const attr = sand.stub(); 18 | global.$ = sand.stub().returns({attr}); 19 | 20 | sand.stub(Radio, 'request') 21 | .withArgs('collections/Configs', 'findConfig', {name: 'theme'}) 22 | .returns('default'); 23 | 24 | theme.applyTheme(); 25 | t.equal(attr.calledWithMatch('href', 'styles/theme-default.css'), true, 26 | 'applies the default theme'); 27 | 28 | theme.applyTheme({name: 'dark'}); 29 | t.equal(attr.calledWith('href', 'styles/theme-dark.css'), true, 30 | 'applies the dark theme'); 31 | 32 | sand.restore(); 33 | t.end(); 34 | }); 35 | 36 | test('utils/theme: initializer()', t => { 37 | const on = sand.stub(Radio, 'on'); 38 | sand.stub(theme, 'applyTheme'); 39 | 40 | theme.initializer(); 41 | 42 | t.equal(theme.applyTheme.called, true, 'applies the theme'); 43 | t.equal(on.calledWith('components/settings', 'changeTheme', theme.applyTheme), 44 | true, 'listens to "changeTheme" event'); 45 | 46 | sand.restore(); 47 | t.end(); 48 | }); 49 | -------------------------------------------------------------------------------- /test/tape/views/brand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test views/Brand 3 | * @file 4 | */ 5 | import test from 'tape'; 6 | import sinon from 'sinon'; 7 | 8 | import Brand from '../../../app/scripts/views/Brand'; 9 | const brand = Brand.prototype; 10 | 11 | let sand; 12 | test('views/Brand: before()', t => { 13 | sand = sinon.sandbox.create(); 14 | t.end(); 15 | }); 16 | 17 | test('views/Brand: onShow()', t => { 18 | brand.$el = {slideDown: sand.stub()}; 19 | 20 | brand.onShow(); 21 | t.equal(brand.$el.slideDown.calledWith('fast'), true, 22 | 'shows the region with animation'); 23 | 24 | brand.$el = null; 25 | t.end(); 26 | }); 27 | 28 | test('views/Brand: onEmpty()', t => { 29 | brand.$el = {slideUp: sand.stub()}; 30 | 31 | brand.onEmpty(); 32 | t.equal(brand.$el.slideUp.calledWith('fast'), true, 33 | 'hides the region with animation'); 34 | 35 | brand.$el = null; 36 | t.end(); 37 | }); 38 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Webpack configuration file for production. 5 | * 6 | * @file 7 | */ 8 | const webpack = require('webpack'), 9 | config = require('./webpack.config'); 10 | 11 | // Disable source maps 12 | config.devtool = false; 13 | 14 | config.plugins = config.plugins.concat([ 15 | 16 | // Optimize chunk IDs 17 | new webpack.optimize.OccurrenceOrderPlugin(), 18 | 19 | // Minimize code 20 | new webpack.optimize.UglifyJsPlugin(), 21 | 22 | // Deduplicate libraries 23 | new webpack.optimize.DedupePlugin(), 24 | 25 | ]); 26 | 27 | module.exports = config; 28 | --------------------------------------------------------------------------------