Created {{ entry.timestamp.strftime('%Y-%m-%d at %H:%M') }}{% if entry.lastedited.strftime('%Y-%m-%d at %H:%M') != entry.timestamp.strftime('%Y-%m-%d at %H:%M') %} | Last edited {{ entry.lastedited.strftime('%Y-%m-%d at %H:%M') }}
18 | {% endif %}
19 |
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/.sandstorm/description.md:
--------------------------------------------------------------------------------
1 | # Permanote Description
2 |
3 | A personal note-taking application designed to be clean and easy to use.
4 |
5 | ## Features
6 |
7 | - Markdown editing for notes
8 | - Copy and paste or drag and drop image uploading
9 | - Full text search
10 | - Tags
11 | - Syntax highlighting
12 | - Rich media embeds (e.g. YouTube videos)
13 | - Archive old notes
14 | - Keyboard shortcuts to create new note and submit note when done editing
15 |
16 | ## Non-features
17 |
18 | - No notebooks - create a new Sandstorm grain
19 | - No user accounts or access control ([because it's a Sandstorm app](https://docs.sandstorm.io/en/latest/developing/handbook/#does-not-implement-user-accounts-or-access-control))
20 | - No WYSIWIG editing - you need to write your own Markdown
21 | - No syncing or offline capability
22 |
--------------------------------------------------------------------------------
/templates/includes/pagination.html:
--------------------------------------------------------------------------------
1 | {% if pagination.get_page_count() > 1 %}
2 |
Created {{ entry.timestamp.strftime('%Y-%m-%d at %H:%M') }}{% if entry.lastedited.strftime('%Y-%m-%d at %H:%M') != entry.timestamp.strftime('%Y-%m-%d at %H:%M') %} | Last edited {{ entry.lastedited.strftime('%Y-%m-%d at %H:%M') }}
23 | {% endif %}
24 |
25 | {% else %}
26 |
No notes to display.
27 | {% endfor %}
28 | {% include "includes/pagination.html" %}
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Tom Atkins
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.db
2 | *.sublime-workspace
3 | *.sublime-project
4 | uploads/
5 | env/
6 | *.pyc
7 | PLAN.md
8 | .idea
9 | sandstorm-keyring
10 | .vagrant/
11 |
12 | # Byte-compiled / optimized / DLL files
13 | __pycache__/
14 | *.py[cod]
15 | *$py.class
16 |
17 | # C extensions
18 | *.so
19 |
20 | # Distribution / packaging
21 | .Python
22 | env/
23 | build/
24 | develop-eggs/
25 | dist/
26 | downloads/
27 | eggs/
28 | .eggs/
29 | lib/
30 | lib64/
31 | parts/
32 | sdist/
33 | var/
34 | *.egg-info/
35 | .installed.cfg
36 | *.egg
37 |
38 | # PyInstaller
39 | # Usually these files are written by a python script from a template
40 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
41 | *.manifest
42 | *.spec
43 |
44 | # Installer logs
45 | pip-log.txt
46 | pip-delete-this-directory.txt
47 |
48 | # Unit test / coverage reports
49 | htmlcov/
50 | .tox/
51 | .coverage
52 | .coverage.*
53 | .cache
54 | nosetests.xml
55 | coverage.xml
56 | *,cover
57 | .hypothesis/
58 |
59 | # Translations
60 | *.mo
61 | *.pot
62 |
63 | # Django stuff:
64 | *.log
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | #Ipython Notebook
73 | .ipynb_checkpoints
74 |
--------------------------------------------------------------------------------
/.sandstorm/app-graphics/permanote-dolphin.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask import Flask
4 | from playhouse.flask_utils import FlaskDB
5 |
6 | # Configuration values.
7 | APP_DIR = os.path.dirname(os.path.realpath(__file__))
8 |
9 | # The playhouse.flask_utils.FlaskDB object accepts database URL configuration
10 | # set db location and debug depending on if the app is running locally or
11 | # in production on Sandstorm
12 | if os.getenv('SANDSTORM'):
13 | DATABASE = 'sqliteext:////var/permanote.db'
14 | else:
15 | DATABASE = 'sqliteext:///%s' % os.path.join(APP_DIR, 'permanote.db')
16 |
17 | application = Flask(__name__)
18 | application.config.from_object(__name__)
19 |
20 | # FlaskDB is a wrapper for a peewee database that sets up pre/post-request
21 | # hooks for managing database connections.
22 | flask_db = FlaskDB(application)
23 |
24 | # The `database` is the actual peewee database, as opposed to flask_db which is
25 | # the wrapper.
26 | database = flask_db.database
27 |
28 | # Upload folder and file allowed extensions
29 | if os.getenv('SANDSTORM'):
30 | application.config['UPLOAD_FOLDER'] = '/var/uploads'
31 | application.config['DEBUG'] = False
32 | else:
33 | application.config['UPLOAD_FOLDER'] = '%s/uploads' % os.path.join(APP_DIR)
34 | application.config['DEBUG'] = True
35 |
36 | # file types allowed for upload
37 | application.config['ALLOWED_EXTENSIONS'] = set(['jpg', 'jpeg', 'png', 'gif', 'webp'])
38 |
39 | # This is used by micawber, which will attempt to generate rich media
40 | # embedded objects with maxwidth=800.
41 | application.config['SITE_WIDTH'] = 800
42 |
43 | # The secret key is used internally by Flask to encrypt session data stored
44 | # in cookies. Make this unique for your app.
45 | application.config['SECRET_KEY'] = 'asdfkj23kjdflkj23lkjs'
46 |
--------------------------------------------------------------------------------
/static/css/hilite.css:
--------------------------------------------------------------------------------
1 | .highlight {
2 | background: #040400;
3 | color: #FFFFFF;
4 | }
5 |
6 | .highlight span.selection { color: #323232; }
7 | .highlight span.gp { color: #9595FF; }
8 | .highlight span.vi { color: #9595FF; }
9 | .highlight span.kn { color: #00C0D1; }
10 | .highlight span.cp { color: #AEE674; }
11 | .highlight span.caret { color: #FFFFFF; }
12 | .highlight span.no { color: #AEE674; }
13 | .highlight span.s2 { color: #BBFB8D; }
14 | .highlight span.nb { color: #A7FDB2; }
15 | .highlight span.nc { color: #C2ABFF; }
16 | .highlight span.nd { color: #AEE674; }
17 | .highlight span.s { color: #BBFB8D; }
18 | .highlight span.nf { color: #AEE674; }
19 | .highlight span.nx { color: #AEE674; }
20 | .highlight span.kp { color: #00C0D1; }
21 | .highlight span.nt { color: #C2ABFF; }
22 | .highlight span.s1 { color: #BBFB8D; }
23 | .highlight span.bg { color: #040400; }
24 | .highlight span.kt { color: #00C0D1; }
25 | .highlight span.support_function { color: #81B864; }
26 | .highlight span.ow { color: #EBE1B4; }
27 | .highlight span.mf { color: #A1FF24; }
28 | .highlight span.bp { color: #9595FF; }
29 | .highlight span.fg { color: #FFFFFF; }
30 | .highlight span.c1 { color: #3379FF; }
31 | .highlight span.kc { color: #9595FF; }
32 | .highlight span.c { color: #3379FF; }
33 | .highlight span.sx { color: #BBFB8D; }
34 | .highlight span.kd { color: #00C0D1; }
35 | .highlight span.ss { color: #A1FF24; }
36 | .highlight span.sr { color: #BBFB8D; }
37 | .highlight span.mo { color: #A1FF24; }
38 | .highlight span.mi { color: #A1FF24; }
39 | .highlight span.mh { color: #A1FF24; }
40 | .highlight span.o { color: #EBE1B4; }
41 | .highlight span.si { color: #DA96A3; }
42 | .highlight span.sh { color: #BBFB8D; }
43 | .highlight span.na { color: #AEE674; }
44 | .highlight span.sc { color: #BBFB8D; }
45 | .highlight span.k { color: #00C0D1; }
46 | .highlight span.se { color: #DA96A3; }
47 | .highlight span.sd { color: #54F79C; }
48 |
--------------------------------------------------------------------------------
/.sandstorm/global-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 |
4 | CURL_OPTS="--silent --show-error"
5 | echo localhost > /etc/hostname
6 | hostname localhost
7 | curl $CURL_OPTS https://install.sandstorm.io/ > /host-dot-sandstorm/caches/install.sh
8 | SANDSTORM_CURRENT_VERSION=$(curl $CURL_OPTS -f "https://install.sandstorm.io/dev?from=0&type=install")
9 | SANDSTORM_PACKAGE="sandstorm-$SANDSTORM_CURRENT_VERSION.tar.xz"
10 | if [[ ! -f /host-dot-sandstorm/caches/$SANDSTORM_PACKAGE ]] ; then
11 | echo -n "Downloading Sandstorm version ${SANDSTORM_CURRENT_VERSION}..."
12 | curl $CURL_OPTS --output "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE.partial" "https://dl.sandstorm.io/$SANDSTORM_PACKAGE"
13 | mv "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE.partial" "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE"
14 | echo "...done."
15 | fi
16 | if [ ! -e /opt/sandstorm/latest/sandstorm ] ; then
17 | echo -n "Installing Sandstorm version ${SANDSTORM_CURRENT_VERSION}..."
18 | bash /host-dot-sandstorm/caches/install.sh -d -e "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE" >/dev/null
19 | echo "...done."
20 | fi
21 | modprobe ip_tables
22 | # Make the vagrant user part of the sandstorm group so that commands like
23 | # `spk dev` work.
24 | usermod -a -G 'sandstorm' 'vagrant'
25 | # Bind to all addresses, so the vagrant port-forward works.
26 | sudo sed --in-place='' \
27 | --expression='s/^BIND_IP=.*/BIND_IP=0.0.0.0/' \
28 | /opt/sandstorm/sandstorm.conf
29 | sudo service sandstorm restart
30 | # Enable apt-cacher-ng proxy to make things faster if one appears to be running on the gateway IP
31 | GATEWAY_IP=$(ip route | grep ^default | cut -d ' ' -f 3)
32 | if nc -z "$GATEWAY_IP" 3142 ; then
33 | echo "Acquire::http::Proxy \"http://$GATEWAY_IP:3142\";" > /etc/apt/apt.conf.d/80httpproxy
34 | fi
35 | # Configure apt to retry fetching things that fail to download.
36 | echo "APT::Acquire::Retries \"10\";" > /etc/apt/apt.conf.d/80sandstorm-retry
37 |
--------------------------------------------------------------------------------
/templates/create.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Create Note{% endblock %}
4 |
5 | {% block content_title %}Create Note{% endblock %}
6 |
7 | {% block content %}
8 |
46 | {% endblock %}
47 |
--------------------------------------------------------------------------------
/templates/edit.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Edit Note{% endblock %}
4 |
5 | {% block content_title %}Edit Note{% endblock %}
6 |
7 | {% block content %}
8 |
47 | {% endblock %}
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ARCHIVED
2 |
3 | The code in this project is no longer maintained. Leaving it here in case it is of interest to anyone making something similar.
4 |
5 | # Permanote
6 |
7 |
8 |
9 | A personal note-taking application designed for Sandstorm.
10 |
11 | It has the features I want. I use it for keeping a journal and recording technical tips and documentation when I need longer form notes. For me it has replaced Evernote and Google Keep - although it clearly doesn't have many of Evernote's features. I still use Google Keep for small quick notes that I need to be synced with mobile devices.
12 |
13 | ## Installation
14 |
15 | If you have a Sandstorm account on [Oasis](https://oasis.sandstorm.io/) or a self-hosted Sandstorm instance, it's simple to [install Permanote from the Sandstorm app market](https://apps.sandstorm.io/app/svwrpwnd3c380d1f99ge7g0qnjdq6y785c36s7qtqryxwkmn20qh).
16 |
17 | If you want to run it locally for development, you can clone this repo into a Python 2 virtualenv. Then just `pip install -r requirements.txt` and `python main.py`
18 |
19 | ## Warning and help appreciated
20 |
21 | This was hacked together over a weekend - please consider it Beta software and don't trust important data just yet. The code is horrible in many places! Pull requests gratefully received if you'd like to clean anything up before I get there.
22 |
23 | ## Features
24 |
25 | - Markdown editing for notes
26 | - Copy and paste or drag and drop image uploading
27 | - Full text search
28 | - Tags
29 | - Syntax highlighting
30 | - ~~Rich media embeds (e.g. YouTube videos)~~ removed for now
31 | - Archive old notes
32 | - Keyboard shortcuts to create new note and submit note when done editing
33 |
34 | ## Non-features
35 |
36 | - No notebooks - create a new Sandstorm grain
37 | - No user accounts or access control ([because it's a Sandstorm app](https://docs.sandstorm.io/en/latest/developing/handbook/#does-not-implement-user-accounts-or-access-control))
38 | - No WYSIWIG editing - you need to write your own Markdown
39 | - No syncing or offline capability
40 |
41 | ## Technology
42 |
43 | - [Flask](http://flask.pocoo.org/)
44 | - [Peewee](http://docs.peewee-orm.com/en/latest/)
45 | - [SQLite](https://www.sqlite.org/)
46 | - [jquery inline-attachment](https://github.com/Rovak/InlineAttachment)
47 |
48 | ## Credits
49 |
50 | ### Charles Leifer
51 |
52 | A lot of the code for this application was lifted directly from [this blog post by Charles Leifer](http://charlesleifer.com/blog/how-to-make-a-flask-blog-in-one-hour-or-less).
53 |
54 | Many thanks to Charles for his helpful blog posts and for creating excellent [support for SQLite](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqlite-extensions) from his [Peewee](http://docs.peewee-orm.com/en/latest/) ORM. This makes a great choice for developing lightweight Sandstorm apps that are easy to develop and fast to load.
55 |
56 | ### Logo
57 |
58 | The dolphin logo is from the [Twitter twemoji collection](https://github.com/twitter/twemoji) ([Creative Commons Attribution License](https://github.com/twitter/twemoji/blob/gh-pages/LICENSE-GRAPHICS))
59 |
60 | And because this gif exists!
61 |
62 | 
63 |
64 | **Why a dolphin for the logo?** Evernote use an elephant for their logo because elephants are known to have good memory.
65 |
66 | Apparently [dolphins have even better memory](http://news.nationalgeographic.com/news/2013/08/130806-dolphins-memories-animals-science-longest/) than elephants. I like how the twemoji dolphin icon has a big head - lots of room to remember stuff in there.
67 |
--------------------------------------------------------------------------------
/models.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from flask import Markup
4 | from markdown import markdown
5 | from markdown.extensions.codehilite import CodeHiliteExtension
6 | from markdown.extensions.extra import ExtraExtension
7 | from playhouse.sqlite_ext import *
8 | from playhouse.fields import ManyToManyField
9 |
10 | from app import application, flask_db, database
11 |
12 |
13 | class Entry(flask_db.Model):
14 | title = CharField()
15 | slug = CharField(unique=True)
16 | content = TextField()
17 | timestamp = DateTimeField(default=datetime.datetime.now, index=True)
18 | lastedited = DateTimeField(default=datetime.datetime.now, index=True)
19 | archived = BooleanField(default=False)
20 |
21 | @property
22 | def html_content(self):
23 | """
24 | Generate HTML representation of the markdown-formatted note,
25 | and also convert any media URLs into rich media objects such as video
26 | players or images.
27 | """
28 | hilite = CodeHiliteExtension(linenums=False, css_class='highlight')
29 | extras = ExtraExtension()
30 | markdown_content = markdown(self.content, extensions=[hilite, extras])
31 | return Markup(markdown_content)
32 |
33 | def save(self, *args, **kwargs):
34 | # Generate a URL-friendly representation of the entry's title.
35 | if not self.slug:
36 | self.slug = re.sub('[^\w]+', '-', self.title.lower()).strip('-')
37 | ret = super(Entry, self).save(*args, **kwargs)
38 |
39 | # Store search content.
40 | self.update_search_index()
41 | return ret
42 |
43 | def update_search_index(self):
44 | # Create a row in the FTSEntry table with the post content. This will
45 | # allow us to use SQLite's awesome full-text search extension to
46 | # search our entries.
47 | try:
48 | fts_entry = FTSEntry.get(FTSEntry.entry_id == self.id)
49 | except FTSEntry.DoesNotExist:
50 | fts_entry = FTSEntry(entry_id=self.id)
51 | force_insert = True
52 | else:
53 | query = FTSEntry.delete().where(FTSEntry.entry_id == self.id)
54 | query.execute()
55 | force_insert = True
56 | fts_entry.content = '\n'.join((self.title, self.content))
57 | fts_entry.save(force_insert=force_insert)
58 |
59 | @classmethod
60 | def public(cls):
61 | return Entry.select().where(Entry.archived == False)
62 |
63 | @classmethod
64 | def all(cls):
65 | return Entry.select()
66 |
67 | @classmethod
68 | def archive(cls):
69 | return Entry.select().where(Entry.archived == True)
70 |
71 | @classmethod
72 | def search(cls, query):
73 | words = [word.strip() for word in query.split() if word.strip()]
74 | if not words:
75 | # Return an empty query.
76 | return Entry.select().where(Entry.id == 0)
77 | else:
78 | search = ' '.join(words)
79 |
80 | # Query the full-text search index for entries matching the given
81 | # search query, then join the actual Entry data on the matching
82 | # search result.
83 | return (FTSEntry
84 | .select(
85 | FTSEntry,
86 | Entry,
87 | FTSEntry.rank().alias('score'))
88 | .join(Entry, on=(FTSEntry.entry_id == Entry.id).alias('entry'))
89 | .where(
90 | (Entry.archived == False) &
91 | (FTSEntry.match(search)))
92 | .order_by(SQL('score').desc()))
93 |
94 |
95 | class Tag(flask_db.Model):
96 | tag = CharField()
97 | entries = ManyToManyField(Entry, related_name='tags')
98 |
99 |
100 | EntryTags = Tag.entries.get_through_model()
101 |
102 |
103 | class FTSEntry(FTSModel):
104 | entry_id = IntegerField(Entry)
105 | content = TextField()
106 |
107 | class Meta:
108 | database = database
109 |
--------------------------------------------------------------------------------
/.sandstorm/service-config/mime.types:
--------------------------------------------------------------------------------
1 |
2 | types {
3 | text/html html htm shtml;
4 | text/css css;
5 | text/xml xml;
6 | image/gif gif;
7 | image/jpeg jpeg jpg;
8 | application/javascript js;
9 | application/atom+xml atom;
10 | application/rss+xml rss;
11 |
12 | text/mathml mml;
13 | text/plain txt;
14 | text/vnd.sun.j2me.app-descriptor jad;
15 | text/vnd.wap.wml wml;
16 | text/x-component htc;
17 |
18 | image/png png;
19 | image/tiff tif tiff;
20 | image/vnd.wap.wbmp wbmp;
21 | image/x-icon ico;
22 | image/x-jng jng;
23 | image/x-ms-bmp bmp;
24 | image/svg+xml svg svgz;
25 | image/webp webp;
26 |
27 | application/font-woff woff;
28 | application/java-archive jar war ear;
29 | application/json json;
30 | application/mac-binhex40 hqx;
31 | application/msword doc;
32 | application/pdf pdf;
33 | application/postscript ps eps ai;
34 | application/rtf rtf;
35 | application/vnd.apple.mpegurl m3u8;
36 | application/vnd.ms-excel xls;
37 | application/vnd.ms-fontobject eot;
38 | application/vnd.ms-powerpoint ppt;
39 | application/vnd.wap.wmlc wmlc;
40 | application/vnd.google-earth.kml+xml kml;
41 | application/vnd.google-earth.kmz kmz;
42 | application/x-7z-compressed 7z;
43 | application/x-cocoa cco;
44 | application/x-java-archive-diff jardiff;
45 | application/x-java-jnlp-file jnlp;
46 | application/x-makeself run;
47 | application/x-perl pl pm;
48 | application/x-pilot prc pdb;
49 | application/x-rar-compressed rar;
50 | application/x-redhat-package-manager rpm;
51 | application/x-sea sea;
52 | application/x-shockwave-flash swf;
53 | application/x-stuffit sit;
54 | application/x-tcl tcl tk;
55 | application/x-x509-ca-cert der pem crt;
56 | application/x-xpinstall xpi;
57 | application/xhtml+xml xhtml;
58 | application/xspf+xml xspf;
59 | application/zip zip;
60 |
61 | application/octet-stream bin exe dll;
62 | application/octet-stream deb;
63 | application/octet-stream dmg;
64 | application/octet-stream iso img;
65 | application/octet-stream msi msp msm;
66 |
67 | application/vnd.openxmlformats-officedocument.wordprocessingml.document docx;
68 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx;
69 | application/vnd.openxmlformats-officedocument.presentationml.presentation pptx;
70 |
71 | audio/midi mid midi kar;
72 | audio/mpeg mp3;
73 | audio/ogg ogg;
74 | audio/x-m4a m4a;
75 | audio/x-realaudio ra;
76 |
77 | video/3gpp 3gpp 3gp;
78 | video/mp2t ts;
79 | video/mp4 mp4;
80 | video/mpeg mpeg mpg;
81 | video/quicktime mov;
82 | video/webm webm;
83 | video/x-flv flv;
84 | video/x-m4v m4v;
85 | video/x-mng mng;
86 | video/x-ms-asf asx asf;
87 | video/x-ms-wmv wmv;
88 | video/x-msvideo avi;
89 | }
90 |
--------------------------------------------------------------------------------
/static/js/jquery.inline-attachment.min.js:
--------------------------------------------------------------------------------
1 | /*! inline-attachment - v2.0.2 - 2015-11-03 */
2 | !function(a,b){"use strict";var c=function(a,b){this.settings=c.util.merge(a,c.defaults),this.editor=b,this.filenameTag="{filename}",this.lastValue=null};c.editors={},c.util={merge:function(){for(var a={},b=arguments.length-1;b>=0;b--){var c=arguments[b];for(var d in c)c.hasOwnProperty(d)&&(a[d]=c[d])}return a},appendInItsOwnLine:function(a,b){return(a+"\n\n[[D]]"+b).replace(/(\n{2,})\[\[D\]\]/,"\n\n").replace(/^(\n*)/,"")},insertTextAtCursor:function(b,c){var d,e=b.scrollTop,f=0,g=!1;b.selectionStart||"0"===b.selectionStart?g="ff":a.selection&&(g="ie"),"ie"===g?(b.focus(),d=a.selection.createRange(),d.moveStart("character",-b.value.length),f=d.text.length):"ff"===g&&(f=b.selectionStart);var h=b.value.substring(0,f),i=b.value.substring(f,b.value.length);b.value=h+c+i,f+=c.length,"ie"===g?(b.focus(),d=a.selection.createRange(),d.moveStart("character",-b.value.length),d.moveStart("character",f),d.moveEnd("character",0),d.select()):"ff"===g&&(b.selectionStart=f,b.selectionEnd=f,b.focus()),b.scrollTop=e}},c.defaults={uploadUrl:"upload_attachment.php",uploadMethod:"POST",uploadFieldName:"file",defaultExtension:"png",jsonFieldName:"filename",allowedTypes:["image/jpeg","image/png","image/jpg","image/gif"],progressText:"![Uploading file...]()",urlText:"",errorText:"Error uploading file",extraParams:{},extraHeaders:{},beforeFileUpload:function(){return!0},onFileReceived:function(){},onFileUploadResponse:function(){return!0},onFileUploadError:function(){return!0},onFileUploaded:function(){}},c.prototype.uploadFile=function(a){var b=this,c=new FormData,d=new XMLHttpRequest,e=this.settings,f=e.defaultExtension||e.defualtExtension;if("function"==typeof e.setupFormData&&e.setupFormData(c,a),a.name){var g=a.name.match(/\.(.+)$/);g&&(f=g[1])}var h="image-"+Date.now()+"."+f;if("function"==typeof e.remoteFilename&&(h=e.remoteFilename(a)),c.append(e.uploadFieldName,a,h),"object"==typeof e.extraParams)for(var i in e.extraParams)e.extraParams.hasOwnProperty(i)&&c.append(i,e.extraParams[i]);if(d.open("POST",e.uploadUrl),"object"==typeof e.extraHeaders)for(var j in e.extraHeaders)e.extraHeaders.hasOwnProperty(j)&&d.setRequestHeader(j,e.extraHeaders[j]);return d.onload=function(){200===d.status||201===d.status?b.onFileUploadResponse(d):b.onFileUploadError(d)},e.beforeFileUpload(d)!==!1&&d.send(c),d},c.prototype.isFileAllowed=function(a){return 0===this.settings.allowedTypes.indexOf("*")?!0:this.settings.allowedTypes.indexOf(a.type)>=0},c.prototype.onFileUploadResponse=function(a){if(this.settings.onFileUploadResponse.call(this,a)!==!1){var b=JSON.parse(a.responseText),c=b[this.settings.jsonFieldName];if(b&&c){var d=this.settings.urlText.replace(this.filenameTag,c),e=this.editor.getValue().replace(this.lastValue,d);this.editor.setValue(e),this.settings.onFileUploaded.call(this,c)}}},c.prototype.onFileUploadError=function(a){if(this.settings.onFileUploadError.call(this,a)!==!1){var b=this.editor.getValue().replace(this.lastValue,"");this.editor.setValue(b)}},c.prototype.onFileInserted=function(a){this.settings.onFileReceived.call(this,a)!==!1&&(this.lastValue=this.settings.progressText,this.editor.insertValue(this.lastValue))},c.prototype.onPaste=function(a){var b,c=!1,d=a.clipboardData;if("object"==typeof d){b=d.items||d.files||[];for(var e=0;e
2 |
3 |
4 | Permanote
5 |
6 |
7 |
8 |
9 |
10 |
11 | {% block extra_head %}{% endblock %}
12 |
13 |
14 |
15 |
16 | {% block extra_scripts %}{% endblock %}
17 |
18 |
19 |
20 |
69 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/.sandstorm/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | # Guess at a reasonable name for the VM based on the folder vagrant-spk is
5 | # run from. The timestamp is there to avoid conflicts if you have multiple
6 | # folders with the same name.
7 | VM_NAME = File.basename(File.dirname(File.dirname(__FILE__))) + "_sandstorm_#{Time.now.utc.to_i}"
8 |
9 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
10 | VAGRANTFILE_API_VERSION = "2"
11 |
12 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
13 | # Base on the Sandstorm snapshots of the official Debian 8 (jessie) box.
14 | config.vm.box = "sandstorm/debian-jessie64"
15 |
16 | if Vagrant.has_plugin?("vagrant-vbguest") then
17 | # vagrant-vbguest is a Vagrant plugin that upgrades
18 | # the version of VirtualBox Guest Additions within each
19 | # guest. If you have the vagrant-vbguest plugin, then it
20 | # needs to know how to compile kernel modules, etc., and so
21 | # we give it this hint about operating system type.
22 | config.vm.guest = "debian"
23 | end
24 |
25 | # We forward port 6080, the Sandstorm web port, so that developers can
26 | # visit their sandstorm app from their browser as local.sandstorm.io:6080
27 | # (aka 127.0.0.1:6080).
28 | config.vm.network :forwarded_port, guest: 6080, host: 6080
29 |
30 | # Use a shell script to "provision" the box. This installs Sandstorm using
31 | # the bundled installer.
32 | config.vm.provision "shell", inline: "sudo bash /opt/app/.sandstorm/global-setup.sh", keep_color: true
33 | # Then, do stack-specific and app-specific setup.
34 | config.vm.provision "shell", inline: "sudo bash /opt/app/.sandstorm/setup.sh", keep_color: true
35 |
36 | # Shared folders are configured per-provider since vboxsf can't handle >4096 open files,
37 | # NFS requires privilege escalation every time you bring a VM up,
38 | # and 9p is only available on libvirt.
39 |
40 | # Calculate the number of CPUs and the amount of RAM the system has,
41 | # in a platform-dependent way; further logic below.
42 | cpus = nil
43 | total_kB_ram = nil
44 |
45 | host = RbConfig::CONFIG['host_os']
46 | if host =~ /darwin/
47 | cpus = `sysctl -n hw.ncpu`.to_i
48 | total_kB_ram = `sysctl -n hw.memsize`.to_i / 1024
49 | elsif host =~ /linux/
50 | cpus = `nproc`.to_i
51 | total_kB_ram = `grep MemTotal /proc/meminfo | awk '{print $2}'`.to_i
52 | elsif host =~ /mingw/
53 | # powershell may not be available on Windows XP and Vista, so wrap this in a rescue block
54 | begin
55 | cpus = `powershell -Command "(Get-WmiObject Win32_Processor -Property NumberOfLogicalProcessors | Select-Object -Property NumberOfLogicalProcessors | Measure-Object NumberOfLogicalProcessors -Sum).Sum"`.to_i
56 | total_kB_ram = `powershell -Command "Get-CimInstance -class cim_physicalmemory | % $_.Capacity}"`.to_i / 1024
57 | rescue
58 | end
59 | end
60 | # Use the same number of CPUs within Vagrant as the system, with 1
61 | # as a default.
62 | #
63 | # Use at least 512MB of RAM, and if the system has more than 2GB of
64 | # RAM, use 1/4 of the system RAM. This seems a reasonable compromise
65 | # between having the Vagrant guest operating system not run out of
66 | # RAM entirely (which it basically would if we went much lower than
67 | # 512MB) and also allowing it to use up a healthily large amount of
68 | # RAM so it can run faster on systems that can afford it.
69 | if cpus.nil? or cpus.zero?
70 | cpus = 1
71 | end
72 | if total_kB_ram.nil? or total_kB_ram < 2048000
73 | assign_ram_mb = 512
74 | else
75 | assign_ram_mb = (total_kB_ram / 1024 / 4)
76 | end
77 | # Actually apply these CPU/memory values to the providers.
78 | config.vm.provider :virtualbox do |vb, override|
79 | vb.cpus = cpus
80 | vb.memory = assign_ram_mb
81 | vb.name = VM_NAME
82 | vb.customize ["modifyvm", :id, "--nictype1", "Am79C973"]
83 |
84 | override.vm.synced_folder "..", "/opt/app"
85 | override.vm.synced_folder ENV["HOME"] + "/.sandstorm", "/host-dot-sandstorm"
86 | override.vm.synced_folder "..", "/vagrant", disabled: true
87 | end
88 | config.vm.provider :libvirt do |libvirt, override|
89 | libvirt.cpus = cpus
90 | libvirt.memory = assign_ram_mb
91 | libvirt.default_prefix = VM_NAME
92 |
93 | override.vm.synced_folder "..", "/opt/app", type: "9p", accessmode: "passthrough"
94 | override.vm.synced_folder ENV["HOME"] + "/.sandstorm", "/host-dot-sandstorm", type: "9p", accessmode: "passthrough"
95 | override.vm.synced_folder "..", "/vagrant", type: "9p", accessmode: "passthrough", disabled: true
96 | end
97 | end
98 |
--------------------------------------------------------------------------------
/static/js/jquery.hotkeys.js:
--------------------------------------------------------------------------------
1 | /*jslint browser: true*/
2 | /*jslint jquery: true*/
3 |
4 | /*
5 | * jQuery Hotkeys Plugin
6 | * Copyright 2010, John Resig
7 | * Dual licensed under the MIT or GPL Version 2 licenses.
8 | *
9 | * Based upon the plugin by Tzury Bar Yochay:
10 | * https://github.com/tzuryby/jquery.hotkeys
11 | *
12 | * Original idea by:
13 | * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
14 | */
15 |
16 | /*
17 | * One small change is: now keys are passed by object { keys: '...' }
18 | * Might be useful, when you want to pass some other data to your handler
19 | */
20 |
21 | (function(jQuery) {
22 |
23 | jQuery.hotkeys = {
24 | version: "0.2.0",
25 |
26 | specialKeys: {
27 | 8: "backspace",
28 | 9: "tab",
29 | 10: "return",
30 | 13: "return",
31 | 16: "shift",
32 | 17: "ctrl",
33 | 18: "alt",
34 | 19: "pause",
35 | 20: "capslock",
36 | 27: "esc",
37 | 32: "space",
38 | 33: "pageup",
39 | 34: "pagedown",
40 | 35: "end",
41 | 36: "home",
42 | 37: "left",
43 | 38: "up",
44 | 39: "right",
45 | 40: "down",
46 | 45: "insert",
47 | 46: "del",
48 | 59: ";",
49 | 61: "=",
50 | 96: "0",
51 | 97: "1",
52 | 98: "2",
53 | 99: "3",
54 | 100: "4",
55 | 101: "5",
56 | 102: "6",
57 | 103: "7",
58 | 104: "8",
59 | 105: "9",
60 | 106: "*",
61 | 107: "+",
62 | 109: "-",
63 | 110: ".",
64 | 111: "/",
65 | 112: "f1",
66 | 113: "f2",
67 | 114: "f3",
68 | 115: "f4",
69 | 116: "f5",
70 | 117: "f6",
71 | 118: "f7",
72 | 119: "f8",
73 | 120: "f9",
74 | 121: "f10",
75 | 122: "f11",
76 | 123: "f12",
77 | 144: "numlock",
78 | 145: "scroll",
79 | 173: "-",
80 | 186: ";",
81 | 187: "=",
82 | 188: ",",
83 | 189: "-",
84 | 190: ".",
85 | 191: "/",
86 | 192: "`",
87 | 219: "[",
88 | 220: "\\",
89 | 221: "]",
90 | 222: "'"
91 | },
92 |
93 | shiftNums: {
94 | "`": "~",
95 | "1": "!",
96 | "2": "@",
97 | "3": "#",
98 | "4": "$",
99 | "5": "%",
100 | "6": "^",
101 | "7": "&",
102 | "8": "*",
103 | "9": "(",
104 | "0": ")",
105 | "-": "_",
106 | "=": "+",
107 | ";": ": ",
108 | "'": "\"",
109 | ",": "<",
110 | ".": ">",
111 | "/": "?",
112 | "\\": "|"
113 | },
114 |
115 | // excludes: button, checkbox, file, hidden, image, password, radio, reset, search, submit, url
116 | textAcceptingInputTypes: [
117 | "text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime",
118 | "datetime-local", "search", "color", "tel"],
119 |
120 | // default input types not to bind to unless bound directly
121 | textInputTypes: /textarea|input|select/i,
122 |
123 | options: {
124 | filterInputAcceptingElements: true,
125 | filterTextInputs: true,
126 | filterContentEditable: true
127 | }
128 | };
129 |
130 | function keyHandler(handleObj) {
131 | if (typeof handleObj.data === "string") {
132 | handleObj.data = {
133 | keys: handleObj.data
134 | };
135 | }
136 |
137 | // Only care when a possible input has been specified
138 | if (!handleObj.data || !handleObj.data.keys || typeof handleObj.data.keys !== "string") {
139 | return;
140 | }
141 |
142 | var origHandler = handleObj.handler,
143 | keys = handleObj.data.keys.toLowerCase().split(" ");
144 |
145 | handleObj.handler = function(event) {
146 | // Don't fire in text-accepting inputs that we didn't directly bind to
147 | if (this !== event.target &&
148 | (jQuery.hotkeys.options.filterInputAcceptingElements &&
149 | jQuery.hotkeys.textInputTypes.test(event.target.nodeName) ||
150 | (jQuery.hotkeys.options.filterContentEditable && jQuery(event.target).attr('contenteditable')) ||
151 | (jQuery.hotkeys.options.filterTextInputs &&
152 | jQuery.inArray(event.target.type, jQuery.hotkeys.textAcceptingInputTypes) > -1))) {
153 | return;
154 | }
155 |
156 | var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[event.which],
157 | character = String.fromCharCode(event.which).toLowerCase(),
158 | modif = "",
159 | possible = {};
160 |
161 | jQuery.each(["alt", "ctrl", "shift"], function(index, specialKey) {
162 |
163 | if (event[specialKey + 'Key'] && special !== specialKey) {
164 | modif += specialKey + '+';
165 | }
166 | });
167 |
168 | // metaKey is triggered off ctrlKey erronously
169 | if (event.metaKey && !event.ctrlKey && special !== "meta") {
170 | modif += "meta+";
171 | }
172 |
173 | if (event.metaKey && special !== "meta" && modif.indexOf("alt+ctrl+shift+") > -1) {
174 | modif = modif.replace("alt+ctrl+shift+", "hyper+");
175 | }
176 |
177 | if (special) {
178 | possible[modif + special] = true;
179 | }
180 | else {
181 | possible[modif + character] = true;
182 | possible[modif + jQuery.hotkeys.shiftNums[character]] = true;
183 |
184 | // "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
185 | if (modif === "shift+") {
186 | possible[jQuery.hotkeys.shiftNums[character]] = true;
187 | }
188 | }
189 |
190 | for (var i = 0, l = keys.length; i < l; i++) {
191 | if (possible[keys[i]]) {
192 | return origHandler.apply(this, arguments);
193 | }
194 | }
195 | };
196 | }
197 |
198 | jQuery.each(["keydown", "keyup", "keypress"], function() {
199 | jQuery.event.special[this] = {
200 | add: keyHandler
201 | };
202 | });
203 |
204 | })(jQuery || this.jQuery || window.jQuery);
205 |
--------------------------------------------------------------------------------
/views.py:
--------------------------------------------------------------------------------
1 | import os
2 | import datetime
3 | import urllib
4 |
5 | from flask import (flash, redirect, render_template, request,
6 | Response, url_for, jsonify, send_from_directory)
7 | from flask import url_for as flask_url_for
8 | from werkzeug.utils import secure_filename
9 | from playhouse.flask_utils import get_object_or_404, object_list
10 | from playhouse.sqlite_ext import *
11 |
12 | from app import application
13 | from models import Entry, Tag, EntryTags
14 |
15 | # Make url_for use https when hosted on Sandstorm with SSL
16 | def url_for(endpoint, **kwargs):
17 | if os.getenv('SANDSTORM'):
18 | kwargs.setdefault('_external', True)
19 | if request.headers.get('X-Forwarded-Proto') == "https":
20 | kwargs.setdefault('_scheme', 'https')
21 | else:
22 | kwargs.setdefault('_scheme', 'http')
23 | return flask_url_for(endpoint, **kwargs)
24 |
25 | @application.route('/')
26 | def index():
27 | search_query = request.args.get('q')
28 | if search_query:
29 | query = Entry.search(search_query)
30 | else:
31 | query = Entry.public().order_by(Entry.timestamp.desc())
32 |
33 | # The `object_list` helper will take a base query and then handle
34 | # paginating the results if there are more than 20. For more info see
35 | # the docs:
36 | # http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#object_list
37 | return object_list(
38 | 'index.html',
39 | query,
40 | search=search_query,
41 | check_bounds=False)
42 |
43 | @application.route('/archive/')
44 | def archive():
45 | query = Entry.archive().order_by(Entry.timestamp.desc())
46 | return object_list(
47 | 'index.html',
48 | query,
49 | archive=archive,
50 | check_bounds=False)
51 |
52 | @application.route('/create/', methods=['GET', 'POST'])
53 | def create():
54 | if request.method == 'POST':
55 | if request.form.get('title') and request.form.get('content'):
56 | try:
57 | entry = Entry.create(
58 | title=request.form['title'],
59 | content=request.form['content'],
60 | archived=request.form.get('archived') or False)
61 | tags = request.form['tags'].split()
62 | # present is a check to see if the tag exists
63 | present = 0
64 | # add or create tags
65 | for tag in tags:
66 | for entrytag in entry.tags:
67 | if tag == entrytag.tag:
68 | present = 1
69 | if present == 0:
70 | try:
71 | thistag = Tag.get(Tag.tag == tag)
72 | entry.tags.add(thistag)
73 | except:
74 | tag_obj, was_created = Tag.create_or_get(tag=tag)
75 | EntryTags.create(tag=tag_obj, entry=entry)
76 | present = 0
77 | flash('Entry created successfully.', 'success')
78 | return redirect(url_for('detail', slug=entry.slug))
79 | except:
80 | flash('Note title already exists', 'danger')
81 | return render_template('create.html')
82 | # TODO Refactor the below and above to make it more DRY or not
83 | # to need to display seconds (e.g. add some kind of suffix if entry
84 | # already exists)
85 | elif request.form.get('content'):
86 | entry = Entry.create(
87 | title="{:%a %d %b %Y at %H:%M:%S}".format(datetime.datetime.now()),
88 | content=request.form['content'])
89 | flash('Note created successfully.', 'success')
90 | return redirect(url_for('detail', slug=entry.slug))
91 | else:
92 | flash('Content is required.', 'danger')
93 | return render_template('create.html')
94 |
95 |
96 | @application.route('//')
97 | def detail(slug):
98 | query = Entry.all()
99 | entry = get_object_or_404(query, Entry.slug == slug)
100 | tags = ""
101 | for tag in entry.tags:
102 | tags = tags + " " + tag.tag
103 | return render_template('detail.html', entry=entry, tags=tags)
104 |
105 |
106 | @application.route('//edit/', methods=['GET', 'POST'])
107 | def edit(slug):
108 | entry = get_object_or_404(Entry, Entry.slug == slug)
109 | tags = ""
110 | for tag in entry.tags:
111 | tags = tags + " " + tag.tag
112 | if request.method == 'POST':
113 | if request.form.get('title') and request.form.get('content'):
114 | try:
115 | entry.title = request.form['title']
116 | entry.content = request.form['content']
117 | entry.archived = request.form.get('archived') or False
118 | entry.lastedited = datetime.datetime.now()
119 | # convert the string of tags to a list
120 | tags = request.form['tags'].split()
121 | # present is a check to see if the tag exists
122 | present = 0
123 | # add or create tags
124 | for tag in tags:
125 | for entrytag in entry.tags:
126 | if tag == entrytag.tag:
127 | present = 1
128 | if present == 0:
129 | try:
130 | thistag = Tag.get(Tag.tag == tag)
131 | entry.tags.add(thistag)
132 | except:
133 | tag_obj, was_created = Tag.create_or_get(tag=tag)
134 | EntryTags.create(tag=tag_obj, entry=entry)
135 | present = 0
136 | # remove tags
137 | for entrytag in entry.tags:
138 | for tag in tags:
139 | if entrytag.tag == tag:
140 | present = 1
141 | if present == 0:
142 | thistag = Tag.get(Tag.tag == entrytag.tag)
143 | entry.tags.remove(thistag)
144 | present = 0
145 | entry.save()
146 |
147 | flash('Note updated successfully.', 'success')
148 | return redirect(url_for('detail', slug=entry.slug))
149 | except:
150 | flash('Note title already exists', 'danger')
151 | return render_template('create.html')
152 | else:
153 | flash('Title and Content are required.', 'danger')
154 |
155 | return render_template('edit.html', entry=entry, tags=tags)
156 |
157 |
158 | @application.route('/tags/')
159 | def taglist():
160 | count = fn.COUNT(EntryTags.id)
161 | tags_with_counts = (Tag
162 | .select(Tag, count.alias('entry_count'))
163 | .join(EntryTags)
164 | .join(Entry)
165 | .where(Entry.archived==False)
166 | .group_by(Tag)
167 | .order_by(count.desc(), Tag.tag))
168 | return object_list('taglist.html', tags_with_counts, check_bounds=False)
169 |
170 |
171 | @application.route('/tag//')
172 | def thistag(tag):
173 | search_query = request.args.get('q')
174 | query = (Entry.public()
175 | .select()
176 | .join(EntryTags)
177 | .join(Tag)
178 | .where(
179 | (Tag.tag == tag))
180 | .order_by(Entry.timestamp.desc()))
181 | return object_list(
182 | 'index.html',
183 | query,
184 | tag=tag,
185 | search=search_query,
186 | check_bounds=False)
187 |
188 |
189 | @application.route('/upload/', methods=['GET', 'POST'])
190 | def upload_file():
191 | if request.method == 'POST':
192 | file = request.files['file']
193 | if file and allowed_file(file.filename):
194 | filename = secure_filename(file.filename)
195 | file.save(os.path.join(application.config['UPLOAD_FOLDER'], filename))
196 | filenamedict = dict([("filename", os.path.join('/uploads/', filename))])
197 | else:
198 | filenamedict = dict([("error", "Error while uploading file")])
199 | # see http://stackoverflow.com/a/13089975/94908 for explanation of the below
200 | return jsonify(**filenamedict)
201 |
202 |
203 | def allowed_file(filename):
204 | return '.' in filename and \
205 | filename.rsplit('.', 1)[1] in application.config['ALLOWED_EXTENSIONS']
206 |
207 |
208 | @application.route('/uploads/')
209 | def uploaded_file(filename):
210 | return send_from_directory(application.config['UPLOAD_FOLDER'],
211 | filename)
212 |
213 |
214 | @application.template_filter('clean_querystring')
215 | def clean_querystring(request_args, *keys_to_remove, **new_values):
216 | # We'll use this template filter in the pagination include. This filter
217 | # will take the current URL and allow us to preserve the arguments in the
218 | # querystring while replacing any that we need to overwrite. For instance
219 | # if your URL is /?q=search+query&page=2 and we want to preserve the search
220 | # term but make a link to page 3, this filter will allow us to do that.
221 | querystring = dict((key, value) for key, value in request_args.items())
222 | for key in keys_to_remove:
223 | querystring.pop(key, None)
224 | querystring.update(new_values)
225 | return urllib.urlencode(querystring)
226 |
227 |
228 | @application.errorhandler(404)
229 | def not_found(exc):
230 | return Response('
Not found
'), 404
231 |
--------------------------------------------------------------------------------
/.sandstorm/sandstorm-pkgdef.capnp:
--------------------------------------------------------------------------------
1 | @0xa821a41ffa687175;
2 |
3 | using Spk = import "/sandstorm/package.capnp";
4 | # This imports:
5 | # $SANDSTORM_HOME/latest/usr/include/sandstorm/package.capnp
6 | # Check out that file to see the full, documented package definition format.
7 |
8 | const pkgdef :Spk.PackageDefinition = (
9 | # The package definition. Note that the spk tool looks specifically for the
10 | # "pkgdef" constant.
11 |
12 | id = "svwrpwnd3c380d1f99ge7g0qnjdq6y785c36s7qtqryxwkmn20qh",
13 | # Your app ID is actually its public key. The private key was placed in
14 | # your keyring. All updates must be signed with the same key.
15 |
16 | manifest = (
17 | # This manifest is included in your app package to tell Sandstorm
18 | # about your app.
19 |
20 | appTitle = (defaultText = "Permanote"),
21 |
22 | appVersion = 5, # Increment this for every release.
23 |
24 | appMarketingVersion = (defaultText = "0.1.4"),
25 | # Human-readable representation of appVersion. Should match the way you
26 | # identify versions of your app in documentation and marketing.
27 |
28 | actions = [
29 | # Define your "new document" handlers here.
30 | ( title = (defaultText = "New Notebook"),
31 | command = .myCommand
32 | # The command to run when starting for the first time. (".myCommand"
33 | # is just a constant defined at the bottom of the file.)
34 | )
35 | ],
36 |
37 | continueCommand = .myCommand,
38 | # This is the command called to start your app back up after it has been
39 | # shut down for inactivity. Here we're using the same command as for
40 | # starting a new instance, but you could use different commands for each
41 | # case.
42 |
43 | metadata = (
44 | icons = (
45 | appGrid = (png = (dpi1x = embed "app-graphics/permanote-dolphin128.png")),
46 | grain = (png = (dpi1x = embed "app-graphics/permanote-dolphin24.png",
47 | dpi2x = embed "app-graphics/permanote-dolphin48.png")),
48 | market = (png = (dpi1x = embed "app-graphics/permanote-dolphin150.png")),
49 | marketBig = (png = (dpi1x = embed "app-graphics/permanote-dolphin300.png"))
50 | ),
51 | website = "https://github.com/keybits/permanote",
52 | codeUrl = "https://github.com/keybits/permanote",
53 | license = (openSource = mit),
54 | categories = [productivity],
55 | author = (
56 | contactEmail = "tom@keybits.net",
57 | pgpSignature = embed "pgp-signature",
58 | ),
59 | pgpKeyring = embed "pgp-keyring",
60 | description = (defaultText = embed "description.md"),
61 | shortDescription = (defaultText = "Note-taking"),
62 | screenshots = [
63 | # Screenshots to use for marketing purposes. Examples below.
64 | # Sizes are given in device-independent pixels, so if you took these
65 | # screenshots on a Retina-style high DPI screen, divide each dimension by two.
66 |
67 | (width = 841, height = 618, png = embed "screenshots/permanote-screen-0.png"),
68 | (width = 870, height = 765, png = embed "screenshots/permanote-screen-1.png"),
69 | (width = 883, height = 548, png = embed "screenshots/permanote-screen-2.png"),
70 | (width = 842, height = 536, png = embed "screenshots/permanote-screen-3.png"),
71 | (width = 858, height = 804, png = embed "screenshots/permanote-screen-4.png"),
72 | (width = 869, height = 813, png = embed "screenshots/permanote-screen-5.png"),
73 | (width = 858, height = 467, png = embed "screenshots/permanote-screen-6.png"),
74 | ],
75 | changeLog = (defaultText = embed "CHANGELOG.md"),
76 | # Documents the history of changes in Github-flavored markdown format (with the same restrictions
77 | # as govern `description`). We recommend formatting this with an H1 heading for each version
78 | # followed by a bullet list of changes.
79 | ),
80 | ),
81 |
82 | sourceMap = (
83 | # Here we defined where to look for files to copy into your package. The
84 | # `spk dev` command actually figures out what files your app needs
85 | # automatically by running it on a FUSE filesystem. So, the mappings
86 | # here are only to tell it where to find files that the app wants.
87 | searchPath = [
88 | ( sourcePath = "." ), # Search this directory first.
89 | ( sourcePath = "/", # Then search the system root directory.
90 | hidePaths = [ "home", "proc", "sys",
91 | "etc/passwd", "etc/hosts", "etc/host.conf",
92 | "etc/nsswitch.conf", "etc/resolv.conf" ]
93 | # You probably don't want the app pulling files from these places,
94 | # so we hide them. Note that /dev, /var, and /tmp are implicitly
95 | # hidden because Sandstorm itself provides them.
96 | )
97 | ]
98 | ),
99 |
100 | fileList = "sandstorm-files.list",
101 | # `spk dev` will write a list of all the files your app uses to this file.
102 | # You should review it later, before shipping your app.
103 |
104 | alwaysInclude = [],
105 | # Fill this list with more names of files or directories that should be
106 | # included in your package, even if not listed in sandstorm-files.list.
107 | # Use this to force-include stuff that you know you need but which may
108 | # not have been detected as a dependency during `spk dev`. If you list
109 | # a directory here, its entire contents will be included recursively.
110 |
111 | #bridgeConfig = (
112 | # # Used for integrating permissions and roles into the Sandstorm shell
113 | # # and for sandstorm-http-bridge to pass to your app.
114 | # # Uncomment this block and adjust the permissions and roles to make
115 | # # sense for your app.
116 | # # For more information, see high-level documentation at
117 | # # https://docs.sandstorm.io/en/latest/developing/auth/
118 | # # and advanced details in the "BridgeConfig" section of
119 | # # https://github.com/sandstorm-io/sandstorm/blob/master/src/sandstorm/package.capnp
120 | # viewInfo = (
121 | # # For details on the viewInfo field, consult "ViewInfo" in
122 | # # https://github.com/sandstorm-io/sandstorm/blob/master/src/sandstorm/grain.capnp
123 | #
124 | # permissions = [
125 | # # Permissions which a user may or may not possess. A user's current
126 | # # permissions are passed to the app as a comma-separated list of `name`
127 | # # fields in the X-Sandstorm-Permissions header with each request.
128 | # #
129 | # # IMPORTANT: only ever append to this list! Reordering or removing fields
130 | # # will change behavior and permissions for existing grains! To deprecate a
131 | # # permission, or for more information, see "PermissionDef" in
132 | # # https://github.com/sandstorm-io/sandstorm/blob/master/src/sandstorm/grain.capnp
133 | # (
134 | # name = "editor",
135 | # # Name of the permission, used as an identifier for the permission in cases where string
136 | # # names are preferred. Used in sandstorm-http-bridge's X-Sandstorm-Permissions HTTP header.
137 | #
138 | # title = (defaultText = "editor"),
139 | # # Display name of the permission, e.g. to display in a checklist of permissions
140 | # # that may be assigned when sharing.
141 | #
142 | # description = (defaultText = "grants ability to modify data"),
143 | # # Prose describing what this role means, suitable for a tool tip or similar help text.
144 | # ),
145 | # ],
146 | # roles = [
147 | # # Roles are logical collections of permissions. For instance, your app may have
148 | # # a "viewer" role and an "editor" role
149 | # (
150 | # title = (defaultText = "editor"),
151 | # # Name of the role. Shown in the Sandstorm UI to indicate which users have which roles.
152 | #
153 | # permissions = [true],
154 | # # An array indicating which permissions this role carries.
155 | # # It should be the same length as the permissions array in
156 | # # viewInfo, and the order of the lists must match.
157 | #
158 | # verbPhrase = (defaultText = "can make changes to the document"),
159 | # # Brief explanatory text to show in the sharing UI indicating
160 | # # what a user assigned this role will be able to do with the grain.
161 | #
162 | # description = (defaultText = "editors may view all site data and change settings."),
163 | # # Prose describing what this role means, suitable for a tool tip or similar help text.
164 | # ),
165 | # (
166 | # title = (defaultText = "viewer"),
167 | # permissions = [false],
168 | # verbPhrase = (defaultText = "can view the document"),
169 | # description = (defaultText = "viewers may view what other users have written."),
170 | # ),
171 | # ],
172 | # ),
173 | # #apiPath = "/api",
174 | # # Apps can export an API to the world. The API is to be used primarily by Javascript
175 | # # code and native apps, so it can't serve out regular HTML to browsers. If a request
176 | # # comes in to your app's API, sandstorm-http-bridge will prefix the request's path with
177 | # # this string, if specified.
178 | #),
179 | );
180 |
181 | const myCommand :Spk.Manifest.Command = (
182 | # Here we define the command used to start up your server.
183 | argv = ["/sandstorm-http-bridge", "8000", "--", "/opt/app/.sandstorm/launcher.sh"],
184 | environ = [
185 | # Note that this defines the *entire* environment seen by your app.
186 | (key = "PATH", value = "/usr/local/bin:/usr/bin:/bin"),
187 | (key = "PYTHONPATH", value = "/opt/app"),
188 | (key = "HOME", value = "/var"),
189 | (key = "SANDSTORM", value = "1"),
190 | ]
191 | );
192 |
--------------------------------------------------------------------------------
/static/js/bootstrap.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.1.1 (http://getbootstrap.com)
3 | * Copyright 2011-2014 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.isLoading=!1};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",f.resetText||d.data("resetText",d[e]()),d[e](f[b]||this.options[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},b.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});return this.$element.trigger(j),j.isDefaultPrevented()?void 0:(this.sliding=!0,f&&this.pause(),this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")),f&&this.cycle(),this)};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("collapse in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);!e&&f.toggle&&"show"==c&&(c=!c),e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(b){a(d).remove(),a(e).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('').insertAfter(a(this)).on("click",b);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown",h),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=" li:not(.divider):visible a",i=f.find("[role=menu]"+h+", [role=listbox]"+h);if(i.length){var j=i.index(i.filter(":focus"));38==b.keyCode&&j>0&&j--,40==b.keyCode&&j').appendTo(document.body),this.$element.on("click.dismiss.bs.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());c.is("a")&&b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'