Compare commits

...

10 commits

28 changed files with 288 additions and 156 deletions

View file

@ -221,6 +221,8 @@ Here are some thoughts about the kinds of features that need to exist within the
## Mirrors
https://git.voussoir.net/voussoir/etiquette
https://github.com/voussoir/etiquette
https://gitlab.com/voussoir/etiquette

View file

@ -198,7 +198,7 @@ CREATE INDEX IF NOT EXISTS index_tag_group_rel_memberid on tag_group_rel(memberi
CREATE TABLE IF NOT EXISTS tag_synonyms(
name TEXT PRIMARY KEY NOT NULL,
mastername TEXT NOT NULL,
created INT,
created INT
);
CREATE INDEX IF NOT EXISTS index_tag_synonyms_name on tag_synonyms(name);
CREATE INDEX IF NOT EXISTS index_tag_synonyms_mastername on tag_synonyms(mastername);

View file

@ -3,7 +3,9 @@ This file provides functions which are used in various places throughout the
codebase but don't deserve to be methods of any class.
'''
import bs4
import io
import datetime
import kkroening_ffmpeg
import hashlib
import os
import PIL.Image
@ -18,6 +20,9 @@ from voussoirkit import imagetools
from voussoirkit import pathclass
from voussoirkit import stringtools
from voussoirkit import timetools
from voussoirkit import vlogging
log = vlogging.get_logger(__name__)
from . import constants
from . import exceptions
@ -218,6 +223,7 @@ def _generate_image_thumbnail(filepath, max_width, max_height) -> PIL.Image:
if not os.path.isfile(filepath):
raise FileNotFoundError(filepath)
image = PIL.Image.open(filepath)
image = imagetools.convert_to_srgb(image)
(image, exif) = imagetools.rotate_by_exif(image)
(image_width, image_height) = image.size
(new_width, new_height) = imagetools.fit_into_bounds(
@ -254,13 +260,23 @@ def generate_image_thumbnail(*args, trusted_file=False, **kwargs) -> PIL.Image:
finally:
PIL.Image.MAX_IMAGE_PIXELS = _max_pixels
def image_is_mostly_black(image):
tiny = image.copy()
tiny.thumbnail((64, 64))
pixels = list(tiny.getdata())
black_count = 0
if tiny.mode == 'RGB':
black_count = sum(1 for pixel in pixels if sum(pixel) <= 24)
return (black_count / len(pixels)) > 0.5
def generate_video_thumbnail(filepath, width, height, **special) -> PIL.Image:
if not os.path.isfile(filepath):
raise FileNotFoundError(filepath)
file = pathclass.Path(filepath)
file.assert_is_file()
probe = constants.ffmpeg.probe(filepath)
if not probe or not probe.video:
return False
return None
size = imagetools.fit_into_bounds(
image_width=probe.video.video_width,
@ -268,26 +284,25 @@ def generate_video_thumbnail(filepath, width, height, **special) -> PIL.Image:
frame_width=width,
frame_height=height,
)
size = '%dx%d' % size
duration = probe.video.duration
if 'timestamp' in special:
timestamp = special['timestamp']
elif duration < 3:
timestamp = 0
timestamp_choices = [special['timestamp']]
else:
timestamp = 2
timestamp_choices = list(range(0, int(duration), 3))
image = None
for this_time in timestamp_choices:
log.debug('Attempting video thumbnail at t=%d', this_time)
command = kkroening_ffmpeg.input(file.absolute_path, ss=this_time)
command = command.filter('scale', size[0], size[1])
command = command.output('pipe:', vcodec='bmp', format='image2pipe', vframes=1)
(out, trash) = command.run(capture_stdout=True, capture_stderr=True)
bio = io.BytesIO(out)
image = PIL.Image.open(bio)
if not image_is_mostly_black(image):
break
outfile = tempfile.NamedTemporaryFile(suffix='.jpg', delete=False)
constants.ffmpeg.thumbnail(
filepath,
outfile=outfile.name,
quality=2,
size=size,
time=timestamp,
)
outfile.close()
image = PIL.Image.open(outfile.name)
return image
def get_mimetype(extension) -> typing.Optional[str]:

View file

@ -1150,7 +1150,7 @@ class PhotoDB(
self.COLUMNS = constants.SQL_COLUMNS
self.COLUMN_INDEX = constants.SQL_INDEX
def _init_sql(self, create, skip_version_check):
def _init_sql(self, create=False, skip_version_check=False):
if self.ephemeral:
existing_database = False
self.sql_write = self._make_sqlite_write_connection(':memory:')

View file

@ -99,8 +99,11 @@ def before_request():
if site.localhost_only and not request.is_localhost:
return flask.abort(403)
# Since we don't define this route, I can't just add this where it belongs.
# Sorry.
if request.url_rule is None:
return flask.abort(404)
# Since we don't define this route (/static/ is a default from flask),
# I can't just add this where it belongs. Sorry.
if request.url_rule.rule == '/static/<path:filename>':
permission_manager.global_public()

View file

@ -67,7 +67,7 @@ def get_file(photo_id, basename=None):
@site.route('/photo/<photo_id>/thumbnail')
@site.route('/photo/<photo_id>/thumbnail/<basename>')
@common.permission_manager.basic_decorator
@flasktools.cached_endpoint(max_age=common.BROWSER_CACHE_DURATION)
@flasktools.cached_endpoint(max_age=common.BROWSER_CACHE_DURATION, etag_function=lambda: common.P.last_commit_id)
def get_thumbnail(photo_id, basename=None):
photo_id = photo_id.split('.')[0]
photo = common.P_photo(photo_id, response_type='html')
@ -186,16 +186,35 @@ def post_batch_photos_remove_tag():
# Photo metadata operations ########################################################################
def post_photo_generate_thumbnail_core(photo_ids, special={}):
if isinstance(photo_ids, str):
photo_ids = stringtools.comma_space_split(photo_ids)
with common.P.transaction:
photos = list(common.P_photos(photo_ids, response_type='json'))
for photo in photos:
photo._uncache()
photo = common.P_photo(photo.id, response_type='json')
try:
photo.generate_thumbnail()
except Exception:
log.warning(traceback.format_exc())
return flasktools.json_response({})
@site.route('/photo/<photo_id>/generate_thumbnail', methods=['POST'])
def post_photo_generate_thumbnail(photo_id):
common.permission_manager.basic()
special = request.form.to_dict()
response = post_photo_generate_thumbnail_core(photo_ids=photo_id, special=special)
return response
with common.P.transaction:
photo = common.P_photo(photo_id, response_type='json')
photo.generate_thumbnail(**special)
response = flasktools.json_response({})
@site.route('/batch/photos/generate_thumbnail', methods=['POST'])
def post_batch_photos_generate_thumbnail():
common.permission_manager.basic()
special = request.form.to_dict()
response = post_photo_generate_thumbnail_core(photo_ids=request.form['photo_ids'], special=special)
return response
def post_photo_refresh_metadata_core(photo_ids):

View file

@ -63,6 +63,10 @@ class SessionManager:
# Send the token back to the client
# but only if the endpoint didn't manually set the cookie.
function_cookies = response.headers.get_all('Set-Cookie')
if not hasattr(request, 'session') or not request.session:
return response
if not any('etiquette_session=' in cookie for cookie in function_cookies):
response.set_cookie(
'etiquette_session',

View file

@ -203,3 +203,105 @@ is hovered over.
{
flex: 2;
}
/******************************************************************************/
html.theme_slate
{
--color_primary: #222;
--color_secondary: #3b4d5d;
--color_text_normal: #efefef;
--color_text_link: #1edeff;
--color_text_bubble: black;
--color_textfields: var(--color_secondary);
--color_text_placeholder: gray;
--color_transparency: rgba(255, 255, 255, 0.05);
--color_dropshadow: rgba(0, 0, 0, 0.25);
--color_shadow: rgba(0, 0, 0, 0.5);
--color_highlight: rgba(255, 255, 255, 0.5);
--color_tag_card_bg: #e6e6e6;
--color_tag_card_fg: black;
}
html.theme_slate button,
html.theme_slate button *
{
color: black;
}
/******************************************************************************/
html.theme_hotdogstand
{
--color_primary: yellow;
--color_secondary: red;
--color_text_normal: black;
--color_text_link: rebeccapurple;
--color_text_bubble: black;
--color_textfields: var(--color_secondary);
--color_text_placeholder: black;
--color_transparency: yellow;
--color_dropshadow: rgba(0, 0, 0, 0.25);
--color_shadow: rgba(0, 0, 0, 0.5);
--color_highlight: rgba(255, 255, 255, 0.5);
--color_tag_card_bg: red;
--color_tag_card_fg: white;
}
html.theme_hotdogstand button,
html.theme_hotdogstand button *
{
color: black;
}
html.theme_hotdogstand .panel
{
border: 1px solid black;
}
/******************************************************************************/
html.theme_pearl
{
--color_primary: #f6ffff;
--color_secondary: #aad7ff;
--color_text_normal: black;
--color_text_link: #00f;
--color_text_bubble: black;
--color_textfields: white;
--color_text_placeholder: gray;
--color_transparency: rgba(0, 0, 0, 0.1);
--color_dropshadow: rgba(0, 0, 0, 0.25);
--color_shadow: rgba(0, 0, 0, 0.5);
--color_highlight: rgba(255, 255, 255, 0.5);
--color_tag_card_bg: #fff;
--color_tag_card_fg: black;
}
/******************************************************************************/
html.theme_turquoise
{
--color_primary: #00d8f4;
--color_secondary: #ffffd4;
--color_text_normal: black;
--color_text_link: blue;
--color_text_bubble: black;
--color_textfields: white;
--color_text_placeholder: gray;
--color_transparency: rgba(0, 0, 0, 0.1);
--color_dropshadow: rgba(0, 0, 0, 0.25);
--color_shadow: rgba(0, 0, 0, 0.5);
--color_highlight: rgba(255, 255, 255, 0.5);
--color_tag_card_bg: #fff;
--color_tag_card_fg: blue;
}

View file

@ -1,31 +0,0 @@
:root
{
--color_primary: yellow;
--color_secondary: red;
--color_text_normal: black;
--color_text_link: rebeccapurple;
--color_text_bubble: black;
--color_textfields: var(--color_secondary);
--color_text_placeholder: black;
--color_transparency: yellow;
--color_dropshadow: rgba(0, 0, 0, 0.25);
--color_shadow: rgba(0, 0, 0, 0.5);
--color_highlight: rgba(255, 255, 255, 0.5);
--color_tag_card_bg: red;
--color_tag_card_fg: white;
}
button,
button *
{
color: black;
}
.panel
{
border: 1px solid black;
}

View file

@ -1,20 +0,0 @@
:root
{
--color_primary: #f6ffff;
--color_secondary: #aad7ff;
--color_text_normal: black;
--color_text_link: #00f;
--color_text_bubble: black;
--color_textfields: white;
--color_text_placeholder: gray;
--color_transparency: rgba(0, 0, 0, 0.1);
--color_dropshadow: rgba(0, 0, 0, 0.25);
--color_shadow: rgba(0, 0, 0, 0.5);
--color_highlight: rgba(255, 255, 255, 0.5);
--color_tag_card_bg: #fff;
--color_tag_card_fg: black;
}

View file

@ -1,26 +0,0 @@
:root
{
--color_primary: #222;
--color_secondary: #3b4d5d;
--color_text_normal: #efefef;
--color_text_link: #1edeff;
--color_text_bubble: black;
--color_textfields: var(--color_secondary);
--color_text_placeholder: gray;
--color_transparency: rgba(255, 255, 255, 0.05);
--color_dropshadow: rgba(0, 0, 0, 0.25);
--color_shadow: rgba(0, 0, 0, 0.5);
--color_highlight: rgba(255, 255, 255, 0.5);
--color_tag_card_bg: #e6e6e6;
--color_tag_card_fg: black;
}
button,
button *
{
color: black;
}

View file

@ -1,20 +0,0 @@
:root
{
--color_primary: #00d8f4;
--color_secondary: #ffffd4;
--color_text_normal: black;
--color_text_link: blue;
--color_text_bubble: black;
--color_textfields: white;
--color_text_placeholder: gray;
--color_transparency: rgba(0, 0, 0, 0.1);
--color_dropshadow: rgba(0, 0, 0, 0.25);
--color_shadow: rgba(0, 0, 0, 0.5);
--color_highlight: rgba(255, 255, 255, 0.5);
--color_tag_card_bg: #fff;
--color_tag_card_fg: blue;
}

View file

@ -236,7 +236,17 @@ function batch_add_tag(photo_ids, tagname, callback)
return http.post({
url: "/batch/photos/add_tag",
data: {"photo_ids": photo_ids.join(","), "tagname": tagname},
add_remove_tag_callback: callback,
callback: callback,
});
}
api.photos.batch_generate_thumbnail =
function batch_generate_thumbnail(photo_ids, callback)
{
return http.post({
url: "/batch/photos/generate_thumbnail",
data: {"photo_ids": photo_ids.join(",")},
callback: callback,
});
}
@ -256,7 +266,7 @@ function batch_remove_tag(photo_ids, tagname, callback)
return http.post({
url: "/batch/photos/remove_tag",
data: {"photo_ids": photo_ids.join(","), "tagname": tagname},
add_remove_tag_callback: callback,
callback: callback,
});
}

View file

@ -71,6 +71,54 @@ function join_and_trail(list, separator)
return list.join(separator) + separator
}
common.hms_render_colons =
function hms_render_colons(hours, minutes, seconds)
{
const parts = [];
if (hours !== null)
{
parts.push(hours.toLocaleString(undefined, {minimumIntegerDigits: 2}));
}
if (minutes !== null)
{
parts.push(minutes.toLocaleString(undefined, {minimumIntegerDigits: 2}));
}
parts.push(seconds.toLocaleString(undefined, {minimumIntegerDigits: 2}));
return parts.join(":")
}
common.seconds_to_hms =
function seconds_to_hms(seconds, args)
{
args = args || {};
const renderer = args["renderer"] || common.hms_render_colons;
const force_minutes = args["force_minutes"] || false;
const force_hours = args["force_hours"] || false;
if (seconds > 0 && seconds < 1)
{
seconds = 1;
}
else
{
seconds = Math.round(seconds);
}
let minutes = Math.floor(seconds / 60);
seconds = seconds % 60;
let hours = Math.floor(minutes / 60);
minutes = minutes % 60;
if (hours == 0 && force_hours == false)
{
hours = null;
}
if (minutes == 0 && force_minutes == false)
{
minutes = null;
}
return renderer(hours, minutes, seconds);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// HTML & DOM //////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
{% import "header.html" as header %}
<title>Admin control</title>
@ -8,7 +8,6 @@
<link rel="icon" href="/favicon.png" type="image/png"/>
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/etiquette.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/http.js"></script>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
{% macro shared_css() %}
<style>
@ -89,7 +89,6 @@
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/etiquette.css">
<link rel="stylesheet" href="/static/css/cards.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/cards.js"></script>
@ -146,7 +145,6 @@ const ALBUM_ID = undefined;
<link rel="stylesheet" href="/static/css/etiquette.css">
<link rel="stylesheet" href="/static/css/clipboard_tray.css">
<link rel="stylesheet" href="/static/css/cards.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/album_autocomplete.js"></script>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
{% import "header.html" as header %}
{% import "cards.html" as cards %}
@ -10,7 +10,6 @@
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/etiquette.css">
<link rel="stylesheet" href="/static/css/cards.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/cards.js"></script>

View file

@ -151,7 +151,7 @@ ondrop="return cards.photos.drag_drop(event);"
draggable="true"
>
<div class="photo_card_filename">
<a href="/photo/{{photo.id}}" draggable="false">{{photo.basename}}</a>
<a target="_blank" href="/photo/{{photo.id}}" draggable="false">{{photo.basename}}</a>
</div>
<span class="photo_card_metadata">
@ -172,7 +172,7 @@ draggable="true"
{% set thumbnail_src = "/static/basic_thumbnails/" ~ thumbnail_src ~ ".png" %}
{% endif -%}{# if thumbnail #}
<a class="photo_card_thumbnail" href="/photo/{{photo.id}}" draggable="false">
<a class="photo_card_thumbnail" target="_blank" href="/photo/{{photo.id}}" draggable="false">
<img loading="lazy" src="{{thumbnail_src}}" draggable="false">
</a>
{% endif %}{# if grid #}

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
{% import "header.html" as header %}
{% import "clipboard_tray.html" as clipboard_tray %}
@ -11,7 +11,6 @@
<link rel="stylesheet" href="/static/css/etiquette.css">
<link rel="stylesheet" href="/static/css/cards.css">
<link rel="stylesheet" href="/static/css/clipboard_tray.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/cards.js"></script>
@ -122,6 +121,10 @@
<button class="green_button button_with_spinner" id="refresh_metadata_button" data-spinner-delay="500" onclick="return refresh_metadata_form();">Refresh metadata</button>
</div>
<div id="generate_thumbnail_area">
<button class="green_button button_with_spinner" id="generate_thumbnail_button" data-spinner-delay="500" onclick="return generate_thumbnail_form();">Generate thumbnail</button>
</div>
<div id="searchhidden_area">
<button class="yellow_button" id="set_searchhidden_button" onclick="return set_searchhidden_form();">Searchhide</button>
<button class="yellow_button" id="unset_searchhidden_button" onclick="return unset_searchhidden_form();">Unhide</button>
@ -369,6 +372,42 @@ function refresh_metadata_form()
////////////////////////////////////////////////////////////////////////////////
const generate_thumbnail_button = document.getElementById("generate_thumbnail_button");
function generate_thumbnail_callback(response)
{
window[generate_thumbnail_button.dataset.spinnerCloser]();
if (! response.meta.json_ok)
{
alert(JSON.stringify(response));
return;
}
if ("error_type" in response.data)
{
const message_area = document.getElementById("message_area");
const message_positivity = "message_negative";
const message_text = response.data.error_message;
common.create_message_bubble(message_area, message_positivity, message_text, 8000);
}
else
{
common.refresh();
}
}
function generate_thumbnail_form()
{
if (photo_clipboard.clipboard.size == 0)
{
return spinners.BAIL;
}
const photo_ids = Array.from(photo_clipboard.clipboard);
api.photos.batch_generate_thumbnail(photo_ids, generate_thumbnail_callback);
}
////////////////////////////////////////////////////////////////////////////////
function set_unset_searchhidden_callback(response)
{
if (! response.meta.json_ok)

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
{% import "header.html" as header %}
<title>Login/Register</title>
@ -8,7 +8,6 @@
<link rel="icon" href="/favicon.png" type="image/png"/>
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/etiquette.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/http.js"></script>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
{% import "header.html" as header %}
{% import "cards.html" as cards %}
@ -10,7 +10,6 @@
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/etiquette.css">
<link rel="stylesheet" href="/static/css/cards.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/hotkeys.js"></script>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
<title>Etiquette</title>
<meta charset="UTF-8">
@ -7,7 +7,6 @@
<link rel="icon" href="/favicon.png" type="image/png"/>
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/etiquette.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/http.js"></script>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
{% import "header.html" as header %}
{% import "cards.html" as cards %}
@ -12,7 +12,6 @@
<link rel="stylesheet" href="/static/css/etiquette.css">
<link rel="stylesheet" href="/static/css/cards.css">
<link rel="stylesheet" href="/static/css/clipboard_tray.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/hotkeys.js"></script>

View file

@ -1,10 +1,9 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/etiquette.css">
<link rel="stylesheet" href="/static/css/cards.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<style>
body

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
{% import "header.html" as header %}
{% import "clipboard_tray.html" as clipboard_tray %}
@ -11,7 +11,6 @@
<link rel="stylesheet" href="/static/css/etiquette.css">
<link rel="stylesheet" href="/static/css/cards.css">
<link rel="stylesheet" href="/static/css/clipboard_tray.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/cards.js"></script>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
{% import "header.html" as header %}
{% import "cards.html" as cards %}
@ -14,7 +14,6 @@
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/etiquette.css">
<link rel="stylesheet" href="/static/css/cards.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/cards.js"></script>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
{% import "header.html" as header %}
<title>Flasksite</title>
@ -8,7 +8,6 @@
<link rel="icon" href="/favicon.png" type="image/png"/>
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/etiquette.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/http.js"></script>

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html class="theme_{{theme}}">
<head>
{% import "header.html" as header %}
{% import "cards.html" as cards %}
@ -10,7 +10,6 @@
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/etiquette.css">
<link rel="stylesheet" href="/static/css/cards.css">
{% if theme %}<link rel="stylesheet" href="/static/css/theme_{{theme}}.css">{% endif %}
<script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/editor.js"></script>