manetta
3 years ago
commit
266d5fc1cd
18 changed files with 62777 additions and 0 deletions
@ -0,0 +1,9 @@ |
|||
default: run |
|||
|
|||
setup: |
|||
@python3 -m venv .venv |
|||
@.venv/bin/pip install -r requirements.txt |
|||
|
|||
run: |
|||
@.venv/bin/python feedmode.py |
|||
|
@ -0,0 +1,12 @@ |
|||
# feedmode |
|||
|
|||
Multifeeder/RSS feeds → selection + CSS + template → PDF (very beta) |
|||
|
|||
## Use feedmode locally |
|||
|
|||
`make setup` (sets up a virtual environment and install the requirements) |
|||
|
|||
`make run` (runs the Flask application) |
|||
|
|||
Open the application at <http://localhost:5001>. |
|||
|
@ -0,0 +1,4 @@ |
|||
class Config(object): |
|||
PORTNUMBER = 5001 |
|||
PAD_API_URL = 'https://pad.vvvvvvaria.org/api/1.2.15/' |
|||
PAD_API_KEY = '<insert API key here>' |
@ -0,0 +1,138 @@ |
|||
import flask |
|||
from flask import request, redirect |
|||
from urllib.request import urlopen |
|||
from urllib.parse import urlencode |
|||
import json |
|||
import os |
|||
import pypandoc |
|||
from jinja2 import Template |
|||
|
|||
APP = flask.Flask(__name__) |
|||
APP.config.from_object("config.Config") |
|||
|
|||
# --- |
|||
|
|||
def get_pad_content(pad): |
|||
arguments = { |
|||
'padID' : pad, |
|||
'apikey' : APP.config['PAD_API_KEY'] |
|||
} |
|||
api_call = 'getText' |
|||
response = json.load(urlopen(f"{ APP.config['PAD_API_URL'] }{ api_call }", data=urlencode(arguments).encode())) |
|||
content = response['data']['text'] |
|||
|
|||
return content |
|||
|
|||
def update_pad_contents(): |
|||
# download the stylesheet + template pad |
|||
css = get_pad_content('feedmode.css') |
|||
template = get_pad_content('feedmode.template') |
|||
# !!! this breaks the whole idea that this application can be shared by multiple projects at the same time |
|||
# !!! but py_pandoc needs to run with files........ hmmm |
|||
with open('templates/pandoc-template.html', 'w') as f: |
|||
f.write(template) |
|||
|
|||
return css, template |
|||
|
|||
def get_multifeeder_feed(query): |
|||
feed_json = json.load(urlopen(query)) |
|||
multifeeder_template = open('templates/multifeeder-template.html').read() |
|||
jinja_template = Template(multifeeder_template) |
|||
feed_html = jinja_template.render(response=feed_json) |
|||
|
|||
return feed_html, feed_json |
|||
|
|||
def load_query(): |
|||
query = open('static/query.txt').read().strip() # !!! needs fixing |
|||
|
|||
return query |
|||
|
|||
# --- |
|||
|
|||
@APP.route('/', methods=['GET']) |
|||
def index(): |
|||
return redirect("/preview/", code=302) |
|||
|
|||
@APP.route('/update/', methods=['POST']) |
|||
def update(): |
|||
query = request.form['query'] |
|||
with open('static/query.txt', 'w') as out: |
|||
out.write(query) |
|||
|
|||
return redirect("/select/", code=302) |
|||
|
|||
@APP.route('/select/', methods=['GET']) |
|||
def select(): |
|||
# get feed contents |
|||
# render selection template with checkboxes |
|||
query = load_query() |
|||
x, feed_json = get_multifeeder_feed(query) |
|||
|
|||
return flask.render_template('selection-template.html', feed=feed_json, query=query) |
|||
|
|||
@APP.route('/preview/', methods=['GET']) |
|||
def preview(): |
|||
# update pad contents |
|||
x, template_content = update_pad_contents() |
|||
# get feed contents |
|||
query = load_query() |
|||
feed_html, x = get_multifeeder_feed(query) |
|||
# render multifeeder feed in template |
|||
template = Template(template_content) |
|||
html = template.render(feed=feed_html, mode="screen") |
|||
|
|||
return flask.render_template('html.html', html=html, query=query) |
|||
|
|||
@APP.route('/stylesheet/', methods=['GET']) |
|||
def stylesheet(): |
|||
ext = '.css' |
|||
query = load_query() |
|||
|
|||
return flask.render_template('pad.html', name="feedmode", ext=ext, query=query) |
|||
|
|||
@APP.route('/template/', methods=['GET']) |
|||
def template(): |
|||
ext = '.template' |
|||
query = load_query() |
|||
|
|||
return flask.render_template('pad.html', name="feedmode", ext=ext, query=query) |
|||
|
|||
@APP.route('/pdf/') |
|||
def pdf(): |
|||
query = load_query() |
|||
|
|||
return flask.render_template('pdf.html', query=query) |
|||
|
|||
# ////////////// |
|||
# rendered resources (not saved as a file on the server) |
|||
|
|||
@APP.route('/print.css') |
|||
def css(): |
|||
css, x = update_pad_contents() |
|||
|
|||
return css, 200, {'Content-Type': 'text/css; charset=utf-8'} |
|||
|
|||
@APP.route('/pandoc-template.html') |
|||
def pandoc_template(): |
|||
x, template = update_pad_contents() |
|||
|
|||
return template, 200, {'Content-Type': 'text/html; charset=utf-8'} |
|||
|
|||
@APP.route('/pagedjs.html') |
|||
def pagedjs(): |
|||
# update pad contents |
|||
x, template = update_pad_contents() |
|||
# get feed contents |
|||
query = "https://multi.vvvvvvaria.org/API/latest/50" # !!! needs fixing |
|||
feed_html, feed_json = get_multifeeder_feed(query) |
|||
# render multifeeder feed in template |
|||
jinja_template = Template(template) |
|||
html = jinja_template.render(feed=feed_html, mode="print") |
|||
|
|||
return html, 200, {'Content-Type': 'text/html; charset=utf-8'} |
|||
|
|||
# ////////////// |
|||
|
|||
if __name__ == '__main__': |
|||
APP.debug=True |
|||
APP.run(host="0.0.0.0", port=f'{ APP.config["PORTNUMBER"] }', threaded=True) |
@ -0,0 +1,17 @@ |
|||
click==8.0.3 |
|||
Flask==2.0.2 |
|||
importlib-metadata==4.10.1 |
|||
itsdangerous==2.0.1 |
|||
Jinja2==3.0.3 |
|||
Markdown==3.3.6 |
|||
MarkupSafe==2.0.1 |
|||
pandoc==2.0.1 |
|||
pkg-resources==0.0.0 |
|||
plumbum==1.7.2 |
|||
ply==3.11 |
|||
pypandoc==1.7.2 |
|||
typing-extensions==4.0.1 |
|||
urllib3==1.26.8 |
|||
Werkzeug==2.0.2 |
|||
zipp==3.7.0 |
|||
|
@ -0,0 +1,76 @@ |
|||
body{ |
|||
min-width: 900px; |
|||
} |
|||
|
|||
/* GENERAL RULES */ |
|||
|
|||
/* main title element that says "in feedmode" */ |
|||
h1 em.feedmode{ |
|||
color: darkorchid; |
|||
} |
|||
|
|||
/* navigation */ |
|||
div#nav{ |
|||
position: fixed; |
|||
width: calc(100% - 1em); |
|||
top: 0; |
|||
left: 0; |
|||
margin: 0; |
|||
padding: 0 0.5em; |
|||
} |
|||
div#nav h1{ |
|||
position: absolute; |
|||
width: auto; |
|||
line-height: 0; |
|||
margin: 0.75em 15px; |
|||
float: left; |
|||
font-size: 24px; |
|||
} |
|||
div#nav div#buttons{ |
|||
margin: 0.5em 15px; |
|||
float: right; |
|||
} |
|||
div#nav input{ |
|||
min-width: 300px; |
|||
} |
|||
div#nav input#html{ |
|||
min-width: unset; |
|||
} |
|||
div#nav form#update{ |
|||
float: left; |
|||
margin: 0.5em 15px 0 300px; |
|||
} |
|||
|
|||
/* iframe rules */ |
|||
iframe{ |
|||
width: 100%; |
|||
height: 100%; |
|||
border: none; |
|||
} |
|||
|
|||
/* main content area */ |
|||
div#wrapper{ |
|||
/* pages with an iframe are on fixed mode */ |
|||
position: fixed; |
|||
top: 50px; |
|||
left: 25px; |
|||
width: calc(100vw - 25px - 25px); |
|||
height: calc(100vh - 50px - 25px); |
|||
} |
|||
div#wrapper.scroll{ |
|||
/* the HTML page is on scroll mode */ |
|||
position: relative; |
|||
top: 0; |
|||
left: 0; |
|||
margin: 5em 0 0 0; |
|||
} |
|||
|
|||
/* Z-INDEX */ |
|||
|
|||
div#wrapper, |
|||
div.pagedjs_pages{ |
|||
z-index: 1; |
|||
} |
|||
div#nav{ |
|||
z-index: 11; |
|||
} |
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,180 @@ |
|||
/* CSS for Paged.js interface – v0.2 */ |
|||
|
|||
/* Change the look */ |
|||
:root { |
|||
--color-background: whitesmoke; |
|||
--color-pageSheet: #cfcfcf; |
|||
--color-pageBox: violet; |
|||
--color-paper: white; |
|||
--color-marginBox: transparent; |
|||
--pagedjs-crop-color: black; |
|||
--pagedjs-crop-shadow: white; |
|||
--pagedjs-crop-stroke: 1px; |
|||
} |
|||
|
|||
/* To define how the book look on the screen: */ |
|||
@media screen { |
|||
body { |
|||
background-color: var(--color-background); |
|||
} |
|||
|
|||
.pagedjs_pages { |
|||
display: flex; |
|||
width: calc(var(--pagedjs-width) * 2); |
|||
flex: 0; |
|||
flex-wrap: wrap; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
.pagedjs_page { |
|||
background-color: var(--color-paper); |
|||
box-shadow: 0 0 0 1px var(--color-pageSheet); |
|||
margin: 0; |
|||
flex-shrink: 0; |
|||
flex-grow: 0; |
|||
margin-top: 10mm; |
|||
} |
|||
|
|||
.pagedjs_first_page { |
|||
margin-left: var(--pagedjs-width); |
|||
} |
|||
|
|||
.pagedjs_page:last-of-type { |
|||
margin-bottom: 10mm; |
|||
} |
|||
|
|||
.pagedjs_pagebox{ |
|||
box-shadow: 0 0 0 1px var(--color-pageBox); |
|||
} |
|||
|
|||
.pagedjs_left_page{ |
|||
z-index: 20; |
|||
width: calc(var(--pagedjs-bleed-left) + var(--pagedjs-pagebox-width))!important; |
|||
} |
|||
|
|||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-crop { |
|||
border-color: transparent; |
|||
} |
|||
|
|||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-middle{ |
|||
width: 0; |
|||
} |
|||
|
|||
.pagedjs_right_page{ |
|||
z-index: 10; |
|||
position: relative; |
|||
left: calc(var(--pagedjs-bleed-left)*-1); |
|||
} |
|||
|
|||
/* show the margin-box */ |
|||
|
|||
.pagedjs_margin-top-left-corner-holder, |
|||
.pagedjs_margin-top, |
|||
.pagedjs_margin-top-left, |
|||
.pagedjs_margin-top-center, |
|||
.pagedjs_margin-top-right, |
|||
.pagedjs_margin-top-right-corner-holder, |
|||
.pagedjs_margin-bottom-left-corner-holder, |
|||
.pagedjs_margin-bottom, |
|||
.pagedjs_margin-bottom-left, |
|||
.pagedjs_margin-bottom-center, |
|||
.pagedjs_margin-bottom-right, |
|||
.pagedjs_margin-bottom-right-corner-holder, |
|||
.pagedjs_margin-right, |
|||
.pagedjs_margin-right-top, |
|||
.pagedjs_margin-right-middle, |
|||
.pagedjs_margin-right-bottom, |
|||
.pagedjs_margin-left, |
|||
.pagedjs_margin-left-top, |
|||
.pagedjs_margin-left-middle, |
|||
.pagedjs_margin-left-bottom { |
|||
box-shadow: 0 0 0 1px inset var(--color-marginBox); |
|||
} |
|||
|
|||
/* uncomment this part for recto/verso book : ------------------------------------ */ |
|||
/* |
|||
|
|||
.pagedjs_pages { |
|||
flex-direction: column; |
|||
width: 100%; |
|||
} |
|||
|
|||
.pagedjs_first_page { |
|||
margin-left: 0; |
|||
} |
|||
|
|||
.pagedjs_page { |
|||
margin: 0 auto; |
|||
margin-top: 10mm; |
|||
} |
|||
|
|||
.pagedjs_left_page{ |
|||
width: calc(var(--pagedjs-bleed-left) + var(--pagedjs-pagebox-width) + var(--pagedjs-bleed-left))!important; |
|||
} |
|||
|
|||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-crop{ |
|||
border-color: var(--pagedjs-crop-color); |
|||
} |
|||
|
|||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-middle{ |
|||
width: var(--pagedjs-cross-size)!important; |
|||
} |
|||
|
|||
.pagedjs_right_page{ |
|||
left: 0; |
|||
} |
|||
*/ |
|||
|
|||
|
|||
|
|||
/*--------------------------------------------------------------------------------------*/ |
|||
|
|||
|
|||
|
|||
/* uncomment this par to see the baseline : -------------------------------------------*/ |
|||
|
|||
/* |
|||
.pagedjs_pagebox { |
|||
--pagedjs-baseline: 22px; |
|||
--pagedjs-baseline-position: 5px; |
|||
--pagedjs-baseline-color: cyan; |
|||
background: linear-gradient(transparent 0%, transparent calc(var(--pagedjs-baseline) - 1px), var(--pagedjs-baseline-color) calc(var(--pagedjs-baseline) - 1px), var(--pagedjs-baseline-color) var(--pagedjs-baseline)), transparent; |
|||
background-size: 100% var(--pagedjs-baseline); |
|||
background-repeat: repeat-y; |
|||
background-position-y: var(--pagedjs-baseline-position); |
|||
} */ |
|||
|
|||
|
|||
/*--------------------------------------------------------------------------------------*/ |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
/* Marks (to delete when merge in paged.js) */ |
|||
|
|||
.pagedjs_marks-crop{ |
|||
z-index: 999999999999; |
|||
|
|||
} |
|||
|
|||
.pagedjs_bleed-top .pagedjs_marks-crop, |
|||
.pagedjs_bleed-bottom .pagedjs_marks-crop{ |
|||
box-shadow: 1px 0px 0px 0px var(--pagedjs-crop-shadow); |
|||
} |
|||
|
|||
.pagedjs_bleed-top .pagedjs_marks-crop:last-child, |
|||
.pagedjs_bleed-bottom .pagedjs_marks-crop:last-child{ |
|||
box-shadow: -1px 0px 0px 0px var(--pagedjs-crop-shadow); |
|||
} |
|||
|
|||
.pagedjs_bleed-left .pagedjs_marks-crop, |
|||
.pagedjs_bleed-right .pagedjs_marks-crop{ |
|||
box-shadow: 0px 1px 0px 0px var(--pagedjs-crop-shadow); |
|||
} |
|||
|
|||
.pagedjs_bleed-left .pagedjs_marks-crop:last-child, |
|||
.pagedjs_bleed-right .pagedjs_marks-crop:last-child{ |
|||
box-shadow: 0px -1px 0px 0px var(--pagedjs-crop-shadow); |
|||
} |
@ -0,0 +1,13 @@ |
|||
div.post{ |
|||
width: 250px; |
|||
border: 1px solid magenta; |
|||
padding: 1em; |
|||
margin: 0.5em; |
|||
max-height: 400px; |
|||
float: left; |
|||
overflow-y: scroll; |
|||
font-size: 12px; |
|||
} |
|||
div.post img{ |
|||
max-width: 100%; |
|||
} |
@ -0,0 +1,44 @@ |
|||
<!DOCTYPE html> |
|||
<html lang='en'> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<title>feedmode</title> |
|||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}"> |
|||
{% block head %} |
|||
{% endblock %} |
|||
</head> |
|||
<body> |
|||
<div id="wrapper" class="{% block pagetype %}{% endblock %}"> |
|||
{% block content %} |
|||
{% endblock %} |
|||
</div> |
|||
</body> |
|||
<script> |
|||
window.addEventListener('load', function () { |
|||
|
|||
// Insert the nav buttons, after the page is loaded |
|||
const nav = document.createElement('div'); |
|||
nav.id = 'nav'; |
|||
|
|||
nav.innerHTML = ` |
|||
<h1><em class="feedmode">feedmode</em><sup>(very beta)</sup></h1> |
|||
<form id="update" action="/update/" method="post"> |
|||
<input type="text" name="query" value="{% if query %}{{ query }}{% else %}https://multi.vvvvvvaria.org/API/latest/50{% endif %}"> |
|||
<input id="html" type="submit" value="update"> |
|||
</form> |
|||
<div id="buttons"> |
|||
<a href="/select/"><button>select</button></a> |
|||
<a href="/preview/"><button>preview</button></a> |
|||
<a href="/pdf/"><button>pdf</button></a> |
|||
<a href="/stylesheet/"><button>stylesheet</button></a>: <input type="text" name="pad" value="https://pad.vvvvvvaria.org/feedmode.css"> |
|||
<a href="/template/"><button>template</button></a>: <input type="text" name="pad" value="https://pad.vvvvvvaria.org/feedmode.template"> |
|||
</div> |
|||
`; |
|||
|
|||
document.body.insertBefore(nav, document.body.firstChild); |
|||
|
|||
}) |
|||
</script> |
|||
{% block footer %} |
|||
{% endblock %} |
|||
</html> |
@ -0,0 +1,9 @@ |
|||
{% extends "base.html" %} |
|||
|
|||
{% block pagetype %} |
|||
scroll |
|||
{% endblock %} |
|||
|
|||
{% block content %} |
|||
{{ html | safe }} |
|||
{% endblock %} |
@ -0,0 +1,5 @@ |
|||
{% extends "base.html" %} |
|||
|
|||
{% block content %} |
|||
<iframe src="{{ url }}"></iframe> |
|||
{% endblock %} |
@ -0,0 +1,10 @@ |
|||
{% for post in response %} |
|||
<div class="post"> |
|||
<!-- <input type="checkbox" name="check" value="{{ loop.index }}"><br><br> --> |
|||
<h1>{{ post.title }}</h1> |
|||
<!-- <div><small>{{ post.author }}</small></div> --> |
|||
<div><small>{{ post.published }}</small></div> |
|||
<div><small><a href="{{ post.link }}">{{ post.link }}</a></small></div> |
|||
<div>{{ post.summary }}</div> |
|||
</div> |
|||
{% endfor %} |
@ -0,0 +1,5 @@ |
|||
{% extends "base.html" %} |
|||
|
|||
{% block content %} |
|||
<iframe src="https://pad.vvvvvvaria.org/{{ name }}{{ ext }}"></iframe> |
|||
{% endblock %} |
@ -0,0 +1,36 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
|
|||
<head> |
|||
<meta charset="utf-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
{% if mode == "print" %} |
|||
<script src="/static/paged.js" type="text/javascript"></script> |
|||
<script src="/static/paged.polyfill.js" type="text/javascript"></script> |
|||
<link href="/static/pagedjs.css" rel="stylesheet" type="text/css" media="screen"> |
|||
{% endif %} |
|||
<link href="/print.css" rel="stylesheet" type="text/css" media="{{ mode }}"> |
|||
<title>The Everydaily</title> |
|||
</head> |
|||
|
|||
<body> |
|||
|
|||
<section id="cover"> |
|||
<h1 id="title">The Everydaily</h1> |
|||
</section> |
|||
|
|||
<section id="header"> |
|||
<div>This is the header...</div> |
|||
</section> |
|||
|
|||
<section id="feed"> |
|||
{{ feed }} |
|||
</section> |
|||
|
|||
<section id="footer"> |
|||
<div>Anything in the footer here...</div> |
|||
</section> |
|||
</body> |
|||
</html> |
|||
|
|||
|
@ -0,0 +1,30 @@ |
|||
{% extends "base.html" %} |
|||
|
|||
{% block content %} |
|||
<iframe id="pdf" name="pdf" src="/pagedjs.html"></iframe> |
|||
{% endblock %} |
|||
|
|||
{% block footer %} |
|||
<script> |
|||
function printPage(){ |
|||
window.frames["pdf"].focus(); |
|||
window.frames["pdf"].print(); |
|||
} |
|||
|
|||
window.addEventListener('load', function () { |
|||
|
|||
// Load the main.css again, to load the stylesheet for the nav |
|||
var cssLink = document.createElement('link'); |
|||
cssLink.rel = 'stylesheet'; |
|||
cssLink.href = '/static/main.css'; |
|||
var head = document.getElementsByTagName('head')[0]; |
|||
head.insertBefore(cssLink, head.firstChild); |
|||
|
|||
// Insert the SAVE button |
|||
const nav = document.getElementById('buttons'); |
|||
const save = '<a href="#"><button id="save" onClick="printPage()">save</button></a>'; |
|||
nav.innerHTML = nav.innerHTML + save; |
|||
|
|||
}) |
|||
</script> |
|||
{% endblock %} |
@ -0,0 +1,21 @@ |
|||
{% extends "base.html" %} |
|||
|
|||
{% block head %} |
|||
<link rel="stylesheet" type="text/css" href="/static/select.css"> |
|||
{% endblock %} |
|||
|
|||
{% block pagetype %} |
|||
scroll |
|||
{% endblock %} |
|||
|
|||
{% block content %} |
|||
{% for post in feed %} |
|||
<div class="post"> |
|||
<input type="checkbox" name="check" value="{{ loop.index }}"><br><br> |
|||
<h1>{{ post.title }}</h1> |
|||
<div><small>{{ post.published }}</small></div> |
|||
<div><small><a href="{{ post.link }}">{{ post.link }}</a></small></div> |
|||
<div>{{ post.summary | safe }}</div> |
|||
</div> |
|||
{% endfor %} |
|||
{% endblock %} |
Loading…
Reference in new issue