init repo
commit
3719f26082
@ -0,0 +1,14 @@
|
|||||||
|
venv/
|
||||||
|
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
instance/
|
||||||
|
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
@ -0,0 +1,41 @@
|
|||||||
|
import os
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(test_config=None):
|
||||||
|
# create and configure the app
|
||||||
|
app = Flask(__name__, instance_relative_config=True)
|
||||||
|
app.config.from_mapping(
|
||||||
|
SECRET_KEY='dev',
|
||||||
|
DATABASE=os.path.join(app.instance_path, 'exquisite'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if test_config is None:
|
||||||
|
# load the instance config, if it exists, when not testing
|
||||||
|
app.config.from_pyfile('config.py', silent=True)
|
||||||
|
else:
|
||||||
|
# load the test config if passed in
|
||||||
|
app.config.from_mapping(test_config)
|
||||||
|
|
||||||
|
# ensure the instance folder exists
|
||||||
|
try:
|
||||||
|
os.makedirs(app.instance_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
db.init_app(app)
|
||||||
|
|
||||||
|
from . import draw
|
||||||
|
app.register_blueprint(draw.bp)
|
||||||
|
|
||||||
|
from . import share
|
||||||
|
app.register_blueprint(share.bp)
|
||||||
|
|
||||||
|
from . import display
|
||||||
|
app.register_blueprint(display.bp)
|
||||||
|
|
||||||
|
from . import home
|
||||||
|
app.register_blueprint(home.bp)
|
||||||
|
|
||||||
|
return app
|
@ -0,0 +1,43 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
import click
|
||||||
|
from flask import current_app, g
|
||||||
|
from flask.cli import with_appcontext
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
if 'db' not in g:
|
||||||
|
g.db = sqlite3.connect(
|
||||||
|
current_app.config['DATABASE'],
|
||||||
|
detect_types=sqlite3.PARSE_DECLTYPES
|
||||||
|
)
|
||||||
|
g.db.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
return g.db
|
||||||
|
|
||||||
|
|
||||||
|
def close_db(e=None):
|
||||||
|
db = g.pop('db', None)
|
||||||
|
|
||||||
|
if db is not None:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
db = get_db()
|
||||||
|
|
||||||
|
with current_app.open_resource('schema.sql') as f:
|
||||||
|
db.executescript(f.read().decode('utf8'))
|
||||||
|
|
||||||
|
|
||||||
|
@click.command('init-db')
|
||||||
|
@with_appcontext
|
||||||
|
def init_db_command():
|
||||||
|
"""Clear the existing data and create new tables."""
|
||||||
|
init_db()
|
||||||
|
click.echo('Initialized the database.')
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app):
|
||||||
|
app.teardown_appcontext(close_db)
|
||||||
|
app.cli.add_command(init_db_command)
|
@ -0,0 +1,35 @@
|
|||||||
|
from flask import (Blueprint, flash, g, redirect,
|
||||||
|
render_template, request, session, url_for)
|
||||||
|
|
||||||
|
from exquisite_branch.db import get_db
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('display', __name__, url_prefix='/display')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def display():
|
||||||
|
db = get_db()
|
||||||
|
|
||||||
|
branches = db.execute(
|
||||||
|
"SELECT content, branch, parent FROM branches"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
streams = []
|
||||||
|
for branch in branches[::-1]:
|
||||||
|
if branch not in flatten(streams):
|
||||||
|
stream = [branch]
|
||||||
|
parent = branch['parent']
|
||||||
|
while parent != 'NEW':
|
||||||
|
current = next(
|
||||||
|
(x for x in branches if x['branch'] == parent), None)
|
||||||
|
parent = current['parent']
|
||||||
|
stream.append(current)
|
||||||
|
|
||||||
|
streams.append(stream[::-1])
|
||||||
|
|
||||||
|
return render_template('display.html', branches=branches, streams=streams)
|
||||||
|
|
||||||
|
|
||||||
|
def flatten(t):
|
||||||
|
return [item for sublist in t for item in sublist]
|
@ -0,0 +1,85 @@
|
|||||||
|
from flask import (Blueprint, flash, g, redirect,
|
||||||
|
render_template, request, session, url_for)
|
||||||
|
|
||||||
|
from exquisite_branch.db import get_db
|
||||||
|
from werkzeug.exceptions import abort
|
||||||
|
|
||||||
|
from shortuuid import uuid
|
||||||
|
|
||||||
|
bp = Blueprint('draw', __name__, url_prefix='/draw')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<parent>', methods=('GET', 'POST'))
|
||||||
|
def draw(parent=None):
|
||||||
|
db = get_db()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
content = request.form.get('content')
|
||||||
|
branch = request.form.get('branch')
|
||||||
|
|
||||||
|
print(content)
|
||||||
|
|
||||||
|
db.execute(
|
||||||
|
'INSERT INTO branches (content, parent, branch) VALUES (?, ?, ?)',
|
||||||
|
(content, parent, branch,)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return redirect(url_for('share.share', branch=branch))
|
||||||
|
|
||||||
|
branch = uuid()
|
||||||
|
|
||||||
|
previous = db.execute(
|
||||||
|
"SELECT content, branch, parent FROM branches"
|
||||||
|
" WHERE branch = ?",
|
||||||
|
(parent,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if previous is None:
|
||||||
|
abort(404, f"Previous with id {parent} doesn't exist")
|
||||||
|
|
||||||
|
return render_template('draw.html', parent=parent, content=previous['content'], branch=branch)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/last', methods=('GET', 'POST'))
|
||||||
|
def last():
|
||||||
|
|
||||||
|
branch = uuid()
|
||||||
|
db = get_db()
|
||||||
|
previous = db.execute(
|
||||||
|
'SELECT * FROM branches ORDER BY id DESC LIMIT 1'
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
parent = previous['branch']
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
content = request.form['content']
|
||||||
|
branch = request.form['branch']
|
||||||
|
|
||||||
|
db.execute(
|
||||||
|
'INSERT INTO branches (content, parent, branch) VALUES (?, ?, ?)',
|
||||||
|
(content, parent, branch,)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return redirect(url_for('share.share', branch=branch))
|
||||||
|
|
||||||
|
return render_template('draw.html', parent=parent, content=previous['content'], branch=branch)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/', methods=('GET', 'POST'))
|
||||||
|
def new():
|
||||||
|
db = get_db()
|
||||||
|
branch = uuid()
|
||||||
|
parent = 'NEW'
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
content = request.form['content']
|
||||||
|
branch = request.form['branch']
|
||||||
|
|
||||||
|
db.execute(
|
||||||
|
'INSERT INTO branches (content, parent, branch) VALUES (?, ?, ?)',
|
||||||
|
(content, parent, branch,)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return redirect(url_for('share.share', branch=branch))
|
||||||
|
|
||||||
|
return render_template('draw.html', parent=parent, branch=branch)
|
@ -0,0 +1,7 @@
|
|||||||
|
from flask import (Blueprint, render_template)
|
||||||
|
bp = Blueprint('home', __name__, url_prefix='/')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/')
|
||||||
|
def home():
|
||||||
|
return render_template('home.html')
|
@ -0,0 +1,8 @@
|
|||||||
|
DROP TABLE IF EXISTS branches;
|
||||||
|
|
||||||
|
CREATE TABLE branches (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
branch TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
parent TEXT NOT NULL
|
||||||
|
);
|
@ -0,0 +1,10 @@
|
|||||||
|
from flask import (Blueprint, flash, g, redirect,
|
||||||
|
render_template, request, session, url_for)
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('share', __name__, url_prefix='/share')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<branch>')
|
||||||
|
def share(branch=None):
|
||||||
|
return render_template('share.html', branch=branch)
|
@ -0,0 +1,14 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
// Great resource from https://stackoverflow.com/a/40700068
|
||||||
|
// Thank you ConnorFan
|
||||||
|
|
||||||
|
var strokeWidth = 2;
|
||||||
|
var bufferSize;
|
||||||
|
|
||||||
|
var svgElement = document.getElementById("svgElement");
|
||||||
|
var rect = svgElement.getBoundingClientRect();
|
||||||
|
var path = null;
|
||||||
|
var strPath;
|
||||||
|
var buffer = []; // Contains the last positions of the mouse cursor
|
||||||
|
|
||||||
|
svgElement.addEventListener("mousedown", function (e) {
|
||||||
|
bufferSize = document.getElementById("cmbBufferSize").value;
|
||||||
|
path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
|
path.setAttribute("fill", "none");
|
||||||
|
path.setAttribute("stroke", "#000");
|
||||||
|
path.setAttribute("stroke-width", strokeWidth);
|
||||||
|
buffer = [];
|
||||||
|
var pt = getMousePosition(e);
|
||||||
|
appendToBuffer(pt);
|
||||||
|
strPath = "M" + pt.x + " " + pt.y;
|
||||||
|
path.setAttribute("d", strPath);
|
||||||
|
svgElement.appendChild(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
svgElement.addEventListener("mousemove", function (e) {
|
||||||
|
if (path) {
|
||||||
|
appendToBuffer(getMousePosition(e));
|
||||||
|
updateSvgPath();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
svgElement.addEventListener("mouseup", function () {
|
||||||
|
if (path) {
|
||||||
|
path = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var getMousePosition = function (e) {
|
||||||
|
return {
|
||||||
|
x: e.pageX - rect.left,
|
||||||
|
y: e.pageY - rect.top,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
var appendToBuffer = function (pt) {
|
||||||
|
buffer.push(pt);
|
||||||
|
while (buffer.length > bufferSize) {
|
||||||
|
buffer.shift();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate the average point, starting at offset in the buffer
|
||||||
|
var getAveragePoint = function (offset) {
|
||||||
|
var len = buffer.length;
|
||||||
|
if (len % 2 === 1 || len >= bufferSize) {
|
||||||
|
var totalX = 0;
|
||||||
|
var totalY = 0;
|
||||||
|
var pt, i;
|
||||||
|
var count = 0;
|
||||||
|
for (i = offset; i < len; i++) {
|
||||||
|
count++;
|
||||||
|
pt = buffer[i];
|
||||||
|
totalX += pt.x;
|
||||||
|
totalY += pt.y;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: totalX / count,
|
||||||
|
y: totalY / count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
var updateSvgPath = function () {
|
||||||
|
var pt = getAveragePoint(0);
|
||||||
|
|
||||||
|
if (pt) {
|
||||||
|
// Get the smoothed part of the path that will not change
|
||||||
|
strPath += " L" + pt.x + " " + pt.y;
|
||||||
|
|
||||||
|
// Get the last part of the path (close to the current mouse position)
|
||||||
|
// This part will change if the mouse moves again
|
||||||
|
var tmpPath = "";
|
||||||
|
for (var offset = 2; offset < buffer.length; offset += 2) {
|
||||||
|
pt = getAveragePoint(offset);
|
||||||
|
tmpPath += " L" + pt.x + " " + pt.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the complete current path coordinates
|
||||||
|
path.setAttribute("d", strPath + tmpPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// SAVE THE BRANCH
|
||||||
|
|
||||||
|
const send = document.getElementById("send");
|
||||||
|
send.addEventListener("click", (e) => saveSVG(e));
|
||||||
|
|
||||||
|
function saveSVG(e) {
|
||||||
|
let wrapper = document.createElement("div");
|
||||||
|
wrapper.appendChild(svgElement);
|
||||||
|
|
||||||
|
fetch(`${svgElement.dataset.parent}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: wrapper.innerHTML,
|
||||||
|
parent: svgElement.dataset.parent,
|
||||||
|
branch: svgElement.dataset.branch,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 28 KiB |
@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Display</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Exquisite Branch</h1>
|
||||||
|
<h2>Entries</h2>
|
||||||
|
{% for branch in branches %} {{branch['content']}} {%endfor%}
|
||||||
|
<h2>Branches</h2>
|
||||||
|
<ul>
|
||||||
|
{% for stream in streams %}
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{% for branch in stream %}
|
||||||
|
<span>{{branch['content']}}</span>
|
||||||
|
{%endfor%}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{%endfor%}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Draw</title>
|
||||||
|
|
||||||
|
<script src="{{url_for('static', filename='js/draw.js')}}" defer></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{url_for('static', filename='css/draw.css')}}" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Draw</h1>
|
||||||
|
<div id="divSmoothingFactor">
|
||||||
|
<label for="cmbBufferSize">Buffer size:</label>
|
||||||
|
<select id="cmbBufferSize">
|
||||||
|
<option value="1">1 - No smoothing</option>
|
||||||
|
<option value="4">4 - Sharp curves</option>
|
||||||
|
<option value="8" selected="selected">8 - Smooth curves</option>
|
||||||
|
<option value="12">12 - Very smooth curves</option>
|
||||||
|
<option value="16">16 - Super smooth curves</option>
|
||||||
|
<option value="20">20 - Hyper smooth curves</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
version="1.1"
|
||||||
|
id="svgElement"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
width="600px"
|
||||||
|
height="400px"
|
||||||
|
viewBox="0 0 600 400"
|
||||||
|
enable-background="new 0 0 600 400"
|
||||||
|
xml:space="preserve"
|
||||||
|
data-parent="{{parent or None}}"
|
||||||
|
data-branch="{{branch}}"
|
||||||
|
></svg>
|
||||||
|
|
||||||
|
<button id="send">Send</button>
|
||||||
|
|
||||||
|
<h2>Previous content</h2>
|
||||||
|
|
||||||
|
{% if content %} {{content}} {% endif %}
|
||||||
|
|
||||||
|
<h2>Parent</h2>
|
||||||
|
|
||||||
|
{%if parent %} {{parent}} {%endif %}
|
||||||
|
|
||||||
|
<h2>Branch</h2>
|
||||||
|
{%if branch %} {{branch}} {%endif %}`
|
||||||
|
|
||||||
|
<form class="canvas" method="POST">
|
||||||
|
<input type="text" name="content" />
|
||||||
|
<input type="hidden" name="branch" value="{{branch}}" />
|
||||||
|
<input type="submit" />
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Exquisite Branch</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Exquisite Branch</h1>
|
||||||
|
<a href="{{url_for('draw.new')}}">Start new</a> <br />
|
||||||
|
<a href="{{url_for('draw.last')}}">Continue from last</a> <br />
|
||||||
|
<a href="{{url_for('display.display')}}">Display results</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Share</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Send this link to your friends:
|
||||||
|
<a href="{{url_for('draw.draw', parent=branch)}}"
|
||||||
|
>{{url_for('draw.draw', parent=branch)}}</a
|
||||||
|
>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue