client/snapshots: add snapshots browser

This commit is contained in:
rr- 2016-08-17 13:01:17 +02:00
parent 80af79779d
commit 9014baab92
12 changed files with 338 additions and 27 deletions

44
client/css/snapshots.styl Normal file
View file

@ -0,0 +1,44 @@
$snapshot-created-background-color = #E0F5E0
$snapshot-modified-background-color = #E0F5FF
$snapshot-deleted-background-color = #FDE5E5
$snapshot-merged-background-color = #FEC
.snapshot-list
text-align: left
ul
margin: 0 auto
width: 100%
max-width: 35em
list-style-type: none
li
.time
float: right
div
padding: 0.1em 0.5em
.thumbnail
margin-left: 0
&:empty
padding: 0
div.operation-created
background: $snapshot-created-background-color
&+.details
background: lighten($snapshot-created-background-color, 50%)
div.operation-modified
background: $snapshot-modified-background-color
&+.details
background: lighten($snapshot-modified-background-color, 50%)
div.operation-deleted
background: $snapshot-deleted-background-color
&+.details
background: lighten($snapshot-deleted-background-color, 50%)
div.operation-merged
background: $snapshot-merged-background-color
&+.details
background: lighten($snapshot-merged-background-color, 50%)
div.details
margin-bottom: 2em

View file

@ -1,7 +1,7 @@
<div class='post-container'></div>
<% if (ctx.featuredPost) { %>
<aside>
Featured post: <%= ctx.makePostLink(ctx.featuredPost.id) %>,
Featured post: <%= ctx.makePostLink(ctx.featuredPost.id, true) %>,
posted
<%= ctx.makeRelativeTime(ctx.featuredPost.creationTime) %>
by

View file

@ -4,3 +4,7 @@
<span class=sep>&bull;</span>
Build <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%- ctx.version %></a>
from <%= ctx.makeRelativeTime(ctx.buildDate) %>
<% if (ctx.canListSnapshots) { %>
<span class=sep>&bull;</span>
<a href='/history'>History</a>
<% } %>

View file

@ -0,0 +1,31 @@
<div class='snapshot-list'>
<% if (ctx.results.length) { %>
<ul>
<% for (let item of ctx.results) { %>
<li>
<div class='header operation-<%= item.operation %>'>
<span class='time'>
<%= ctx.makeRelativeTime(item.time) %>
</span>
<%= ctx.makeUserLink(item.user) %>
<%= item.operation %>
<%= ctx.makeResourceLink(item.type, item.id) %>
</div>
<div class='details'><!--
--><% if (item.operation === 'created') { %><!--
--><%= ctx.makeItemCreation(item.type, item.data) %><!--
--><% } else if (item.operation === 'modified') { %><!--
--><%= ctx.makeItemModification(item.type, item.data) %><!--
--><% } else if (item.operation === 'merged') { %><!--
-->Merged to <%= ctx.makeResourceLink(item.data[0], item.data[1]) %><!--
--><% } %><!--
--></div>
</li>
<% } %>
</ul>
<% } %>
</div>

View file

@ -1,16 +0,0 @@
'use strict';
const topNavigation = require('../models/top_navigation.js');
class HistoryController {
constructor() {
topNavigation.activate('');
topNavigation.setTitle('History');
}
}
module.exports = router => {
router.enter('/history', (ctx, next) => {
ctx.controller = new HistoryController();
});
};

View file

@ -15,6 +15,7 @@ class HomeController {
name: config.name,
version: config.meta.version,
buildDate: config.meta.buildDate,
canListSnapshots: api.hasPrivilege('snapshots:list'),
canListPosts: api.hasPrivilege('posts:list'),
});

View file

@ -0,0 +1,41 @@
'use strict';
const api = require('../api.js');
const misc = require('../util/misc.js');
const SnapshotList = require('../models/snapshot_list.js');
const PageController = require('../controllers/page_controller.js');
const topNavigation = require('../models/top_navigation.js');
const SnapshotsPageView = require('../views/snapshots_page_view.js');
class SnapshotsController {
constructor(ctx) {
topNavigation.activate('');
topNavigation.setTitle('History');
this._pageController = new PageController({
parameters: ctx.parameters,
getClientUrlForPage: page => {
const parameters = Object.assign(
{}, ctx.parameters, {page: page});
return '/history/' + misc.formatUrlParameters(parameters);
},
requestPage: page => {
return SnapshotList.search('', page, 25);
},
pageRenderer: pageCtx => {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'),
canViewUsers: api.hasPrivilege('users:view'),
canViewTags: api.hasPrivilege('tags:view'),
});
return new SnapshotsPageView(pageCtx);
},
});
}
}
module.exports = router => {
router.enter('/history/:parameters?',
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
(ctx, next) => { ctx.controller = new SnapshotsController(ctx); });
};

View file

@ -33,7 +33,7 @@ controllers.push(require('./controllers/help_controller.js'));
controllers.push(require('./controllers/auth_controller.js'));
controllers.push(require('./controllers/password_reset_controller.js'));
controllers.push(require('./controllers/comments_controller.js'));
controllers.push(require('./controllers/history_controller.js'));
controllers.push(require('./controllers/snapshots_controller.js'));
controllers.push(require('./controllers/post_controller.js'));
controllers.push(require('./controllers/post_list_controller.js'));
controllers.push(require('./controllers/post_upload_controller.js'));

View file

@ -0,0 +1,40 @@
'use strict';
const api = require('../api.js');
const events = require('../events.js');
class Snapshot extends events.EventTarget {
constructor() {
super();
this._orig = {};
this._updateFromResponse({});
}
get operation() { return this._operation; }
get type() { return this._type; }
get id() { return this._id; }
get user() { return this._user; }
get data() { return this._data; }
get time() { return this._time; }
static fromResponse(response) {
const ret = new Snapshot();
ret._updateFromResponse(response);
return ret;
}
_updateFromResponse(response) {
const map = {
_operation: response.operation,
_type: response.type,
_id: response.id,
_user: response.user,
_data: response.data,
_time: response.time,
};
Object.assign(this, map);
}
}
module.exports = Snapshot;

View file

@ -0,0 +1,25 @@
'use strict';
const api = require('../api.js');
const AbstractList = require('./abstract_list.js');
const Snapshot = require('./snapshot.js');
class SnapshotList extends AbstractList {
static search(text, page, pageSize) {
const url =
`/snapshots/?query=${text}` +
`&page=${page}` +
`&pageSize=${pageSize}`;
return api.get(url).then(response => {
return Promise.resolve(Object.assign(
{},
response,
{results: SnapshotList.fromResponse(response.results)}));
});
}
}
SnapshotList._itemClass = Snapshot;
SnapshotList._itemName = 'snapshot';
module.exports = SnapshotList;

View file

@ -169,28 +169,36 @@ function getPostEditUrl(id, parameters) {
return url;
}
function makePostLink(id) {
const text = '@' + id;
function makePostLink(id, includeHash) {
let text = id;
if (includeHash) {
text = '@' + id;
}
return api.hasPrivilege('posts:view') ?
makeNonVoidElement(
'a', {'href': '/post/' + encodeURIComponent(id)}, text) :
text;
}
function makeTagLink(name) {
function makeTagLink(name, includeHash) {
const tag = tags.getTagByName(name);
const category = tag ? tag.category : 'unknown';
let text = name;
if (includeHash === true) {
text = '#' + text;
}
return api.hasPrivilege('tags:view') ?
makeNonVoidElement(
'a', {
'a',
{
'href': '/tag/' + encodeURIComponent(name),
'class': misc.makeCssName(category, 'tag'),
}, name) :
makeNonVoidElement(
'span', {
'class': misc.makeCssName(category, 'tag'),
},
name);
text) :
makeNonVoidElement(
'span',
{'class': misc.makeCssName(category, 'tag')},
text);
}
function makeUserLink(user) {
@ -477,6 +485,8 @@ module.exports = misc.arrayToObject([
decorateValidator,
makeVoidElement,
makeNonVoidElement,
makeTagLink,
makePostLink,
syncScrollPosition,
slideDown,
slideUp,

View file

@ -0,0 +1,131 @@
'use strict';
const views = require('../util/views.js');
const template = views.getTemplate('snapshots-page');
function _extend(target, source) {
target.push.apply(target, source);
}
function _formatBasicChange(diff, text) {
const lines = [];
if (diff.type === 'list change') {
const addedItems = diff.added;
const removedItems = diff.removed;
if (addedItems && addedItems.length) {
lines.push(`Added ${text} (${addedItems.join(', ')})`);
}
if (removedItems && removedItems.length) {
lines.push(`Removed ${text} (${removedItems.join(', ')})`);
}
} else if (diff.type === 'primitive change') {
const oldValue = diff['old-value'];
const newValue = diff['new-value'];
lines.push(`Changed ${text} (${oldValue} &rarr; ${newValue})`);
} else {
lines.push(`Changed ${text}`);
}
return lines;
}
function _makeResourceLink(type, id) {
if (type === 'post') {
return views.makePostLink(id, true);
} else if (type === 'tag') {
return views.makeTagLink(id, true);
} else if (type === 'tag_category') {
return 'category "' + id + '"';
}
}
function _makeItemCreation(type, data) {
const lines = [];
for (let key of Object.keys(data)) {
if (!data[key]) {
continue;
}
let text = key[0].toUpperCase() + key.substr(1).toLowerCase();
if (Array.isArray(data[key])) {
if (data[key].length) {
lines.push(`${text}: ${data[key].join(', ')}`);
}
} else {
lines.push(`${text}: ${data[key]}`);
}
}
return lines.join('<br/>');
}
function _makeItemModification(type, data) {
const lines = [];
const diff = data.value;
if (type === 'tag_category') {
if (diff.name) {
_extend(lines, _formatBasicChange(diff.name, 'name'));
}
if (diff.color) {
_extend(lines, _formatBasicChange(diff.color, 'color'));
}
if (diff.default) {
_extend(lines, ['Made into default category']);
}
} else if (type === 'tag') {
if (diff.names) {
_extend(lines, _formatBasicChange(diff.names, 'names'));
}
if (diff.category) {
_extend(
lines, _formatBasicChange(diff.category, 'category'));
}
if (diff.suggestions) {
_extend(
lines, _formatBasicChange(diff.suggestions, 'suggestions'));
}
if (diff.implications) {
_extend(
lines, _formatBasicChange(diff.implications, 'implications'));
}
} else if (type === 'post') {
if (diff.checksum) {
_extend(lines, ['Changed content']);
}
if (diff.featured) {
_extend(lines, ['Featured on front page']);
}
if (diff.source) {
_extend(lines, _formatBasicChange(diff.source, 'source'));
}
if (diff.safety) {
_extend(lines, _formatBasicChange(diff.safety, 'safety'));
}
if (diff.tags) {
_extend(lines, _formatBasicChange(diff.tags, 'tags'));
}
if (diff.relations) {
_extend(lines, _formatBasicChange(diff.relations, 'relations'));
}
if (diff.notes) {
_extend(lines, ['Changed notes']);
}
if (diff.flags) {
_extend(lines, ['Changed flags']);
}
}
return lines.join('<br/>');
}
class SnapshotsPageView {
constructor(ctx) {
views.replaceContent(ctx.hostNode, template(Object.assign({
makeResourceLink: _makeResourceLink,
makeItemCreation: _makeItemCreation,
makeItemModification: _makeItemModification,
}, ctx)));
}
}
module.exports = SnapshotsPageView;