mirror of
https://github.com/standardnotes/app
synced 2026-01-16 19:04:58 -05:00
initial commit
This commit is contained in:
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||
#
|
||||
# If you find yourself ignoring temporary files generated by your text editor
|
||||
# or operating system, you probably want to add a global ignore instead:
|
||||
# git config --global core.excludesfile '~/.gitignore_global'
|
||||
|
||||
# OS & IDE
|
||||
.DS_Store
|
||||
|
||||
# Ignore bundler config.
|
||||
/.bundle
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
/log/*
|
||||
!/log/.keep
|
||||
/tmp
|
||||
|
||||
/app/assets/templates/generated/
|
||||
|
||||
# Ignore bower components
|
||||
/vendor/assets/bower_components/
|
||||
/node_modules
|
||||
/vendor/assets/javascripts/app.js
|
||||
/vendor/assets/javascripts/compiled.js
|
||||
/vendor/assets/javascripts/compiled.min.js
|
||||
/vendor/assets/javascripts/lib.js
|
||||
/vendor/assets/javascripts/templates.js
|
||||
|
||||
/vendor/assets/stylesheets/app.css
|
||||
/vendor/assets/stylesheets/app.css.map
|
||||
|
||||
/.sass-cache
|
||||
|
||||
# Ignore ENV variables config
|
||||
.env
|
||||
.ssh
|
||||
|
||||
dump.rdb
|
||||
|
||||
# Ignore compiled assets
|
||||
/public/assets
|
||||
|
||||
# Ignore user uploads
|
||||
/public/uploads/*
|
||||
!/public/uploads/.keep
|
||||
31
Capfile
Normal file
31
Capfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# Load DSL and set up stages
|
||||
require "capistrano/setup"
|
||||
|
||||
# Include default deployment tasks
|
||||
require "capistrano/deploy"
|
||||
|
||||
# Include tasks from other gems included in your Gemfile
|
||||
#
|
||||
# For documentation on these, see for example:
|
||||
#
|
||||
# https://github.com/capistrano/rvm
|
||||
# https://github.com/capistrano/rbenv
|
||||
# https://github.com/capistrano/chruby
|
||||
# https://github.com/capistrano/bundler
|
||||
# https://github.com/capistrano/rails
|
||||
# https://github.com/capistrano/passenger
|
||||
#
|
||||
require 'capistrano/rvm'
|
||||
# require 'capistrano/rbenv'
|
||||
# require 'capistrano/chruby'
|
||||
require 'capistrano/bundler'
|
||||
require 'capistrano/rails/assets'
|
||||
# require 'capistrano/rails/migrations'
|
||||
require 'capistrano/passenger'
|
||||
# require 'capistrano/sidekiq'
|
||||
require 'capistrano/git-submodule-strategy'
|
||||
# require "whenever/capistrano" # Update crontab on deploy
|
||||
|
||||
|
||||
# Load custom tasks from `lib/capistrano/tasks` if you have any defined
|
||||
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }
|
||||
48
Gemfile
Normal file
48
Gemfile
Normal file
@@ -0,0 +1,48 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||
gem 'rails', '5.0.0.1'
|
||||
|
||||
gem 'sass'
|
||||
|
||||
gem "non-stupid-digest-assets"
|
||||
|
||||
gem 'uglifier'
|
||||
|
||||
gem 'activemodel-serializers-xml'
|
||||
|
||||
gem 'rack-cors', :require => 'rack/cors'
|
||||
|
||||
gem 'dotenv-rails', '~> 2.1.1'
|
||||
|
||||
gem 'bower-rails', '~> 0.10.0'
|
||||
|
||||
gem 'devise_token_auth', '~> 0.1.39'
|
||||
|
||||
# bundle exec rake doc:rails generates the API under doc/api.
|
||||
gem 'sdoc', '~> 0.4.0', group: :doc
|
||||
|
||||
# Used for 'respond_to' feature
|
||||
gem 'responders', '~> 2.0'
|
||||
|
||||
group :development, :test do
|
||||
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
|
||||
gem 'byebug'
|
||||
|
||||
# Access an IRB console on exception pages or by using <%= console %> in views
|
||||
gem 'web-console', '~> 2.0'
|
||||
|
||||
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
||||
gem 'spring'
|
||||
|
||||
gem 'puma'
|
||||
|
||||
# Deployment tools
|
||||
gem 'capistrano'
|
||||
gem 'capistrano-bundler'
|
||||
gem 'capistrano-passenger', '>= 0.2.0'
|
||||
gem 'capistrano-rails'
|
||||
gem 'capistrano-rvm'
|
||||
gem 'capistrano-sidekiq'
|
||||
gem 'capistrano-git-submodule-strategy', '~> 0.1.22'
|
||||
end
|
||||
220
Gemfile.lock
Normal file
220
Gemfile.lock
Normal file
@@ -0,0 +1,220 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (5.0.0.1)
|
||||
actionpack (= 5.0.0.1)
|
||||
nio4r (~> 1.2)
|
||||
websocket-driver (~> 0.6.1)
|
||||
actionmailer (5.0.0.1)
|
||||
actionpack (= 5.0.0.1)
|
||||
actionview (= 5.0.0.1)
|
||||
activejob (= 5.0.0.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.0.0.1)
|
||||
actionview (= 5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
rack (~> 2.0)
|
||||
rack-test (~> 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
builder (~> 3.1)
|
||||
erubis (~> 2.7.0)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
activejob (5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
activemodel-serializers-xml (1.0.1)
|
||||
activemodel (> 5.x)
|
||||
activerecord (> 5.x)
|
||||
activesupport (> 5.x)
|
||||
builder (~> 3.1)
|
||||
activerecord (5.0.0.1)
|
||||
activemodel (= 5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
arel (~> 7.0)
|
||||
activesupport (5.0.0.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
airbrussh (1.1.1)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
arel (7.1.4)
|
||||
bcrypt (3.1.11)
|
||||
binding_of_caller (0.7.2)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bower-rails (0.10.0)
|
||||
builder (3.2.2)
|
||||
byebug (9.0.6)
|
||||
capistrano (3.6.1)
|
||||
airbrussh (>= 1.0.0)
|
||||
capistrano-harrow
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
sshkit (>= 1.9.0)
|
||||
capistrano-bundler (1.2.0)
|
||||
capistrano (~> 3.1)
|
||||
sshkit (~> 1.2)
|
||||
capistrano-git-submodule-strategy (0.1.22)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-harrow (0.5.3)
|
||||
capistrano-passenger (0.2.0)
|
||||
capistrano (~> 3.0)
|
||||
capistrano-rails (1.2.0)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (~> 1.1)
|
||||
capistrano-rvm (0.1.2)
|
||||
capistrano (~> 3.0)
|
||||
sshkit (~> 1.2)
|
||||
capistrano-sidekiq (0.10.0)
|
||||
capistrano
|
||||
sidekiq (>= 3.4)
|
||||
concurrent-ruby (1.0.2)
|
||||
connection_pool (2.2.1)
|
||||
debug_inspector (0.0.2)
|
||||
devise (4.2.0)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0, < 5.1)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise_token_auth (0.1.39)
|
||||
devise (> 3.5.2, <= 4.2)
|
||||
rails (< 6)
|
||||
dotenv (2.1.1)
|
||||
dotenv-rails (2.1.1)
|
||||
dotenv (= 2.1.1)
|
||||
railties (>= 4.0, < 5.1)
|
||||
erubis (2.7.0)
|
||||
execjs (2.7.0)
|
||||
globalid (0.3.7)
|
||||
activesupport (>= 4.1.0)
|
||||
i18n (0.7.0)
|
||||
json (1.8.3)
|
||||
loofah (2.0.3)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.6.4)
|
||||
mime-types (>= 1.16, < 4)
|
||||
method_source (0.8.2)
|
||||
mime-types (3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.9.1)
|
||||
net-scp (1.2.1)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (3.2.0)
|
||||
nio4r (1.2.1)
|
||||
nokogiri (1.6.8.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
non-stupid-digest-assets (1.0.9)
|
||||
sprockets (>= 2.0)
|
||||
orm_adapter (0.5.0)
|
||||
puma (3.6.2)
|
||||
rack (2.0.1)
|
||||
rack-cors (0.4.0)
|
||||
rack-protection (1.5.3)
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
rails (5.0.0.1)
|
||||
actioncable (= 5.0.0.1)
|
||||
actionmailer (= 5.0.0.1)
|
||||
actionpack (= 5.0.0.1)
|
||||
actionview (= 5.0.0.1)
|
||||
activejob (= 5.0.0.1)
|
||||
activemodel (= 5.0.0.1)
|
||||
activerecord (= 5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 5.0.0.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.1)
|
||||
activesupport (>= 4.2.0, < 6.0)
|
||||
nokogiri (~> 1.6.0)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
railties (5.0.0.1)
|
||||
actionpack (= 5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rake (11.3.0)
|
||||
rdoc (4.3.0)
|
||||
redis (3.3.2)
|
||||
responders (2.3.0)
|
||||
railties (>= 4.2.0, < 5.1)
|
||||
sass (3.4.22)
|
||||
sdoc (0.4.2)
|
||||
json (~> 1.7, >= 1.7.7)
|
||||
rdoc (~> 4.0)
|
||||
sidekiq (4.2.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (~> 3.2, >= 3.2.1)
|
||||
spring (2.0.0)
|
||||
activesupport (>= 4.2)
|
||||
sprockets (3.7.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.0)
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sshkit (1.11.4)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
thor (0.19.4)
|
||||
thread_safe (0.3.5)
|
||||
tzinfo (1.2.2)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (3.0.3)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
warden (1.2.6)
|
||||
rack (>= 1.0)
|
||||
web-console (2.3.0)
|
||||
activemodel (>= 4.0)
|
||||
binding_of_caller (>= 0.7.2)
|
||||
railties (>= 4.0)
|
||||
sprockets-rails (>= 2.0, < 4.0)
|
||||
websocket-driver (0.6.4)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.2)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
activemodel-serializers-xml
|
||||
bower-rails (~> 0.10.0)
|
||||
byebug
|
||||
capistrano
|
||||
capistrano-bundler
|
||||
capistrano-git-submodule-strategy (~> 0.1.22)
|
||||
capistrano-passenger (>= 0.2.0)
|
||||
capistrano-rails
|
||||
capistrano-rvm
|
||||
capistrano-sidekiq
|
||||
devise_token_auth (~> 0.1.39)
|
||||
dotenv-rails (~> 2.1.1)
|
||||
non-stupid-digest-assets
|
||||
puma
|
||||
rack-cors
|
||||
rails (= 5.0.0.1)
|
||||
responders (~> 2.0)
|
||||
sass
|
||||
sdoc (~> 0.4.0)
|
||||
spring
|
||||
uglifier
|
||||
web-console (~> 2.0)
|
||||
|
||||
BUNDLED WITH
|
||||
1.13.6
|
||||
149
Gruntfile.js
Normal file
149
Gruntfile.js
Normal file
@@ -0,0 +1,149 @@
|
||||
module.exports = function(grunt) {
|
||||
|
||||
grunt.initConfig({
|
||||
|
||||
watch: {
|
||||
haml: {
|
||||
files: ['app/assets/templates/**/*.haml'],
|
||||
tasks: ['newer:haml', 'ngtemplates', 'concat'],
|
||||
options: {
|
||||
spawn: false,
|
||||
},
|
||||
},
|
||||
|
||||
js: {
|
||||
files: ['app/assets/javascripts/**/*.js'],
|
||||
tasks: ['concat'],
|
||||
options: {
|
||||
spawn: false,
|
||||
},
|
||||
},
|
||||
|
||||
css: {
|
||||
files: ['app/assets/stylesheets/**/*.scss'],
|
||||
tasks: ['sass', 'concat'],
|
||||
options: {
|
||||
spawn: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
sass: {
|
||||
dist: {
|
||||
options: {
|
||||
style: 'expanded'
|
||||
},
|
||||
files: {
|
||||
'vendor/assets/stylesheets/app.css': 'app/assets/stylesheets/frontend.css.scss'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
haml: {
|
||||
dist: {
|
||||
expand: true,
|
||||
ext: '.html',
|
||||
extDot: 'last',
|
||||
src: ['app/assets/templates/**/*.haml'],
|
||||
dest: 'app/assets/templates/generated/',
|
||||
rename: function (dest, src) {
|
||||
return dest + src.replace(".html", "");
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
ngtemplates: {
|
||||
templates: {
|
||||
cwd: 'app/assets/templates/generated/app/assets/templates',
|
||||
src: ['**/*.html'],
|
||||
dest: 'vendor/assets/javascripts/templates.js',
|
||||
options: {
|
||||
module: 'app.frontend'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
concat: {
|
||||
options: {
|
||||
separator: ';',
|
||||
},
|
||||
app: {
|
||||
src: [
|
||||
'app/assets/javascripts/app/*.js',
|
||||
'app/assets/javascripts/app/frontend/*.js',
|
||||
'app/assets/javascripts/app/frontend/controllers/*.js',
|
||||
'app/assets/javascripts/app/frontend/models/*.js',
|
||||
'app/assets/javascripts/app/services/*.js',
|
||||
'app/assets/javascripts/app/services/directives/*.js',
|
||||
'app/assets/javascripts/app/services/helpers/*.js',
|
||||
],
|
||||
dest: 'vendor/assets/javascripts/app.js',
|
||||
},
|
||||
|
||||
lib: {
|
||||
src: [
|
||||
'vendor/assets/bower_components/angular/angular.js',
|
||||
'vendor/assets/bower_components/angular-ui-router/release/angular-ui-router.js',
|
||||
'vendor/assets/bower_components/ng-token-auth/dist/ng-token-auth.js',
|
||||
'vendor/assets/bower_components/angular-cookie/angular-cookie.js',
|
||||
'vendor/assets/bower_components/lodash/dist/lodash.js',
|
||||
'vendor/assets/bower_components/restangular/dist/restangular.js',
|
||||
'vendor/assets/bower_components/marked/lib/marked.js',
|
||||
'vendor/assets/bower_components/oclazyload/dist/ocLazyLoad.js',
|
||||
'vendor/assets/bower_components/angular-lazy-img/release/angular-lazy-img.js',
|
||||
'vendor/assets/bower_components/ng-dialog/js/ngDialog.min.js',
|
||||
'vendor/assets/javascripts/crypto/*.js'
|
||||
],
|
||||
dest: 'vendor/assets/javascripts/lib.js',
|
||||
},
|
||||
|
||||
dist: {
|
||||
src: ['vendor/assets/javascripts/lib.js', 'vendor/assets/javascripts/app.js', 'vendor/assets/javascripts/templates.js'],
|
||||
dest: 'vendor/assets/javascripts/compiled.js',
|
||||
},
|
||||
|
||||
css: {
|
||||
src: [
|
||||
'vendor/assets/stylesheets/app.css',
|
||||
'vendor/assets/bower_components/ng-dialog/css/ngDialog.css',
|
||||
'vendor/assets/bower_components/ng-dialog/css/ngDialog-theme-default.css',
|
||||
'vendor/assets/bower_components/angular-typewrite/dist/angular-typewrite.css',
|
||||
],
|
||||
dest: 'vendor/assets/stylesheets/app.css'
|
||||
}
|
||||
},
|
||||
|
||||
ngAnnotate: {
|
||||
options: {
|
||||
singleQuotes: true,
|
||||
},
|
||||
|
||||
neeto: {
|
||||
files: {
|
||||
'vendor/assets/javascripts/compiled.js': 'vendor/assets/javascripts/compiled.js',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
uglify: {
|
||||
compiled: {
|
||||
src: ['vendor/assets/javascripts/compiled.js'],
|
||||
dest: 'vendor/assets/javascripts/compiled.min.js'
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
grunt.loadNpmTasks('grunt-newer');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-haml2html');
|
||||
grunt.loadNpmTasks('grunt-angular-templates');
|
||||
grunt.loadNpmTasks('grunt-contrib-cssmin');
|
||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||
grunt.loadNpmTasks('grunt-contrib-sass');
|
||||
grunt.loadNpmTasks('grunt-ng-annotate');
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
|
||||
grunt.registerTask('default', ['haml', 'ngtemplates', 'sass', 'concat', 'ngAnnotate', 'uglify']);
|
||||
};
|
||||
28
README.rdoc
Normal file
28
README.rdoc
Normal file
@@ -0,0 +1,28 @@
|
||||
== README
|
||||
|
||||
This README would normally document whatever steps are necessary to get the
|
||||
application up and running.
|
||||
|
||||
Things you may want to cover:
|
||||
|
||||
* Ruby version
|
||||
|
||||
* System dependencies
|
||||
|
||||
* Configuration
|
||||
|
||||
* Database creation
|
||||
|
||||
* Database initialization
|
||||
|
||||
* How to run the test suite
|
||||
|
||||
* Services (job queues, cache servers, search engines, etc.)
|
||||
|
||||
* Deployment instructions
|
||||
|
||||
* ...
|
||||
|
||||
|
||||
Please feel free to use a different markup language if you do not plan to run
|
||||
<tt>rake doc:app</tt>.
|
||||
6
Rakefile
Normal file
6
Rakefile
Normal file
@@ -0,0 +1,6 @@
|
||||
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
||||
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
||||
|
||||
require File.expand_path('../config/application', __FILE__)
|
||||
|
||||
Rails.application.load_tasks
|
||||
0
app/assets/images/.keep
Normal file
0
app/assets/images/.keep
Normal file
BIN
app/assets/images/archive.png
Normal file
BIN
app/assets/images/archive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/assets/images/encryption.png
Normal file
BIN
app/assets/images/encryption.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/assets/images/filter.png
Normal file
BIN
app/assets/images/filter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/assets/images/logo.png
Normal file
BIN
app/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
16
app/assets/javascripts/app/app.frontend.js
Normal file
16
app/assets/javascripts/app/app.frontend.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
var Neeto = Neeto || {};
|
||||
|
||||
angular
|
||||
.module('app.frontend', [
|
||||
'app.services',
|
||||
'ui.router',
|
||||
'ng-token-auth',
|
||||
'restangular',
|
||||
'ipCookie',
|
||||
'oc.lazyLoad',
|
||||
'angularLazyImg',
|
||||
'ngDialog',
|
||||
])
|
||||
.config(configureAuth);
|
||||
23
app/assets/javascripts/app/app.services.js
Normal file
23
app/assets/javascripts/app/app.services.js
Normal file
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('app.services', [
|
||||
'restangular'
|
||||
])
|
||||
|
||||
// Configure path to API
|
||||
.config(function (RestangularProvider, apiControllerProvider) {
|
||||
var url = apiControllerProvider.defaultServerURL();
|
||||
RestangularProvider.setBaseUrl(url);
|
||||
})
|
||||
|
||||
// Shared function for configure auth service. Can be overwritten.
|
||||
function configureAuth ($authProvider, apiControllerProvider) {
|
||||
var url = apiControllerProvider.defaultServerURL();
|
||||
$authProvider.configure([{
|
||||
default: {
|
||||
apiUrl: url,
|
||||
passwordResetSuccessUrl: window.location.protocol + '//' + window.location.host + '/auth/reset',
|
||||
}
|
||||
}]);
|
||||
}
|
||||
20
app/assets/javascripts/app/frontend/controllers/_base.js
Normal file
20
app/assets/javascripts/app/frontend/controllers/_base.js
Normal file
@@ -0,0 +1,20 @@
|
||||
angular.module('app.frontend')
|
||||
.controller('BaseCtrl', function ($rootScope, $scope, $state, $auth, apiController) {
|
||||
$rootScope.$on('auth:password-change-success', function(ev) {
|
||||
$state.go("home");
|
||||
});
|
||||
|
||||
$rootScope.$on('auth:password-change-error', function(ev, reason) {
|
||||
alert("Error: " + reason);
|
||||
});
|
||||
|
||||
$rootScope.resetPasswordSubmit = function() {
|
||||
var new_keys = Neeto.crypto.generateEncryptionKeysForUser($rootScope.resetData.password, $rootScope.resetData.email);
|
||||
var data = _.clone($rootScope.resetData);
|
||||
data.password = new_keys.pw;
|
||||
data.password_confirmation = new_keys.pw;
|
||||
$auth.updatePassword(data);
|
||||
apiController.setGk(new_keys.gk);
|
||||
}
|
||||
|
||||
});
|
||||
296
app/assets/javascripts/app/frontend/controllers/editor.js
Normal file
296
app/assets/javascripts/app/frontend/controllers/editor.js
Normal file
@@ -0,0 +1,296 @@
|
||||
angular.module('app.frontend')
|
||||
.directive("editorSection", function($timeout){
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
save: "&",
|
||||
remove: "&",
|
||||
note: "=",
|
||||
user: "="
|
||||
},
|
||||
templateUrl: 'frontend/editor.html',
|
||||
replace: true,
|
||||
controller: 'EditorCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
|
||||
var handler = function(event) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
switch (String.fromCharCode(event.which).toLowerCase()) {
|
||||
case 's':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.saveNote(event);
|
||||
});
|
||||
break;
|
||||
case 'e':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.clickedEditNote();
|
||||
})
|
||||
break;
|
||||
case 'm':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.toggleMarkdown();
|
||||
})
|
||||
break;
|
||||
case 'o':
|
||||
event.preventDefault();
|
||||
$timeout(function(){
|
||||
ctrl.toggleFullScreen();
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handler);
|
||||
|
||||
scope.$on('$destroy', function(){
|
||||
window.removeEventListener('keydown', handler);
|
||||
})
|
||||
|
||||
scope.$watch('ctrl.note', function(note, oldNote){
|
||||
if(note) {
|
||||
ctrl.setNote(note, oldNote);
|
||||
} else {
|
||||
ctrl.note = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope) {
|
||||
|
||||
this.demoNotes = [
|
||||
{name: "Live print a file with tail", content: "tail -f log/production.log"},
|
||||
{name: "Create SSH tunnel", content: "ssh -i .ssh/key.pem -N -L 3306:example.com:3306 ec2-user@example.com"},
|
||||
{name: "List of processes running on port", content: "lsof -i:8080"},
|
||||
{name: "Set ENV from file", content: "export $(cat .envfile | xargs)"},
|
||||
{name: "Find process by name", content: "ps -ax | grep <application name>"},
|
||||
{name: "NPM install without sudo", content: "sudo chown -R $(whoami) ~/.npm"},
|
||||
{name: "Email validation regex", content: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"},
|
||||
{name: "Ruby generate 256 bit key", content: "Digest::SHA256.hexdigest(SecureRandom.random_bytes(32))"},
|
||||
{name: "Mac add user to user group", content: "sudo dseditgroup -o edit -a USERNAME -t user GROUPNAME"},
|
||||
{name: "Kill Mac OS System Apache", content: "sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist"},
|
||||
{name: "Docker run with mount binding and port", content: "docker run -v /home/vagrant/www/app:/var/www/app -p 8080:80 -d kpi/s3"},
|
||||
{name: "MySQL grant privileges", content: "GRANT [type of permission] ON [database name].[table name] TO ‘[username]’@'%’;"},
|
||||
{name: "MySQL list users", content: "SELECT User FROM mysql.user;"},
|
||||
];
|
||||
|
||||
this.showSampler = !this.user.id && this.user.filteredNotes().length == 0;
|
||||
|
||||
this.demoNoteNames = _.map(this.demoNotes, function(note){
|
||||
return note.name;
|
||||
});
|
||||
|
||||
this.currentDemoContent = {text: null};
|
||||
|
||||
this.prebeginFn = function() {
|
||||
this.currentDemoContent.text = null;
|
||||
}.bind(this)
|
||||
|
||||
this.callback = function(index) {
|
||||
this.currentDemoContent.text = this.demoNotes[index].content;
|
||||
}.bind(this)
|
||||
|
||||
this.contentCallback = function(index) {
|
||||
}
|
||||
|
||||
this.setNote = function(note, oldNote) {
|
||||
this.editorMode = 'edit';
|
||||
if(note.content.length == 0) {
|
||||
this.focusTitle(100);
|
||||
}
|
||||
|
||||
if(oldNote && oldNote != note) {
|
||||
if(oldNote.hasChanges) {
|
||||
this.save()(oldNote, null);
|
||||
} else if(oldNote.dummy) {
|
||||
this.remove()(oldNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.onPreviewDoubleClick = function() {
|
||||
this.editorMode = 'edit';
|
||||
this.focusEditor(100);
|
||||
}
|
||||
|
||||
this.focusEditor = function(delay) {
|
||||
setTimeout(function(){
|
||||
var element = document.getElementById("note-text-editor");
|
||||
element.focus();
|
||||
}, delay)
|
||||
}
|
||||
|
||||
this.focusTitle = function(delay) {
|
||||
setTimeout(function(){
|
||||
document.getElementById("note-title-editor").focus();
|
||||
}, delay)
|
||||
}
|
||||
|
||||
this.clickedTextArea = function() {
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.renderedContent = function() {
|
||||
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText(this.note.content));
|
||||
}
|
||||
|
||||
var statusTimeout;
|
||||
|
||||
this.saveNote = function($event) {
|
||||
var note = this.note;
|
||||
note.dummy = false;
|
||||
this.save()(note, function(success){
|
||||
if(success) {
|
||||
apiController.clearDraft();
|
||||
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
statusTimeout = $timeout(function(){
|
||||
this.noteStatus = "All changes saved"
|
||||
}.bind(this), 200)
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.saveTitle = function($event) {
|
||||
$event.target.blur();
|
||||
this.saveNote($event);
|
||||
this.focusEditor();
|
||||
}
|
||||
|
||||
var saveTimeout;
|
||||
this.changesMade = function() {
|
||||
this.note.hasChanges = true;
|
||||
this.note.dummy = false;
|
||||
apiController.saveDraftToDisk(this.note);
|
||||
|
||||
if(saveTimeout) $timeout.cancel(saveTimeout);
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
saveTimeout = $timeout(function(){
|
||||
this.noteStatus = "Saving...";
|
||||
this.saveNote();
|
||||
}.bind(this), 150)
|
||||
}
|
||||
|
||||
|
||||
this.contentChanged = function() {
|
||||
this.changesMade();
|
||||
}
|
||||
|
||||
this.nameChanged = function() {
|
||||
this.changesMade();
|
||||
}
|
||||
|
||||
this.onNameFocus = function() {
|
||||
this.editingName = true;
|
||||
}
|
||||
|
||||
this.onContentFocus = function() {
|
||||
this.showSampler = false;
|
||||
$rootScope.$broadcast("editorFocused");
|
||||
this.editingUrl = false;
|
||||
}
|
||||
|
||||
this.onNameBlur = function() {
|
||||
this.editingName = false;
|
||||
}
|
||||
|
||||
this.toggleFullScreen = function() {
|
||||
this.fullscreen = !this.fullscreen;
|
||||
if(this.fullscreen) {
|
||||
if(this.editorMode == 'edit') {
|
||||
// refocus
|
||||
this.focusEditor(0);
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedMenuItem = function() {
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.toggleMarkdown = function() {
|
||||
if(this.editorMode == 'preview') {
|
||||
this.editorMode = 'edit';
|
||||
} else {
|
||||
this.editorMode = 'preview';
|
||||
}
|
||||
}
|
||||
|
||||
this.editUrlPressed = function() {
|
||||
this.showMenu = false;
|
||||
var url = this.publicUrlForNote(this.note);
|
||||
url = url.replace(this.note.presentation.root_path, "");
|
||||
this.url = {base: url, token : this.note.presentation.root_path};
|
||||
this.editingUrl = true;
|
||||
}
|
||||
|
||||
this.saveUrl = function($event) {
|
||||
$event.target.blur();
|
||||
var original = this.note.presentation.root_path;
|
||||
this.note.presentation.root_path = this.url.token;
|
||||
apiController.saveNote(this.user, this.note, function(note){
|
||||
if(!note) {
|
||||
this.note.token = original;
|
||||
this.url.token = original;
|
||||
alert("This URL is not available.");
|
||||
} else {
|
||||
this.editingUrl = false;
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.shareNote = function() {
|
||||
|
||||
function openInNewTab(url) {
|
||||
var a = document.createElement("a");
|
||||
a.target = "_blank";
|
||||
a.href = url;
|
||||
a.click();
|
||||
}
|
||||
|
||||
apiController.shareNote(this.user, this.note, function(note){
|
||||
openInNewTab(this.publicUrlForNote(note));
|
||||
}.bind(this))
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.unshareNote = function() {
|
||||
apiController.unshareNote(this.user, this.note, function(note){
|
||||
|
||||
})
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.publicUrlForNote = function() {
|
||||
return this.note.presentationURL();
|
||||
}
|
||||
|
||||
this.clickedMenu = function() {
|
||||
if(this.note.locked) {
|
||||
alert("This note has been shared without an account, and can therefore not be changed.")
|
||||
} else {
|
||||
this.showMenu = !this.showMenu;
|
||||
}
|
||||
}
|
||||
|
||||
this.deleteNote = function() {
|
||||
apiController.clearDraft();
|
||||
this.remove()(this.note);
|
||||
this.showMenu = false;
|
||||
}
|
||||
|
||||
this.clickedEditNote = function() {
|
||||
this.editorMode = 'edit';
|
||||
this.focusEditor(100);
|
||||
}
|
||||
|
||||
});
|
||||
116
app/assets/javascripts/app/frontend/controllers/groups.js
Normal file
116
app/assets/javascripts/app/frontend/controllers/groups.js
Normal file
@@ -0,0 +1,116 @@
|
||||
angular.module('app.frontend')
|
||||
.directive("groupsSection", function(){
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
addNew: "&",
|
||||
selectionMade: "&",
|
||||
willSelect: "&",
|
||||
save: "&",
|
||||
groups: "=",
|
||||
allGroup: "=",
|
||||
user: "=",
|
||||
updateNoteGroup: "&"
|
||||
},
|
||||
templateUrl: 'frontend/groups.html',
|
||||
replace: true,
|
||||
controller: 'GroupsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
scope.$watch('ctrl.groups', function(newGroups){
|
||||
if(newGroups) {
|
||||
ctrl.setGroups(newGroups);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('GroupsCtrl', function (apiController) {
|
||||
|
||||
var initialLoad = true;
|
||||
|
||||
this.setGroups = function(groups) {
|
||||
if(initialLoad) {
|
||||
initialLoad = false;
|
||||
this.selectGroup(this.allGroup);
|
||||
} else {
|
||||
if(groups && groups.length > 0) {
|
||||
this.selectGroup(groups[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.selectGroup = function(group) {
|
||||
this.willSelect()(group);
|
||||
this.selectedGroup = group;
|
||||
this.selectionMade()(group);
|
||||
}
|
||||
|
||||
this.clickedAddNewGroup = function() {
|
||||
if(this.editingGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.newGroup = {notes : []};
|
||||
if(!this.user.id) {
|
||||
this.newGroup.id = Neeto.crypto.generateRandomKey()
|
||||
}
|
||||
this.selectedGroup = this.newGroup;
|
||||
this.editingGroup = this.newGroup;
|
||||
this.addNew()(this.newGroup);
|
||||
}
|
||||
|
||||
var originalGroupName = "";
|
||||
this.onGroupTitleFocus = function(group) {
|
||||
originalGroupName = group.name;
|
||||
}
|
||||
|
||||
this.groupTitleDidChange = function(group) {
|
||||
this.editingGroup = group;
|
||||
}
|
||||
|
||||
this.saveGroup = function($event, group) {
|
||||
this.editingGroup = null;
|
||||
if(group.name.length == 0) {
|
||||
group.name = originalGroupName;
|
||||
originalGroupName = "";
|
||||
return;
|
||||
}
|
||||
|
||||
$event.target.blur();
|
||||
if(!group.name || group.name.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.save()(group, function(savedGroup){
|
||||
_.merge(group, savedGroup);
|
||||
this.selectGroup(group);
|
||||
this.newGroup = null;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.noteCount = function(group) {
|
||||
var validNotes = apiController.filterDummyNotes(group.notes);
|
||||
return validNotes.length;
|
||||
}
|
||||
|
||||
this.handleDrop = function(e, newGroup, note) {
|
||||
if(this.selectedGroup.all) {
|
||||
// coming from all, remove from original group if applicable
|
||||
if(note.group_id) {
|
||||
var originalGroup = this.groups.filter(function(group){
|
||||
return group.id == note.group_id;
|
||||
})[0];
|
||||
_.remove(originalGroup.notes, note);
|
||||
}
|
||||
} else {
|
||||
_.remove(this.selectedGroup.notes, note);
|
||||
}
|
||||
|
||||
this.updateNoteGroup()(note, newGroup, this.selectedGroup);
|
||||
}.bind(this)
|
||||
|
||||
|
||||
});
|
||||
232
app/assets/javascripts/app/frontend/controllers/header.js
Normal file
232
app/assets/javascripts/app/frontend/controllers/header.js
Normal file
@@ -0,0 +1,232 @@
|
||||
angular.module('app.frontend')
|
||||
.directive("header", function(){
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
user: "=",
|
||||
logout: "&"
|
||||
},
|
||||
templateUrl: 'frontend/header.html',
|
||||
replace: true,
|
||||
controller: 'HeaderCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
scope.$on('auth:login-success', function(event, user) {
|
||||
ctrl.onAuthSuccess(user);
|
||||
});
|
||||
|
||||
scope.$on('auth:validation-success', function(ev) {
|
||||
setTimeout(function(){
|
||||
ctrl.onValidationSuccess();
|
||||
})
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('HeaderCtrl', function ($auth, $state, apiController, serverSideValidation, $timeout) {
|
||||
|
||||
this.changePasswordPressed = function() {
|
||||
this.showNewPasswordForm = !this.showNewPasswordForm;
|
||||
}
|
||||
|
||||
this.accountMenuPressed = function() {
|
||||
this.serverData = {url: apiController.getServer()};
|
||||
this.showAccountMenu = !this.showAccountMenu;
|
||||
this.showFaq = false;
|
||||
this.showNewPasswordForm = false;
|
||||
}
|
||||
|
||||
this.changeServer = function() {
|
||||
apiController.setServer(this.serverData.url, true);
|
||||
}
|
||||
|
||||
this.signOutPressed = function() {
|
||||
this.showAccountMenu = false;
|
||||
$auth.signOut();
|
||||
this.logout()();
|
||||
apiController.clearGk();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
this.submitPasswordChange = function() {
|
||||
this.passwordChangeData.status = "Generating New Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
var current_keys = Neeto.crypto.generateEncryptionKeysForUser(this.passwordChangeData.current_password, this.user.email);
|
||||
var new_keys = Neeto.crypto.generateEncryptionKeysForUser(this.passwordChangeData.new_password, this.user.email);
|
||||
// var new_pw_conf_keys = Neeto.crypto.generateEncryptionKeysForUser(this.passwordChangeData.new_password_confirmation, this.user.email);
|
||||
|
||||
var data = {};
|
||||
data.current_password = current_keys.pw;
|
||||
data.password = new_keys.pw;
|
||||
data.password_confirmation = new_keys.pw;
|
||||
|
||||
var user = this.user;
|
||||
|
||||
if(data.password == data.password_confirmation) {
|
||||
$auth.updatePassword(data)
|
||||
.then(function(response) {
|
||||
this.showNewPasswordForm = false;
|
||||
if(user.local_encryption_enabled) {
|
||||
// reencrypt data with new gk
|
||||
apiController.reencryptAllNotesAndSave(user, new_keys.gk, current_keys.gk, function(success){
|
||||
if(success) {
|
||||
apiController.setGk(new_keys.gk);
|
||||
alert("Your password has been changed and your data re-encrypted.");
|
||||
} else {
|
||||
// rollback password
|
||||
$auth.updatePassword({current_password: new_keys.pw, password: current_keys.pw, password_confirmation: current_keys.pw })
|
||||
.then(function(response){
|
||||
alert("There was an error changing your password. Your password has been rolled back.");
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert("Your password has been changed.");
|
||||
}
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
this.showNewPasswordForm = false;
|
||||
alert("There was an error changing your password. Please try again.");
|
||||
}.bind(this))
|
||||
|
||||
} else {
|
||||
alert("Your new password does not match its confirmation.");
|
||||
}
|
||||
}.bind(this))
|
||||
|
||||
}
|
||||
|
||||
this.hasLocalData = function() {
|
||||
return this.user.filteredNotes().length > 0;
|
||||
}
|
||||
|
||||
this.mergeLocalChanged = function() {
|
||||
if(!this.user.shouldMerge) {
|
||||
if(!confirm("Unchecking this option means any locally stored groups and notes you have now will be deleted. Are you sure you want to continue?")) {
|
||||
this.user.shouldMerge = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.loginSubmitPressed = function() {
|
||||
this.loginData.status = "Generating Login Keys...";
|
||||
$timeout(function(){
|
||||
var keys = Neeto.crypto.generateEncryptionKeysForUser(this.loginData.user_password, this.loginData.email);
|
||||
var data = {password: keys.pw, email: this.loginData.email};
|
||||
|
||||
apiController.setGk(keys.gk);
|
||||
$auth.submitLogin(data)
|
||||
.then(function(response){
|
||||
|
||||
})
|
||||
.catch(function(response){
|
||||
this.loginData.status = response.errors[0];
|
||||
}.bind(this))
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.submitRegistrationForm = function() {
|
||||
this.loginData.status = "Generating Account Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
var keys = Neeto.crypto.generateEncryptionKeysForUser(this.loginData.user_password, this.loginData.email);
|
||||
var data = {password: keys.pw, email: this.loginData.email};
|
||||
|
||||
apiController.setGk(keys.gk);
|
||||
|
||||
$auth.submitRegistration(data)
|
||||
.then(function(response) {
|
||||
$auth.user.id = response.data.data.id;
|
||||
this.onAuthSuccess($auth.user);
|
||||
}.bind(this))
|
||||
.catch(function(response) {
|
||||
this.loginData.status = response.data.errors.full_messages[0];
|
||||
}.bind(this));
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.forgotPasswordSubmit = function() {
|
||||
$auth.requestPasswordReset(this.resetData)
|
||||
.then(function(resp) {
|
||||
this.resetData.response = "Success";
|
||||
// handle success response
|
||||
}.bind(this))
|
||||
.catch(function(resp) {
|
||||
// handle error response
|
||||
this.resetData.response = "Error";
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.onValidationSuccess = function() {
|
||||
if(this.user.local_encryption_enabled) {
|
||||
apiController.verifyEncryptionStatusOfAllNotes(this.user, function(success){
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.encryptionStatusForNotes = function() {
|
||||
var allNotes = this.user.filteredNotes();
|
||||
var countEncrypted = 0;
|
||||
allNotes.forEach(function(note){
|
||||
if(note.isEncrypted()) {
|
||||
countEncrypted++;
|
||||
}
|
||||
}.bind(this))
|
||||
|
||||
return countEncrypted + "/" + allNotes.length + " notes encrypted";
|
||||
}
|
||||
|
||||
this.toggleEncryptionStatus = function() {
|
||||
this.encryptionConfirmation = true;
|
||||
}
|
||||
|
||||
this.cancelEncryptionChange = function() {
|
||||
this.encryptionConfirmation = false;
|
||||
}
|
||||
|
||||
this.confirmEncryptionChange = function() {
|
||||
|
||||
var callback = function(success, enabled) {
|
||||
if(success) {
|
||||
this.encryptionConfirmation = false;
|
||||
this.user.local_encryption_enabled = enabled;
|
||||
}
|
||||
}.bind(this)
|
||||
|
||||
if(this.user.local_encryption_enabled) {
|
||||
apiController.disableEncryptionForUser(this.user, callback);
|
||||
} else {
|
||||
apiController.enableEncryptionForUser(this.user, callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.downloadDataArchive = function() {
|
||||
var link = document.createElement('a');
|
||||
link.setAttribute('download', 'neeto.json');
|
||||
link.href = apiController.notesDataFile(this.user);
|
||||
link.click();
|
||||
}
|
||||
|
||||
this.onAuthSuccess = function(user) {
|
||||
this.user.id = user.id;
|
||||
|
||||
if(this.user.shouldMerge && this.hasLocalData()) {
|
||||
apiController.mergeLocalDataRemotely(this.user, function(){
|
||||
window.location.reload();
|
||||
});
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
this.showLogin = false;
|
||||
this.showRegistration = false;
|
||||
}
|
||||
|
||||
});
|
||||
188
app/assets/javascripts/app/frontend/controllers/home.js
Normal file
188
app/assets/javascripts/app/frontend/controllers/home.js
Normal file
@@ -0,0 +1,188 @@
|
||||
angular.module('app.frontend')
|
||||
.controller('HomeCtrl', function ($scope, $rootScope, Restangular, $timeout, $state, $sce, $auth, apiController) {
|
||||
$rootScope.bodyClass = "app-body-class";
|
||||
$rootScope.title = "Notes — Neeto, a secure code box for developers";
|
||||
$rootScope.description = "A secure code box for developers to store common commands and useful notes.";
|
||||
|
||||
var onUserSet = function() {
|
||||
|
||||
$scope.defaultUser.notes = _.map($scope.defaultUser.notes, function(json_obj) {
|
||||
return new Note(json_obj);
|
||||
});
|
||||
|
||||
$scope.defaultUser.filteredNotes = function() {
|
||||
return apiController.filterDummyNotes($scope.defaultUser.notes);
|
||||
}
|
||||
var groups = $scope.defaultUser.groups;
|
||||
var allNotes = $scope.defaultUser.notes;
|
||||
groups.forEach(function(group){
|
||||
var notes = allNotes.filter(function(note){return note.group_id && note.group_id == group.id});
|
||||
group.notes = notes;
|
||||
})
|
||||
$scope.allGroup = {name: "All", all: true};
|
||||
$scope.groups = groups;
|
||||
}
|
||||
|
||||
$auth.validateUser()
|
||||
.then(function () {
|
||||
$scope.defaultUser = new User($auth.user);
|
||||
$rootScope.title = "Notes — Neeto";
|
||||
onUserSet();
|
||||
|
||||
})
|
||||
.catch(function () {
|
||||
$scope.defaultUser = new User(apiController.localUser());
|
||||
onUserSet();
|
||||
});
|
||||
|
||||
/*
|
||||
Groups Ctrl Callbacks
|
||||
*/
|
||||
|
||||
$scope.updateAllGroup = function() {
|
||||
var allNotes = apiController.filterDummyNotes($scope.defaultUser.notes);
|
||||
$scope.defaultUser.notes = allNotes;
|
||||
$scope.allGroup.notes = allNotes;
|
||||
}
|
||||
|
||||
$scope.groupsWillMakeSelection = function(group) {
|
||||
if(group.all) {
|
||||
$scope.updateAllGroup();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.groupsSelectionMade = function(group) {
|
||||
if(!group.notes) {
|
||||
group.notes = [];
|
||||
}
|
||||
$scope.selectedGroup = group;
|
||||
}
|
||||
|
||||
$scope.groupsAddNew = function(group) {
|
||||
$scope.defaultUser.groups.unshift(group);
|
||||
}
|
||||
|
||||
$scope.groupsSave = function(group, callback) {
|
||||
apiController.saveGroup($scope.defaultUser, group, callback);
|
||||
}
|
||||
|
||||
/*
|
||||
Called to update the group of a note after drag and drop change
|
||||
The note object is a copy of the original
|
||||
*/
|
||||
$scope.groupsUpdateNoteGroup = function(noteCopy, newGroup, oldGroup) {
|
||||
|
||||
var originalNote = _.find($scope.defaultUser.notes, {id: noteCopy.id});
|
||||
|
||||
if(newGroup.all) {
|
||||
// going to new group, nil out group_id
|
||||
originalNote.group_id = null;
|
||||
} else {
|
||||
originalNote.group_id = newGroup.id
|
||||
newGroup.notes.unshift(originalNote);
|
||||
newGroup.notes.sort(function(a,b){
|
||||
//subtract to get a value that is either negative, positive, or zero.
|
||||
return new Date(b.created_at) - new Date(a.created_at);
|
||||
});
|
||||
}
|
||||
|
||||
originalNote.shared_via_group = newGroup.presentation && newGroup.presentation.enabled;
|
||||
|
||||
apiController.saveNote($scope.defaultUser, originalNote, function(note){
|
||||
_.merge(originalNote, note);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Notes Ctrl Callbacks
|
||||
*/
|
||||
|
||||
$scope.notesRemoveGroup = function(group) {
|
||||
var validNotes = apiController.filterDummyNotes(group.notes);
|
||||
if(validNotes == 0) {
|
||||
// if no more notes, delete group
|
||||
apiController.deleteGroup($scope.defaultUser, group, function(){
|
||||
// force scope groups to update on sub directives
|
||||
$scope.groups = [];
|
||||
$timeout(function(){
|
||||
$scope.groups = $scope.defaultUser.groups;
|
||||
})
|
||||
});
|
||||
} else {
|
||||
alert("To delete this group, remove all its notes first.");
|
||||
}
|
||||
}
|
||||
|
||||
$scope.notesSelectionMade = function(note) {
|
||||
$scope.selectedNote = note;
|
||||
}
|
||||
|
||||
$scope.notesAddNew = function(note) {
|
||||
if(!$scope.defaultUser.id) {
|
||||
// generate local id for note
|
||||
note.id = Neeto.crypto.generateRandomKey();
|
||||
}
|
||||
|
||||
$scope.defaultUser.notes.unshift(note);
|
||||
|
||||
if(!$scope.selectedGroup.all) {
|
||||
$scope.selectedGroup.notes.unshift(note);
|
||||
note.group_id = $scope.selectedGroup.id;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Shared Callbacks
|
||||
*/
|
||||
|
||||
$scope.saveNote = function(note, callback) {
|
||||
apiController.saveNote($scope.defaultUser, note, function(){
|
||||
// add to All notes if it doesnt exist
|
||||
if(!_.find($scope.defaultUser.notes, {id: note.id})) {
|
||||
$scope.defaultUser.notes.unshift(note);
|
||||
}
|
||||
note.hasChanges = false;
|
||||
|
||||
if(callback) {
|
||||
callback(true);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$scope.deleteNote = function(note) {
|
||||
_.remove($scope.defaultUser.notes, note);
|
||||
if($scope.selectedGroup.all && note.group_id) {
|
||||
var originalGroup = _.find($scope.groups, {id: note.group_id});
|
||||
if(originalGroup) {
|
||||
_.remove(originalGroup.notes, note);
|
||||
}
|
||||
} else {
|
||||
_.remove($scope.selectedGroup.notes, note);
|
||||
}
|
||||
|
||||
if(note == $scope.selectedNote) {
|
||||
$scope.selectedNote = null;
|
||||
}
|
||||
|
||||
note.onDelete();
|
||||
|
||||
if(note.dummy) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiController.deleteNote($scope.defaultUser, note, function(success){
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Header Ctrl Callbacks
|
||||
*/
|
||||
|
||||
$scope.headerLogout = function() {
|
||||
$scope.defaultUser = apiController.localUser();
|
||||
$scope.groups = $scope.defaultUser.groups;
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
173
app/assets/javascripts/app/frontend/controllers/notes.js
Normal file
173
app/assets/javascripts/app/frontend/controllers/notes.js
Normal file
@@ -0,0 +1,173 @@
|
||||
angular.module('app.frontend')
|
||||
.directive("notesSection", function(){
|
||||
return {
|
||||
scope: {
|
||||
addNew: "&",
|
||||
selectionMade: "&",
|
||||
remove: "&",
|
||||
group: "=",
|
||||
user: "=",
|
||||
removeGroup: "&"
|
||||
},
|
||||
templateUrl: 'frontend/notes.html',
|
||||
replace: true,
|
||||
controller: 'NotesCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
bindToController: true,
|
||||
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
scope.$watch('ctrl.group', function(group, oldGroup){
|
||||
if(group) {
|
||||
ctrl.groupDidChange(group, oldGroup);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('NotesCtrl', function (apiController, $timeout, ngDialog, $rootScope) {
|
||||
|
||||
$rootScope.$on("editorFocused", function(){
|
||||
this.showMenu = false;
|
||||
}.bind(this))
|
||||
|
||||
var isFirstLoad = true;
|
||||
|
||||
this.groupDidChange = function(group, oldGroup) {
|
||||
this.showMenu = false;
|
||||
|
||||
if(this.selectedNote && this.selectedNote.dummy) {
|
||||
_.remove(oldGroup.notes, this.selectedNote);
|
||||
}
|
||||
|
||||
this.noteFilter.text = "";
|
||||
this.setNotes(group.notes, false);
|
||||
|
||||
if(isFirstLoad) {
|
||||
$timeout(function(){
|
||||
var draft = apiController.getDraft();
|
||||
if(draft) {
|
||||
var note = draft;
|
||||
this.selectNote(note);
|
||||
} else {
|
||||
this.createNewNote();
|
||||
isFirstLoad = false;
|
||||
}
|
||||
}.bind(this))
|
||||
} else if(group.notes.length == 0) {
|
||||
this.createNewNote();
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedGroupDelete = function() {
|
||||
this.showMenu = false;
|
||||
this.removeGroup()(this.group);
|
||||
}
|
||||
|
||||
this.selectedGroupShare = function() {
|
||||
this.showMenu = false;
|
||||
|
||||
if(!this.user.id) {
|
||||
alert("You must be signed in to share a group.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.group.all) {
|
||||
alert("You cannot share the 'All' group.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.user.local_encryption_enabled) {
|
||||
if(!confirm("Sharing this group will disable local encryption on all group notes.")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var callback = function(username) {
|
||||
apiController.shareGroup(this.user, this.group, function(response){
|
||||
})
|
||||
}.bind(this);
|
||||
|
||||
if(!this.user.getUsername()) {
|
||||
ngDialog.open({
|
||||
template: 'frontend/modals/username.html',
|
||||
controller: 'UsernameModalCtrl',
|
||||
resolve: {
|
||||
user: function() {return this.user}.bind(this),
|
||||
callback: function() {return callback}
|
||||
},
|
||||
className: 'ngdialog-theme-default',
|
||||
disableAnimation: true
|
||||
});
|
||||
} else {
|
||||
callback(this.user.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedGroupUnshare = function() {
|
||||
this.showMenu = false;
|
||||
apiController.unshareGroup(this.user, this.group, function(response){
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
this.publicUrlForGroup = function() {
|
||||
return this.group.presentation.url;
|
||||
}
|
||||
|
||||
this.setNotes = function(notes, createNew) {
|
||||
this.notes = notes;
|
||||
notes.forEach(function(note){
|
||||
note.visible = true;
|
||||
})
|
||||
apiController.decryptNotesWithLocalKey(notes);
|
||||
this.selectFirstNote(createNew);
|
||||
}
|
||||
|
||||
this.selectFirstNote = function(createNew) {
|
||||
var visibleNotes = this.notes.filter(function(note){
|
||||
return note.visible;
|
||||
});
|
||||
|
||||
if(visibleNotes.length > 0) {
|
||||
this.selectNote(visibleNotes[0]);
|
||||
} else if(createNew) {
|
||||
this.createNewNote();
|
||||
}
|
||||
}
|
||||
|
||||
this.selectNote = function(note) {
|
||||
this.selectedNote = note;
|
||||
this.selectionMade()(note);
|
||||
|
||||
note.onDelete = function() {
|
||||
this.setNotes(this.group.notes, false);
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
this.createNewNote = function() {
|
||||
var name = "New Note" + (this.notes ? (" " + (this.notes.length + 1)) : "");
|
||||
this.newNote = new Note({name: name, content: '', dummy: true});
|
||||
this.newNote.shared_via_group = this.group.presentation && this.group.presentation.enabled;
|
||||
this.selectNote(this.newNote);
|
||||
this.addNew()(this.newNote);
|
||||
}
|
||||
|
||||
this.noteFilter = {text : ''};
|
||||
|
||||
this.filterNotes = function(note) {
|
||||
if(this.noteFilter.text.length == 0) {
|
||||
note.visible = true;
|
||||
} else {
|
||||
note.visible = note.name.toLowerCase().includes(this.noteFilter.text) || note.content.toLowerCase().includes(this.noteFilter.text);
|
||||
}
|
||||
return note.visible;
|
||||
}.bind(this)
|
||||
|
||||
this.filterTextChanged = function() {
|
||||
$timeout(function(){
|
||||
if(!this.selectedNote.visible) {
|
||||
this.selectFirstNote(false);
|
||||
}
|
||||
}.bind(this), 100)
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
angular.module('app.frontend')
|
||||
.controller('UsernameModalCtrl', function ($scope, apiController, Restangular, user, callback, $timeout) {
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.saveUsername = function() {
|
||||
apiController.setUsername(user, $scope.formData.username, function(response){
|
||||
var username = response.root_path;
|
||||
user.presentation = response;
|
||||
callback(username);
|
||||
$scope.closeThisDialog();
|
||||
})
|
||||
}
|
||||
});
|
||||
20
app/assets/javascripts/app/frontend/models/note.js
Normal file
20
app/assets/javascripts/app/frontend/models/note.js
Normal file
@@ -0,0 +1,20 @@
|
||||
var Note = function (json_obj) {
|
||||
_.merge(this, json_obj);
|
||||
};
|
||||
|
||||
/* Returns true if note is shared individually or via group */
|
||||
Note.prototype.isPublic = function() {
|
||||
return this.hasEnabledPresentation() || this.shared_via_group;
|
||||
};
|
||||
|
||||
Note.prototype.isEncrypted = function() {
|
||||
return this.local_eek ? true : false;
|
||||
}
|
||||
|
||||
Note.prototype.hasEnabledPresentation = function() {
|
||||
return this.presentation && this.presentation.enabled;
|
||||
}
|
||||
|
||||
Note.prototype.presentationURL = function() {
|
||||
return this.presentation.url;
|
||||
}
|
||||
10
app/assets/javascripts/app/frontend/models/user.js
Normal file
10
app/assets/javascripts/app/frontend/models/user.js
Normal file
@@ -0,0 +1,10 @@
|
||||
var User = function (json_obj) {
|
||||
_.merge(this, json_obj);
|
||||
};
|
||||
|
||||
User.prototype.getUsername = function() {
|
||||
if(!this.presentation) {
|
||||
return null;
|
||||
}
|
||||
return this.presentation.root_path;
|
||||
};
|
||||
116
app/assets/javascripts/app/frontend/routes.js
Normal file
116
app/assets/javascripts/app/frontend/routes.js
Normal file
@@ -0,0 +1,116 @@
|
||||
angular.module('app.frontend')
|
||||
.config(function ($stateProvider, $urlRouterProvider, $locationProvider) {
|
||||
|
||||
$stateProvider
|
||||
.state('base', {
|
||||
abstract: true,
|
||||
})
|
||||
|
||||
// Homepage
|
||||
.state('home', {
|
||||
url: '/',
|
||||
parent: 'base',
|
||||
views: {
|
||||
'content@' : {
|
||||
templateUrl: 'frontend/home.html',
|
||||
controller: 'HomeCtrl'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
.state('presentation', {
|
||||
url: '/:root_path',
|
||||
parent: 'base',
|
||||
views: {
|
||||
'content@' : {
|
||||
templateUrl: 'frontend/presentation.html',
|
||||
controller: "PresentationCtrl"
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
presentation: getPresentation
|
||||
}
|
||||
})
|
||||
|
||||
.state('group', {
|
||||
url: '/:root_path/:secondary_path',
|
||||
parent: 'base',
|
||||
views: {
|
||||
'content@' : {
|
||||
templateUrl: 'frontend/presentation.html',
|
||||
controller: "PresentationCtrl"
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
presentation: getPresentation
|
||||
}
|
||||
})
|
||||
|
||||
// Auth routes
|
||||
.state('auth', {
|
||||
abstract: true,
|
||||
url: '/auth',
|
||||
parent: 'base',
|
||||
views: {
|
||||
'content@' : {
|
||||
templateUrl: 'frontend/auth/wrapper.html',
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('auth.login', {
|
||||
url: '/login',
|
||||
templateUrl: 'frontend/auth/login.html',
|
||||
})
|
||||
.state('auth.forgot', {
|
||||
url: '/forgot',
|
||||
templateUrl: 'frontend/auth/forgot.html',
|
||||
})
|
||||
.state('auth.reset', {
|
||||
url: '/reset?reset_password_token&email',
|
||||
templateUrl: 'frontend/auth/reset.html',
|
||||
controller: function($rootScope, $stateParams) {
|
||||
$rootScope.resetData = {reset_password_token: $stateParams.reset_password_token, email: $stateParams.email};
|
||||
|
||||
// Clear reset_password_token on change state
|
||||
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams, options) {
|
||||
$rootScope.reset_password_token = null;
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// 404 Error
|
||||
.state('404', {
|
||||
parent: 'base',
|
||||
views: {
|
||||
'content@' : {
|
||||
templateUrl: 'frontend/errors/404.html'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function getPresentation ($q, $state, $stateParams, Restangular) {
|
||||
var deferred = $q.defer();
|
||||
var restangularQuery = Restangular.one('presentations', 'show_by_path');
|
||||
restangularQuery.get({root_path: $stateParams.root_path, secondary_path: $stateParams.secondary_path})
|
||||
.then(function(response) {
|
||||
deferred.resolve(response);
|
||||
})
|
||||
.catch(function(response) {
|
||||
$state.go('404');
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Default fall back route
|
||||
$urlRouterProvider.otherwise(function($injector, $location){
|
||||
var state = $injector.get('$state');
|
||||
state.go('404');
|
||||
return $location.path();
|
||||
});
|
||||
|
||||
// enable HTML5 Mode for SEO
|
||||
$locationProvider.html5Mode(true);
|
||||
|
||||
});
|
||||
0
app/assets/javascripts/app/services/.keep
Normal file
0
app/assets/javascripts/app/services/.keep
Normal file
528
app/assets/javascripts/app/services/apiController.js
Normal file
528
app/assets/javascripts/app/services/apiController.js
Normal file
@@ -0,0 +1,528 @@
|
||||
angular.module('app.services')
|
||||
.provider('apiController', function () {
|
||||
|
||||
function domainName() {
|
||||
var domain_comps = location.hostname.split(".");
|
||||
var domain = domain_comps[domain_comps.length - 2] + "." + domain_comps[domain_comps.length - 1];
|
||||
return domain;
|
||||
}
|
||||
|
||||
var url;
|
||||
|
||||
this.defaultServerURL = function() {
|
||||
if(!url) {
|
||||
url = localStorage.getItem("server");
|
||||
if(!url) {
|
||||
url = location.protocol + "//" + domainName() + (location.port ? ':' + location.port: '');
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
this.$get = function(Restangular) {
|
||||
return new ApiController(Restangular);
|
||||
}
|
||||
|
||||
function ApiController(Restangular) {
|
||||
|
||||
/*
|
||||
Config
|
||||
*/
|
||||
|
||||
this.getServer = function() {
|
||||
if(!url) {
|
||||
url = localStorage.getItem("server");
|
||||
if(!url) {
|
||||
url = location.protocol + "//" + domainName() + (location.port ? ':' + location.port: '');
|
||||
this.setServer(url);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
this.setServer = function(url, refresh) {
|
||||
localStorage.setItem("server", url);
|
||||
if(refresh) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
User
|
||||
*/
|
||||
|
||||
this.setUsername = function(user, username, callback) {
|
||||
var request = Restangular.one("users", user.id).one("set_username");
|
||||
request.username = username;
|
||||
request.post().then(function(response){
|
||||
callback(response.plain());
|
||||
})
|
||||
}
|
||||
|
||||
this.enableEncryptionForUser = function(user, callback) {
|
||||
Restangular.one("users", user.id).one('enable_encryption').post().then(function(response){
|
||||
|
||||
var enabled = response.plain().local_encryption_enabled;
|
||||
if(!enabled) {
|
||||
callback(false, enabled);
|
||||
return;
|
||||
}
|
||||
this.handleEncryptionStatusChange(user, enabled, callback);
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.disableEncryptionForUser = function(user, callback) {
|
||||
|
||||
Restangular.one("users", user.id).one('disable_encryption').post().then(function(response){
|
||||
var enabled = response.plain().local_encryption_enabled;
|
||||
|
||||
if(enabled) {
|
||||
// something went wrong
|
||||
callback(false, enabled);
|
||||
return;
|
||||
}
|
||||
this.handleEncryptionStatusChange(user, enabled, callback);
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.handleEncryptionStatusChange = function(user, encryptionEnabled, callback) {
|
||||
var allNotes = user.filteredNotes();
|
||||
if(encryptionEnabled) {
|
||||
allNotes = allNotes.filter(function(note){return note.isPublic() == false});
|
||||
this.encryptNotes(allNotes, this.retrieveGk());
|
||||
} else {
|
||||
this.decryptNotes(allNotes, this.retrieveGk());
|
||||
}
|
||||
|
||||
this.saveBatchNotes(user, allNotes, encryptionEnabled, function(success) {
|
||||
callback(success, encryptionEnabled);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
/*
|
||||
Ensures that if encryption is disabled, all local notes are uncrypted,
|
||||
and that if it's enabled, that all local notes are encrypted
|
||||
*/
|
||||
this.verifyEncryptionStatusOfAllNotes = function(user, callback) {
|
||||
var allNotes = user.filteredNotes();
|
||||
var notesNeedingUpdate = [];
|
||||
var key = this.retrieveGk();
|
||||
allNotes.forEach(function(note){
|
||||
if(user.local_encryption_enabled && !note.isPublic()) {
|
||||
if(!note.isEncrypted()) {
|
||||
// needs encryption
|
||||
this.encryptSingleNote(note, key);
|
||||
notesNeedingUpdate.push(note);
|
||||
}
|
||||
} else {
|
||||
if(note.isEncrypted()) {
|
||||
// needs decrypting
|
||||
this.decryptSingleNote(note, key);
|
||||
notesNeedingUpdate.push(note);
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
|
||||
if(notesNeedingUpdate.length > 0) {
|
||||
this.saveBatchNotes(user, notesNeedingUpdate, user.local_encryption_enabled, callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Groups
|
||||
*/
|
||||
|
||||
this.restangularizeGroup = function(group, user) {
|
||||
var request = Restangular.one("users", user.id).one("groups", group.id);
|
||||
_.merge(request, group);
|
||||
return request;
|
||||
}
|
||||
|
||||
this.saveGroup = function(user, group, callback) {
|
||||
if(user.id) {
|
||||
if(!group.route) {
|
||||
group = this.restangularizeGroup(group, user);
|
||||
}
|
||||
group.customOperation(group.id ? "put" : "post").then(function(response) {
|
||||
callback(response.plain());
|
||||
})
|
||||
} else {
|
||||
this.writeUserToLocalStorage(user);
|
||||
callback(group);
|
||||
}
|
||||
}
|
||||
|
||||
this.deleteGroup = function(user, group, callback) {
|
||||
if(!user.id) {
|
||||
_.remove(user.groups, group);
|
||||
this.writeUserToLocalStorage(user);
|
||||
callback(true);
|
||||
} else {
|
||||
Restangular.one("users", user.id).one("groups", group.id).remove()
|
||||
.then(function(response) {
|
||||
_.remove(user.groups, group);
|
||||
callback(true);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.shareGroup = function(user, group, callback) {
|
||||
var shareFn = function() {
|
||||
Restangular.one("users", user.id).one("groups", group.id).one("share").post()
|
||||
.then(function(response){
|
||||
var obj = response.plain();
|
||||
group.notes.forEach(function(note){
|
||||
note.shared_via_group = true;
|
||||
});
|
||||
_.merge(group, {presentation: obj.presentation});
|
||||
callback(obj);
|
||||
})
|
||||
}
|
||||
|
||||
if(user.local_encryption_enabled && group.notes.length > 0) {
|
||||
// decrypt group notes first
|
||||
var notes = group.notes;
|
||||
this.decryptNotesWithLocalKey(notes);
|
||||
this.saveBatchNotes(user, notes, false, function(success){
|
||||
shareFn();
|
||||
})
|
||||
} else {
|
||||
shareFn();
|
||||
}
|
||||
}
|
||||
|
||||
this.unshareGroup = function(user, group, callback) {
|
||||
Restangular.one("users", user.id).one("groups", group.id).one("unshare").post()
|
||||
.then(function(response){
|
||||
var obj = response.plain();
|
||||
_.merge(group, {presentation: obj.presentation});
|
||||
callback(obj);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Notes
|
||||
*/
|
||||
|
||||
this.saveBatchNotes = function(user, notes, encryptionEnabled, callback) {
|
||||
notes = _.cloneDeep(notes);
|
||||
notes.forEach(function(note){
|
||||
if(encryptionEnabled && !note.isPublic()) {
|
||||
note.content = null;
|
||||
note.name = null;
|
||||
} else {
|
||||
note.local_encrypted_content = null;
|
||||
note.local_eek = null;
|
||||
}
|
||||
})
|
||||
|
||||
var request = Restangular.one("users", user.id).one("notes/batch_update");
|
||||
request.notes = notes;
|
||||
request.put().then(function(response){
|
||||
var success = response.plain().success;
|
||||
callback(success);
|
||||
})
|
||||
}
|
||||
|
||||
this.preprocessNoteForSaving = function(note, user) {
|
||||
if(user.local_encryption_enabled && !note.pending_share && !note.isPublic()) {
|
||||
// encrypt
|
||||
this.encryptSingleNote(note, this.retrieveGk());
|
||||
note.content = null; // dont send unencrypted content to server
|
||||
note.name = null;
|
||||
}
|
||||
else {
|
||||
// decrypt
|
||||
note.local_encrypted_content = null;
|
||||
note.local_eek = null;
|
||||
note.local_encryption_scheme = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.saveNote = function(user, note, callback) {
|
||||
if(!user.id) {
|
||||
this.writeUserToLocalStorage(user);
|
||||
callback(note);
|
||||
return;
|
||||
}
|
||||
|
||||
var snipCopy = _.cloneDeep(note);
|
||||
|
||||
this.preprocessNoteForSaving(snipCopy, user);
|
||||
|
||||
var request = Restangular.one("users", user.id).one("notes", note.id);
|
||||
_.merge(request, snipCopy);
|
||||
|
||||
request.customOperation(request.id ? "put" : "post")
|
||||
.then(function(response) {
|
||||
var responseObject = response.plain();
|
||||
responseObject.content = note.content;
|
||||
responseObject.name = note.name;
|
||||
_.merge(note, responseObject);
|
||||
callback(note);
|
||||
})
|
||||
.catch(function(response){
|
||||
callback(null);
|
||||
})
|
||||
}
|
||||
|
||||
this.deleteNote = function(user, note, callback) {
|
||||
if(!user.id) {
|
||||
this.writeUserToLocalStorage(user);
|
||||
callback(true);
|
||||
} else {
|
||||
Restangular.one("users", user.id).one("notes", note.id).remove()
|
||||
.then(function(response) {
|
||||
callback(true);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.shareNote = function(user, note, callback) {
|
||||
if(!user.id) {
|
||||
if(confirm("Note: You are not signed in. Any note you share cannot be edited or unshared.")) {
|
||||
var request = Restangular.one("notes").one("share");
|
||||
_.merge(request, {name: note.name, content: note.content});
|
||||
request.post().then(function(response){
|
||||
var obj = response.plain();
|
||||
_.merge(note, {presentation: obj.presentation});
|
||||
note.locked = true;
|
||||
this.writeUserToLocalStorage(user);
|
||||
callback(note);
|
||||
}.bind(this))
|
||||
}
|
||||
} else {
|
||||
var shareFn = function(note, callback) {
|
||||
Restangular.one("users", user.id).one("notes", note.id).one("share").post()
|
||||
.then(function(response){
|
||||
var obj = response.plain();
|
||||
_.merge(note, {presentation: obj.presentation});
|
||||
callback(note);
|
||||
})
|
||||
}
|
||||
|
||||
if(user.local_encryption_enabled) {
|
||||
if(confirm("Note: Sharing this note will remove its local encryption.")) {
|
||||
note.pending_share = true;
|
||||
this.saveNote(user, note, function(saved_note){
|
||||
shareFn(saved_note, callback);
|
||||
})
|
||||
}
|
||||
} else {
|
||||
shareFn(note, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.unshareNote = function(user, note, callback) {
|
||||
Restangular.one("users", user.id).one("notes", note.id).one("unshare").post()
|
||||
.then(function(response){
|
||||
var obj = response.plain();
|
||||
_.merge(note, {presentation: obj.presentation});
|
||||
callback(note);
|
||||
})
|
||||
}
|
||||
|
||||
this.notesDataFile = function(user) {
|
||||
var textFile = null;
|
||||
var makeTextFile = function (text) {
|
||||
var data = new Blob([text], {type: 'text/json'});
|
||||
|
||||
// If we are replacing a previously generated file we need to
|
||||
// manually revoke the object URL to avoid memory leaks.
|
||||
if (textFile !== null) {
|
||||
window.URL.revokeObjectURL(textFile);
|
||||
}
|
||||
|
||||
textFile = window.URL.createObjectURL(data);
|
||||
|
||||
// returns a URL you can use as a href
|
||||
return textFile;
|
||||
}.bind(this);
|
||||
|
||||
// remove irrelevant keys
|
||||
var notes = _.map(user.filteredNotes(), function(note){
|
||||
return {
|
||||
id: note.id,
|
||||
name: note.name,
|
||||
content: note.content,
|
||||
created_at: note.created_at,
|
||||
modified_at: note.modified_at,
|
||||
group_id: note.group_id
|
||||
}
|
||||
});
|
||||
return makeTextFile(JSON.stringify(notes, null, 2 /* pretty print */));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Merging
|
||||
*/
|
||||
this.mergeLocalDataRemotely = function(user, callback) {
|
||||
var request = Restangular.one("users", user.id).one("merge");
|
||||
var groups = user.groups;
|
||||
request.notes = user.notes;
|
||||
request.notes.forEach(function(note){
|
||||
if(note.group_id) {
|
||||
var group = groups.filter(function(group){return group.id == note.group_id})[0];
|
||||
note.group_name = group.name;
|
||||
}
|
||||
})
|
||||
request.post().then(function(response){
|
||||
callback();
|
||||
localStorage.removeItem('user');
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
this.filterDummyNotes = function(notes) {
|
||||
var filtered = notes.filter(function(note){return note.dummy == false || note.dummy == null});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
this.staticifyObject = function(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
|
||||
this.writeUserToLocalStorage = function(user) {
|
||||
var saveUser = _.cloneDeep(user);
|
||||
saveUser.notes = this.filterDummyNotes(saveUser.notes);
|
||||
saveUser.groups.forEach(function(group){
|
||||
group.notes = null;
|
||||
}.bind(this))
|
||||
this.writeToLocalStorage('user', saveUser);
|
||||
}
|
||||
|
||||
this.writeToLocalStorage = function(key, value) {
|
||||
localStorage.setItem(key, angular.toJson(value));
|
||||
}
|
||||
|
||||
this.localUser = function() {
|
||||
var user = JSON.parse(localStorage.getItem('user'));
|
||||
if(!user) {
|
||||
user = {notes: [], groups: []};
|
||||
}
|
||||
user.shouldMerge = true;
|
||||
return user;
|
||||
}
|
||||
|
||||
/*
|
||||
Drafts
|
||||
*/
|
||||
|
||||
this.saveDraftToDisk = function(draft) {
|
||||
localStorage.setItem("draft", JSON.stringify(draft));
|
||||
}
|
||||
|
||||
this.clearDraft = function() {
|
||||
localStorage.removeItem("draft");
|
||||
}
|
||||
|
||||
this.getDraft = function() {
|
||||
var draftString = localStorage.getItem("draft");
|
||||
if(!draftString || draftString == 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return new Note(JSON.parse(draftString));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Encrpytion
|
||||
*/
|
||||
|
||||
this.retrieveGk = function() {
|
||||
if(!this.gk) {
|
||||
this.gk = localStorage.getItem("gk");
|
||||
}
|
||||
return this.gk;
|
||||
}
|
||||
|
||||
this.setGk = function(gk) {
|
||||
localStorage.setItem('gk', gk);
|
||||
}
|
||||
|
||||
this.clearGk = function() {
|
||||
localStorage.removeItem("gk");
|
||||
}
|
||||
|
||||
this.encryptSingleNote = function(note, key) {
|
||||
var ek = null;
|
||||
if(note.isEncrypted()) {
|
||||
ek = Neeto.crypto.decryptText(note.local_eek, key);
|
||||
} else {
|
||||
ek = Neeto.crypto.generateRandomEncryptionKey();
|
||||
note.local_eek = Neeto.crypto.encryptText(ek, key);
|
||||
}
|
||||
var text = JSON.stringify({name: note.name, content: note.content});
|
||||
note.local_encrypted_content = Neeto.crypto.encryptText(text, ek);
|
||||
note.local_encryption_scheme = "1.0";
|
||||
}
|
||||
|
||||
this.encryptNotes = function(notes, key) {
|
||||
notes.forEach(function(note){
|
||||
this.encryptSingleNote(note, key);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.encryptSingleNoteWithLocalKey = function(note) {
|
||||
this.encryptSingleNote(note, this.retrieveGk());
|
||||
}
|
||||
|
||||
this.encryptNotesWithLocalKey = function(notes) {
|
||||
this.encryptNotes(notes, this.retrieveGk());
|
||||
}
|
||||
|
||||
this.decryptSingleNoteWithLocalKey = function(note) {
|
||||
this.decryptSingleNote(note, this.retrieveGk());
|
||||
}
|
||||
|
||||
this.decryptSingleNote = function(note, key) {
|
||||
var ek = Neeto.crypto.decryptText(note.local_eek, key);
|
||||
var obj = JSON.parse(Neeto.crypto.decryptText(note.local_encrypted_content, ek));
|
||||
note.name = obj.name;
|
||||
note.content = obj.content;
|
||||
}
|
||||
|
||||
this.decryptNotes = function(notes, key) {
|
||||
notes.forEach(function(note){
|
||||
if(note.isEncrypted()) {
|
||||
this.decryptSingleNote(note, key);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.decryptNotesWithLocalKey = function(notes) {
|
||||
this.decryptNotes(notes, this.retrieveGk());
|
||||
}
|
||||
|
||||
this.reencryptAllNotesAndSave = function(user, newKey, oldKey, callback) {
|
||||
var notes = user.filteredNotes();
|
||||
notes.forEach(function(note){
|
||||
if(note.isEncrypted()) {
|
||||
// first decrypt eek with old key
|
||||
var ek = Neeto.crypto.decryptText(note.local_eek, oldKey);
|
||||
// now encrypt ek with new key
|
||||
note.local_eek = Neeto.crypto.encryptText(ek, newKey);
|
||||
}
|
||||
});
|
||||
|
||||
this.saveBatchNotes(user, notes, true, function(success) {
|
||||
callback(success);
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
});
|
||||
17
app/assets/javascripts/app/services/directives/autofocus.js
Normal file
17
app/assets/javascripts/app/services/directives/autofocus.js
Normal file
@@ -0,0 +1,17 @@
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('mbAutofocus', ['$timeout', function($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
shouldFocus: "="
|
||||
},
|
||||
link : function($scope, $element) {
|
||||
$timeout(function() {
|
||||
if($scope.shouldFocus) {
|
||||
$element[0].focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
||||
109
app/assets/javascripts/app/services/directives/draggable.js
Normal file
109
app/assets/javascripts/app/services/directives/draggable.js
Normal file
@@ -0,0 +1,109 @@
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('draggable', function() {
|
||||
return {
|
||||
scope: {
|
||||
note: "="
|
||||
},
|
||||
link: function(scope, element) {
|
||||
// this gives us the native JS object
|
||||
var el = element[0];
|
||||
|
||||
el.draggable = true;
|
||||
|
||||
el.addEventListener(
|
||||
'dragstart',
|
||||
function(e) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('Note', JSON.stringify(scope.note));
|
||||
this.classList.add('drag');
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
el.addEventListener(
|
||||
'dragend',
|
||||
function(e) {
|
||||
this.classList.remove('drag');
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('droppable', function() {
|
||||
return {
|
||||
scope: {
|
||||
drop: '&',
|
||||
bin: '=',
|
||||
group: "="
|
||||
},
|
||||
link: function(scope, element) {
|
||||
// again we need the native object
|
||||
var el = element[0];
|
||||
|
||||
el.addEventListener(
|
||||
'dragover',
|
||||
function(e) {
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
// allows us to drop
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
this.classList.add('over');
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
var counter = 0;
|
||||
|
||||
el.addEventListener(
|
||||
'dragenter',
|
||||
function(e) {
|
||||
counter++;
|
||||
this.classList.add('over');
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
el.addEventListener(
|
||||
'dragleave',
|
||||
function(e) {
|
||||
counter--;
|
||||
if (counter === 0) {
|
||||
this.classList.remove('over');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
el.addEventListener(
|
||||
'drop',
|
||||
function(e) {
|
||||
// Stops some browsers from redirecting.
|
||||
if (e.stopPropagation) e.stopPropagation();
|
||||
|
||||
this.classList.remove('over');
|
||||
|
||||
var binId = this.id;
|
||||
var note = new Note(JSON.parse(e.dataTransfer.getData('Note')));
|
||||
scope.$apply(function(scope) {
|
||||
var fn = scope.drop();
|
||||
if ('undefined' !== typeof fn) {
|
||||
fn(e, scope.group, note);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
20
app/assets/javascripts/app/services/directives/lowercase.js
Normal file
20
app/assets/javascripts/app/services/directives/lowercase.js
Normal file
@@ -0,0 +1,20 @@
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('lowercase', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, modelCtrl) {
|
||||
var lowercase = function(inputValue) {
|
||||
if (inputValue == undefined) inputValue = '';
|
||||
var lowercased = inputValue.toLowerCase();
|
||||
if (lowercased !== inputValue) {
|
||||
modelCtrl.$setViewValue(lowercased);
|
||||
modelCtrl.$render();
|
||||
}
|
||||
return lowercased;
|
||||
}
|
||||
modelCtrl.$parsers.push(lowercase);
|
||||
lowercase(scope[attrs.ngModel]);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('selectOnClick', ['$window', function ($window) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
element.on('focus', function () {
|
||||
if (!$window.getSelection().toString()) {
|
||||
// Required for mobile Safari
|
||||
this.setSelectionRange(0, this.value.length)
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
17
app/assets/javascripts/app/services/directives/snippet.js
Normal file
17
app/assets/javascripts/app/services/directives/snippet.js
Normal file
@@ -0,0 +1,17 @@
|
||||
angular
|
||||
.module('app.services')
|
||||
.directive('note', function($timeout) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: 'SingleNoteCtrl',
|
||||
templateUrl: "frontend/directives/note.html",
|
||||
scope: {
|
||||
note: "="
|
||||
},
|
||||
}
|
||||
})
|
||||
.controller('SingleNoteCtrl', function ($rootScope, $scope, $state, markdownRenderer) {
|
||||
$scope.renderedContent = function() {
|
||||
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText($scope.note.content));
|
||||
}
|
||||
});
|
||||
192
app/assets/javascripts/app/services/directives/typewrite.js
Normal file
192
app/assets/javascripts/app/services/directives/typewrite.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* AngularJS directive that simulates the effect of typing on a text editor - with a blinking cursor.
|
||||
* This directive works as an attribute to any HTML element, and it changes the speed/delay of its animation.
|
||||
*
|
||||
* There's also a simple less file included for basic styling of the dialog, which can be overridden.
|
||||
* The config object also lets the user define custom CSS classes for the modal.
|
||||
*
|
||||
* How to use:
|
||||
*
|
||||
* Just add the desired text to the 'text' attribute of the element and the directive takes care of the rest.
|
||||
* The 'text' attribute can be a single string or an array of string. In case an array is passed, the string
|
||||
* on each index is erased so the next item can be printed. When the last index is reached, that string stays
|
||||
* on the screen. (So if you want to erase the last string, just push an empty string to the end of the array)
|
||||
*
|
||||
* These are the optional preferences:
|
||||
* - initial delay: set an 'initial-delay' attribute for the element
|
||||
* - type delay: set a 'type-delay' attribute for the element
|
||||
* - erase delay: set a 'erase-delay' attribute for the element
|
||||
* - specify cursor : set a 'cursor' attribute for the element, specifying which cursor to use
|
||||
* - turn off cursor blinking: set the 'blink-cursor' attribute to "false"
|
||||
* - cursor blinking speed: set a 'blink-delay' attribute for the element
|
||||
* - scope callback: pass the desired scope callback as the 'callback-fn' attribute of the element
|
||||
*
|
||||
* Note:
|
||||
* Each time/delay value should be set either on seconds (1s) or milliseconds (1000)
|
||||
*
|
||||
* Dependencies:
|
||||
* The directive needs the css file provided in order to replicate the cursor blinking effect.
|
||||
*/
|
||||
|
||||
|
||||
angular
|
||||
.module('app.services').directive('typewrite', ['$timeout', function ($timeout) {
|
||||
function linkFunction($scope, $element, $attrs) {
|
||||
var timer = null,
|
||||
initialDelay = $attrs.initialDelay ? getTypeDelay($attrs.initialDelay) : 200,
|
||||
typeDelay = $attrs.typeDelay || 200,
|
||||
eraseDelay = $attrs.eraseDelay || typeDelay / 2,
|
||||
blinkDelay = $attrs.blinkDelay ? getAnimationDelay($attrs.blinkDelay) : false,
|
||||
cursor = $attrs.cursor || '|',
|
||||
blinkCursor = typeof $attrs.blinkCursor !== 'undefined' ? $attrs.blinkCursor === 'true' : true,
|
||||
currentText,
|
||||
textArray,
|
||||
running,
|
||||
auxStyle;
|
||||
|
||||
|
||||
|
||||
if ($scope.text) {
|
||||
if ($scope.text instanceof Array) {
|
||||
textArray = $scope.text;
|
||||
currentText = textArray[0];
|
||||
} else {
|
||||
currentText = $scope.text;
|
||||
}
|
||||
}
|
||||
if (typeof $scope.start === 'undefined' || $scope.start) {
|
||||
typewrite();
|
||||
}
|
||||
|
||||
function typewrite() {
|
||||
timer = $timeout(function () {
|
||||
updateIt($element, 0, 0, currentText);
|
||||
}, initialDelay);
|
||||
}
|
||||
|
||||
function updateIt(element, charIndex, arrIndex, text) {
|
||||
if (charIndex <= text.length) {
|
||||
updateValue(element, text.substring(0, charIndex) + cursor);
|
||||
charIndex++;
|
||||
timer = $timeout(function () {
|
||||
updateIt(element, charIndex, arrIndex, text);
|
||||
}, typeDelay);
|
||||
return;
|
||||
} else {
|
||||
charIndex--;
|
||||
|
||||
if($scope.iterationCallback) {
|
||||
$scope.iterationCallback()(arrIndex);
|
||||
}
|
||||
|
||||
// check if it's an array
|
||||
if (textArray && arrIndex < textArray.length - 1) {
|
||||
timer = $timeout(function () {
|
||||
cleanAndRestart(element, charIndex, arrIndex, textArray[arrIndex]);
|
||||
}, $scope.iterationDelay);
|
||||
} else {
|
||||
if ($scope.callbackFn) {
|
||||
$scope.callbackFn();
|
||||
}
|
||||
blinkIt(element, charIndex, currentText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function blinkIt(element, charIndex) {
|
||||
var text = element.text().substring(0, element.text().length - 1);
|
||||
if (blinkCursor) {
|
||||
if (blinkDelay) {
|
||||
auxStyle = '-webkit-animation:blink-it steps(1) ' + blinkDelay + ' infinite;-moz-animation:blink-it steps(1) ' + blinkDelay + ' infinite ' +
|
||||
'-ms-animation:blink-it steps(1) ' + blinkDelay + ' infinite;-o-animation:blink-it steps(1) ' + blinkDelay + ' infinite; ' +
|
||||
'animation:blink-it steps(1) ' + blinkDelay + ' infinite;';
|
||||
updateValue(element, text.substring(0, charIndex) + '<span class="blink" style="' + auxStyle + '">' + cursor + '</span>');
|
||||
} else {
|
||||
updateValue(element, text.substring(0, charIndex) + '<span class="blink">' + cursor + '</span>');
|
||||
}
|
||||
} else {
|
||||
updateValue(element, text.substring(0, charIndex));
|
||||
}
|
||||
}
|
||||
|
||||
function cleanAndRestart(element, charIndex, arrIndex, currentText) {
|
||||
if(charIndex == 0) {
|
||||
if($scope.prebeginFn) {
|
||||
$scope.prebeginFn()();
|
||||
}
|
||||
}
|
||||
if (charIndex > 0) {
|
||||
currentText = currentText.slice(0, -1);
|
||||
// element.html(currentText.substring(0, currentText.length - 1) + cursor);
|
||||
updateValue(element, currentText + cursor);
|
||||
charIndex--;
|
||||
timer = $timeout(function () {
|
||||
cleanAndRestart(element, charIndex, arrIndex, currentText);
|
||||
}, eraseDelay);
|
||||
return;
|
||||
} else {
|
||||
arrIndex++;
|
||||
currentText = textArray[arrIndex];
|
||||
timer = $timeout(function () {
|
||||
updateIt(element, 0, arrIndex, currentText);
|
||||
}, typeDelay);
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeDelay(delay) {
|
||||
if (typeof delay === 'string') {
|
||||
return delay.charAt(delay.length - 1) === 's' ? parseInt(delay.substring(0, delay.length - 1), 10) * 1000 : +delay;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getAnimationDelay(delay) {
|
||||
if (typeof delay === 'string') {
|
||||
return delay.charAt(delay.length - 1) === 's' ? delay : parseInt(delay.substring(0, delay.length - 1), 10) / 1000;
|
||||
}
|
||||
}
|
||||
|
||||
function updateValue(element, value) {
|
||||
if (element.prop('nodeName').toUpperCase() === 'INPUT') {
|
||||
return element.val(value);
|
||||
}
|
||||
return element.html(value);
|
||||
}
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
if (timer) {
|
||||
$timeout.cancel(timer);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('start', function (newVal) {
|
||||
if (!running && newVal) {
|
||||
running = !running;
|
||||
typewrite();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('text', function (newVal, oldVal) {
|
||||
if(!(newVal instanceof Array)) {
|
||||
currentText = newVal;
|
||||
typewrite();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: linkFunction,
|
||||
replace: true,
|
||||
scope: {
|
||||
text: '=',
|
||||
callbackFn: '&',
|
||||
iterationCallback: '&',
|
||||
iterationDelay: '=',
|
||||
prebeginFn: '&',
|
||||
start: '='
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
48
app/assets/javascripts/app/services/helpers/crypto.js
Normal file
48
app/assets/javascripts/app/services/helpers/crypto.js
Normal file
@@ -0,0 +1,48 @@
|
||||
var Neeto = Neeto || {};
|
||||
|
||||
Neeto.crypto = {
|
||||
|
||||
generateRandomKey: function() {
|
||||
return CryptoJS.lib.WordArray.random(256/8).toString();
|
||||
},
|
||||
|
||||
decryptText: function(encrypted_content, key) {
|
||||
return CryptoJS.AES.decrypt(encrypted_content, key).toString(CryptoJS.enc.Utf8);
|
||||
},
|
||||
|
||||
encryptText: function(text, key) {
|
||||
return CryptoJS.AES.encrypt(text, key).toString();
|
||||
},
|
||||
|
||||
generateRandomEncryptionKey: function() {
|
||||
var salt = Neeto.crypto.generateRandomKey();
|
||||
var passphrase = Neeto.crypto.generateRandomKey();
|
||||
return CryptoJS.PBKDF2(passphrase, salt, { keySize: 256/32 }).toString();
|
||||
},
|
||||
|
||||
sha256: function(text) {
|
||||
return CryptoJS.SHA256(text).toString();
|
||||
},
|
||||
|
||||
/** Generates two deterministic 256 bit keys based on one input */
|
||||
generateAsymmetricKeyPair: function(input, salt) {
|
||||
var output = CryptoJS.PBKDF2(input, salt, { keySize: 512/32, hasher: CryptoJS.algo.SHA512, iterations: 3000 });
|
||||
var firstHalf = _.clone(output);
|
||||
var secondHalf = _.clone(output);
|
||||
var sigBytes = output.sigBytes/2;
|
||||
var outputLength = output.words.length;
|
||||
firstHalf.words = output.words.slice(0, outputLength/2);
|
||||
secondHalf.words = output.words.slice(outputLength/2, outputLength);
|
||||
firstHalf.sigBytes = sigBytes;
|
||||
secondHalf.sigBytes = sigBytes;
|
||||
return [firstHalf.toString(), secondHalf.toString()];
|
||||
},
|
||||
|
||||
generateEncryptionKeysForUser: function(password, email) {
|
||||
var keys = Neeto.crypto.generateAsymmetricKeyPair(password, email);
|
||||
var pw = keys[0];
|
||||
var gk = keys[1];
|
||||
|
||||
return {pw: pw, gk: gk};
|
||||
}
|
||||
};
|
||||
21
app/assets/javascripts/app/services/markdownRenderer.js
Normal file
21
app/assets/javascripts/app/services/markdownRenderer.js
Normal file
@@ -0,0 +1,21 @@
|
||||
angular.module('app.services')
|
||||
.service('markdownRenderer', function ($sce) {
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
sanitize: true
|
||||
});
|
||||
|
||||
this.renderedContentForText = function(text) {
|
||||
if(!text || text.length == 0) {
|
||||
return "";
|
||||
}
|
||||
return marked(text);
|
||||
}
|
||||
|
||||
this.renderHtml = function(html_code) {
|
||||
return $sce.trustAsHtml(html_code);
|
||||
};
|
||||
|
||||
|
||||
});
|
||||
20
app/assets/javascripts/app/services/serverSideValidation.js
Normal file
20
app/assets/javascripts/app/services/serverSideValidation.js
Normal file
@@ -0,0 +1,20 @@
|
||||
angular.module('app.services')
|
||||
.service('serverSideValidation', function ($sce) {
|
||||
// Show validation errors in form.
|
||||
this.showErrors = function (formErrors, form) {
|
||||
angular.forEach(formErrors, function (errors, key) {
|
||||
if (typeof form[key] !== 'undefined') {
|
||||
form[key].$setDirty();
|
||||
form[key].$setValidity('server', false);
|
||||
form[key].$error.server = $sce.trustAsHtml(errors.join(', '));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Get validation errors from server response and show them in form.
|
||||
this.parseErrors = function (response, form) {
|
||||
if (response.status === 422) {
|
||||
this.showErrors(response.data, form);
|
||||
}
|
||||
};
|
||||
});
|
||||
5
app/assets/javascripts/frontend.js
Normal file
5
app/assets/javascripts/frontend.js
Normal file
@@ -0,0 +1,5 @@
|
||||
//= require app/app.services.js
|
||||
//= require_tree ./app/services
|
||||
|
||||
//= require app/app.frontend.js
|
||||
//= require_tree ./app/frontend
|
||||
142
app/assets/stylesheets/app/_common.scss
Normal file
142
app/assets/stylesheets/app/_common.scss
Normal file
@@ -0,0 +1,142 @@
|
||||
.nt-dropdown-menu {
|
||||
border-radius: 0;
|
||||
padding: 0 0;
|
||||
margin-top: 15px;
|
||||
border: none;
|
||||
width: 280px;
|
||||
|
||||
li {
|
||||
height: 35px;
|
||||
overflow: hidden;
|
||||
// padding-top: 2px;
|
||||
|
||||
.text {
|
||||
padding: 10px;
|
||||
padding-top: 7px;
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
opacity: 0.5;
|
||||
margin-top: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nt-dropdown-menu.dark {
|
||||
background-color: $dark-gray;
|
||||
color: white;
|
||||
|
||||
li {
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: black;
|
||||
a {
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: white !important;
|
||||
height: 100%;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.text {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nt-dropdown-menu.light {
|
||||
background-color: white;
|
||||
color: black;
|
||||
|
||||
li {
|
||||
&:hover {
|
||||
background-color: $dark-gray;
|
||||
color: white;
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: black;
|
||||
}
|
||||
.text {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.menu-right-container {
|
||||
float: right;
|
||||
margin-top: 3px;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
width: 70%;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
|
||||
a {
|
||||
color: white;
|
||||
// font-size: 12px;
|
||||
}
|
||||
|
||||
.public-link {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
height: 20px;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
.url {
|
||||
text-align: right;
|
||||
|
||||
.icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 90%;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.edit-url {
|
||||
// float: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
> .icon {
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
min-width: 255px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
margin-right: 18px;
|
||||
margin-bottom: 18px;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
239
app/assets/stylesheets/app/_editor.scss
Normal file
239
app/assets/stylesheets/app/_editor.scss
Normal file
@@ -0,0 +1,239 @@
|
||||
.editor {
|
||||
width: 50%;
|
||||
|
||||
&.fullscreen {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
z-index: 200;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.section-title-bar {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
$heading-height: 100px;
|
||||
|
||||
.editor-heading {
|
||||
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
padding-top: 0px;
|
||||
background-color: white;
|
||||
|
||||
min-height: 100px;
|
||||
|
||||
width: 100%;
|
||||
padding-right: 10px;
|
||||
|
||||
> .title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
padding-top: 0px;
|
||||
width: 100%;
|
||||
|
||||
> .input {
|
||||
float: left;
|
||||
text-overflow:ellipsis;
|
||||
width: 90%;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
|
||||
&:disabled {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.save-status {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
margin-top: -18px;
|
||||
width: 120px;
|
||||
text-align: right;
|
||||
color: rgba(black, 0.5);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
max-height: 100%;
|
||||
|
||||
height: 100%;
|
||||
clear: both;
|
||||
min-width: 0;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
overflow: auto;
|
||||
|
||||
padding-top: $heading-height;
|
||||
|
||||
.sampler-container {
|
||||
margin-top: 10px;
|
||||
padding: 15px;
|
||||
padding-top: 17px;
|
||||
font-size: 17px;
|
||||
// opacity: 0.5;
|
||||
}
|
||||
|
||||
.sampler {
|
||||
// opacity: 0.5;
|
||||
color: rgba(black, 0.3);
|
||||
}
|
||||
|
||||
.editable {
|
||||
font-family: monospace;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 15px;
|
||||
padding-top: 17px;
|
||||
font-size: 17px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.preview {
|
||||
// font-family: monospace;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
line-height: 23px;
|
||||
overflow-y: scroll;
|
||||
padding: 0px 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
margin-top: 6px;
|
||||
margin-left: 15px;
|
||||
float: right;
|
||||
text-align: right;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.full-screen-button {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
right: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.editor-menu {
|
||||
padding-top: 0px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
padding-left: inherit;
|
||||
padding-right: inherit;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0px;
|
||||
background-color: $dark-gray;
|
||||
color: white;
|
||||
padding-top: 8px;
|
||||
height: 36px;
|
||||
cursor: default;
|
||||
|
||||
ul {
|
||||
li {
|
||||
text-align: left;
|
||||
a {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding: 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
list-style-position: inside;
|
||||
-webkit-margin-before: 1em;
|
||||
-webkit-margin-after: 1em;
|
||||
-webkit-margin-start: 0px;
|
||||
-webkit-margin-end: 0px;
|
||||
-webkit-padding-start: 0px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nav-tabs {
|
||||
a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.nav {
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
}
|
||||
ol, ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nav-pills>li {
|
||||
float: left;
|
||||
}
|
||||
.nav>li {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover {
|
||||
color: #fff;
|
||||
background-color: #337ab7;
|
||||
}
|
||||
.nav-pills>li>a {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.nav>li>a {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.nav-tabs>li {
|
||||
float: left;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.nav>li {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-tabs>li.active>a, .nav-tabs>li.active>a:focus, .nav-tabs>li.active>a:hover {
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
.nav-tabs>li>a {
|
||||
margin-right: 2px;
|
||||
line-height: 1.42857143;
|
||||
border: 1px solid transparent;
|
||||
// border-radius: 4px 4px 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav>li>a {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
92
app/assets/stylesheets/app/_fonts.scss
Normal file
92
app/assets/stylesheets/app/_fonts.scss
Normal file
@@ -0,0 +1,92 @@
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
src: font-url('ProximaNova/ProximaNova-Regular.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Regular.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Regular.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Regular.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-style: italic;
|
||||
src: font-url('ProximaNova/ProximaNova-Regular-Italic.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Regular-Italic.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Regular-Italic.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Regular-Italic.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: 600;
|
||||
src: font-url('ProximaNova/ProximaNova-Semibold.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
src: font-url('ProximaNova/ProximaNova-Semibold-Italic.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold-Italic.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold-Italic.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Semibold-Italic.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: bold;
|
||||
src: font-url('ProximaNova/ProximaNova-Bold.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Bold.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Bold.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Bold.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
src: font-url('ProximaNova/ProximaNova-Bold-Italic.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Bold-Italic.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Bold-Italic.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Bold-Italic.svg') format('svg');
|
||||
}
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: 100;
|
||||
src: font-url('fonts/ProximaNova-Thin.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Thin.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Thin.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Thin.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
src: font-url('fonts/ProximaNova-Thin-Italic.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Thin-Italic.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Thin-Italic.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Thin-Italic.svg') format('svg');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: ProximaNova;
|
||||
font-weight: 900;
|
||||
src: font-url('ProximaNova/ProximaNova-Extrabold.eot');
|
||||
src: local('☺'),
|
||||
font-url('ProximaNova/ProximaNova-Extrabold.woff') format('woff'),
|
||||
font-url('ProximaNova/ProximaNova-Extrabold.ttf') format('truetype'),
|
||||
font-url('ProximaNova/ProximaNova-Extrabold.svg') format('svg');
|
||||
}
|
||||
68
app/assets/stylesheets/app/_groups.scss
Normal file
68
app/assets/stylesheets/app/_groups.scss
Normal file
@@ -0,0 +1,68 @@
|
||||
.groups {
|
||||
width: 22%;
|
||||
|
||||
.groups-title-bar {
|
||||
color: #0707ff;
|
||||
}
|
||||
|
||||
.group {
|
||||
height: 50px;
|
||||
border-bottom: 1px solid $bg-color;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
transition: height .1s ease-in-out;
|
||||
position: relative;
|
||||
|
||||
> .icon {
|
||||
float: left;
|
||||
padding-top: 6px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
> .title {
|
||||
width: 80%;
|
||||
background-color: transparent;
|
||||
font-weight: 600;
|
||||
float: left;
|
||||
color: $main-text-color;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-overflow: ellipsis;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
> .count {
|
||||
position: absolute;
|
||||
right: 17px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #5151ff;
|
||||
color: white;
|
||||
> .title {
|
||||
color: white;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
|
||||
/* When a note is dragged over group */
|
||||
&.over {
|
||||
background-color: rgba(#5151ff, 0.8);
|
||||
color: white;
|
||||
border: 2px dashed white;
|
||||
> .title {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.selected) {
|
||||
background-color: rgba(#5151ff, 0.8);
|
||||
color: white;
|
||||
> .title {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
292
app/assets/stylesheets/app/_header.scss
Normal file
292
app/assets/stylesheets/app/_header.scss
Normal file
@@ -0,0 +1,292 @@
|
||||
.header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: $bg-color;
|
||||
height: $header-height;
|
||||
max-height: $header-height;
|
||||
z-index: 100;
|
||||
font-size: 14px;
|
||||
color: $dark-gray;
|
||||
// padding-top: 5px;
|
||||
margin-top: 4px;
|
||||
|
||||
a {
|
||||
color: $dark-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
margin-bottom: 0px;
|
||||
padding-top: 0px;
|
||||
border-radius: 0px;
|
||||
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
|
||||
.header-name {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
padding-left: 11px;
|
||||
margin-left: 0px;
|
||||
height: 22px;
|
||||
margin-top: 2px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.panel-status-text {
|
||||
margin-top: 20px;
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
|
||||
float: right;
|
||||
padding-top: 5px;
|
||||
margin-top: 10px;
|
||||
color: black;
|
||||
z-index: 1000;
|
||||
margin-bottom: 0px;
|
||||
font-size: 18px;
|
||||
|
||||
.login-panel .login-input {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.items {
|
||||
|
||||
.advanced-brand {
|
||||
font-size: 18px;
|
||||
|
||||
&.btn {
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
font-weight: bold;
|
||||
.n {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.advanced {
|
||||
margin-left: -4px;
|
||||
text-transform: uppercase;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.beta {
|
||||
font-size: 7px;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
|
||||
display: inline-block;
|
||||
margin-right: 7px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
color: $dark-gray;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
min-width: 300px;
|
||||
z-index: 1000;
|
||||
margin-top: 15px;
|
||||
box-shadow: 0px 0px 15px rgba(black, 0.2);
|
||||
border: none;
|
||||
cursor: default;
|
||||
max-height: 85vh;
|
||||
overflow: scroll;
|
||||
background-color: white;
|
||||
|
||||
|
||||
.storage-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.half-button {
|
||||
$spacing: 2px;
|
||||
width: calc(50% - #{$spacing});
|
||||
margin-left: $spacing/2.0;
|
||||
margin-right: $spacing/2.0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.item.account {
|
||||
|
||||
|
||||
.link-item {
|
||||
margin-bottom: 8px;
|
||||
a {
|
||||
font-size: 14px;
|
||||
color: #00228f;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.account-panel {
|
||||
|
||||
padding: 12px;
|
||||
padding-bottom: 6px;
|
||||
|
||||
.account-items {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.account-item {
|
||||
width: 100%;
|
||||
margin-bottom: 34px;
|
||||
|
||||
a {
|
||||
color: #00228f;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> .icon-container {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
> .meta-container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> .action-container {
|
||||
font-size: 14px;
|
||||
margin-top: 6px;
|
||||
.status-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.subtext {
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.encryption-confirmation {
|
||||
position: relative;
|
||||
.buttons {
|
||||
.cancel {
|
||||
font-weight: normal;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
> .icon-container {
|
||||
margin-bottom: 10px;
|
||||
.icon {
|
||||
height: 35px;
|
||||
&.archive {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.meta-container {
|
||||
> .title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .desc {
|
||||
font-size: 14px;
|
||||
margin-top: 3px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.membership-settings {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.account-form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.registration-login {
|
||||
|
||||
.login-forgot {
|
||||
margin-top: 20px;
|
||||
clear: both;
|
||||
a {
|
||||
display: block;
|
||||
font-size: 13px !important;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.account-menu {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.faq-panel {
|
||||
width: 350px;
|
||||
font-size: 16px;
|
||||
z-index: 1000;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.faq-item {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.question {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.answer {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
181
app/assets/stylesheets/app/_main.scss
Normal file
181
app/assets/stylesheets/app/_main.scss
Normal file
@@ -0,0 +1,181 @@
|
||||
$main-text-color: black;
|
||||
$secondary-text-color: rgba($main-text-color, 0.8);
|
||||
$bg-color: #e3e3e3;
|
||||
|
||||
@mixin MQ-Small() {
|
||||
@media (max-width: $screen-xs-max) {
|
||||
@content;
|
||||
}
|
||||
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin MQ-Medium() {
|
||||
@media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin MQ-Large() {
|
||||
@media (min-width: $screen-lg-min) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
[ng\:cloak], [ng-cloak], .ng-cloak {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans",
|
||||
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||
color: $main-text-color;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dark-button {
|
||||
background-color: #2e2e2e;
|
||||
border: 0;
|
||||
padding: 6px 18px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
border-radius: 2px;
|
||||
border: 1px solid transparent;
|
||||
-webkit-appearance: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
line-height: 1.45;
|
||||
background-color: #f7f7f7;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
code {
|
||||
word-wrap: break-word;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
p {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.main-ui-view {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.app-body-class {
|
||||
height: 100%;
|
||||
background-color: $bg-color;
|
||||
min-width: 100px;
|
||||
overflow: auto;
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
$header-height: 50px;
|
||||
|
||||
.app-container {
|
||||
display: table;
|
||||
background-color: $bg-color;
|
||||
width: 100%;
|
||||
height: calc(100% - #{$header-height});
|
||||
padding: 15px;
|
||||
padding-top: 0px;
|
||||
font-size: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
$section-header-height: 70px;
|
||||
|
||||
.app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: table-row;
|
||||
vertical-align: top;
|
||||
|
||||
.light-button {
|
||||
background-color: $bg-color;
|
||||
font-weight: bold;
|
||||
color: $main-text-color;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
height: 35px;
|
||||
border-radius: 4px;
|
||||
padding-top: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: #cdcdcd;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 8px;
|
||||
padding-bottom: 0px;
|
||||
|
||||
display: block;
|
||||
height: 100%;
|
||||
float: left;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
min-width: 0;
|
||||
|
||||
font-size: 17px;
|
||||
|
||||
> .content {
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
position: relative;
|
||||
box-shadow: 0px 0px 2px rgba(gray, 0.3);
|
||||
}
|
||||
|
||||
.section-title-bar {
|
||||
padding: 20px;
|
||||
height: $section-header-height;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid $bg-color;
|
||||
|
||||
> .title {
|
||||
float: left;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 85%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .add-button {
|
||||
float: right;
|
||||
font-size: 30px;
|
||||
margin-top: -10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
410
app/assets/stylesheets/app/_mostrap.scss
Normal file
410
app/assets/stylesheets/app/_mostrap.scss
Normal file
@@ -0,0 +1,410 @@
|
||||
//== Media queries breakpoints
|
||||
//
|
||||
//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
|
||||
|
||||
// Extra small screen / phone
|
||||
//** Deprecated `$screen-xs` as of v3.0.1
|
||||
$screen-xs: 480px !default;
|
||||
//** Deprecated `$screen-xs-min` as of v3.2.0
|
||||
$screen-xs-min: $screen-xs !default;
|
||||
//** Deprecated `$screen-phone` as of v3.0.1
|
||||
$screen-phone: $screen-xs-min !default;
|
||||
|
||||
// Small screen / tablet
|
||||
//** Deprecated `$screen-sm` as of v3.0.1
|
||||
$screen-sm: 768px !default;
|
||||
$screen-sm-min: $screen-sm !default;
|
||||
//** Deprecated `$screen-tablet` as of v3.0.1
|
||||
$screen-tablet: $screen-sm-min !default;
|
||||
|
||||
// Medium screen / desktop
|
||||
//** Deprecated `$screen-md` as of v3.0.1
|
||||
$screen-md: 992px !default;
|
||||
$screen-md-min: $screen-md !default;
|
||||
//** Deprecated `$screen-desktop` as of v3.0.1
|
||||
$screen-desktop: $screen-md-min !default;
|
||||
|
||||
// Large screen / wide desktop
|
||||
//** Deprecated `$screen-lg` as of v3.0.1
|
||||
$screen-lg: 1200px !default;
|
||||
$screen-lg-min: $screen-lg !default;
|
||||
//** Deprecated `$screen-lg-desktop` as of v3.0.1
|
||||
$screen-lg-desktop: $screen-lg-min !default;
|
||||
|
||||
// So media queries don't overlap when required, provide a maximum
|
||||
$screen-xs-max: ($screen-sm-min - 1) !default;
|
||||
$screen-sm-max: ($screen-md-min - 1) !default;
|
||||
$screen-md-max: ($screen-lg-min - 1) !default;
|
||||
|
||||
@mixin MQ-Xsmall() {
|
||||
@media (max-width: $screen-xs-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin MQ-Small() {
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin MQ-Medium() {
|
||||
@media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin MQ-Large() {
|
||||
@media (min-width: $screen-lg-min) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*:focus {outline:0;}
|
||||
|
||||
.navbar {
|
||||
min-height: 0px !important;
|
||||
background-color: white;
|
||||
height: 80px;
|
||||
margin-bottom: 0px;
|
||||
padding-top: 10px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.navbar {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
position: relative;
|
||||
min-height: 50px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.navbar-header {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand {
|
||||
margin-left: -15px;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
float: left;
|
||||
padding: 15px 15px;
|
||||
font-size: 18px;
|
||||
line-height: 20px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.navbar-collapse.collapse {
|
||||
display: block !important;
|
||||
height: auto !important;
|
||||
padding-bottom: 0;
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.navbar-collapse {
|
||||
width: auto;
|
||||
border-top: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
overflow-x: visible;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
border-top: 1px solid transparent;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.navbar-right {
|
||||
float: right !important;
|
||||
margin-right: -15px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.navbar-text {
|
||||
float: left;
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
.navbar-text {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dropup, .dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
// display: none;
|
||||
float: left;
|
||||
min-width: 160px;
|
||||
padding: 5px 0;
|
||||
margin: 2px 0 0;
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.dropdown-menu>li>a {
|
||||
display: block;
|
||||
padding: 3px 20px;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
line-height: 1.42857143;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.caret {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: 2px;
|
||||
vertical-align: middle;
|
||||
border-top: 4px dashed;
|
||||
border-top: 4px solid\9;
|
||||
border-right: 4px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
button:focus {outline:0;}
|
||||
|
||||
.dropdown-menu-right {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.open > .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
background-image: none;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857;
|
||||
border-radius: 4px;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// ul, menu, dir {
|
||||
// display: block;
|
||||
// list-style-type: disc;
|
||||
// -webkit-margin-before: 1em;
|
||||
// -webkit-margin-after: 1em;
|
||||
// -webkit-margin-start: 0px;
|
||||
// -webkit-margin-end: 0px;
|
||||
// -webkit-padding-start: 40px;
|
||||
// }
|
||||
|
||||
.dropdown-menu .divider {
|
||||
height: 1px;
|
||||
margin: 9px 0;
|
||||
overflow: hidden;
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
min-width: 300px;
|
||||
z-index: 1000;
|
||||
margin-top: 10px;
|
||||
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.2);
|
||||
border: none;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.panel-top {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.panel-left {
|
||||
left: -50px;
|
||||
}
|
||||
|
||||
.panel-centered {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857;
|
||||
color: #555555;
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
|
||||
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
}
|
||||
|
||||
input, button, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.has-feedback {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background-color: transparent;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-link:hover, .btn-link:focus {
|
||||
color: #23527c;
|
||||
text-decoration: underline;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.animated {
|
||||
-webkit-animation-duration: 1s;
|
||||
animation-duration: 1s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.animated-fast {
|
||||
-webkit-animation-duration: 0.5s;
|
||||
animation-duration: 0.5s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.fadeInDown {
|
||||
-webkit-animation-name: fadeInDown;
|
||||
animation-name: fadeInDown;
|
||||
}
|
||||
|
||||
.fadeIn {
|
||||
-webkit-animation-name: fadeIn;
|
||||
animation-name: fadeIn;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0,-100%,0);
|
||||
transform: translate3d(0,-100%,0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
78
app/assets/stylesheets/app/_notes.scss
Normal file
78
app/assets/stylesheets/app/_notes.scss
Normal file
@@ -0,0 +1,78 @@
|
||||
.notes {
|
||||
width: 28%;
|
||||
|
||||
.notes-title-bar {
|
||||
color: #ff6551;
|
||||
height: 136px !important;
|
||||
}
|
||||
|
||||
.group-menu-bar {
|
||||
position: relative;
|
||||
margin: 0 -20px;
|
||||
width: auto;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
clear: left;
|
||||
height: 32px;
|
||||
margin-top: 14px;
|
||||
|
||||
.filter-bar {
|
||||
background-color: $bg-color;
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
color: #909090;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
line-height: 35px;
|
||||
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.notes-footer {
|
||||
border-top: 1px solid $bg-color;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
margin-top: 1px solid $bg-color;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
|
||||
> .new-button {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.note {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
height: 70px;
|
||||
border-bottom: 1px solid $bg-color;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
> .name {
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
> .date {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #ff6551;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:hover:not(.selected) {
|
||||
background-color: rgba(#ff6551, 0.8);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
app/assets/stylesheets/frontend.css.scss
Normal file
63
app/assets/stylesheets/frontend.css.scss
Normal file
@@ -0,0 +1,63 @@
|
||||
$dark-gray: #2e2e2e;
|
||||
|
||||
@import "app/mostrap";
|
||||
@import "app/common";
|
||||
@import "app/main";
|
||||
@import "app/header";
|
||||
@import "app/groups";
|
||||
@import "app/notes";
|
||||
@import "app/editor";
|
||||
|
||||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('icomoon/icomoon.eot');
|
||||
src: url('icomoon/icomoon.eot') format('embedded-opentype'),
|
||||
url('icomoon/icomoon.ttf') format('truetype'),
|
||||
url('icomoon/icomoon.woff') format('woff'),
|
||||
url('icomoon/icomoon.svg') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'icomoon' !important;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
// line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
.inline-icon {
|
||||
display: inline-block;
|
||||
// margin-right: -5px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.icon-lock:before {
|
||||
content: "\e98f";
|
||||
}
|
||||
|
||||
.icon-rss:before {
|
||||
content: "\ea9c";
|
||||
}
|
||||
|
||||
.icon-markdown:before {
|
||||
content: "\e901";
|
||||
}
|
||||
|
||||
.icon-keyboard:before {
|
||||
content: "\e900";
|
||||
}
|
||||
|
||||
.icon-enlarge:before {
|
||||
content: "\e989";
|
||||
}
|
||||
16
app/assets/templates/frontend/auth/forgot.html.haml
Normal file
16
app/assets/templates/frontend/auth/forgot.html.haml
Normal file
@@ -0,0 +1,16 @@
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
%h3.panel-title Forgot Your Password?
|
||||
.panel-body
|
||||
%p We'll send reset instructions to your email.
|
||||
%form{'ng-submit' => 'requestPasswordReset(forgotData)', 'ng-init' => 'forgotData = {}'}
|
||||
.form-group.has-feedback
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'forgotData.email'}/
|
||||
%span.glyphicon.glyphicon-envelope.form-control-feedback
|
||||
.row
|
||||
.col-sm-8.hidden-xs
|
||||
%a.btn.btn-link{'ui-sref' => 'auth.login'} Go back to the login page
|
||||
.col-sm-4
|
||||
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Reset Password
|
||||
.col-xs-12.visible-xs
|
||||
%a.btn.btn-link.btn-block{'ui-sref' => 'auth.login'} Go back to the login page
|
||||
18
app/assets/templates/frontend/auth/login.html.haml
Normal file
18
app/assets/templates/frontend/auth/login.html.haml
Normal file
@@ -0,0 +1,18 @@
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
%h3.panel-title Sign in to start your session
|
||||
.panel-body
|
||||
%form{'ng-submit' => 'submitLogin(loginData)', 'ng-init' => 'loginData = {}'}
|
||||
.form-group.has-feedback
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Username', :required => true, :type => 'email', 'ng-model' => 'loginData.email'}/
|
||||
%span.glyphicon.glyphicon-user.form-control-feedback
|
||||
.form-group.has-feedback
|
||||
%input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'loginData.password'}/
|
||||
%span.glyphicon.glyphicon-lock.form-control-feedback
|
||||
.row
|
||||
.col-sm-8.hidden-xs
|
||||
%a.btn.btn-link{'ui-sref' => 'auth.forgot'} I forgot my password
|
||||
.col-sm-4
|
||||
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Sign in
|
||||
.col-xs-12.visible-xs
|
||||
%a.btn.btn-link.btn-block{'ui-sref' => 'auth.forgot'} I forgot my password
|
||||
15
app/assets/templates/frontend/auth/reset.html.haml
Normal file
15
app/assets/templates/frontend/auth/reset.html.haml
Normal file
@@ -0,0 +1,15 @@
|
||||
.panel.panel-default.panel-centered
|
||||
.panel-heading
|
||||
%h3.panel-title Reset Password
|
||||
.panel-body
|
||||
%p Type your new password.
|
||||
%form{'ng-submit' => 'resetPasswordSubmit()'}
|
||||
.form-group.has-feedback
|
||||
%input.form-control{:placeholder => 'New password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'resetData.password'}/
|
||||
%span.glyphicon.glyphicon-lock.form-control-feedback
|
||||
.form-group.has-feedback
|
||||
%input.form-control{:placeholder => 'Password confirmation', :name => 'password_confirmation', :required => true, :type => 'password', 'ng-model' => 'resetData.password_confirmation'}/
|
||||
%span.glyphicon.glyphicon-lock.form-control-feedback
|
||||
.row
|
||||
.col-sm-4.col-sm-offset-8
|
||||
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Update Password
|
||||
6
app/assets/templates/frontend/auth/wrapper.html.haml
Normal file
6
app/assets/templates/frontend/auth/wrapper.html.haml
Normal file
@@ -0,0 +1,6 @@
|
||||
.login-box.margin-auto{"style" => "margin-top: 150px;"}
|
||||
|
||||
%uib-alert{:type => '{{data.authAlert.type}}', :close => 'data.authAlert = null', 'ng-if' => 'data.authAlert'}
|
||||
{{data.authAlert.msg}}
|
||||
|
||||
%ui-view
|
||||
61
app/assets/templates/frontend/editor.html.haml
Normal file
61
app/assets/templates/frontend/editor.html.haml
Normal file
@@ -0,0 +1,61 @@
|
||||
.section.editor{"ng-class" => "{'fullscreen' : ctrl.fullscreen}"}
|
||||
.content
|
||||
.section-title-bar.editor-heading{"ng-class" => "{'shared' : ctrl.note.isPublic() }"}
|
||||
.title
|
||||
%input.input#note-title-editor{"ng-model" => "ctrl.note.name", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)",
|
||||
"ng-disabled" => "ctrl.note.locked", "ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()",
|
||||
"select-on-click" => "true"}
|
||||
.save-status {{ctrl.noteStatus}}
|
||||
.editor-menu
|
||||
%ul.nav.nav-pills
|
||||
%li.dropdown
|
||||
%a.dropdown-toggle{"ng-click" => "ctrl.clickedMenu()"}
|
||||
File
|
||||
%span.caret{"ng-if" => "!ctrl.note.locked"}
|
||||
%span{"ng-if" => " ctrl.note.locked"}
|
||||
.inline-icon.icon-lock
|
||||
%span.sr-only
|
||||
|
||||
%ul.dropdown-menu.dropdown-menu-left.nt-dropdown-menu.dark{"ng-if" => "ctrl.showMenu && !ctrl.note.locked"}
|
||||
-# %li
|
||||
-# %a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.saveNote($event, note)"} Save
|
||||
-# .shortcut Cmd + S
|
||||
%li
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.toggleFullScreen()"} Toggle Fullscreen
|
||||
.shortcut Cmd + O
|
||||
%li
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.toggleMarkdown()"} Toggle Markdown Preview
|
||||
.shortcut Cmd + M
|
||||
%li{"ng-if" => "!ctrl.note.hasEnabledPresentation()"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.shareNote()"} Share
|
||||
%li{"ng-if" => "ctrl.note.hasEnabledPresentation()"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.editUrlPressed()"} Edit URL
|
||||
%li{"ng-if" => "ctrl.note.hasEnabledPresentation()"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.unshareNote()"} Unshare
|
||||
%li
|
||||
%a.text{"ng-click" => "ctrl.deleteNote()"} Delete
|
||||
.markdown.icon{"ng-if" => "ctrl.editorMode == 'preview'", "ng-click" => "ctrl.showMarkdown = !ctrl.showMarkdown"}
|
||||
.icon-markdown
|
||||
.panel.panel-default.info-panel{"ng-if" => "ctrl.showMarkdown"}
|
||||
.panel-body{"style" => "text-align: center; color: black;"}
|
||||
This editor is Markdown enabled.
|
||||
.menu-right-container
|
||||
.public-link{"ng-if" => "ctrl.note.hasEnabledPresentation()"}
|
||||
%a.url{"ng-if" => "!ctrl.editingUrl", "href" => "{{ctrl.publicUrlForNote(ctrl.note)}}", "target" => "_blank"}
|
||||
%span.icon-rss.icon
|
||||
{{ctrl.publicUrlForNote(note)}}
|
||||
.edit-url{"ng-if" => "ctrl.editingUrl"}
|
||||
{{ctrl.url.base}}
|
||||
%input.input{"ng-model" => "ctrl.url.token", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveUrl($event)",
|
||||
"ng-disabled" => "ctrl.note.locked", "ng-change" => "ctrl.urlChanged()", "ng-focus" => "ctrl.onUrlFocus()",
|
||||
"select-on-click" => "true", "autofocus" => "true"}
|
||||
|
||||
.editor-content{"ng-class" => "{'shared' : ctrl.note.isPublic() }"}
|
||||
.sampler-container{"ng-if" => "ctrl.showSampler", "ng-click" => "ctrl.focusEditor()"}
|
||||
%strong.name-sampler.sampler{"typewrite" => "true", "text" => "ctrl.demoNoteNames", "type-delay" => "30", "initial-delay" => "1.5s",
|
||||
"iteration-callback" => "ctrl.callback", "prebegin-fn" => "ctrl.prebeginFn", "iteration-delay" => "2000", "cursor" => ""}
|
||||
%code{"ng-if" => "ctrl.currentDemoContent.text"}
|
||||
.content-sampler.sampler{"typewrite" => "true", "text" => "ctrl.currentDemoContent.text", "type-delay" => "10", "iteration-callback" => "ctrl.contentCallback"}
|
||||
%textarea.editable#note-text-editor{"ng-disabled" => "ctrl.note.locked", "ng-show" => "ctrl.editorMode == 'edit'", "ng-model" => "ctrl.note.content",
|
||||
"ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()"}
|
||||
.preview{"ng-if" => "ctrl.editorMode == 'preview'", "ng-bind-html" => "ctrl.renderedContent()", "ng-dblclick" => "ctrl.onPreviewDoubleClick()"}
|
||||
16
app/assets/templates/frontend/groups.html.haml
Normal file
16
app/assets/templates/frontend/groups.html.haml
Normal file
@@ -0,0 +1,16 @@
|
||||
.section.groups
|
||||
.content
|
||||
.section-title-bar.groups-title-bar
|
||||
.title Groups
|
||||
.add-button{"ng-click" => "ctrl.clickedAddNewGroup()"} +
|
||||
.group{"ng-if" => "ctrl.allGroup", "ng-click" => "ctrl.selectGroup(ctrl.allGroup)", "ng-class" => "{'selected' : ctrl.selectedGroup == ctrl.allGroup}",
|
||||
"droppable" => true, "drop" => "ctrl.handleDrop", "group" => "ctrl.allGroup"}
|
||||
%input.title{"ng-disabled" => "true", "ng-model" => "ctrl.allGroup.name"}
|
||||
.count {{ctrl.noteCount(ctrl.allGroup)}}
|
||||
.group{"ng-repeat" => "group in ctrl.groups", "ng-click" => "ctrl.selectGroup(group)", "ng-class" => "{'selected' : ctrl.selectedGroup == group}",
|
||||
"droppable" => true, "drop" => "ctrl.handleDrop", "group" => "group"}
|
||||
.icon.icon-rss{"ng-if" => "group.presentation.enabled"}
|
||||
%input.title{"ng-disabled" => "group != ctrl.selectedGroup", "ng-model" => "group.name",
|
||||
"ng-keyup" => "$event.keyCode == 13 && ctrl.saveGroup($event, group)", "mb-autofocus" => "true", "should-focus" => "ctrl.newGroup",
|
||||
"ng-change" => "ctrl.groupTitleDidChange(group)", "ng-focus" => "ctrl.onGroupTitleFocus(group)"}
|
||||
.count {{ctrl.noteCount(group)}}
|
||||
137
app/assets/templates/frontend/header.html.haml
Normal file
137
app/assets/templates/frontend/header.html.haml
Normal file
@@ -0,0 +1,137 @@
|
||||
.header
|
||||
.header-content
|
||||
%nav.animated-fast.fadeInDown
|
||||
%a.navbar-brand{"ui-sref" => "home"}
|
||||
.header-name
|
||||
neeto
|
||||
%span.tagline{"ng-if" => "!ctrl.user.id", "ng-cloak" => "true"} secure code box for developers
|
||||
|
||||
.menu.navbar-text.navbar-right
|
||||
.items
|
||||
.item.account
|
||||
%button.btn.dark-button.advanced-brand{"ng-click" => "ctrl.accountMenuPressed()"}
|
||||
%div{"ng-if" => "ctrl.user.email"} {{ctrl.user.email}}
|
||||
%div{"ng-if" => "!ctrl.user.email"} Sign in or Register
|
||||
.panel.panel-default.account-panel{"ng-if" => "ctrl.showAccountMenu"}
|
||||
.panel-body
|
||||
.account-items
|
||||
.account-item.registration-login{"ng-if" => "!ctrl.user.email"}
|
||||
.meta-container
|
||||
.title Sign in or Register
|
||||
.desc
|
||||
%form.account-form{'name' => "loginForm"}
|
||||
.form-group.has-feedback
|
||||
%input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'ctrl.loginData.email'}
|
||||
.form-group.has-feedback
|
||||
%input.form-control.login-input{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.loginData.user_password'}
|
||||
.checkbox{"ng-if" => "ctrl.hasLocalData()"}
|
||||
%label
|
||||
%input{"type" => "checkbox", "ng-model" => "ctrl.user.shouldMerge", "ng-bind" => "true", "ng-change" => "ctrl.mergeLocalChanged()"}
|
||||
Merge local notes
|
||||
%button.btn.dark-button.half-button{"ng-click" => "ctrl.loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span Sign In
|
||||
%button.btn.dark-button.half-button{"ng-click" => "ctrl.submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span Register
|
||||
%br
|
||||
.login-forgot
|
||||
%a.btn.btn-link{"ng-click" => "ctrl.showResetForm = !ctrl.showResetForm"} I forgot my password
|
||||
.panel-status-text{"ng-if" => "ctrl.loginData.status", "style" => "font-size: 14px;"} {{ctrl.loginData.status}}
|
||||
|
||||
%form{"style" => "margin-top: 20px;", "ng-if" => "ctrl.showResetForm", "ng-init" => "ctrl.resetData = {}", 'ng-submit' => 'ctrl.forgotPasswordSubmit()', 'name' => "resetForm"}
|
||||
.form-group.has-feedback
|
||||
%input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'ctrl.resetData.email'}
|
||||
%button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span Send Reset Email
|
||||
.panel-status-text{"ng-if" => "ctrl.resetData.response", "style" => "font-size: 14px;"}
|
||||
{{ctrl.resetData.response}}
|
||||
|
||||
.account-item{"ng-if" => "ctrl.user.email"}
|
||||
.icon-container
|
||||
%img.icon{"lazy-img" => "assets/encryption.png"}
|
||||
.meta-container
|
||||
.title Local Encryption
|
||||
.desc Encrypt notes locally before sending to server. Neither the server owner nor an intrusive government can decrypt your locally encrypted notes.
|
||||
.action-container
|
||||
%span.status-title Status:
|
||||
{{ctrl.user.local_encryption_enabled ? 'enabled' : 'disabled'}}
|
||||
{{" | "}}
|
||||
%a{"ng-click" => "ctrl.toggleEncryptionStatus()"}
|
||||
{{ctrl.user.local_encryption_enabled ? 'Disable' : 'Enable'}}
|
||||
.subtext{"ng-if" => "ctrl.user.local_encryption_enabled"}
|
||||
{{ctrl.encryptionStatusForNotes()}} (shared notes not encrypted)
|
||||
.encryption-confirmation{"ng-if" => "ctrl.encryptionConfirmation"}
|
||||
%div{"ng-if" => "ctrl.user.local_encryption_enabled"}
|
||||
%p Are you sure you want to disable local encryption? All currently encrypted notes will be decrypted locally, then sent back to Neeto servers over a secure connection.
|
||||
%div{"ng-if" => "!ctrl.user.local_encryption_enabled"}
|
||||
%p We're glad you're taking privacy and security into your own hands. There are a couple things you should note about moving to local encryption:
|
||||
%ul
|
||||
%li
|
||||
If you forget your password, there is no way to reset it or recover it. Your data will be forever lost without your password.
|
||||
(You can however still change your password, as long as you know your current password.)
|
||||
%li
|
||||
The strength of the encryption is tied to the strength of your password. If you take your security seriously, you should use a password of at least 32 characters long.
|
||||
%p Are you sure you want to enable local encryption?
|
||||
.buttons
|
||||
%a.cancel{"ng-click" => "ctrl.cancelEncryptionChange()"} Cancel
|
||||
%a.confirm{"ng-click" => "ctrl.confirmEncryptionChange()"} Confirm
|
||||
.account-item{"ng-if" => "ctrl.user.email"}
|
||||
.icon-container
|
||||
%img.icon.archive{"lazy-img" => "assets/archive.png"}
|
||||
.meta-container
|
||||
.title Data Archives
|
||||
.desc Note: data archives that you download using the link below are decrypted before save. You should take care to store them in a safe location.
|
||||
.action-container
|
||||
%a#download-archive{"ng-click" => "ctrl.downloadDataArchive()"} Download Latest Data Archive
|
||||
|
||||
.account-item
|
||||
.meta-container
|
||||
.title Server
|
||||
.desc Use a custom Neeto server to store and retrieve your account data.
|
||||
.action-container
|
||||
%form.account-form{'ng-submit' => 'ctrl.changeServer()', 'name' => "serverChangeForm"}
|
||||
.form-group.has-feedback
|
||||
%input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'ctrl.serverData.url'}
|
||||
%button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span.ladda-label Change Server
|
||||
|
||||
.links{"ng-if" => "ctrl.user.email"}
|
||||
.link-item
|
||||
%a{"ng-click" => "ctrl.changePasswordPressed()"} Change Password
|
||||
%form.account-form{"ng-if" => "ctrl.showNewPasswordForm", 'ng-submit' => 'ctrl.submitPasswordChange()', 'name' => "passwordChangeForm"}
|
||||
.form-group.has-feedback
|
||||
%input.form-control.login-input{:autofocus => 'autofocus', :name => 'current', :placeholder => 'Current password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.current_password'}
|
||||
.form-group.has-feedback
|
||||
%input.form-control.login-input{:placeholder => 'New password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password', "autocomplete" => "new-password"}
|
||||
.form-group.has-feedback
|
||||
%input.form-control.login-input{:placeholder => 'Confirm password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password_confirmation', "autocomplete" => "new-password"}
|
||||
%button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span.ladda-label Change Password
|
||||
.panel-status-text{"ng-if" => "ctrl.passwordChangeData.status", "style" => "font-size: 14px;"}
|
||||
{{ctrl.passwordChangeData.status}}
|
||||
.link-item
|
||||
%a{"ng-click" => "ctrl.signOutPressed()"} Sign Out
|
||||
|
||||
.item
|
||||
%a.menuItem{"ng-click" => "ctrl.showFaq = !ctrl.showFaq"}
|
||||
faq
|
||||
.panel.panel-default.faq-panel{"ng-if" => "ctrl.showFaq"}
|
||||
.panel-body
|
||||
.faq-item
|
||||
%strong.question What is Neeto?
|
||||
%p.answer Neeto is a secure code box for developers to store common commands and useful notes.
|
||||
.faq-item
|
||||
%strong.question What privacy features does Neeto have?
|
||||
%p.answer
|
||||
We don't want your data. We really don't. Storing your data in our database is a liability for us. But there's a tradeoff between privacy and convenience. We try to be convenient but also focus highly on privacy and security.
|
||||
%ul
|
||||
%li Neeto can be used locally, so that no data is sent to our servers whatsoever. This means however that your data is controlled by your browser, and can vanish without notice.
|
||||
%li When signed in, your data is always sent through a secure connection to Neeto servers.
|
||||
%li When the server receives your data, it is encrypted before being saved.
|
||||
%li Your password is never sent to Neeto servers. Instead, we derive a key from your password and send that instead. This way, Neeto can never know the original contents of your password.
|
||||
%li With Neeto Advanced, you can enable Local Encryption. This encrypts your notes locally on your machine <strong>before</strong> sending to Neeto servers. This means that technically even Neeto couldn't see the contents of your notes (and for that matter any government eavesdroppers.)
|
||||
%li Neeto does not use Google Analytics, a seemingly harmless tool that Google uses to track your web usage patterns and sells it to advertisers.
|
||||
.faq-item
|
||||
%strong.question How does local encryption work?
|
||||
%p.answer Users who opt into using local encryption can add an additional layer of security over their notes. These notes will be encrypted locally on your machine before being sent over the wire. This means that when Neeto receives your notes, we have no idea what the contents are. And if a government ever forces us to give up your data, we couldn't decrypt it for them even if we wanted to.
|
||||
%p This encryption is based on your password, which is also never sent over the air. The strength of this encryption is directly tied to the strength of your password.
|
||||
|
||||
12
app/assets/templates/frontend/home.html.haml
Normal file
12
app/assets/templates/frontend/home.html.haml
Normal file
@@ -0,0 +1,12 @@
|
||||
.main-ui-view
|
||||
%header{"user" => "defaultUser", "logout" => "headerLogout"}
|
||||
.app-container
|
||||
.app
|
||||
%groups-section{"save" => "groupsSave", "add-new" => "groupsAddNew", "will-select" => "groupsWillMakeSelection", "selection-made" => "groupsSelectionMade", "all-group" => "allGroup",
|
||||
"groups" => "groups", "user" => "defaultUser", "update-note-group" => "groupsUpdateNoteGroup"}
|
||||
|
||||
%notes-section{"remove-group" => "notesRemoveGroup", "user" => "defaultUser", "add-new" => "notesAddNew", "selection-made" => "notesSelectionMade",
|
||||
"group" => "selectedGroup", "user-id" => "defaultUser.id", "remove" => "deleteNote"}
|
||||
|
||||
%editor-section{"ng-if" => "selectedNote", "note" => "selectedNote", "remove" => "deleteNote",
|
||||
"user" => "defaultUser", "save" => "saveNote"}
|
||||
6
app/assets/templates/frontend/layouts/about.html.haml
Normal file
6
app/assets/templates/frontend/layouts/about.html.haml
Normal file
@@ -0,0 +1,6 @@
|
||||
.about.animated.fadeIn
|
||||
.title About
|
||||
.summary Namewhale helps you find a unique name for your startup. Using an intelligent, seed-based algorithm, names are generated based on the sound, style, and feel of the seed words you chose.
|
||||
.links
|
||||
%a{"href" => "https://itunes.apple.com/us/app/namewhale/id1028881375?ls=1&mt=8", "target" => "_blank"} Namewhale on the AppStore
|
||||
%a{"href" => "https://twitter.com/namewhale", "target" => "_blank"} @namewhale
|
||||
4
app/assets/templates/frontend/layouts/footer.html.haml
Normal file
4
app/assets/templates/frontend/layouts/footer.html.haml
Normal file
@@ -0,0 +1,4 @@
|
||||
%footer.footer{"ng-class" => "footerClass"}
|
||||
.container
|
||||
.row
|
||||
.footer-about-section
|
||||
2
app/assets/templates/frontend/modals/username.html.haml
Normal file
2
app/assets/templates/frontend/modals/username.html.haml
Normal file
@@ -0,0 +1,2 @@
|
||||
%strong Choose a public username for all your shared note groups.
|
||||
%input{"style" => "margin-top: 10px; padding-left: 8px;", "type" => "text", "ng-keyup" => "$event.keyCode == 13 && saveUsername($event)", "ng-model" => "formData.username"}
|
||||
38
app/assets/templates/frontend/notes.html.haml
Normal file
38
app/assets/templates/frontend/notes.html.haml
Normal file
@@ -0,0 +1,38 @@
|
||||
.section.notes
|
||||
.content
|
||||
.section-title-bar.notes-title-bar
|
||||
.title {{ctrl.group.name}} notes
|
||||
.add-button{"ng-click" => "ctrl.createNewNote()"} +
|
||||
%br
|
||||
.filter-section
|
||||
%input.filter-bar{"select-on-click" => "true", "ng-model" => "ctrl.noteFilter.text", "placeholder" => "Filter", "ng-change" => "ctrl.filterTextChanged()", "lowercase" => "true"}
|
||||
.editor-menu.group-menu-bar
|
||||
%ul.nav.nav-pills
|
||||
%li.dropdown
|
||||
%a.dropdown-toggle{"ng-click" => "ctrl.showMenu = !ctrl.showMenu"}
|
||||
File
|
||||
%span.caret
|
||||
%span.sr-only
|
||||
%ul.dropdown-menu.dropdown-menu-left.nt-dropdown-menu.dark{"ng-if" => "ctrl.showMenu"}
|
||||
%li{"ng-if" => "!ctrl.group.presentation.enabled"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedGroupShare($event)"} Share Group
|
||||
%li{"ng-if" => "ctrl.group.presentation.enabled"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedGroupUnshare()"} Unshare Group
|
||||
%li{"ng-if" => "!ctrl.group.all"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedGroupDelete()"} Delete Group
|
||||
.menu-right-container
|
||||
.public-link{"ng-if" => "ctrl.group.presentation.enabled"}
|
||||
%a.url{"ng-if" => "!ctrl.editingUrl", "href" => "{{ctrl.publicUrlForGroup(ctrl.group)}}", "target" => "_blank"}
|
||||
%span.icon-rss.icon
|
||||
{{ctrl.publicUrlForGroup()}}
|
||||
.edit-url{"ng-if" => "ctrl.editingUrl"}
|
||||
{{ctrl.url.base}}
|
||||
%input.input{"ng-model" => "ctrl.url.token", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveUrl($event)",
|
||||
"ng-change" => "ctrl.urlChanged()", "ng-focus" => "ctrl.onUrlFocus()",
|
||||
"select-on-click" => "true", "autofocus" => "true"}
|
||||
.note{"ng-repeat" => "note in ctrl.notes | filter: ctrl.filterNotes",
|
||||
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}",
|
||||
"ng-attr-draggable" => "{{note.dummy ? undefined : 'true'}}", "note" => "note"}
|
||||
.name
|
||||
{{note.name}}
|
||||
.date {{note.created_at || 'Now'}}
|
||||
0
app/assets/templates/services/.keep
Normal file
0
app/assets/templates/services/.keep
Normal file
27
app/controllers/application_controller.rb
Normal file
27
app/controllers/application_controller.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include DeviseTokenAuth::Concerns::SetUserByToken
|
||||
# Prevent CSRF attacks by raising an exception.
|
||||
# For APIs, you may want to use :null_session instead.
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
after_action :set_csrf_cookie
|
||||
|
||||
layout :false
|
||||
|
||||
def frontend
|
||||
set_app_domain
|
||||
end
|
||||
|
||||
rescue_from ActionView::MissingTemplate do |exception|
|
||||
end
|
||||
protected
|
||||
|
||||
def set_app_domain
|
||||
@appDomain = request.domain
|
||||
@appDomain << ':' + request.port.to_s unless request.port.blank?
|
||||
end
|
||||
def set_csrf_cookie
|
||||
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
|
||||
end
|
||||
|
||||
end
|
||||
0
app/controllers/concerns/.keep
Normal file
0
app/controllers/concerns/.keep
Normal file
2
app/helpers/application_helper.rb
Normal file
2
app/helpers/application_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
module ApplicationHelper
|
||||
end
|
||||
0
app/mailers/.keep
Normal file
0
app/mailers/.keep
Normal file
0
app/models/.keep
Normal file
0
app/models/.keep
Normal file
61
app/views/application/frontend.html.erb
Normal file
61
app/views/application/frontend.html.erb
Normal file
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html ng-app="app.frontend" ng-controller="BaseCtrl">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
||||
|
||||
<link href="favicon/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180"></link>
|
||||
<link href="favicon/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"></link>
|
||||
<link href="favicon/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"></link>
|
||||
<link href="favicon/manifest.json" rel="manifest"></link>
|
||||
|
||||
<link color="#5bbad5" href="favicon/safari-pinned-tab.svg" rel="mask-icon"></link>
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<meta ng-bind="title" content="Neeto" name="apple-mobile-web-app-title"/>
|
||||
<meta ng-bind="title" content="Neeto" name="application-name"/>
|
||||
<base href="/"></base>
|
||||
|
||||
<title ng-bind="title">Neeto</title>
|
||||
<meta name="description" content="A private and secure personal notes/blogging system."/>
|
||||
|
||||
<meta name="twitter:title" content="Neeto, a private and secure notes app."/>
|
||||
<meta name="twitter:description" content="A private and secure personal notes/blogging system."/>
|
||||
<meta name="twitter:site" content="@neetoapp"/>
|
||||
<meta name="twitter:card" content="summary"/>
|
||||
|
||||
<meta name="og:title" content="Neeto, a private and secure notes app."/>
|
||||
<meta name="og:description" content="A private and secure personal notes/blogging system."/>
|
||||
|
||||
<% if Rails.env.development? %>
|
||||
<%= javascript_include_tag "compiled.js", debug: false %>
|
||||
<% else %>
|
||||
<%= javascript_include_tag "compiled.min.js", debug: false %>
|
||||
|
||||
<!-- Piwik -->
|
||||
<script type="text/javascript">
|
||||
var _paq = _paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="//dash.neeto.io/";
|
||||
_paq.push(['setTrackerUrl', u+'piwik.php']);
|
||||
_paq.push(['setSiteId', '1']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<noscript><p><img src="//dash.neeto.io/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript>
|
||||
<!-- End Piwik Code -->
|
||||
|
||||
<% end %>
|
||||
<%= stylesheet_link_tag "app", media: "all", debug: false %>
|
||||
|
||||
</head>
|
||||
|
||||
<body ng-class="bodyClass">
|
||||
<div ui-view="content"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
14
app/views/layouts/application.html.erb
Normal file
14
app/views/layouts/application.html.erb
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Namewhale</title>
|
||||
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
|
||||
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
|
||||
<%= csrf_meta_tags %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<%= yield %>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
3
bin/bundle
Executable file
3
bin/bundle
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env ruby
|
||||
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
|
||||
load Gem.bin_path('bundler', 'bundle')
|
||||
4
bin/rails
Executable file
4
bin/rails
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env ruby
|
||||
APP_PATH = File.expand_path('../config/application', __dir__)
|
||||
require_relative '../config/boot'
|
||||
require 'rails/commands'
|
||||
4
bin/rake
Executable file
4
bin/rake
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env ruby
|
||||
require_relative '../config/boot'
|
||||
require 'rake'
|
||||
Rake.application.run
|
||||
34
bin/setup
Executable file
34
bin/setup
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env ruby
|
||||
require 'pathname'
|
||||
require 'fileutils'
|
||||
include FileUtils
|
||||
|
||||
# path to your application root.
|
||||
APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
|
||||
|
||||
def system!(*args)
|
||||
system(*args) || abort("\n== Command #{args} failed ==")
|
||||
end
|
||||
|
||||
chdir APP_ROOT do
|
||||
# This script is a starting point to setup your application.
|
||||
# Add necessary setup steps to this file.
|
||||
|
||||
puts '== Installing dependencies =='
|
||||
system! 'gem install bundler --conservative'
|
||||
system('bundle check') || system!('bundle install')
|
||||
|
||||
# puts "\n== Copying sample files =="
|
||||
# unless File.exist?('config/database.yml')
|
||||
# cp 'config/database.yml.sample', 'config/database.yml'
|
||||
# end
|
||||
|
||||
puts "\n== Preparing database =="
|
||||
system! 'bin/rails db:setup'
|
||||
|
||||
puts "\n== Removing old logs and tempfiles =="
|
||||
system! 'bin/rails log:clear tmp:clear'
|
||||
|
||||
puts "\n== Restarting application server =="
|
||||
system! 'bin/rails restart'
|
||||
end
|
||||
15
bin/spring
Executable file
15
bin/spring
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
# This file loads spring without using Bundler, in order to be fast.
|
||||
# It gets overwritten when you run the `spring binstub` command.
|
||||
|
||||
unless defined?(Spring)
|
||||
require "rubygems"
|
||||
require "bundler"
|
||||
|
||||
if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)
|
||||
Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq }
|
||||
gem "spring", match[1]
|
||||
require "spring/binstub"
|
||||
end
|
||||
end
|
||||
29
bin/update
Executable file
29
bin/update
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env ruby
|
||||
require 'pathname'
|
||||
require 'fileutils'
|
||||
include FileUtils
|
||||
|
||||
# path to your application root.
|
||||
APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
|
||||
|
||||
def system!(*args)
|
||||
system(*args) || abort("\n== Command #{args} failed ==")
|
||||
end
|
||||
|
||||
chdir APP_ROOT do
|
||||
# This script is a way to update your development environment automatically.
|
||||
# Add necessary update steps to this file.
|
||||
|
||||
puts '== Installing dependencies =='
|
||||
system! 'gem install bundler --conservative'
|
||||
system('bundle check') || system!('bundle install')
|
||||
|
||||
puts "\n== Updating database =="
|
||||
system! 'bin/rails db:migrate'
|
||||
|
||||
puts "\n== Removing old logs and tempfiles =="
|
||||
system! 'bin/rails log:clear tmp:clear'
|
||||
|
||||
puts "\n== Restarting application server =="
|
||||
system! 'bin/rails restart'
|
||||
end
|
||||
25
bower.json
Normal file
25
bower.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "neeto",
|
||||
"lib": {
|
||||
"name": "bower-rails generated lib assets",
|
||||
"dependencies": {}
|
||||
},
|
||||
"vendor": {
|
||||
"name": "bower-rails generated vendor assets",
|
||||
"dependencies": {
|
||||
"angular": "^1.5.9",
|
||||
"angular-ui-router": "^0.2.18",
|
||||
"restangular": "^1.5.2",
|
||||
"ng-token-auth": "^0.0.29",
|
||||
"oclazyload": "^1.0.9",
|
||||
"font-awesome": "^4.6.2",
|
||||
"angular-cookie": "4.1.0",
|
||||
"marked": "0.3.4",
|
||||
"angular-lazy-img": "1.1.0",
|
||||
"ng-dialog" : "0.6.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "1.5.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
config.ru
Normal file
4
config.ru
Normal file
@@ -0,0 +1,4 @@
|
||||
# This file is used by Rack-based servers to start the application.
|
||||
|
||||
require ::File.expand_path('../config/environment', __FILE__)
|
||||
run Rails.application
|
||||
71
config/application.rb
Normal file
71
config/application.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
require_relative 'boot'
|
||||
# require 'rails/all'
|
||||
|
||||
require "active_model/railtie"
|
||||
require "action_controller/railtie"
|
||||
require "action_mailer/railtie"
|
||||
require "action_view/railtie"
|
||||
require "active_job/railtie" # Only for Rails >= 4.2
|
||||
require "action_cable/engine" # Only for Rails >= 5.0
|
||||
require "sprockets/railtie"
|
||||
require "rails/test_unit/railtie"
|
||||
|
||||
# Require the gems listed in Gemfile, including any gems
|
||||
# you've limited to :test, :development, or :production.
|
||||
Bundler.require(*Rails.groups)
|
||||
|
||||
module Neeto
|
||||
class Application < Rails::Application
|
||||
# Settings in config/environments/* take precedence over those specified here.
|
||||
# Application configuration should go into files in config/initializers
|
||||
# -- all .rb files in that directory are automatically loaded.
|
||||
|
||||
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
|
||||
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
|
||||
# config.time_zone = 'Central Time (US & Canada)'
|
||||
|
||||
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
||||
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
||||
# config.i18n.default_locale = :de
|
||||
|
||||
# Do not swallow errors in after_commit/after_rollback callbacks.
|
||||
# config.active_record.raise_in_transactional_callbacks = true
|
||||
|
||||
# Cross-Origin Resource Sharing (CORS) for Rack compatible web applications.
|
||||
config.middleware.insert_before 0, "Rack::Cors" do
|
||||
allow do
|
||||
origins '*'
|
||||
resource '*', :headers => :any, :methods => [:get, :post, :put, :patch, :delete, :options], :expose => ['Access-Token', 'Client', 'UID']
|
||||
end
|
||||
end
|
||||
|
||||
# config.middleware.use Rack::Deflater
|
||||
|
||||
config.middleware.insert_before(Rack::Sendfile, Rack::Deflater)
|
||||
|
||||
# Disable auto creation of additional resources with "rails generate"
|
||||
config.generators do |g|
|
||||
g.test_framework false
|
||||
g.view_specs false
|
||||
g.helper_specs false
|
||||
g.stylesheets = false
|
||||
g.javascripts = false
|
||||
g.helper = false
|
||||
end
|
||||
|
||||
config.action_mailer.default_url_options = { host: ENV['APP_HOST'] }
|
||||
|
||||
# SMTP settings
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
config.action_mailer.smtp_settings = {
|
||||
:address => ENV['SMTP_HOST'],
|
||||
:port => ENV['SMTP_PORT'],
|
||||
:domain => ENV['SMTP_DOMAIN'],
|
||||
:user_name => ENV['SMTP_USERNAME'],
|
||||
:password => ENV['SMTP_PASSWORD'],
|
||||
:authentication => 'login',
|
||||
:enable_starttls_auto => true # detects and uses STARTTLS
|
||||
}
|
||||
|
||||
end
|
||||
end
|
||||
3
config/boot.rb
Normal file
3
config/boot.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
||||
|
||||
require 'bundler/setup' # Set up gems listed in the Gemfile.
|
||||
10
config/cable.yml
Normal file
10
config/cable.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
development:
|
||||
adapter: redis
|
||||
url: redis://localhost:6379
|
||||
|
||||
test:
|
||||
adapter: async
|
||||
|
||||
production:
|
||||
adapter: redis
|
||||
url: redis://localhost:6379/1
|
||||
40
config/database.yml
Normal file
40
config/database.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
# MySQL. Versions 5.0+ are recommended.
|
||||
#
|
||||
# Install the MYSQL driver
|
||||
# gem install mysql2
|
||||
#
|
||||
# Ensure the MySQL gem is defined in your Gemfile
|
||||
# gem 'mysql2'
|
||||
#
|
||||
# And be sure to use new-style password hashing:
|
||||
# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
|
||||
#
|
||||
default: &default
|
||||
adapter: mysql2
|
||||
encoding: utf8
|
||||
pool: 5
|
||||
username: <%= ENV['DB_USERNAME'] %>
|
||||
password: <%= ENV['DB_PASSWORD'] %>
|
||||
database: <%= ENV['DB_DATABASE'] %>
|
||||
host: <%= ENV['DB_HOST'] %>
|
||||
port: <%= ENV['DB_PORT'] %>
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
||||
# Warning: The database defined as "test" will be erased and
|
||||
# re-generated from your development database when you run "rake".
|
||||
# Do not set this db to the same as development or production.
|
||||
test:
|
||||
<<: *default
|
||||
|
||||
# You can use this database configuration with:
|
||||
#
|
||||
# production:
|
||||
# url: <%= ENV['DATABASE_URL'] %>
|
||||
#
|
||||
staging:
|
||||
<<: *default
|
||||
|
||||
production:
|
||||
<<: *default
|
||||
70
config/deploy.rb
Normal file
70
config/deploy.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
CAP_CONFIG = YAML.load_file("config/cap.yml")
|
||||
# config valid only for current version of Capistrano
|
||||
lock '3.6.1'
|
||||
|
||||
set :application, 'neeto'
|
||||
set :repo_url, CAP_CONFIG["default"]["repo_url"]
|
||||
|
||||
# Default branch is :master
|
||||
# ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp
|
||||
|
||||
# Default deploy_to directory is /var/www/my_app_name
|
||||
# set :deploy_to, '/var/www/my_app_name'
|
||||
|
||||
# Default value for :scm is :git
|
||||
set :scm, :git
|
||||
set :git_strategy, Capistrano::Git::SubmoduleStrategy
|
||||
|
||||
# Default value for :format is :airbrussh.
|
||||
# set :format, :airbrussh
|
||||
|
||||
# You can configure the Airbrussh format using :format_options.
|
||||
# These are the defaults.
|
||||
# set :format_options, command_output: true, log_file: 'log/capistrano.log', color: :auto, truncate: :auto
|
||||
|
||||
# Default value for :pty is false
|
||||
# set :pty, true
|
||||
|
||||
# Default value for :linked_files is []
|
||||
# set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml')
|
||||
set :linked_files, fetch(:linked_files, []).push('.env')
|
||||
|
||||
# Default value for linked_dirs is []
|
||||
# set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system')
|
||||
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system', 'public/uploads')
|
||||
|
||||
# Default value for keep_releases is 5
|
||||
# set :keep_releases, 5
|
||||
|
||||
set :rvm_ruby_version, '2.3.0'
|
||||
|
||||
namespace :deploy do
|
||||
|
||||
task :npm_install do
|
||||
on roles(:app) do
|
||||
within release_path do
|
||||
# string commands dont work, have to use special *%w syntax
|
||||
execute *%w[ npm install ]
|
||||
execute *%w[ grunt ]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
after :restart, :clear_cache do
|
||||
on roles(:web), in: :groups, limit: 3, wait: 5 do
|
||||
# Here we can do anything such as:
|
||||
within release_path do
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before 'deploy:compile_assets', 'bower:install'
|
||||
after 'bower:install', 'deploy:npm_install'
|
||||
|
||||
set :ssh_options, {
|
||||
keys: %W( #{CAP_CONFIG['default']['key_path']} ),
|
||||
forward_agent: false,
|
||||
auth_methods: %w(publickey)
|
||||
}
|
||||
63
config/deploy/production.rb
Normal file
63
config/deploy/production.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
# server-based syntax
|
||||
# ======================
|
||||
# Defines a single server with a list of roles and multiple properties.
|
||||
# You can define all roles on a single server, or split them:
|
||||
|
||||
# server 'example.com', user: 'deploy', roles: %w{app db web}, my_property: :my_value
|
||||
# server 'example.com', user: 'deploy', roles: %w{app web}, other_property: :other_value
|
||||
# server 'db.example.com', user: 'deploy', roles: %w{db}
|
||||
|
||||
server CAP_CONFIG['production']['server'], user: CAP_CONFIG['production']['user'], roles: %w{app db web}
|
||||
|
||||
set :deploy_to, CAP_CONFIG['production']['deploy_to']
|
||||
|
||||
# role-based syntax
|
||||
# ==================
|
||||
|
||||
# Defines a role with one or multiple servers. The primary server in each
|
||||
# group is considered to be the first unless any hosts have the primary
|
||||
# property set. Specify the username and a domain or IP for the server.
|
||||
# Don't use `:all`, it's a meta role.
|
||||
|
||||
# role :app, %w{deploy@example.com}, my_property: :my_value
|
||||
# role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value
|
||||
# role :db, %w{deploy@example.com}
|
||||
|
||||
|
||||
|
||||
# Configuration
|
||||
# =============
|
||||
# You can set any configuration variable like in config/deploy.rb
|
||||
# These variables are then only loaded and set in this stage.
|
||||
# For available Capistrano configuration variables see the documentation page.
|
||||
# http://capistranorb.com/documentation/getting-started/configuration/
|
||||
# Feel free to add new variables to customise your setup.
|
||||
|
||||
|
||||
|
||||
# Custom SSH Options
|
||||
# ==================
|
||||
# You may pass any option but keep in mind that net/ssh understands a
|
||||
# limited set of options, consult the Net::SSH documentation.
|
||||
# http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start
|
||||
#
|
||||
# Global options
|
||||
# --------------
|
||||
# set :ssh_options, {
|
||||
# keys: %w(/home/rlisowski/.ssh/id_rsa),
|
||||
# forward_agent: false,
|
||||
# auth_methods: %w(password)
|
||||
# }
|
||||
#
|
||||
# The server-based syntax can be used to override options:
|
||||
# ------------------------------------
|
||||
# server 'example.com',
|
||||
# user: 'user_name',
|
||||
# roles: %w{web app},
|
||||
# ssh_options: {
|
||||
# user: 'user_name', # overrides user setting above
|
||||
# keys: %w(/home/user_name/.ssh/id_rsa),
|
||||
# forward_agent: false,
|
||||
# auth_methods: %w(publickey password)
|
||||
# # password: 'please use keys'
|
||||
# }
|
||||
65
config/deploy/staging.rb
Normal file
65
config/deploy/staging.rb
Normal file
@@ -0,0 +1,65 @@
|
||||
# server-based syntax
|
||||
# ======================
|
||||
# Defines a single server with a list of roles and multiple properties.
|
||||
# You can define all roles on a single server, or split them:
|
||||
|
||||
# server 'example.com', user: 'deploy', roles: %w{app db web}, my_property: :my_value
|
||||
# server 'example.com', user: 'deploy', roles: %w{app web}, other_property: :other_value
|
||||
# server 'db.example.com', user: 'deploy', roles: %w{db}
|
||||
|
||||
server CAP_CONFIG['staging']['server'], user: CAP_CONFIG['staging']['user'], roles: %w{app db web}
|
||||
|
||||
set :branch, CAP_CONFIG['staging']['branch']
|
||||
|
||||
set :deploy_to, CAP_CONFIG['staging']['deploy_to']
|
||||
|
||||
# role-based syntax
|
||||
# ==================
|
||||
|
||||
# Defines a role with one or multiple servers. The primary server in each
|
||||
# group is considered to be the first unless any hosts have the primary
|
||||
# property set. Specify the username and a domain or IP for the server.
|
||||
# Don't use `:all`, it's a meta role.
|
||||
|
||||
# role :app, %w{deploy@example.com}, my_property: :my_value
|
||||
# role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value
|
||||
# role :db, %w{deploy@example.com}
|
||||
|
||||
|
||||
|
||||
# Configuration
|
||||
# =============
|
||||
# You can set any configuration variable like in config/deploy.rb
|
||||
# These variables are then only loaded and set in this stage.
|
||||
# For available Capistrano configuration variables see the documentation page.
|
||||
# http://capistranorb.com/documentation/getting-started/configuration/
|
||||
# Feel free to add new variables to customise your setup.
|
||||
|
||||
|
||||
|
||||
# Custom SSH Options
|
||||
# ==================
|
||||
# You may pass any option but keep in mind that net/ssh understands a
|
||||
# limited set of options, consult the Net::SSH documentation.
|
||||
# http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start
|
||||
#
|
||||
# Global options
|
||||
# --------------
|
||||
# set :ssh_options, {
|
||||
# keys: %w(/home/rlisowski/.ssh/id_rsa),
|
||||
# forward_agent: false,
|
||||
# auth_methods: %w(password)
|
||||
# }
|
||||
#
|
||||
# The server-based syntax can be used to override options:
|
||||
# ------------------------------------
|
||||
# server 'example.com',
|
||||
# user: 'user_name',
|
||||
# roles: %w{web app},
|
||||
# ssh_options: {
|
||||
# user: 'user_name', # overrides user setting above
|
||||
# keys: %w(/home/user_name/.ssh/id_rsa),
|
||||
# forward_agent: false,
|
||||
# auth_methods: %w(publickey password)
|
||||
# # password: 'please use keys'
|
||||
# }
|
||||
5
config/environment.rb
Normal file
5
config/environment.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# Load the Rails application.
|
||||
require_relative 'application'
|
||||
|
||||
# Initialize the Rails application.
|
||||
Rails.application.initialize!
|
||||
51
config/environments/development.rb
Normal file
51
config/environments/development.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
Rails.application.configure do
|
||||
# Settings specified here will take precedence over those in config/application.rb.
|
||||
|
||||
# config.assets.js_compressor = Uglifier.new(mangle: false)\
|
||||
|
||||
# config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
||||
|
||||
# In the development environment your application's code is reloaded on
|
||||
# every request. This slows down response time but is perfect for development
|
||||
# since you don't have to restart the web server when you make code changes.
|
||||
config.cache_classes = false
|
||||
config.reload_classes_only_on_change = true
|
||||
|
||||
# Do not eager load code on boot.
|
||||
config.eager_load = false
|
||||
|
||||
# Show full error reports and disable caching.
|
||||
config.consider_all_requests_local = true
|
||||
config.action_controller.perform_caching = false
|
||||
|
||||
# Don't care if the mailer can't send.
|
||||
config.action_mailer.raise_delivery_errors = false
|
||||
|
||||
# config.public_file_server.enabled = true
|
||||
|
||||
# Print deprecation notices to the Rails logger.
|
||||
config.active_support.deprecation = :log
|
||||
|
||||
# Raise an error on page load if there are pending migrations.
|
||||
# config.active_record.migration_error = :page_load
|
||||
|
||||
# Debug mode disables concatenation and preprocessing of assets.
|
||||
# This option may cause significant delays in view rendering with a large
|
||||
# number of complex assets.
|
||||
config.assets.debug = true
|
||||
|
||||
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
|
||||
# yet still be able to expire them through the digest params.
|
||||
config.assets.digest = true
|
||||
|
||||
# Adds additional error checking when serving assets at runtime.
|
||||
# Checks for improperly declared sprockets dependencies.
|
||||
# Raises helpful error messages.
|
||||
config.assets.raise_runtime_errors = true
|
||||
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 }
|
||||
|
||||
# Raises error for missing translations
|
||||
# config.action_view.raise_on_missing_translations = true
|
||||
end
|
||||
96
config/environments/production.rb
Normal file
96
config/environments/production.rb
Normal file
@@ -0,0 +1,96 @@
|
||||
Rails.application.configure do
|
||||
# Settings specified here will take precedence over those in config/application.rb.
|
||||
|
||||
# Code is not reloaded between requests.
|
||||
config.cache_classes = true
|
||||
|
||||
# Use a different logger for distributed setups.
|
||||
# require 'syslog/logger'
|
||||
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
|
||||
|
||||
if ENV["RAILS_LOG_TO_STDOUT"].present?
|
||||
logger = ActiveSupport::Logger.new(STDOUT)
|
||||
logger.formatter = config.log_formatter
|
||||
config.logger = ActiveSupport::TaggedLogging.new(logger)
|
||||
end
|
||||
|
||||
# Eager load code on boot. This eager loads most of Rails and
|
||||
# your application in memory, allowing both threaded web servers
|
||||
# and those relying on copy on write to perform better.
|
||||
# Rake tasks automatically ignore this option for performance.
|
||||
config.eager_load = true
|
||||
|
||||
# Full error reports are disabled and caching is turned on.
|
||||
config.consider_all_requests_local = false
|
||||
config.action_controller.perform_caching = true
|
||||
|
||||
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
|
||||
|
||||
# Mount Action Cable outside main process or domain
|
||||
# config.action_cable.mount_path = nil
|
||||
# config.action_cable.url = 'wss://example.com/cable'
|
||||
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
|
||||
|
||||
|
||||
|
||||
# Enable Rack::Cache to put a simple HTTP cache in front of your application
|
||||
# Add `rack-cache` to your Gemfile before enabling this.
|
||||
# For large-scale production use, consider using a caching reverse proxy like
|
||||
# NGINX, varnish or squid.
|
||||
# config.action_dispatch.rack_cache = true
|
||||
|
||||
# Compress JavaScripts and CSS.
|
||||
config.assets.compress = true
|
||||
# config.assets.js_compressor = Uglifier.new(mangle: false)
|
||||
config.assets.js_compressor = :uglifier
|
||||
config.assets.css_compressor = :sass
|
||||
|
||||
# Do not fallback to assets pipeline if a precompiled asset is missed.
|
||||
config.assets.compile = false
|
||||
|
||||
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
|
||||
# yet still be able to expire them through the digest params.
|
||||
config.assets.digest = true
|
||||
|
||||
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
|
||||
|
||||
# Specifies the header that your server uses for sending files.
|
||||
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
|
||||
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
|
||||
|
||||
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
|
||||
# config.force_ssl = true
|
||||
|
||||
# Use the lowest log level to ensure availability of diagnostic information
|
||||
# when problems arise.
|
||||
config.log_level = :debug
|
||||
|
||||
# Prepend all log lines with the following tags.
|
||||
# config.log_tags = [ :subdomain, :uuid ]
|
||||
|
||||
# Use a different logger for distributed setups.
|
||||
# config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
|
||||
|
||||
# Use a different cache store in production.
|
||||
# config.cache_store = :mem_cache_store
|
||||
|
||||
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
|
||||
# config.action_controller.asset_host = 'http://assets.example.com'
|
||||
|
||||
# Ignore bad email addresses and do not raise email delivery errors.
|
||||
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
|
||||
# config.action_mailer.raise_delivery_errors = false
|
||||
|
||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||
# the I18n.default_locale when a translation cannot be found).
|
||||
config.i18n.fallbacks = true
|
||||
|
||||
# Send deprecation notices to registered listeners.
|
||||
config.active_support.deprecation = :notify
|
||||
|
||||
# Use default logging formatter so that PID and timestamp are not suppressed.
|
||||
config.log_formatter = ::Logger::Formatter.new
|
||||
|
||||
# Do not dump schema after migrations.
|
||||
# config.active_record.dump_schema_after_migration = false
|
||||
end
|
||||
81
config/environments/staging.rb
Normal file
81
config/environments/staging.rb
Normal file
@@ -0,0 +1,81 @@
|
||||
Rails.application.configure do
|
||||
# Settings specified here will take precedence over those in config/application.rb.
|
||||
|
||||
# Code is not reloaded between requests.
|
||||
config.cache_classes = true
|
||||
|
||||
# Use a different logger for distributed setups.
|
||||
# require 'syslog/logger'
|
||||
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
|
||||
|
||||
if ENV["RAILS_LOG_TO_STDOUT"].present?
|
||||
logger = ActiveSupport::Logger.new(STDOUT)
|
||||
logger.formatter = config.log_formatter
|
||||
config.logger = ActiveSupport::TaggedLogging.new(logger)
|
||||
end
|
||||
|
||||
# Eager load code on boot. This eager loads most of Rails and
|
||||
# your application in memory, allowing both threaded web servers
|
||||
# and those relying on copy on write to perform better.
|
||||
# Rake tasks automatically ignore this option for performance.
|
||||
config.eager_load = true
|
||||
|
||||
# Full error reports are disabled and caching is turned on.
|
||||
config.consider_all_requests_local = true
|
||||
config.action_controller.perform_caching = true
|
||||
|
||||
# Compress JavaScripts and CSS.
|
||||
config.assets.compress = true
|
||||
# config.assets.js_compressor = Uglifier.new(mangle: false)
|
||||
config.assets.js_compressor = :uglifier
|
||||
config.assets.css_compressor = :sass
|
||||
|
||||
# Do not fallback to assets pipeline if a precompiled asset is missed.
|
||||
config.assets.compile = false
|
||||
|
||||
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
|
||||
# yet still be able to expire them through the digest params.
|
||||
# config.assets.digest = true
|
||||
|
||||
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
|
||||
|
||||
# Specifies the header that your server uses for sending files.
|
||||
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
|
||||
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
|
||||
|
||||
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
|
||||
# config.force_ssl = true
|
||||
|
||||
# Use the lowest log level to ensure availability of diagnostic information
|
||||
# when problems arise.
|
||||
config.log_level = :debug
|
||||
|
||||
# Prepend all log lines with the following tags.
|
||||
# config.log_tags = [ :subdomain, :uuid ]
|
||||
|
||||
# Use a different logger for distributed setups.
|
||||
# config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
|
||||
|
||||
# Use a different cache store in production.
|
||||
# config.cache_store = :mem_cache_store
|
||||
|
||||
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
|
||||
# config.action_controller.asset_host = 'http://assets.example.com'
|
||||
|
||||
# Ignore bad email addresses and do not raise email delivery errors.
|
||||
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
|
||||
# config.action_mailer.raise_delivery_errors = false
|
||||
|
||||
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
|
||||
# the I18n.default_locale when a translation cannot be found).
|
||||
config.i18n.fallbacks = true
|
||||
|
||||
# Send deprecation notices to registered listeners.
|
||||
config.active_support.deprecation = :notify
|
||||
|
||||
# Use default logging formatter so that PID and timestamp are not suppressed.
|
||||
config.log_formatter = ::Logger::Formatter.new
|
||||
|
||||
# Do not dump schema after migrations.
|
||||
# config.active_record.dump_schema_after_migration = false
|
||||
end
|
||||
42
config/environments/test.rb
Normal file
42
config/environments/test.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
Rails.application.configure do
|
||||
# Settings specified here will take precedence over those in config/application.rb.
|
||||
|
||||
# The test environment is used exclusively to run your application's
|
||||
# test suite. You never need to work with it otherwise. Remember that
|
||||
# your test database is "scratch space" for the test suite and is wiped
|
||||
# and recreated between test runs. Don't rely on the data there!
|
||||
config.cache_classes = true
|
||||
|
||||
# Do not eager load code on boot. This avoids loading your whole application
|
||||
# just for the purpose of running a single test. If you are using a tool that
|
||||
# preloads Rails for running tests, you may have to set it to true.
|
||||
config.eager_load = false
|
||||
|
||||
# Configure public file server for tests with Cache-Control for performance.
|
||||
config.public_file_server.enabled = true
|
||||
config.public_file_server.headers = {
|
||||
'Cache-Control' => 'public, max-age=3600'
|
||||
}
|
||||
|
||||
# Show full error reports and disable caching.
|
||||
config.consider_all_requests_local = true
|
||||
config.action_controller.perform_caching = false
|
||||
|
||||
# Raise exceptions instead of rendering exception templates.
|
||||
config.action_dispatch.show_exceptions = false
|
||||
|
||||
# Disable request forgery protection in test environment.
|
||||
config.action_controller.allow_forgery_protection = false
|
||||
config.action_mailer.perform_caching = false
|
||||
|
||||
# Tell Action Mailer not to deliver emails to the real world.
|
||||
# The :test delivery method accumulates sent emails in the
|
||||
# ActionMailer::Base.deliveries array.
|
||||
config.action_mailer.delivery_method = :test
|
||||
|
||||
# Print deprecation notices to the stderr.
|
||||
config.active_support.deprecation = :stderr
|
||||
|
||||
# Raises error for missing translations
|
||||
# config.action_view.raise_on_missing_translations = true
|
||||
end
|
||||
6
config/initializers/application_controller_renderer.rb
Normal file
6
config/initializers/application_controller_renderer.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# ApplicationController.renderer.defaults.merge!(
|
||||
# http_host: 'example.org',
|
||||
# https: false
|
||||
# )
|
||||
16
config/initializers/assets.rb
Normal file
16
config/initializers/assets.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# Version of your assets, change this if you want to expire all your assets.
|
||||
Rails.application.config.assets.version = '1.0'
|
||||
|
||||
# Add additional assets to the asset load path
|
||||
# Rails.application.config.assets.paths << Emoji.images_path
|
||||
|
||||
Rails.application.config.assets.paths << Rails.root.join('vendor', 'assets', 'fonts')
|
||||
|
||||
Rails.application.config.assets.precompile << /\.(?:svg|eot|woff|ttf)\z/
|
||||
|
||||
# Precompile additional assets.
|
||||
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
|
||||
# Rails.application.config.assets.precompile += %w( search.js )
|
||||
Rails.application.config.assets.precompile += %w( app.css compiled.min.js compiled.js )
|
||||
7
config/initializers/backtrace_silencers.rb
Normal file
7
config/initializers/backtrace_silencers.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
|
||||
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
|
||||
|
||||
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
|
||||
# Rails.backtrace_cleaner.remove_silencers!
|
||||
19
config/initializers/bower_rails.rb
Normal file
19
config/initializers/bower_rails.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
BowerRails.configure do |bower_rails|
|
||||
# Tell bower-rails what path should be considered as root. Defaults to Dir.pwd
|
||||
# bower_rails.root_path = Dir.pwd
|
||||
|
||||
# Invokes rake bower:install before precompilation. Defaults to false
|
||||
# bower_rails.install_before_precompile = true
|
||||
|
||||
# Invokes rake bower:resolve before precompilation. Defaults to false
|
||||
# bower_rails.resolve_before_precompile = true
|
||||
|
||||
# Invokes rake bower:clean before precompilation. Defaults to false
|
||||
# bower_rails.clean_before_precompile = true
|
||||
|
||||
# Invokes rake bower:install:deployment instead rake bower:install. Defaults to false
|
||||
# bower_rails.use_bower_install_deployment = true
|
||||
#
|
||||
# Invokes rake bower:install and rake bower:install:deployment with -F (force) flag. Defaults to false
|
||||
# bower_rails.force_install = true
|
||||
end
|
||||
5
config/initializers/cookies_serializer.rb
Normal file
5
config/initializers/cookies_serializer.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# Specify a serializer for the signed and encrypted cookie jars.
|
||||
# Valid options are :json, :marshal, and :hybrid.
|
||||
Rails.application.config.action_dispatch.cookies_serializer = :json
|
||||
48
config/initializers/devise_token_auth.rb
Normal file
48
config/initializers/devise_token_auth.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
DeviseTokenAuth.setup do |config|
|
||||
# By default the authorization headers will change after each request. The
|
||||
# client is responsible for keeping track of the changing tokens. Change
|
||||
# this to false to prevent the Authorization header from changing after
|
||||
# each request.
|
||||
# config.change_headers_on_each_request = true
|
||||
|
||||
# By default, users will need to re-authenticate after 2 weeks. This setting
|
||||
# determines how long tokens will remain valid after they are issued.
|
||||
# config.token_lifespan = 2.weeks
|
||||
|
||||
# Sets the max number of concurrent devices per user, which is 10 by default.
|
||||
# After this limit is reached, the oldest tokens will be removed.
|
||||
# config.max_number_of_devices = 10
|
||||
|
||||
# Sometimes it's necessary to make several requests to the API at the same
|
||||
# time. In this case, each request in the batch will need to share the same
|
||||
# auth token. This setting determines how far apart the requests can be while
|
||||
# still using the same auth token.
|
||||
# config.batch_request_buffer_throttle = 5.seconds
|
||||
|
||||
# This route will be the prefix for all oauth2 redirect callbacks. For
|
||||
# example, using the default '/omniauth', the github oauth2 provider will
|
||||
# redirect successful authentications to '/omniauth/github/callback'
|
||||
# config.omniauth_prefix = "/omniauth"
|
||||
|
||||
# By default sending current password is not needed for the password update.
|
||||
# Uncomment to enforce current_password param to be checked before all
|
||||
# attribute updates. Set it to :password if you want it to be checked only if
|
||||
# password is updated.
|
||||
config.check_current_password_before_update = :password
|
||||
|
||||
# By default we will use callbacks for single omniauth.
|
||||
# It depends on fields like email, provider and uid.
|
||||
# config.default_callbacks = true
|
||||
|
||||
# Makes it possible to change the headers names
|
||||
# config.headers_names = {:'access-token' => 'access-token',
|
||||
# :'client' => 'client',
|
||||
# :'expiry' => 'expiry',
|
||||
# :'uid' => 'uid',
|
||||
# :'token-type' => 'token-type' }
|
||||
|
||||
# By default, only Bearer Token authentication is implemented out of the box.
|
||||
# If, however, you wish to integrate with legacy Devise authentication, you can
|
||||
# do so by enabling this flag. NOTE: This feature is highly experimental!
|
||||
# config.enable_standard_devise_support = false
|
||||
end
|
||||
4
config/initializers/filter_parameter_logging.rb
Normal file
4
config/initializers/filter_parameter_logging.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# Configure sensitive parameters which will be filtered from the log file.
|
||||
Rails.application.config.filter_parameters += [:password, :content, :name, :local_encrypted_content, :local_eek]
|
||||
16
config/initializers/inflections.rb
Normal file
16
config/initializers/inflections.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# Add new inflection rules using the following format. Inflections
|
||||
# are locale specific, and you may define rules for as many different
|
||||
# locales as you wish. All of these examples are active by default:
|
||||
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||
# inflect.plural /^(ox)$/i, '\1en'
|
||||
# inflect.singular /^(ox)en/i, '\1'
|
||||
# inflect.irregular 'person', 'people'
|
||||
# inflect.uncountable %w( fish sheep )
|
||||
# end
|
||||
|
||||
# These inflection rules are supported but not enabled by default:
|
||||
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||
# inflect.acronym 'RESTful'
|
||||
# end
|
||||
4
config/initializers/mime_types.rb
Normal file
4
config/initializers/mime_types.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# Add new mime types for use in respond_to blocks:
|
||||
# Mime::Type.register "text/richtext", :rtf
|
||||
23
config/initializers/new_framework_defaults.rb
Normal file
23
config/initializers/new_framework_defaults.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
#
|
||||
# This file contains migration options to ease your Rails 5.0 upgrade.
|
||||
#
|
||||
# Once upgraded flip defaults one by one to migrate to the new default.
|
||||
#
|
||||
# Read the Rails 5.0 release notes for more info on each option.
|
||||
|
||||
# Enable per-form CSRF tokens. Previous versions had false.
|
||||
Rails.application.config.action_controller.per_form_csrf_tokens = false
|
||||
|
||||
# Enable origin-checking CSRF mitigation. Previous versions had false.
|
||||
Rails.application.config.action_controller.forgery_protection_origin_check = false
|
||||
|
||||
# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
|
||||
# Previous versions had false.
|
||||
ActiveSupport.to_time_preserves_timezone = false
|
||||
|
||||
# Require `belongs_to` associations by default. Previous versions had false.
|
||||
# Rails.application.config.active_record.belongs_to_required_by_default = false
|
||||
|
||||
# Do not halt callback chains when a callback returns false. Previous versions had true.
|
||||
ActiveSupport.halt_callback_chains_on_return_false = true
|
||||
3
config/initializers/session_store.rb
Normal file
3
config/initializers/session_store.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
Rails.application.config.session_store :cookie_store, key: '_neeto_session'
|
||||
14
config/initializers/wrap_parameters.rb
Normal file
14
config/initializers/wrap_parameters.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# This file contains settings for ActionController::ParamsWrapper which
|
||||
# is enabled by default.
|
||||
|
||||
# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
|
||||
ActiveSupport.on_load(:action_controller) do
|
||||
wrap_parameters format: [:json]
|
||||
end
|
||||
|
||||
# To enable root element in JSON for ActiveRecord objects.
|
||||
# ActiveSupport.on_load(:active_record) do
|
||||
# self.include_root_in_json = true
|
||||
# end
|
||||
23
config/locales/en.yml
Normal file
23
config/locales/en.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
# Files in the config/locales directory are used for internationalization
|
||||
# and are automatically loaded by Rails. If you want to use locales other
|
||||
# than English, add the necessary files in this directory.
|
||||
#
|
||||
# To use the locales, use `I18n.t`:
|
||||
#
|
||||
# I18n.t 'hello'
|
||||
#
|
||||
# In views, this is aliased to just `t`:
|
||||
#
|
||||
# <%= t('hello') %>
|
||||
#
|
||||
# To use a different locale, set it with `I18n.locale`:
|
||||
#
|
||||
# I18n.locale = :es
|
||||
#
|
||||
# This would use the information in config/locales/es.yml.
|
||||
#
|
||||
# To learn more, please read the Rails Internationalization guide
|
||||
# available at http://guides.rubyonrails.org/i18n.html.
|
||||
|
||||
en:
|
||||
hello: "Hello world"
|
||||
53
config/puma.rb
Normal file
53
config/puma.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
# Puma can serve each request in a thread from an internal thread pool.
|
||||
# The `threads` method setting takes two numbers a minimum and maximum.
|
||||
# Any libraries that use thread pools should be configured to match
|
||||
# the maximum value specified for Puma. Default is set to 5 threads for minimum
|
||||
# and maximum, this matches the default thread size of Active Record.
|
||||
#
|
||||
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
|
||||
threads threads_count, threads_count
|
||||
|
||||
|
||||
#!/usr/bin/env puma
|
||||
|
||||
# start puma with:
|
||||
# RAILS_ENV=production bundle exec puma -C ./config/puma.rb
|
||||
|
||||
# Specifies the `port` that Puma will listen on to receive requests, default is 3000.
|
||||
#
|
||||
port ENV.fetch("PORT") { 3000 }
|
||||
|
||||
# Specifies the `environment` that Puma will run in.
|
||||
#
|
||||
environment ENV.fetch("RAILS_ENV") { "development" }
|
||||
|
||||
# Specifies the number of `workers` to boot in clustered mode.
|
||||
# Workers are forked webserver processes. If using threads and workers together
|
||||
# the concurrency of the application would be max `threads` * `workers`.
|
||||
# Workers do not work on JRuby or Windows (both of which do not support
|
||||
# processes).
|
||||
#
|
||||
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
|
||||
|
||||
# Use the `preload_app!` method when specifying a `workers` number.
|
||||
# This directive tells Puma to first boot the application and load code
|
||||
# before forking the application. This takes advantage of Copy On Write
|
||||
# process behavior so workers use less memory. If you use this option
|
||||
# you need to make sure to reconnect any threads in the `on_worker_boot`
|
||||
# block.
|
||||
#
|
||||
# preload_app!
|
||||
|
||||
# The code in the `on_worker_boot` will be called if you are using
|
||||
# clustered mode by specifying a number of `workers`. After each worker
|
||||
# process is booted this block will be run, if you are using `preload_app!`
|
||||
# option you will want to use this block to reconnect to any threads
|
||||
# or connections that may have been created at application boot, Ruby
|
||||
# cannot share connections between processes.
|
||||
#
|
||||
# on_worker_boot do
|
||||
# ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
|
||||
# end
|
||||
|
||||
# Allow puma to be restarted by `rails restart` command.
|
||||
plugin :tmp_restart
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user