client/home: view featured post, add search form
This commit is contained in:
parent
6d6cce20dd
commit
53fd6fb29b
10 changed files with 310 additions and 20 deletions
|
@ -3,6 +3,27 @@
|
||||||
|
|
||||||
h1
|
h1
|
||||||
margin-top: 0
|
margin-top: 0
|
||||||
|
margin-bottom: 0.5em
|
||||||
|
|
||||||
.message
|
.message
|
||||||
margin-bottom: 2em
|
margin-bottom: 2em
|
||||||
|
|
||||||
|
.post-container
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
nav
|
||||||
|
margin-bottom: 0.5em
|
||||||
|
a
|
||||||
|
padding: 0.5em
|
||||||
|
|
||||||
|
form
|
||||||
|
width: auto
|
||||||
|
|
||||||
|
aside
|
||||||
|
margin-top: 0.2em
|
||||||
|
margin-bottom: 1em
|
||||||
|
|
||||||
|
footer
|
||||||
|
font-size: 80%
|
||||||
|
|
|
@ -132,7 +132,6 @@ a .access-key
|
||||||
.access-key
|
.access-key
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
.thumbnail
|
.thumbnail
|
||||||
vertical-align: middle
|
|
||||||
width: 1.5em
|
width: 1.5em
|
||||||
height: 1.5em
|
height: 1.5em
|
||||||
margin: calc((2.3em - 1.5em) / 2)
|
margin: calc((2.3em - 1.5em) / 2)
|
||||||
|
@ -161,15 +160,19 @@ a .access-key
|
||||||
|
|
||||||
.thumbnail
|
.thumbnail
|
||||||
/*background-image: attr(data-src url)*/ /* not available yet */
|
/*background-image: attr(data-src url)*/ /* not available yet */
|
||||||
|
vertical-align: middle
|
||||||
background-repeat: no-repeat
|
background-repeat: no-repeat
|
||||||
background-size: cover
|
background-size: cover
|
||||||
background-position: center
|
background-position: center
|
||||||
display: inline-block
|
display: inline-block
|
||||||
|
img
|
||||||
.thumbnail img
|
opacity: 0
|
||||||
opacity: 0
|
width: 100%
|
||||||
width: 100%
|
height: 100%
|
||||||
height: 100%
|
span>.thumbnail
|
||||||
|
width: 20px
|
||||||
|
height: 20px
|
||||||
|
margin: 0 0.4em 0 0.4em
|
||||||
|
|
||||||
.flexbox-dummy
|
.flexbox-dummy
|
||||||
height: 0 !important
|
height: 0 !important
|
||||||
|
|
29
client/css/posts.styl
Normal file
29
client/css/posts.styl
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
@import colors
|
||||||
|
|
||||||
|
.post-container
|
||||||
|
text-align: center
|
||||||
|
.post-content
|
||||||
|
text-align: left
|
||||||
|
margin: 0 auto
|
||||||
|
max-height: 100%
|
||||||
|
max-width: 100%
|
||||||
|
object-fit: contain
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
img, object, video, .post-overlay
|
||||||
|
position: absolute
|
||||||
|
height: 100%
|
||||||
|
width: 100%
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
top: 0
|
||||||
|
bottom: 0
|
||||||
|
|
||||||
|
.post-overlay>*
|
||||||
|
position: absolute
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
top: 0
|
||||||
|
bottom: 0
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
|
@ -2,7 +2,53 @@
|
||||||
<div class='messages'></div>
|
<div class='messages'></div>
|
||||||
<header>
|
<header>
|
||||||
<h1><%= ctx.name %></h1>
|
<h1><%= ctx.name %></h1>
|
||||||
<p>Serving <%= ctx.postCount %> posts (<%= ctx.makeFileSize(ctx.diskUsage) %>)</p>
|
|
||||||
</header>
|
</header>
|
||||||
<footer>Version: <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%= ctx.version %></a> (built <%= ctx.makeRelativeTime(ctx.buildDate) %>)</footer>
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<% if (ctx.canListPosts) { %>
|
||||||
|
<li><a href='/posts'><%= ctx.makeAccessKey('Posts', 'P') %></a></li>
|
||||||
|
<% } %>
|
||||||
|
<% if (ctx.canListComments) { %>
|
||||||
|
<li><a href='/comments'><%= ctx.makeAccessKey('Comments', 'C') %></a></li>
|
||||||
|
<% } %>
|
||||||
|
<% if (ctx.canListTags) { %>
|
||||||
|
<li><a href='/tags'><%= ctx.makeAccessKey('Tags', 'T') %></a></li>
|
||||||
|
<% } %>
|
||||||
|
<% if (ctx.canListUsers) { %>
|
||||||
|
<li><a href='/users'><%= ctx.makeAccessKey('Users', 'U') %></a></li>
|
||||||
|
<% } %>
|
||||||
|
<li><a href='/help'><%= ctx.makeAccessKey('Help', 'E') %></a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<% if (ctx.canListPosts) { %>
|
||||||
|
<form class='horizontal'>
|
||||||
|
<div class='input'>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<%= ctx.makeTextInput({id: 'search-text', name: 'search-text'}) %>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class='buttons'>
|
||||||
|
<input type='submit' value='Search'/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<% } %>
|
||||||
|
<div class='post-container'></div>
|
||||||
|
<% if (ctx.featuredPost) { %>
|
||||||
|
<aside>
|
||||||
|
<%= ctx.makePostLink(ctx.featuredPost.id) %>
|
||||||
|
uploaded
|
||||||
|
<%= ctx.makeRelativeTime(ctx.featuredPost.creationTime) %>
|
||||||
|
by
|
||||||
|
<%= ctx.makeUserLink(ctx.featuredPost.user) %>
|
||||||
|
</aside>
|
||||||
|
<% } %>
|
||||||
|
<footer>
|
||||||
|
Serving <%= ctx.postCount %> posts (<%= ctx.makeFileSize(ctx.diskUsage) %>)
|
||||||
|
•
|
||||||
|
Running <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%= ctx.version %></a>
|
||||||
|
•
|
||||||
|
Built <%= ctx.makeRelativeTime(ctx.buildDate) %>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
30
client/html/post_content.tpl
Normal file
30
client/html/post_content.tpl
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<div class='post-content post-type-<%= ctx.post.type %>'>
|
||||||
|
<% if (['image', 'animation'].includes(ctx.post.type)) { %>
|
||||||
|
|
||||||
|
<img alt='<%= ctx.post.id %>' src='<%= ctx.post.contentUrl %>'/>
|
||||||
|
|
||||||
|
<% } else if (ctx.post.type === 'flash') { %>
|
||||||
|
|
||||||
|
<object width='<%= ctx.post.canvasWidth %>' height='<%= ctx.post.canvasHeight %>' data='<%= ctx.post.contentUrl %>'>
|
||||||
|
<param name='wmode' value='opaque'/>
|
||||||
|
<param name='movie' value='<%= ctx.post.contentUrl %>'/>
|
||||||
|
</object>
|
||||||
|
|
||||||
|
<% } else if (ctx.post.type === 'video') { %>
|
||||||
|
|
||||||
|
<% if (ctx.post.flags.includes('loop')) { %>
|
||||||
|
<video id='video' controls loop='loop'>
|
||||||
|
<% } else { %>
|
||||||
|
<video id='video' controls>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<source type='<%= ctx.post.mimeType %>' src='<%= ctx.post.contentUrl %>'/>
|
||||||
|
|
||||||
|
Your browser doesn't support HTML5 videos.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
<% } else { console.log(new Error('Unknown post type')); } %>
|
||||||
|
|
||||||
|
<div class='post-overlay'>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -24,13 +24,22 @@ class HomeController {
|
||||||
api.get('/info')
|
api.get('/info')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this._homeView.render({
|
this._homeView.render({
|
||||||
|
canListPosts: api.hasPrivilege('posts:list'),
|
||||||
|
canListComments: api.hasPrivilege('comments:list'),
|
||||||
|
canListTags: api.hasPrivilege('tags:list'),
|
||||||
|
canListUsers: api.hasPrivilege('users:list'),
|
||||||
diskUsage: response.diskUsage,
|
diskUsage: response.diskUsage,
|
||||||
postCount: response.postCount,
|
postCount: response.postCount,
|
||||||
featuredPost: response.featuredPost,
|
featuredPost: response.featuredPost,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
response => {
|
response => {
|
||||||
this._homeView.render({});
|
this._homeView.render({
|
||||||
|
canListPosts: api.hasPrivilege('posts:list'),
|
||||||
|
canListComments: api.hasPrivilege('comments:list'),
|
||||||
|
canListTags: api.hasPrivilege('tags:list'),
|
||||||
|
canListUsers: api.hasPrivilege('users:list'),
|
||||||
|
});
|
||||||
events.notify(events.Error, response.description);
|
events.notify(events.Error, response.description);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
79
client/js/controls/post_content_control.js
Normal file
79
client/js/controls/post_content_control.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const views = require('../util/views.js');
|
||||||
|
const optimizedResize = require('../util/optimized_resize.js');
|
||||||
|
|
||||||
|
class PostContentControl {
|
||||||
|
constructor(containerNode, post, viewportSizeCalculator) {
|
||||||
|
post.canvasWidth = post.canvasWidth || 800;
|
||||||
|
post.canvasHeight = post.canvasHeight || 450;
|
||||||
|
|
||||||
|
this._post = post;
|
||||||
|
this._viewportSizeCalculator = viewportSizeCalculator;
|
||||||
|
this._containerNode = containerNode;
|
||||||
|
this._template = views.getTemplate('post-content');
|
||||||
|
|
||||||
|
this._install();
|
||||||
|
|
||||||
|
this._currentFitFunction = this.fitBoth;
|
||||||
|
this._currentFitFunction();
|
||||||
|
}
|
||||||
|
|
||||||
|
fitWidth() {
|
||||||
|
this._currentFitFunction = this.fitWidth;
|
||||||
|
const mul = this._post.canvasHeight / this._post.canvasWidth;
|
||||||
|
this._resize(this._viewportWidth, this._viewportWidth * mul);
|
||||||
|
}
|
||||||
|
|
||||||
|
fitHeight() {
|
||||||
|
this._currentFitFunction = this.fitHeight;
|
||||||
|
const mul = this._post.canvasWidth / this._post.canvasHeight;
|
||||||
|
this._resize(this._viewportHeight * mul, this._viewportHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
fitBoth() {
|
||||||
|
this._currentFitFunction = this.fitBoth;
|
||||||
|
let mul = this._post.canvasHeight / this._post.canvasWidth;
|
||||||
|
if (this._viewportWidth * mul < this._viewportHeight) {
|
||||||
|
this._resize(this._viewportWidth, this._viewportWidth * mul);
|
||||||
|
} else {
|
||||||
|
mul = this._post.canvasWidth / this._post.canvasHeight;
|
||||||
|
this._resize(this._viewportHeight * mul, this._viewportHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get _viewportWidth() {
|
||||||
|
return this._viewportSizeCalculator()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
get _viewportHeight() {
|
||||||
|
return this._viewportSizeCalculator()[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
_resize(width, height) {
|
||||||
|
const postContentNode =
|
||||||
|
this._containerNode.querySelector('.post-content');
|
||||||
|
postContentNode.style.width = width + 'px';
|
||||||
|
postContentNode.style.height = height + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
_refreshSize() {
|
||||||
|
this._currentFitFunction();
|
||||||
|
}
|
||||||
|
|
||||||
|
_install() {
|
||||||
|
const postContentNode = this._template({
|
||||||
|
post: this._post,
|
||||||
|
});
|
||||||
|
this._containerNode.appendChild(postContentNode);
|
||||||
|
optimizedResize.add(() => this._refreshSize());
|
||||||
|
views.monitorNodeRemoval(
|
||||||
|
this._containerNode, () => { this._uninstall(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
_uninstall() {
|
||||||
|
optimizedResize.remove(() => this._refreshSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PostContentControl;
|
33
client/js/util/optimized_resize.js
Normal file
33
client/js/util/optimized_resize.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let callbacks = [];
|
||||||
|
let running = false;
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
if (!running) {
|
||||||
|
running = true;
|
||||||
|
if (window.requestAnimationFrame) {
|
||||||
|
window.requestAnimationFrame(runCallbacks);
|
||||||
|
} else {
|
||||||
|
setTimeout(runCallbacks, 66);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCallbacks() {
|
||||||
|
callbacks.forEach(function(callback) {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(callback) {
|
||||||
|
callbacks.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(callback) {
|
||||||
|
callbacks = callbacks.filter(c => c !== callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', resize);
|
||||||
|
module.exports = {add: add, remove: remove};
|
|
@ -141,19 +141,27 @@ function makeColorInput(options) {
|
||||||
'label', {class: 'color'}, colorInput + textInput);
|
'label', {class: 'color'}, colorInput + textInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makePostLink(id) {
|
||||||
|
return makeNonVoidElement('a', {
|
||||||
|
'href': '/post/' + id,
|
||||||
|
}, '@' + id);
|
||||||
|
}
|
||||||
|
|
||||||
function makeTagLink(name) {
|
function makeTagLink(name) {
|
||||||
let category = null;
|
const tag = tags.getTagByName(name);
|
||||||
try {
|
let category = tag ? tag.category : 'unknown';
|
||||||
category = tags.getTagByName(name).category;
|
|
||||||
} catch (e) {
|
|
||||||
category = 'unknown';
|
|
||||||
}
|
|
||||||
return makeNonVoidElement('a', {
|
return makeNonVoidElement('a', {
|
||||||
'href': '/tag/' + name,
|
'href': '/tag/' + name,
|
||||||
'class': 'tag-' + category,
|
'class': 'tag-' + category,
|
||||||
}, name);
|
}, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeUserLink(user) {
|
||||||
|
return makeNonVoidElement('span', {class: 'user'},
|
||||||
|
makeThumbnail(user.avatarUrl) +
|
||||||
|
makeNonVoidElement('a', {'href': '/user/' + user.name}, user.name));
|
||||||
|
}
|
||||||
|
|
||||||
function makeFlexboxAlign(options) {
|
function makeFlexboxAlign(options) {
|
||||||
return Array.from(misc.range(20))
|
return Array.from(misc.range(20))
|
||||||
.map(() => '<li class="flexbox-dummy"></li>').join('');
|
.map(() => '<li class="flexbox-dummy"></li>').join('');
|
||||||
|
@ -265,7 +273,9 @@ function getTemplate(templatePath) {
|
||||||
makePasswordInput: makePasswordInput,
|
makePasswordInput: makePasswordInput,
|
||||||
makeEmailInput: makeEmailInput,
|
makeEmailInput: makeEmailInput,
|
||||||
makeColorInput: makeColorInput,
|
makeColorInput: makeColorInput,
|
||||||
|
makePostLink: makePostLink,
|
||||||
makeTagLink: makeTagLink,
|
makeTagLink: makeTagLink,
|
||||||
|
makeUserLink: makeUserLink,
|
||||||
makeFlexboxAlign: makeFlexboxAlign,
|
makeFlexboxAlign: makeFlexboxAlign,
|
||||||
makeAccessKey: makeAccessKey,
|
makeAccessKey: makeAccessKey,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,25 +1,55 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const page = require('page');
|
||||||
const config = require('../config.js');
|
const config = require('../config.js');
|
||||||
|
const misc = require('../util/misc.js');
|
||||||
const views = require('../util/views.js');
|
const views = require('../util/views.js');
|
||||||
|
const PostContentControl = require('../controls/post_content_control.js');
|
||||||
|
const TagAutoCompleteControl =
|
||||||
|
require('../controls/tag_auto_complete_control.js');
|
||||||
|
|
||||||
class HomeView {
|
class HomeView {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._template = views.getTemplate('home');
|
this._homeTemplate = views.getTemplate('home');
|
||||||
}
|
}
|
||||||
|
|
||||||
render(ctx) {
|
render(ctx) {
|
||||||
const target = document.getElementById('content-holder');
|
Object.assign(ctx, {
|
||||||
const source = this._template({
|
|
||||||
name: config.name,
|
name: config.name,
|
||||||
version: config.meta.version,
|
version: config.meta.version,
|
||||||
buildDate: config.meta.buildDate,
|
buildDate: config.meta.buildDate,
|
||||||
diskUsage: ctx.diskUsage,
|
|
||||||
postCount: ctx.postCount,
|
|
||||||
});
|
});
|
||||||
|
const target = document.getElementById('content-holder');
|
||||||
|
const source = this._homeTemplate(ctx);
|
||||||
|
|
||||||
views.listenToMessages(source);
|
views.listenToMessages(source);
|
||||||
views.showView(target, source);
|
views.showView(target, source);
|
||||||
|
|
||||||
|
const form = source.querySelector('form');
|
||||||
|
if (form) {
|
||||||
|
const searchTextInput = form.querySelector('input');
|
||||||
|
new TagAutoCompleteControl(searchTextInput);
|
||||||
|
form.addEventListener('submit', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const text = searchTextInput.value;
|
||||||
|
searchTextInput.blur();
|
||||||
|
page('/posts/' + misc.formatSearchQuery({text: text}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const postContainerNode = source.querySelector('.post-container');
|
||||||
|
|
||||||
|
if (postContainerNode && ctx.featuredPost) {
|
||||||
|
new PostContentControl(
|
||||||
|
postContainerNode,
|
||||||
|
ctx.featuredPost,
|
||||||
|
() => {
|
||||||
|
return [
|
||||||
|
window.innerWidth * 0.8,
|
||||||
|
window.innerHeight * 0.7,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue