edit settings dynamically
This commit is contained in:
parent
ced259d7e8
commit
d07503dd6f
|
@ -0,0 +1,111 @@
|
||||||
|
(function(){
|
||||||
|
if(!('fetch' in window)){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let status_timeout = null;
|
||||||
|
|
||||||
|
let form = document.forms.settings;
|
||||||
|
let backoff_level = 0
|
||||||
|
|
||||||
|
function hide_status(){
|
||||||
|
status_display.classList.remove('error', 'success', 'saving');
|
||||||
|
status_display.classList.add('hidden');
|
||||||
|
status_display.innerHTML='';
|
||||||
|
}
|
||||||
|
function show_error(e){
|
||||||
|
hide_status();
|
||||||
|
status_display.textContent='Could not save. Retrying...';
|
||||||
|
status_display.classList.add('error');
|
||||||
|
status_display.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
function show_success(){
|
||||||
|
hide_status();
|
||||||
|
status_display.textContent='Saved!';
|
||||||
|
status_display.classList.add('success');
|
||||||
|
status_display.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
function show_saving(){
|
||||||
|
hide_status();
|
||||||
|
status_display.textContent='Saving...';
|
||||||
|
status_display.classList.add('saving');
|
||||||
|
status_display.classList.remove('hidden');
|
||||||
|
status_timeout = setTimeout(show_still_saving, 5000);
|
||||||
|
}
|
||||||
|
function show_still_saving(){
|
||||||
|
status_display.textContent='Still saving...';
|
||||||
|
}
|
||||||
|
|
||||||
|
function on_change(e){
|
||||||
|
hide_status();
|
||||||
|
clearTimeout(status_timeout);
|
||||||
|
status_timeout = setTimeout(show_saving, 70);
|
||||||
|
|
||||||
|
send_settings(get_all_inputs())
|
||||||
|
.then(data => {
|
||||||
|
show_success();
|
||||||
|
clearTimeout(status_timeout);
|
||||||
|
status_timeout = setTimeout(hide_status, 3000);
|
||||||
|
backoff_level = 0;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error('Fetch rejected:', e);
|
||||||
|
show_error();
|
||||||
|
clearTimeout(status_timeout);
|
||||||
|
status_timeout = setTimeout(save, Math.pow(2, backoff_level)*1000);
|
||||||
|
backoff_level += 1;
|
||||||
|
backoff_level = Math.min(backoff_level, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove server-rendered banner
|
||||||
|
let settings_section = document.querySelector('#settings-section');
|
||||||
|
let banner = settings_section.querySelector('.banner');
|
||||||
|
if(banner){
|
||||||
|
settings_section.removeChild(banner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_all_inputs(){
|
||||||
|
let o = Object()
|
||||||
|
for(input of form.elements){
|
||||||
|
if(input.type != 'radio' || input.checked){
|
||||||
|
o[input.name] = input.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
function send_settings(body){
|
||||||
|
return fetch('/api/settings', {
|
||||||
|
method:'PUT',
|
||||||
|
credentials:'same-origin',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(resp => { if(!resp.ok){ return Promise.reject(resp) }; return resp; })
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(data => {
|
||||||
|
if(data.status == 'error'){ return Promise.reject(data) }
|
||||||
|
return data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for(input of form.elements){
|
||||||
|
input.addEventListener('change', on_change);
|
||||||
|
input.addEventListener('change', e=>console.log(e.target));
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove submit button since we're doing live updates
|
||||||
|
let submit = form.querySelector('input[type=submit]');
|
||||||
|
form.removeChild(submit);
|
||||||
|
|
||||||
|
let status_display = document.createElement('span');
|
||||||
|
status_display.classList.add('status-display', 'hidden');
|
||||||
|
let settings_title = document.querySelector('#settings-title');
|
||||||
|
settings_title.appendChild(status_display);
|
||||||
|
|
||||||
|
// silently send_settings in case the user changed settings while the page was loading
|
||||||
|
send_settings(get_all_inputs());
|
||||||
|
})();
|
|
@ -82,17 +82,17 @@ body > section > .banner {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner.success {
|
.success {
|
||||||
|
border-color: #4a2;
|
||||||
background: #dec;
|
background: #dec;
|
||||||
border-left-color: #4a2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner.error {
|
.error {
|
||||||
border-color: #a24;
|
border-color: #a24;
|
||||||
background: #ecd;
|
background: #ecd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner.warning {
|
.warning {
|
||||||
border-color: #dc4;
|
border-color: #dc4;
|
||||||
background: #eec;
|
background: #eec;
|
||||||
}
|
}
|
||||||
|
@ -126,3 +126,57 @@ footer a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-display {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 1ch;
|
||||||
|
padding: 0 0.4em;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-display.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-display.error {
|
||||||
|
background: #ecd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-display.success {
|
||||||
|
display: inline-block;
|
||||||
|
background: #dec;
|
||||||
|
animation: fade-background 2s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-background {
|
||||||
|
to {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-display::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 0.8em;
|
||||||
|
height: 0.8em;
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0.4em;
|
||||||
|
margin-right: 0.3em;
|
||||||
|
|
||||||
|
}
|
||||||
|
.status-display.saving::before {
|
||||||
|
border-color: #666;
|
||||||
|
border-width: 2px;
|
||||||
|
animation: blink infinite 0.6s steps(1) normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
2
dodo.py
2
dodo.py
|
@ -23,7 +23,7 @@ def task_gen_logo():
|
||||||
|
|
||||||
def task_copy_asset():
|
def task_copy_asset():
|
||||||
import shutil
|
import shutil
|
||||||
assets = ('icon.png', 'logotype.png', 'version.js')
|
assets = ('icon.png', 'logotype.png', 'version.js', 'settings_form.js')
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
yield dict(
|
yield dict(
|
||||||
name=asset,
|
name=asset,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from .auth import require_auth
|
from . import auth
|
||||||
from .interval import decompose_interval
|
from .interval import decompose_interval
|
||||||
from .interval import SCALES as interval_scales
|
from .interval import SCALES as interval_scales
|
||||||
from .cachebust import cachebust
|
from .cachebust import cachebust
|
||||||
from .session import set_session_cookie, get_viewer_session, get_viewer
|
from .session import set_session_cookie, get_viewer_session, get_viewer
|
||||||
from . import brotli
|
from . import brotli
|
||||||
|
from . import settings
|
||||||
|
|
24
lib/auth.py
24
lib/auth.py
|
@ -1,18 +1,20 @@
|
||||||
from flask import g, redirect
|
from flask import g, redirect, jsonify, make_response
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
def require_auth(fun, redir=True):
|
def require_auth(fun):
|
||||||
|
@wraps(fun)
|
||||||
from functools import update_wrapper
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
if not g.viewer:
|
if not g.viewer:
|
||||||
if redir:
|
return redirect('/')
|
||||||
return redirect('/')
|
return fun(*args, **kwargs)
|
||||||
else:
|
return wrapper
|
||||||
return 403
|
|
||||||
else:
|
|
||||||
return fun(*args, **kwargs)
|
|
||||||
|
|
||||||
update_wrapper(wrapper, fun)
|
def require_auth_api(fun):
|
||||||
|
@wraps(fun)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if not g.viewer:
|
||||||
|
return make_response((jsonify(status='error', error='not logged in'), 403))
|
||||||
|
return fun(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
attrs = (
|
||||||
|
'policy_keep_favourites',
|
||||||
|
'policy_keep_latest',
|
||||||
|
'policy_delete_every_scale',
|
||||||
|
'policy_delete_every_significand',
|
||||||
|
'policy_keep_younger_scale',
|
||||||
|
'policy_keep_younger_significand',
|
||||||
|
'policy_keep_media',
|
||||||
|
)
|
28
routes.py
28
routes.py
|
@ -2,9 +2,9 @@ from flask import render_template, url_for, redirect, request, g, Response, json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import lib.twitter
|
import lib.twitter
|
||||||
import lib
|
import lib
|
||||||
from lib import require_auth
|
from lib.auth import require_auth, require_auth_api
|
||||||
from lib import set_session_cookie
|
from lib import set_session_cookie
|
||||||
from lib import get_viewer_session
|
from lib import get_viewer_session, get_viewer
|
||||||
from model import Account, Session, Post, TwitterArchive
|
from model import Account, Session, Post, TwitterArchive
|
||||||
from app import app, db, sentry, limiter
|
from app import app, db, sentry, limiter
|
||||||
import tasks
|
import tasks
|
||||||
|
@ -13,6 +13,7 @@ from twitter import TwitterError
|
||||||
from urllib.error import URLError
|
from urllib.error import URLError
|
||||||
import version
|
import version
|
||||||
import lib.brotli
|
import lib.brotli
|
||||||
|
import lib.settings
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def load_viewer():
|
def load_viewer():
|
||||||
|
@ -111,15 +112,7 @@ def upload_tweet_archive():
|
||||||
@app.route('/settings', methods=('POST',))
|
@app.route('/settings', methods=('POST',))
|
||||||
@require_auth
|
@require_auth
|
||||||
def settings():
|
def settings():
|
||||||
for attr in (
|
for attr in lib.settings.attrs:
|
||||||
'policy_keep_favourites',
|
|
||||||
'policy_keep_latest',
|
|
||||||
'policy_delete_every_scale',
|
|
||||||
'policy_delete_every_significand',
|
|
||||||
'policy_keep_younger_scale',
|
|
||||||
'policy_keep_younger_significand',
|
|
||||||
'policy_keep_media',
|
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
if attr in request.form:
|
if attr in request.form:
|
||||||
setattr(g.viewer.account, attr, request.form[attr])
|
setattr(g.viewer.account, attr, request.form[attr])
|
||||||
|
@ -174,3 +167,16 @@ def logout():
|
||||||
@app.route('/api/about')
|
@app.route('/api/about')
|
||||||
def api_about():
|
def api_about():
|
||||||
return jsonify(service='Forget', version=version.version)
|
return jsonify(service='Forget', version=version.version)
|
||||||
|
|
||||||
|
@app.route('/api/settings', methods=('PUT',))
|
||||||
|
@require_auth_api
|
||||||
|
def api_settings_put():
|
||||||
|
viewer = get_viewer()
|
||||||
|
data = request.json
|
||||||
|
updated = dict()
|
||||||
|
for key in lib.settings.attrs:
|
||||||
|
if key in data:
|
||||||
|
setattr(viewer, key, data[key])
|
||||||
|
updated[key] = data[key]
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(status='success', updated=updated)
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
onload="Raven.config('{{sentry_dsn}}').install()"></script>
|
onload="Raven.config('{{sentry_dsn}}').install()"></script>
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
<script defer src="{{ st('version.js') }}"></script>
|
<script defer src="{{ st('version.js') }}"></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
{% from 'lib/interval.html' import interval_input -%}
|
{% from 'lib/interval.html' import interval_input -%}
|
||||||
{% extends 'lib/layout.html' %}
|
{% extends 'lib/layout.html' %}
|
||||||
|
{% block scripts %}
|
||||||
|
<script async src="{{st('settings_form.js')}}"></script>
|
||||||
|
{% endblock %}
|
||||||
{% block body -%}
|
{% block body -%}
|
||||||
|
|
||||||
<section class=viewer>
|
<section class=viewer>
|
||||||
|
@ -30,8 +33,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section id='settings-section'>
|
||||||
<h2>Your expiration rules</h2>
|
<h2 id="settings-title">Your expiration rules</h2>
|
||||||
|
|
||||||
{% if request.args.get('settings_saved') != None %}
|
{% if request.args.get('settings_saved') != None %}
|
||||||
<div class='banner success'>Rules saved successfully</div>
|
<div class='banner success'>Rules saved successfully</div>
|
||||||
|
@ -41,7 +44,7 @@
|
||||||
<div class='banner error'>Something went wrong trying to save your settings. Please try again later.</div>
|
<div class='banner error'>Something went wrong trying to save your settings. Please try again later.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form action='{{url_for("settings")}}' method='post' enctype='multipart/form-data'>
|
<form name='settings' action='{{url_for("settings")}}' method='post' enctype='multipart/form-data'>
|
||||||
<p>Posts that are over
|
<p>Posts that are over
|
||||||
{{interval_input(g.viewer.account, 'policy_keep_younger', scales)}}
|
{{interval_input(g.viewer.account, 'policy_keep_younger', scales)}}
|
||||||
old and are not one of your
|
old and are not one of your
|
||||||
|
|
Loading…
Reference in New Issue