Add album cards and improve album pages.

- album card has placeholder for future thumbnail.
- replaced nested tree hierarchy lists with separate boxes.
- list/grid view also applies to the root listing.
- added a sticky right panel for all the tools. not pretty yet.
- mechanism for adding sticky panel changed. instead of applying
  it to the #right, you apply it to #content_body so that its
  grid layout can be updated properly.
This commit is contained in:
voussoir 2018-11-12 22:15:59 -08:00
parent d0208154e4
commit 707fdcc637
8 changed files with 371 additions and 191 deletions

View file

@ -523,6 +523,11 @@ class Album(ObjectBase, GroupableMixin):
return total return total
def sum_photos(self, recurse=True): def sum_photos(self, recurse=True):
'''
If all you need is the number of photos in the album, this method is
preferable to len(album.get_photos()) because it performs the counting
in the database instead of creating the Photo objects.
'''
query = ''' query = '''
SELECT COUNT(photoid) SELECT COUNT(photoid)
FROM album_photo_rel FROM album_photo_rel

View file

@ -166,7 +166,13 @@ def get_albums_core():
def get_albums_html(): def get_albums_html():
albums = get_albums_core() albums = get_albums_core()
session = session_manager.get(request) session = session_manager.get(request)
return flask.render_template('album.html', albums=albums, session=session) response = flask.render_template(
'album.html',
albums=albums,
session=session,
view=request.args.get('view', 'grid'),
)
return response
@site.route('/albums.json') @site.route('/albums.json')
@session_manager.give_token @session_manager.give_token

View file

@ -13,6 +13,8 @@
--color_site_dropshadow: rgba(0, 0, 0, 0.25); --color_site_dropshadow: rgba(0, 0, 0, 0.25);
--color_3d_shadow: rgba(0, 0, 0, 0.5); --color_3d_shadow: rgba(0, 0, 0, 0.5);
--color_3d_highlight: rgba(255, 255, 255, 0.5); --color_3d_highlight: rgba(255, 255, 255, 0.5);
--size_sticky_side: 300px;
} }
html html
@ -34,8 +36,7 @@ input, select, textarea
margin-top: 2px; margin-top: 2px;
margin-bottom: 2px; margin-bottom: 2px;
padding: 1px; padding: 2px;
padding-left: 2px;
border: none; border: none;
border-radius: 2px; border-radius: 2px;
@ -103,18 +104,50 @@ pre
white-space: pre-line; white-space: pre-line;
} }
#left
{
grid-area: left;
}
#right
{
grid-area: right;
}
.sticky_side_left
{
grid-template:
"left right"
/var(--size_sticky_side) 1fr;
}
.sticky_side_right .sticky_side_right
{ {
position: fixed; grid-template:
right: 8px; "left right"
bottom: 8px; /1fr var(--size_sticky_side);
top: 34px; }
width: 300px;
overflow-y: auto; .sticky_side_left #left,
.sticky_side_right #right
{
grid-area: right; grid-area: right;
display: grid; display: grid;
grid-auto-rows: min-content;
position: fixed;
bottom: 8px;
top: 34px;
width: var(--size_sticky_side);
overflow-y: auto;
background-color: var(--color_site_transparency); background-color: var(--color_site_transparency);
} }
.sticky_side_left #left
{
left: 8px;
}
.sticky_side_right #right
{
right: 8px;
}
.editor_input .editor_input
{ {

View file

@ -1,3 +1,65 @@
.album_card
{
background-color: var(--color_site_secondary);
}
.album_card:hover
{
box-shadow: 2px 2px 5px 0px var(--color_site_dropshadow);
}
.album_card_list
{
display: grid;
grid-template:
"title metadata"
/1fr;
min-width: 600px;
margin: 8px;
padding: 4px;
}
.album_card_grid
{
position: relative;
display: inline-grid;
vertical-align: top;
grid-template:
"thumbnail title" auto
"thumbnail metadata" auto
/auto 1fr;
width: 400px;
margin: 8px;
padding: 8px;
}
.album_card_thumbnail
{
grid-area: thumbnail;
background-color: var(--color_site_transparency);
width: 64px;
height: 64px;
margin-right: 8px;
}
.album_card_thumbnail img
{
max-width: 100%;
max-height: 100%;
margin: auto;
}
.album_card_title
{
grid-area: title;
}
.album_card_metadata
{
grid-area: metadata;
}
.album_card_tools
{
position: absolute;
right: 4px;
bottom: 4px;
}
.photo_card .photo_card
{ {
background-color: var(--color_site_secondary); background-color: var(--color_site_secondary);
@ -5,16 +67,15 @@
.photo_card_list .photo_card_list
{ {
display: grid; display: grid;
grid-template-columns: auto 1fr auto; grid-template:
grid-template-rows: auto; "checkbox filename metadata" auto
grid-template-areas: /auto 1fr auto;
"checkbox filename metadata";
max-width: 800px; max-width: 800px;
margin: 8px; margin: 8px;
padding: 4px; padding: 4px;
} }
.photo_card_list:hover .photo_card:hover
{ {
box-shadow: 2px 2px 5px 0px var(--color_site_dropshadow); box-shadow: 2px 2px 5px 0px var(--color_site_dropshadow);
} }
@ -31,7 +92,7 @@
"thumbnail thumbnail" auto "thumbnail thumbnail" auto
"filename filename" 1fr "filename filename" 1fr
"tags metadata" auto "tags metadata" auto
/auto auto; /10px auto;
min-width: 150px; min-width: 150px;
max-width: 300px; max-width: 300px;
height: 210px; height: 210px;
@ -39,7 +100,6 @@
margin: 8px; margin: 8px;
border-radius: 8px; border-radius: 8px;
box-shadow: 2px 2px 5px 0px var(--color_site_dropshadow);
} }
.photo_card_grid .photo_card_selector_checkbox .photo_card_grid .photo_card_selector_checkbox
{ {
@ -81,12 +141,11 @@
max-height: 30px; max-height: 30px;
background-color: inherit; background-color: inherit;
word-break: break-word; word-break: break-word;
font-size: 12.8px;
} }
.photo_card_grid .photo_card_filename .photo_card_grid .photo_card_filename
{ {
align-self: start; align-self: start;
font-size: 12.8px;
} }
.photo_card_list .photo_card_filename .photo_card_list .photo_card_filename
{ {
@ -106,6 +165,8 @@
font-family: monospace; font-family: monospace;
font-size: 11px; font-size: 11px;
cursor: help;
} }
.photo_card_metadata .photo_card_metadata
{ {

View file

@ -1,42 +1,104 @@
<!DOCTYPE html5> <!DOCTYPE html5>
<html> <html>
{% macro shared_css() %}
<style>
h2, h3
{
margin-top: 0;
}
#description_text
{
font-family: initial;
padding: 8px;
}
.remove_child_button
{
display: none;
}
.remove_child_button:hover,
.album_card:hover .remove_child_button
{
display: initial;
}
#left
{
margin-left: auto;
margin-right: auto;
width: 95%;
}
#left > *
{
background-color: var(--color_site_transparency);
margin-top: 30px;
margin-bottom: 30px;
padding: 8px;
/*border: 1px solid black;*/
border-radius: 5px;
}
@media screen and (max-width: 800px)
{
#content_body
{
grid-template:
"left" 1fr
"right" 150px
/1fr;
}
#right
{
top: unset !important;
width: unset !important;
left: 8px;
right: 8px;
bottom: 8px;
height: 150px;
}
}
</style>
{% endmacro %}
{% if album is not defined %} {## Album listing ###################################################} {% if album is not defined %} {## Album listing ###################################################}
<head> <head>
{% import "header.html" as header %} {% import "header.html" as header %}
{% import "album_card.html" as album_card %}
<title>Albums</title> <title>Albums</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="/static/css/common.css"> <link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/photo_card.css">
<script src="/static/js/common.js"></script> <script src="/static/js/common.js"></script>
<script src="/static/js/api.js"></script> <script src="/static/js/api.js"></script>
<style> {{shared_css()}}
#album_list
{
flex-direction: column;
}
</style>
</head> </head>
<body> <body>
{{header.make_header(session=session)}} {{header.make_header(session=session)}}
<div id="content_body"> <div id="content_body" class="sticky_side_right">
<div id="album_list"> <div id="left">
<h2>Albums</h2> <div id="album_list">
<ul> <h2>Albums</h2>
{% for album in albums %} {% for album in albums %}
<li><a href="/album/{{album.id}}">{{album.display_name}}</a></li> {{album_card.create_album_card(album, view=view)}}
{% endfor %} {% endfor %}
<li> </div>
<button id="create_child_prompt_button" class="green_button" onclick="open_create_child(event);">Create album</button> </div>
<input type="text" id="create_child_title_entry" class="hidden" placeholder="Album title"> <div id="right">
<button id="create_child_submit_button" class="green_button hidden" onclick="create_child_form(event);">Create</button> {% if view != "list" %}
<button id="create_child_cancel_button" class="gray_button hidden" onclick="cancel_create_child(event);">Cancel</button> <a href="?view=list">List view</a>
</li> {% else %}
</ul> <a href="?view=grid">Grid view</a>
{% endif %}
<div>
<button id="create_child_prompt_button" class="green_button" onclick="open_create_child(event);">Create album</button>
<input type="text" id="create_child_title_entry" class="hidden" placeholder="Album title">
<button id="create_child_submit_button" class="green_button hidden" onclick="create_child_form(event);">Create</button>
<button id="create_child_cancel_button" class="gray_button hidden" onclick="cancel_create_child(event);">Cancel</button>
</div>
</div> </div>
</div> </div>
</body> </body>
@ -49,8 +111,9 @@ ALBUM_ID = undefined;
{% else %} {## Individual album ###################################################################} {% else %} {## Individual album ###################################################################}
<head> <head>
{% import "photo_card.html" as photo_card %}
{% import "header.html" as header %} {% import "header.html" as header %}
{% import "album_card.html" as album_card %}
{% import "photo_card.html" as photo_card %}
{% import "clipboard_tray.html" as clipboard_tray %} {% import "clipboard_tray.html" as clipboard_tray %}
<title>{{album.display_name}} | Albums</title> <title>{{album.display_name}} | Albums</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -64,68 +127,91 @@ ALBUM_ID = undefined;
<script src="/static/js/hotkeys.js"></script> <script src="/static/js/hotkeys.js"></script>
<script src="/static/js/photo_clipboard.js"></script> <script src="/static/js/photo_clipboard.js"></script>
<style> {{shared_css()}}
p
{
word-break: break-word;
}
#album_metadata
{
max-width: 800px;
}
#description_text
{
font-family: initial;
padding: 8px;
background-color: var(--color_site_transparency);
}
ul
{
line-height: 1.5;
}
.remove_child_button
{
display: none;
}
.remove_child_button:hover,
li:hover .remove_child_button
{
display: initial;
}
#photo_list
{
padding-left: 40px;
padding-top: 10px;
padding-bottom: 10px;
}
</style>
</head> </head>
<body> <body>
{{header.make_header(session=session)}} {{header.make_header(session=session)}}
<div id="content_body"> <div id="content_body" class="sticky_side_right">
<div id="album_metadata"> <div id="left">
<h2><span <div id="hierarchy_self">
id="title_text" <div id="album_metadata">
data-editor-id="title" <h2><span
data-editor-empty-text="{{album.id}}" id="title_text"
data-editor-placeholder="title" data-editor-id="title"
> data-editor-empty-text="{{album.id}}"
{{-album.display_name-}} data-editor-placeholder="title"
</span></h2> >
{{-album.display_name-}}
</span></h2>
<pre <pre
id="description_text" id="description_text"
data-editor-id="description" data-editor-id="description"
data-editor-placeholder="description" data-editor-placeholder="description"
{% if not album.description %}class="hidden"{% endif %} {% if not album.description %}class="hidden"{% endif %}
> >
{{-album.description-}} {{-album.description-}}
</pre> </pre>
</div>
</div>
<div id="hierarchy_parents">
<h3>Parents</h3>
{% set parents = album.get_parents() %}
{% if parents %}
{% for parent in parents %}
{{album_card.create_album_card(parent, view=view)}}
{% endfor %}
{% else %}
{{album_card.create_root_album_card(view=view)}}
{% endif %}
</div>
{% set sub_albums = album.get_children() %}
{% if sub_albums %}
<div id="hierarchy_children">
<h3>Children</h3>
{% for sub_album in sub_albums|sort(attribute='title') %}
{{album_card.create_album_card(sub_album, view=view, unlink_parent=album)}}
{% endfor %}
</div>
{% endif %}
{% set photos = album.get_photos() %}
{% if photos %}
<div id="hierarchy_photos">
<h3>{{photos|length}} Photos</h3>
<div id="photo_list">
{% for photo in photos %}
{{photo_card.create_photo_card(photo, view=view)}}
{% endfor %}
</div>
</div>
{% endif %}
{% set has_local_photos = photos|length > 0 %}
{% set has_child_photos = album.has_any_subalbum_photo() %}
{% if has_local_photos or has_child_photos %}
<div id="download_links">
<h3>Download</h3>
{% if has_local_photos %}
<p><a id="download_link_single" href="/album/{{album.id}}.zip?recursive=no">These files &ndash; {{album.sum_bytes(recurse=False)|bytestring}}</a></p>
{% endif %}
{% if has_child_photos %}
<p><a id="download_link_recursive" href="/album/{{album.id}}.zip?recursive=yes">Include children &ndash; {{album.sum_bytes(recurse=True)|bytestring}}</a></p>
{% endif %}
</div>
{% endif %}
</div>
<div id="right">
{% if view != "list" %}
<a href="?view=list">List view</a>
{% else %}
<a href="?view=grid">Grid view</a>
{% endif %}
<button <button
class="red_button button_with_confirm" class="red_button button_with_confirm"
@ -136,84 +222,20 @@ li:hover .remove_child_button
> >
Delete Delete
</button> </button>
</div>
<ul id="hierarchy_parents"> <div>
{% set viewparam = "?view=list" if view == "list" else "" %} <button id="create_child_prompt_button" class="green_button" onclick="open_create_child(event);">Create child</button>
{% set parents = album.get_parents() %} <input type="text" id="create_child_title_entry" class="hidden" placeholder="Album title">
{% if parents %} <button id="create_child_submit_button" class="green_button hidden" onclick="create_child_form(event);">Create</button>
{% for parent in parents %} <button id="create_child_cancel_button" class="gray_button hidden" onclick="cancel_create_child(event);">Cancel</button>
<li><a href="/album/{{parent.id}}{{viewparam}}">{{parent.display_name}}</a></li>
{% endfor %}
{% else %}
<li><a href="/albums">Albums</a></li>
{% endif %}
<ul id="hierarchy_self">
<li>{{album.display_name}}</li>
<ul id="heirarchy_children">
{% set sub_albums = album.get_children() %}
{% for sub_album in sub_albums|sort(attribute='title') %}
<li>
<a href="/album/{{sub_album.id}}{{viewparam}}">{{sub_album.display_name}}</a>
<button
class="remove_child_button button_with_confirm red_button"
data-onclick="api.albums.remove_child(ALBUM_ID, '{{sub_album.id}}', common.refresh)"
data-prompt="Remove child?"
data-holder-class="remove_child_button"
data-confirm-class="red_button"
data-cancel-class="gray_button"
>
Unlink
</button>
</li>
{% endfor %}
<li>
<button id="create_child_prompt_button" class="green_button" onclick="open_create_child(event);">Create child</button>
<input type="text" id="create_child_title_entry" class="hidden" placeholder="Album title">
<button id="create_child_submit_button" class="green_button hidden" onclick="create_child_form(event);">Create</button>
<button id="create_child_cancel_button" class="gray_button hidden" onclick="cancel_create_child(event);">Cancel</button>
</li>
<li>
<button id="add_child_prompt_button" class="green_button" onclick="open_add_child(event);">Add child</button>
<input type="text" id="add_child_id_entry" class="hidden" placeholder="Album ID">
<button id="add_child_submit_button" class="green_button hidden" onclick="add_child_form(event);">Add</button>
<button id="add_child_cancel_button" class="gray_button hidden" onclick="cancel_add_child(event);">Cancel</button>
</li>
</ul>
</ul>
</ul>
{% set photos = album.get_photos() %}
{% if photos %}
<h3>{{photos|length}} Photos</h3>
{% if view != "list" %}
<a href="?view=list">List view</a>
{% else %}
<a href="?view=grid">Grid view</a>
{% endif %}
<div id="photo_list">
{% for photo in photos %}
{{photo_card.create_photo_card(photo, view=view)}}
{% endfor %}
</div> </div>
{% endif %} <div>
<button id="add_child_prompt_button" class="green_button" onclick="open_add_child(event);">Add child</button>
{% set has_local_photos = photos|length > 0 %} <input type="text" id="add_child_id_entry" class="hidden" placeholder="Album ID">
{% set has_child_photos = album.has_any_subalbum_photo() %} <button id="add_child_submit_button" class="green_button hidden" onclick="add_child_form(event);">Add</button>
{% if has_local_photos or has_child_photos %} <button id="add_child_cancel_button" class="gray_button hidden" onclick="cancel_add_child(event);">Cancel</button>
<p id="download_links"> </div>
<span>Download:</span> </div>
{% if has_local_photos %}
<a id="download_link_single" href="/album/{{album.id}}.zip?recursive=no">These files ({{album.sum_bytes(recurse=False)|bytestring }})</a>
{% if has_child_photos %}<span>&mdash;</span>{% endif %}
{% endif %}
{% if has_child_photos %}
<a id="download_link_recursive" href="/album/{{album.id}}.zip?recursive=yes">Include children ({{album.sum_bytes(recurse=True)|bytestring }})</a>
{% endif %}
</p>
{% endif %}
{{clipboard_tray.clipboard_tray()}} {{clipboard_tray.clipboard_tray()}}
<div class="my_clipboard_tray_toolbox"> <div class="my_clipboard_tray_toolbox">

View file

@ -0,0 +1,65 @@
{% macro create_root_album_card(view="grid") %}
{% set viewparam = "?view=list" if view == "list" else "" %}
{% if view == "list" %}
<div class="album_card album_card_list">
<div class="album_card_title">
<a href="/albums{{viewparam}}">Albums</a>
</div>
</div>
{% else %}
<div class="album_card album_card_grid">
<a class="album_card_thumbnail" href="/albums">
</a>
<div class="album_card_title">
<a href="/albums">Albums</a>
</div>
</div>
{% endif %}
{% endmacro %}
{% macro create_album_card(album, view="grid", unlink_parent=none) %}
{% set viewparam = "?view=list" if view == "list" else "" %}
{% if view == "list" %}
<div class="album_card album_card_list">
<div class="album_card_title">
<a href="/album/{{album.id}}{{viewparam}}">{{album.display_name}}</a>
</div>
<span class="album_card_metadata">
<span class="album_card_child_count">{{album.get_children()|length}}</span> children
{{-' '-}}|{{-' '-}}
<span class="album_card_photo_count">{{album.sum_photos(recurse=False)}}</span> photos
</span>
</div>
{% else %}
<div class="album_card album_card_grid" data-id="{{album.id}}">
<a class="album_card_thumbnail" href="/album/{{album.id}}{{viewparam}}">
</a>
<div class="album_card_title">
<a href="/album/{{album.id}}{{viewparam}}">{{album.display_name}}</a>
</div>
<span class="album_card_metadata">
<span class="album_card_child_count">{{album.get_children()|length}}</span> children
{{-' '-}}|{{-' '-}}
<span class="album_card_photo_count">{{album.sum_photos(recurse=False)}}</span> photos
</span>
<div class="album_card_tools">
{% if unlink_parent is not none %}
<button
class="remove_child_button button_with_confirm red_button"
data-onclick="api.albums.remove_child('{{unlink_parent.id}}', '{{album.id}}', common.refresh)"
data-prompt="Remove child?"
data-holder-class="remove_child_button"
data-confirm-class="red_button"
data-cancel-class="gray_button"
>Unlink
</button>
{% endif %}
</div>
</div>
{% endif %}
{% endmacro %}

View file

@ -16,13 +16,6 @@
<script src="/static/js/tag_autocomplete.js"></script> <script src="/static/js/tag_autocomplete.js"></script>
<style> <style>
#content_body
{
display: grid;
grid-template:
"left right" 1fr
/1fr 300px;
}
#header #header
{ {
grid-area: header; grid-area: header;
@ -83,8 +76,8 @@
} }
#right #right
{ {
top: unset; top: unset !important;
width: unset; width: unset !important;
left: 8px; left: 8px;
right: 8px; right: 8px;
bottom: 8px; bottom: 8px;
@ -106,14 +99,14 @@
<body> <body>
{{header.make_header(session=session)}} {{header.make_header(session=session)}}
<div id="content_body"> <div id="content_body" class="sticky_side_right">
<div id="left"> <div id="left">
<span>The clipboard contains <span class="clipboard_count">0</span> items.</span> <span>The clipboard contains <span class="clipboard_count">0</span> items.</span>
<button id="clear_clipboard_button" class="red_button" onclick="photo_clipboard.clear_clipboard()">Clear it.</button> <button id="clear_clipboard_button" class="red_button" onclick="photo_clipboard.clear_clipboard()">Clear it.</button>
<div id="photo_card_holder"> <div id="photo_card_holder">
</div> </div>
</div> </div>
<div id="right" class="sticky_side_right"> <div id="right">
<div id="add_tag_area"> <div id="add_tag_area">
<input type="text" id="add_tag_textbox" list="tag_autocomplete_datalist"> <input type="text" id="add_tag_textbox" list="tag_autocomplete_datalist">
<button class="add_tag_button green_button" id="add_tag_button" onclick="add_tag_form();">Add tag</button> <button class="add_tag_button green_button" id="add_tag_button" onclick="add_tag_form();">Add tag</button>

View file

@ -20,12 +20,6 @@
{ {
grid-area: header; grid-area: header;
} }
#content_body
{
grid-template:
"left right"
/1fr 300px;
}
#left #left
{ {
word-break: break-word; word-break: break-word;
@ -64,12 +58,13 @@
{ {
grid-template: grid-template:
"left" 1fr "left" 1fr
"right" 150px; "right" 150px
/1fr;
} }
#right #right
{ {
top: unset; top: unset !important;
width: unset; width: unset !important;
left: 8px; left: 8px;
right: 8px; right: 8px;
bottom: 8px; bottom: 8px;
@ -86,7 +81,7 @@
<body> <body>
{{header.make_header(session=session)}} {{header.make_header(session=session)}}
<div id="content_body"> <div id="content_body" class="sticky_side_right">
<div id="left"> <div id="left">
{% if specific_tag %} {% if specific_tag %}
<div id="tag_metadata"> <div id="tag_metadata">
@ -178,7 +173,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<div id="right" class="sticky_side_right"> <div id="right">
<div id="editor_area"> <div id="editor_area">
<input type="text" id="add_tag_textbox" autofocus> <input type="text" id="add_tag_textbox" autofocus>
<button class="add_tag_button green_button" id="add_tag_button" onclick="easybake_form();">bake</button> <button class="add_tag_button green_button" id="add_tag_button" onclick="easybake_form();">bake</button>