26 Commits
0.7.0 ... 0.7.1

Author SHA1 Message Date
78d0b07c5c Version upgrade (0.7.1) 2014-03-13 20:53:17 +01:00
a2b647432c Better spoiler and tags behaviour 2014-03-13 20:53:17 +01:00
87806bd015 Fixed ATX style header parsing
Markdown Extra that we recently switched to has different implementation from
Markdown (including, but not limited to, regexes), so some of the overwritten
callbacks stopped working.
2014-03-13 19:45:43 +01:00
73fc1830ff Tag relations don't suggest tags already used 2014-03-10 16:16:25 +01:00
fba6a50251 Tweaks to tag relations
* Fixed tag relations background if there are no related tags
* Related tags link to search (LMB adds tag, MMB searches for it in new tab)
2014-03-10 01:37:26 +01:00
394c06a1c5 Added related tag suggesting on tag click 2014-03-10 01:15:48 +01:00
f4d0230166 Refactor to tag autocompletion 2014-03-10 01:15:47 +01:00
e48826dd72 Fixed prev/next post clicking in chrome 2014-03-09 22:09:31 +01:00
4879ba94b0 Fixed problem with keyboard shortcuts on Flash
Previous attempt - f226c3eb0c.
Approach introduced in this commit is theoretically much better, but it still
might not be perfect.
2014-03-05 22:40:50 +01:00
f7837dc190 Fixed word wrapping in registration form 2014-03-05 15:22:36 +01:00
fdb7d57cf0 Fixed user list (again) 2014-03-04 18:15:16 +01:00
1ce0429280 Added order:file_size 2014-03-04 17:33:46 +01:00
d6f02fb724 Added "upvoted" tab 2014-03-03 21:56:10 +01:00
2e3fdf98a0 Fixed 404 page appearance 2014-03-03 21:46:36 +01:00
c633118774 Fixed automatic post featuring 2014-03-03 21:39:24 +01:00
2c73f60824 Fixed searching by min/max score 2014-03-03 21:39:24 +01:00
ada131a7c5 Fixed small bug in date parsing 2014-03-03 21:39:24 +01:00
b13c221a96 Fixed default sort style was set to ascending 2014-03-03 21:39:24 +01:00
806aa0f197 Freshened up syntax help 2014-03-03 21:39:21 +01:00
95bcc89aa6 Switched to MarkdownExtra implementation
It supports tables!
2014-03-03 21:29:12 +01:00
b86362b366 Minor tweaks to search aliases 2014-03-03 21:29:12 +01:00
6470704f43 Added order:fav_date 2014-03-03 21:29:12 +01:00
1081dfb718 Hidden posts can be viewed by moderators 2014-03-03 21:29:12 +01:00
aad6393f9a Fixed changing password 2014-03-02 19:09:05 +01:00
b9a50f9e14 Fixed password reset and account activation 2014-03-02 18:47:46 +01:00
2af8a941ff Fixed user list on Chrome w/ endless pagination 2014-03-02 18:45:53 +01:00
28 changed files with 494 additions and 138 deletions

View File

@ -32,6 +32,7 @@ usersPerPage=8
postsPerPage=20
logsPerPage=250
tagsPerPage=100
tagsRelated=15
thumbWidth=150
thumbHeight=150
thumbStyle=outside
@ -73,11 +74,11 @@ uploadPost=registered
listPosts=anonymous
listPosts.sketchy=registered
listPosts.unsafe=registered
listPosts.hidden=admin
listPosts.hidden=moderator
viewPost=anonymous
viewPost.sketchy=registered
viewPost.unsafe=registered
viewPost.hidden=admin
viewPost.hidden=moderator
retrievePost=anonymous
favoritePost=registered
editPostSafety.own=registered

View File

@ -6,44 +6,67 @@ If you’re not a registered user, you will only see public (Safe) posts. Lo
You can use your keyboard to navigate around the site. There are a few shortcuts:
- focus search field: `[Q]`
- scroll up/down: `[W]` and `[S]`
- go to newer/older post or page: `[A]` and `[D]`
- edit post: `[E]`
- focus first post in post list: `[P]`
Hotkey | Description
--------------- | -----------
`[Q]` | Focus search field
`[W]` and `[S]` | Scroll up / down
`[A]` and `[D]` | Go to newer/older post or page
`[E]` | Edit post
`[P]` | Focus first post in post list
# Search syntax
- contatining tag "Haruhi": [search]Haruhi[/search]
- **not** contatining tag "Kyon": [search]-Kyon[/search]
- uploaded by David: [search]submit:David[/search] (note no spaces)
- favorited by David: [search]fav:David[/search]
- favorited by at least four users: [search]favmin:4[/search]
- commented by David: [search]comment:David[/search]
- having at least three comments: [search]commentmin:3[/search]
- having minimum score of 4: [search]scoremin:4[/search]
- tagged with at least seven tags: [search]tagmin:7[/search]
- exactly from the specified date: [search]date:2001[/search], [search]date:2012-09-29[/search] (yyyy-mm-dd format)
- from the specified date onwards: [search]datemin:2001-01-01[/search]
- up to the specified date: [search]datemax:2004-07[/search]
- having specific ID: [search]id:1,2,3,8[/search]
- having ID no less than specified value: [search]idmin:28[/search]
- by content type: [search]type:img[/search], [search]type:swf[/search], [search]type:yt[/search] (images, flash files and YouTube videos, respectively)
- scored up/down by currently logged in user: [search]special:likes[/search] and [search]special:dislikes[/search]
Command | Description | Aliases |
--------------------------------- | --------------------------------------------------------- | ----------------------------------------------- |
[search]Haruhi[/search] | containing tag "Haruhi" | - |
[search]-Kyon[/search] | **not** containing tag "Kyon" | - |
[search]submit:David[/search] | uploaded by user David | `upload`, `uploads`, `uploaded`, `uploader` |
[search]comment:David[/search] | commented by David | `comments`, `commenter`, `commented` |
[search]fav:David[/search] | favorited by David | `favs`, `favd` |
[search]favmin:4[/search] | favorited by at least four users | `fav_min` |
[search]favmax:4[/search] | favorited by at most four users | `fax_max` |
[search]commentmin:3[/search] | having at least three comments | `comment_min` |
[search]commentmax:3[/search] | having at most three comments | `comment_max` |
[search]scoremin:4[/search] | having minimum score of 4 | `score_min` |
[search]scoremax:4[/search] | having maximum score of 4 | `score_max` |
[search]tagmin:7[/search] | tagged with at least seven tags | `tag_min` |
[search]tagmax:7[/search] | tagged with at most seven tags | `tax_max` |
[search]date:2000[/search] | posted in year 2000 | - |
[search]date:2000-01[/search] | posted in January, 2000 | - |
[search]date:2000-01-01[/search] | posted on January 1st, 2000 | - |
[search]datemin:...[/search] | posted on `...` or later (format like in `date:`) | `date_min` |
[search]datemax:...[/search] | posted on `...` or earlier (format like in `date:`) | `date_max` |
[search]id:1,2,3[/search] | having specific post ID | `ids` |
[search]idmin:5[/search] | posts with ID greater than or equal to @5 | `id_min` |
[search]idmax:5[/search] | posts with ID less than or equal to @5 | `id_max` |
[search]type:img[/search] | only image posts | - |
[search]type:swf[/search] | only Flash posts | - |
[search]type:yt[/search] | only Youtube posts | `type:youtube` |
[search]special:liked[/search] | posts liked by currently logged in user | `special:likes`, `special:like` |
[search]special:disliked[/search] | posts disliked by currently logged in user | `special:dislikes`, `special:dislike` |
[search]special:fav[/search] | posts added to favorites by currently logged in user | `special:favs`, `special:favd` |
[search]special:hidden[/search] | hidden (soft-deleted) posts; moderators only | - |
You can combine tags and negate any of them for interesting results. [search]sea -favmin:8 type:swf submit:Pirate[/search] will show you **flash files** tagged as **sea**, that were **liked by seven people** at most, uploaded by user **Pirate**.
All of the above can be sorted using additional sorting tags:
All of the above can be sorted using additional tag in form of `order:...`:
- as random as it can get: [search]order:random[/search]
- newest to oldest: [search]order:date[/search] (pretty much default browse view)
- oldest to newest: [search]-order:date[/search]
- most commented first: [search]order:comments[/search]
- loved by most: [search]order:favs[/search]
- highest scored: [search]order:score[/search]
- with most tags: [search]order:tags[/search]
Command | Description | Aliases (`order:...`) |
--------------------------------- | -------------------------------------------------------- | ------------------------------------------ |
[search]order:random[/search] | as random as it can get | - |
[search]order:id[/search] | highest to lowest post ID (default browse view) | - |
[search]order:date[/search] | newest to oldest (pretty much same as above) | - |
[search]-order:date[/search] | oldest to newest | - |
[search]order:date,asc[/search] | oldest to newest (ascending order, default = descending) | - |
[search]order:score[/search] | highest scored | - |
[search]order:comments[/search] | most commented first | `comment`, `commentcount`, `comment_count` |
[search]order:favs[/search] | loved by most | `fav`, `favcount`, `fav_count` |
[search]order:tags[/search] | with most tags | `tag`, `tagcount`, `tag_count` |
[search]order:commentdate[/search] | recently commented | `comment_date` |
[search]order:favdate[/search] | recently added to favorites | `fav_date` |
[search]order:filesize[/search] | largest files first | `file_size` |
As shown with [search]-order:date[/search], any of them can be reversed in the same way as negating other tags: by placing a dash before the tag. If there is a "min" tag, there’s also its "max" counterpart, e.g. [search]favmax:7[/search].
As shown with [search]-order:date[/search], any of them can be reversed in the same way as negating other tags: by placing a dash before the tag.
# Registration
@ -62,3 +85,5 @@ Registered users can post comments. Comments support [Markdown syntax](http://da
# Uploads
After registering and activating your account, you gain the power to upload files to the service for everyone else to see.
Remember to follow the [rules](/help/rules)!

View File

@ -3,8 +3,8 @@
max-width: 400px;
}
#content form label {
width: 35%;
#content form.register label {
width: 12em;
}
#content form p {

View File

@ -320,6 +320,28 @@ ul.tagit input {
height: auto !important;
margin: -4px 0 !important;
}
.related-tags {
padding: 0.5em;
background: rgba(255,255,255,0.7);
border-radius: 3px;
margin: 0.4em 0 0.2em 0;
font-size: 95%;
}
.related-tags ul {
list-style-type: none;
margin: 0;
padding: 0;
display: block;
overflow: hidden;
}
.related-tags p {
float: left;
margin: 0 1em 0 0;
}
.related-tags li {
display: inline-block;
margin: 0 1em 0 0;
}
@ -408,6 +430,9 @@ ul.tagit input {
.spoiler:hover {
color: black;
}
.spoiler:not(:hover) a {
color: #eee;
}
img {
border: 0;
@ -427,7 +452,6 @@ blockquote>*:last-child {
}
.ui-state-default,
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default {
.ui-state-default a {
color: hsla(0,70%,45%,0.8) !important;
}

View File

@ -1,7 +1,22 @@
code {
margin: 0 0.5em;
}
.tab-content {
padding-left: 1em;
}
table {
border-spacing: 0;
border: 3px solid #eee;
}
table th {
padding: 0.3em 0.5em;
}
table td {
padding: 0.1em 0.5em;
}
table th {
text-align: left;
background: #eee;
}
table td:first-child {
white-space: pre;
font-family: verdana;
}

View File

@ -88,7 +88,10 @@ embed {
font-weight: bold;
}
#sidebar .left a,
#sidebar .right a {
display: inline-block;
}
i.icon-prev {
background-position: -12px -1px;
margin-left: 8px;

View File

@ -17,17 +17,13 @@ nav.sort-styles li.active {
.users-wrapper {
text-align: center;
}
.users {
column-width: 20em;
-moz-column-width: 20em;
-webkit-column-width: 20em;
}
.user {
text-align: initial;
line-height: 1.5em;
margin-bottom: 1em;
margin-right: 1em;
float: left;
white-space: pre;
}

View File

@ -199,15 +199,10 @@ function split(val)
return val.split(/\s+/);
}
function extractLast(term)
{
return split(term).pop();
}
function retrieveTags(searchTerm, cb)
{
var options = { filter: searchTerm + ' order:popularity,desc' };
$.getJSON('/tags?json', options, function(data)
var options = { search: searchTerm };
$.getJSON('/tags-autocomplete?json', options, function(data)
{
var tags = $.map(data.tags.slice(0, 15), function(tag)
{
@ -232,7 +227,8 @@ $(function()
minLength: 1,
source: function(request, response)
{
var term = extractLast(request.term);
var terms = split(request.term);
var term = terms.pop();
if (term != '')
retrieveTags(term, response);
},
@ -272,17 +268,61 @@ $(function()
});
});
function attachTagIt(element)
function attachTagIt(target)
{
var tagItOptions =
{
caseSensitive: false,
onTagClicked: function(e, ui)
{
var targetTagit = ui.tag.parents('.tagit');
var context = target.tagit('assignedTags');
options = { context: context, tag: ui.tagLabel };
if (targetTagit.siblings('.related-tags:eq(0)').data('for') == options.tag)
{
targetTagit.siblings('.related-tags').slideUp(function()
{
$(this).remove();
});
return;
}
$.getJSON('/tags-related?json', options, function(data)
{
var list = $('<ul>');
$.each(data.tags, function(i, tag)
{
var link = $('<a>');
link.attr('href', '/posts/' + tag.name + '/');
link.text('#' + tag.name);
link.click(function(e)
{
e.preventDefault();
target.tagit('createTag', tag.name);
});
list.append(link.wrap('<li/>').parent());
});
targetTagit.siblings('.related-tags').slideUp(function()
{
$(this).remove();
});
var div = $('<div>');
div.data('for', options.tag);
div.addClass('related-tags');
div.append('<p>Related tags:</p>');
div.append(list);
div.append('<div class="clear"></div>');
div.insertAfter(targetTagit).hide().slideDown();
});
},
autocomplete:
{
source:
function(request, response)
{
var tagit = this;
//var context = tagit.element.tagit('assignedTags');
retrieveTags(request.term.toLowerCase(), function(tags)
{
if (!tagit.options.allowDuplicates)
@ -298,21 +338,65 @@ function attachTagIt(element)
}
};
tagItOptions.placeholderText = element.attr('placeholder');
element.tagit(tagItOptions);
tagItOptions.placeholderText = target.attr('placeholder');
target.tagit(tagItOptions);
}
//prevent keybindings from executing when flash posts are focused
var oldMousetrapBind = Mousetrap.bind;
Mousetrap.bind = function(key, func, args)
{
oldMousetrapBind(key, function()
{
if ($(document.activeElement).parents('.post-type-flash').length > 0)
return false;
func();
}, args);
};
//hotkeys
$(function()
{
Mousetrap.bind('q', function() { $('#top-nav input').focus(); return false; }, 'keyup');
Mousetrap.bind('w', function() { $('body,html').animate({scrollTop: '-=150px'}, 200); });
Mousetrap.bind('s', function() { $('body,html').animate({scrollTop: '+=150px'}, 200); });
Mousetrap.bind('a', function() { var url = $('.paginator:visible .prev:not(.disabled) a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; }, 'keyup');
Mousetrap.bind('d', function() { var url = $('.paginator:visible .next:not(.disabled) a').attr('href'); if (typeof url !== 'undefined') window.location.href = url; }, 'keyup');
Mousetrap.bind('p', function() { $('.post a').eq(0).focus(); return false; }, 'keyup');
Mousetrap.bind('q', function()
{
$('#top-nav input').focus();
return false;
}, 'keyup');
Mousetrap.bind('w', function()
{
$('body,html').animate({scrollTop: '-=150px'}, 200);
});
Mousetrap.bind('s', function()
{
$('body,html').animate({scrollTop: '+=150px'}, 200);
});
Mousetrap.bind('a', function()
{
var url = $('.paginator:visible .prev:not(.disabled) a').attr('href');
if (typeof url !== 'undefined')
window.location.href = url;
}, 'keyup');
Mousetrap.bind('d', function()
{
var url = $('.paginator:visible .next:not(.disabled) a').attr('href');
if (typeof url !== 'undefined')
window.location.href = url;
}, 'keyup');
Mousetrap.bind('p', function()
{
$('.post a').eq(0).focus();
return false;
}, 'keyup');
});

View File

@ -1,7 +1,10 @@
function scrolled()
{
var margin = 150;
if ($(document).height() <= $(window).scrollTop() + $(window).height() + margin)
var target = $('.paginator-content:eq(0)');
var y = $(window).scrollTop() + $(window).height();
var maxY = target.height() + target.position().top;
if (y >= maxY - margin)
{
var pageNext = $(document).data('page-next');
var pageDone = $(document).data('page-done');
@ -17,7 +20,13 @@ function scrolled()
var dom = $(response);
var nextPage = dom.find('.paginator .next:not(.disabled) a').attr('href');
$(document).data('page-next', nextPage);
$('.paginator-content').append($(response).find('.paginator-content').children().css({opacity: 0}).animate({opacity: 1}, 'slow'));
var source = $(response).find('.paginator-content');
target.append(source
.children()
.css({opacity: 0})
.animate({opacity: 1}, 'slow'));
$('body').trigger('dom-update');
scrolled();
});

View File

@ -140,7 +140,31 @@ $(function()
$.ajax(ajaxData);
});
Mousetrap.bind('a', function() { var a = $('#sidebar .left a'); var url = a.attr('href'); if (typeof url !== 'undefined') { a.click(); window.location.href = url; } }, 'keyup');
Mousetrap.bind('d', function() { var a = $('#sidebar .right a'); var url = a.attr('href'); if (typeof url !== 'undefined') { a.click(); window.location.href = url; } }, 'keyup');
Mousetrap.bind('e', function() { $('a.edit-post').trigger('click'); return false; }, 'keyup');
Mousetrap.bind('a', function()
{
var a = $('#sidebar .left a');
var url = a.attr('href');
if (typeof url !== 'undefined')
{
a.click();
window.location.href = url;
}
}, 'keyup');
Mousetrap.bind('d', function()
{
var a = $('#sidebar .right a');
var url = a.attr('href');
if (typeof url !== 'undefined')
{
a.click();
window.location.href = url;
}
}, 'keyup');
Mousetrap.bind('e', function()
{
$('a.edit-post').trigger('click');
return false;
}, 'keyup');
});

View File

@ -14,11 +14,11 @@ class Bootstrap
public function workWrapper($workCallback)
{
$this->config->chibi->baseUrl = 'http://' . rtrim($_SERVER['HTTP_HOST'], '/') . '/';
session_start();
$this->context->handleExceptions = false;
$this->context->viewDecorators []= new CustomAssetViewDecorator();
$this->context->viewDecorators []= new \Chibi\PrettyPrintViewDecorator();
CustomAssetViewDecorator::setTitle($this->config->main->title);
$this->context->handleExceptions = false;
$this->context->json = isset($_GET['json']);
$this->context->layoutName = $this->context->json
? 'layout-json'
@ -26,6 +26,7 @@ class Bootstrap
$this->context->transport = new StdClass;
StatusHelper::init();
session_start();
AuthController::doLogIn();
if (empty($this->context->route))
@ -36,8 +37,6 @@ class Bootstrap
return;
}
$this->context->viewDecorators []= new CustomAssetViewDecorator();
$this->context->viewDecorators []= new \Chibi\PrettyPrintViewDecorator();
try
{
$this->render($workCallback);

View File

@ -104,8 +104,8 @@ class AuthController
if (!empty($context->user) and $context->user->id)
{
$dbUser = UserModel::findById($context->user->id);
$context->user->lastLoginDate = time();
UserModel::save($context->user);
$dbUser->lastLoginDate = time();
UserModel::save($dbUser);
$_SESSION['user'] = serialize($dbUser);
}
else

View File

@ -42,7 +42,7 @@ class IndexController
//check if too old
if (!$featuredPostId or $featuredPostDate + $featuredPostRotationTime < time())
return $this->featureNewPost();
return PropertyModel::featureNewPost();
//check if post was deleted
$featuredPost = PostModel::findById($featuredPostId, false);

View File

@ -151,7 +151,15 @@ class PostController
$this->listAction('favmin:1', $page);
}
/**
* @route /upvoted
* @route /upvoted/{page}
* @validate page \d*
*/
public function upvotedAction($page = 1)
{
$this->listAction('scoremin:1', $page);
}
/**
* @route /random

View File

@ -14,7 +14,7 @@ class TagController
$this->context->viewName = 'tag-list-wrapper';
PrivilegesHelper::confirmWithException(Privilege::ListTags);
$suppliedFilter = $filter ?: InputHelper::get('filter') ?: 'order:alpha,asc';
$suppliedFilter = $filter ?: 'order:alpha,asc';
$page = max(1, intval($page));
$tagsPerPage = intval($this->config->browsing->tagsPerPage);
@ -43,6 +43,53 @@ class TagController
}
}
/**
* @route /tags-autocomplete
*/
public function autoCompleteAction()
{
PrivilegesHelper::confirmWithException(Privilege::ListTags);
$suppliedSearch = InputHelper::get('search');
$filter = $suppliedSearch . ' order:popularity,desc';
$tags = TagSearchService::getEntitiesRows($filter, 15, 1);
$this->context->transport->tags =
array_values(array_map(
function($tag)
{
return [
'name' => $tag['name'],
'count' => $tag['post_count']
];
}, $tags));
}
/**
* @route /tags-related
*/
public function relatedAction()
{
PrivilegesHelper::confirmWithException(Privilege::ListTags);
$suppliedContext = (array) InputHelper::get('context');
$suppliedTag = InputHelper::get('tag');
$limit = intval($this->config->browsing->tagsRelated);
$tags = TagSearchService::getRelatedTagRows($suppliedTag, $suppliedContext, $limit);
$this->context->transport->tags =
array_values(array_map(
function($tag)
{
return [
'name' => $tag['name'],
'count' => $tag['post_count']
];
}, $tags));
}
/**
* @route /tag/merge
*/

View File

@ -552,7 +552,7 @@ class UserController
public function activationAction($token)
{
$this->context->viewName = 'message';
LayoutHelper::setSubTitle('account activation');
CustomAssetViewDecorator::setSubTitle('account activation');
$dbToken = TokenModel::findByToken($token);
TokenModel::checkValidity($dbToken);
@ -585,7 +585,7 @@ class UserController
public function passwordResetAction($token)
{
$this->context->viewName = 'message';
LayoutHelper::setSubTitle('password reset');
CustomAssetViewDecorator::setSubTitle('password reset');
$dbToken = TokenModel::findByToken($token);
TokenModel::checkValidity($dbToken);
@ -619,7 +619,7 @@ class UserController
public function passwordResetProxyAction()
{
$this->context->viewName = 'user-select';
LayoutHelper::setSubTitle('password reset');
CustomAssetViewDecorator::setSubTitle('password reset');
if (InputHelper::get('submit'))
{
@ -639,7 +639,7 @@ class UserController
public function activationProxyAction()
{
$this->context->viewName = 'user-select';
LayoutHelper::setSubTitle('account activation');
CustomAssetViewDecorator::setSubTitle('account activation');
if (InputHelper::get('submit'))
{

View File

@ -1,17 +1,17 @@
<?php
class CustomMarkdown extends \Michelf\Markdown
class CustomMarkdown extends \Michelf\MarkdownExtra
{
protected $simple = false;
public function __construct($simple = false)
{
$this->simple = $simple;
$this->no_markup = true;
$this->span_gamut += ['doSpoilers' => 71];
$this->span_gamut += ['doSearchPermalinks' => 72];
$this->no_markup = $simple;
$this->span_gamut += ['doStrike' => 6];
$this->span_gamut += ['doUsers' => 7];
$this->span_gamut += ['doPosts' => 8];
$this->span_gamut += ['doSpoilers' => 8.5];
$this->span_gamut += ['doSearchPermalinks' => 8.75];
$this->span_gamut += ['doTags' => 9];
$this->span_gamut += ['doAutoLinks2' => 29];
@ -28,11 +28,11 @@ class CustomMarkdown extends \Michelf\Markdown
}
//make atx-style headers require space after hash
protected function doHeaders($text)
protected function _doHeaders_callback_atx($matches)
{
$text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', [&$this, '_doHeaders_callback_setext'], $text);
$text = preg_replace_callback('{^(\#{1,6})[ ]+(.+?)[ ]*\#*\n+}xm', [&$this, '_doHeaders_callback_atx'], $text);
return $text;
if (!preg_match('/^#+\s/', $matches[0]))
return $matches[0];
return parent::_doHeaders_callback_atx($matches);
}
//disable paragraph forming when using simple markdown
@ -80,7 +80,7 @@ class CustomMarkdown extends \Michelf\Markdown
$url = &$matches[4];
else
$url = &$matches[3];
if (!preg_match('/^((https?|ftp):|)\/\//', $url))
if (!preg_match('/^((https?|ftp):|)\//', $url))
$url = 'http://' . $url;
return parent::_doAnchors_inline_callback($matches);
}
@ -122,7 +122,7 @@ class CustomMarkdown extends \Michelf\Markdown
{
if (is_array($text))
$text = $this->hashBlock('<span class="spoiler">') . $this->runSpanGamut($text[1]) . $this->hashBlock('</span>');
return preg_replace_callback('{\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\])|(?R))+)\[\/spoiler\]}is', [__CLASS__, 'doSpoilers'], $text);
return preg_replace_callback('{(?<!#)\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\])|(?R))+)\[\/spoiler\]}is', [__CLASS__, 'doSpoilers'], $text);
}
protected function doPosts($text)
@ -130,7 +130,7 @@ class CustomMarkdown extends \Michelf\Markdown
$link = \Chibi\UrlHelper::route('post', 'view', ['id' => '_post_']);
return preg_replace_callback('/(?:(?<![^\s\(\)\[\]]))@(\d+)/', function($x) use ($link)
{
return $this->hashPart('<a href="' . str_replace('_post_', $x[1], $link) . '">' . $x[0] . '</a>');
return $this->hashPart('<a href="' . str_replace('_post_', $x[1], $link) . '"><code>' . $x[0] . '</code></a>');
}, $text);
}

View File

@ -59,7 +59,7 @@ abstract class AbstractSearchParser
{
$arr = preg_split('/[;,]/', $orderToken);
if (count($arr) == 1)
$arr []= 'asc';
$arr []= 'desc';
if (count($arr) != 2)
throw new SimpleException('Invalid search order token: ' . $orderToken);

View File

@ -65,7 +65,7 @@ class PostSearchParser extends AbstractSearchParser
return Sql\InFunctor::fromArray('post.id', Sql\Binding::fromArray($ids));
}
elseif (in_array($key, ['fav', 'favs']))
elseif (in_array($key, ['fav', 'favs', 'favd']))
{
$user = UserModel::findByNameOrEmail($value);
$innerStmt = (new Sql\SelectStatement)
@ -76,7 +76,7 @@ class PostSearchParser extends AbstractSearchParser
return new Sql\ExistsFunctor($innerStmt);
}
elseif (in_array($key, ['comment', 'commenter']))
elseif (in_array($key, ['comment', 'comments', 'commenter', 'commented']))
{
$user = UserModel::findByNameOrEmail($value);
$innerStmt = (new Sql\SelectStatement)
@ -87,10 +87,10 @@ class PostSearchParser extends AbstractSearchParser
return new Sql\ExistsFunctor($innerStmt);
}
elseif (in_array($key, ['submit', 'upload', 'uploader', 'uploaded']))
elseif (in_array($key, ['submit', 'upload', `uploads`, 'uploader', 'uploaded']))
{
$user = UserModel::findByNameOrEmail($value);
return new Sql\EqualsFunctor('uploader_id', new Sql\Binding($user->id));
return new Sql\EqualsFunctor('post.uploader_id', new Sql\Binding($user->id));
}
elseif (in_array($key, ['idmin', 'id_min']))
@ -100,54 +100,54 @@ class PostSearchParser extends AbstractSearchParser
return new Sql\EqualsOrLesserFunctor('post.id', new Sql\Binding(intval($value)));
elseif (in_array($key, ['scoremin', 'score_min']))
return new Sql\EqualsOrGreaterFunctor('score', new Sql\Binding(intval($value)));
return new Sql\EqualsOrGreaterFunctor('post.score', new Sql\Binding(intval($value)));
elseif (in_array($key, ['scoremax', 'score_max']))
return new Sql\EqualsOrLesserFunctor('score', new Sql\Binding(intval($value)));
return new Sql\EqualsOrLesserFunctor('post.score', new Sql\Binding(intval($value)));
elseif (in_array($key, ['tagmin', 'tag_min']))
return new Sql\EqualsOrGreaterFunctor('tag_count', new Sql\Binding(intval($value)));
return new Sql\EqualsOrGreaterFunctor('post.tag_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['tagmax', 'tag_max']))
return new Sql\EqualsOrLesserFunctor('tag_count', new Sql\Binding(intval($value)));
return new Sql\EqualsOrLesserFunctor('post.tag_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['favmin', 'fav_min']))
return new Sql\EqualsOrGreaterFunctor('fav_count', new Sql\Binding(intval($value)));
return new Sql\EqualsOrGreaterFunctor('post.fav_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['favmax', 'fav_max']))
return new Sql\EqualsOrLesserFunctor('fav_count', new Sql\Binding(intval($value)));
return new Sql\EqualsOrLesserFunctor('post.fav_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['commentmin', 'comment_min']))
return new Sql\EqualsOrGreaterFunctor('comment_count', new Sql\Binding(intval($value)));
return new Sql\EqualsOrGreaterFunctor('post.comment_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['commentmax', 'comment_max']))
return new Sql\EqualsOrLesserFunctor('comment_count', new Sql\Binding(intval($value)));
return new Sql\EqualsOrLesserFunctor('post.comment_count', new Sql\Binding(intval($value)));
elseif (in_array($key, ['date']))
{
list ($dateMin, $dateMax) = self::parseDate($value);
return (new Sql\ConjunctionFunctor)
->add(new Sql\EqualsOrLesserFunctor('upload_date', new Sql\Binding($dateMax)))
->add(new Sql\EqualsOrGreaterFunctor('upload_date', new Sql\Binding($dateMin)));
->add(new Sql\EqualsOrLesserFunctor('post.upload_date', new Sql\Binding($dateMax)))
->add(new Sql\EqualsOrGreaterFunctor('post.upload_date', new Sql\Binding($dateMin)));
}
elseif (in_array($key, ['datemin', 'date_min']))
{
list ($dateMin, $dateMax) = self::parseDate($value);
return new Sql\EqualsOrGreaterFunctor('upload_date', new Sql\Binding($dateMin));
return new Sql\EqualsOrGreaterFunctor('post.upload_date', new Sql\Binding($dateMin));
}
elseif (in_array($key, ['datemax', 'date_max']))
{
list ($dateMin, $dateMax) = self::parseDate($value);
return new Sql\EqualsOrLesserFunctor('upload_date', new Sql\Binding($dateMax));
return new Sql\EqualsOrLesserFunctor('post.upload_date', new Sql\Binding($dateMax));
}
elseif ($key == 'special')
{
$context = \Chibi\Registry::getContext();
$value = strtolower($value);
if (in_array($value, ['fav', 'favs', 'favd', 'favorite', 'favorites']))
if (in_array($value, ['fav', 'favs', 'favd']))
{
return $this->prepareCriterionForComplexToken('fav', $context->user->name);
}
@ -224,22 +224,28 @@ class PostSearchParser extends AbstractSearchParser
$orderColumn = 'post.id';
elseif (in_array($orderByString, ['date']))
$orderColumn = 'upload_date';
elseif (in_array($orderByString, ['comment', 'comments', 'commentcount', 'comment_count']))
$orderColumn = 'comment_count';
elseif (in_array($orderByString, ['fav', 'favs', 'favcount', 'fav_count']))
$orderColumn = 'fav_count';
$orderColumn = 'post.upload_date';
elseif (in_array($orderByString, ['score']))
$orderColumn = 'score';
$orderColumn = 'post.score';
elseif (in_array($orderByString, ['comment', 'comments', 'commentcount', 'comment_count']))
$orderColumn = 'post.comment_count';
elseif (in_array($orderByString, ['fav', 'favs', 'favcount', 'fav_count']))
$orderColumn = 'post.fav_count';
elseif (in_array($orderByString, ['tag', 'tags', 'tagcount', 'tag_count']))
$orderColumn = 'tag_count';
$orderColumn = 'post.tag_count';
elseif (in_array($orderByString, ['commentdate', 'comment_date']))
$orderColumn = 'comment_date';
$orderColumn = 'post.comment_date';
elseif (in_array($orderByString, ['favdate', 'fav_date']))
$orderColumn = 'post.fav_date';
elseif (in_array($orderByString, ['filesize', 'file_size']))
$orderColumn = 'post.file_size';
elseif ($orderByString == 'random')
{
@ -270,11 +276,14 @@ class PostSearchParser extends AbstractSearchParser
protected static function parseDate($value)
{
list ($year, $month, $day) = explode('-', $value . '-0-0');
$yearMin = $yearMax = intval($year);
$monthMin = $monthMax = intval($month);
$year = intval($year);
$month = intval($month);
$day = intval($day);
$yearMin = $yearMax = $year;
$monthMin = $monthMax = $month;
$monthMin = $monthMin ?: 1;
$monthMax = $monthMax ?: 12;
$dayMin = $dayMax = intval($day);
$dayMin = $dayMax = $day;
$dayMin = $dayMin ?: 1;
$dayMax = $dayMax ?: intval(date('t', mktime(0, 0, 0, $monthMax, 1, $year)));
$timeMin = mktime(0, 0, 0, $monthMin, $dayMin, $yearMin);

View File

@ -9,15 +9,73 @@ class TagSearchService extends AbstractSearchService
$stmt->addColumn(new Sql\AliasFunctor(new Sql\CountFunctor('post_tag.post_id'), 'post_count'));
}
public static function getRelatedTagRows($parentTagName, $context, $limit)
{
$parentTagEntity = TagModel::findByName($parentTagName, false);
if (empty($parentTagEntity))
return [];
$parentTagId = $parentTagEntity->id;
//get tags that appear with selected tag along with their occurence frequency
$stmt = (new Sql\SelectStatement)
->setTable('tag')
->addColumn('tag.*')
->addColumn(new Sql\AliasFunctor(new Sql\CountFunctor('post_tag.post_id'), 'post_count'))
->addInnerJoin('post_tag', new Sql\EqualsFunctor('post_tag.tag_id', 'tag.id'))
->setGroupBy('tag.id')
->setOrderBy('post_count', Sql\SelectStatement::ORDER_DESC)
->setCriterion(new Sql\ExistsFunctor((new Sql\SelectStatement)
->setTable('post_tag pt2')
->setCriterion((new Sql\ConjunctionFunctor)
->add(new Sql\EqualsFunctor('pt2.post_id', 'post_tag.post_id'))
->add(new Sql\EqualsFunctor('pt2.tag_id', new Sql\Binding($parentTagId)))
)));
$rows1 = [];
foreach (Database::fetchAll($stmt) as $row)
$rows1[$row['id']] = $row;
//get the same tags, but this time - get global occurence frequency
$stmt = (new Sql\SelectStatement)
->setTable('tag')
->addColumn('tag.*')
->addColumn(new Sql\AliasFunctor(new Sql\CountFunctor('post_tag.post_id'), 'post_count'))
->addInnerJoin('post_tag', new Sql\EqualsFunctor('post_tag.tag_id', 'tag.id'))
->setCriterion(Sql\InFunctor::fromArray('tag.id', Sql\Binding::fromArray(array_keys($rows1))))
->setGroupBy('tag.id');
$rows2 = [];
foreach (Database::fetchAll($stmt) as $row)
$rows2[$row['id']] = $row;
$rows = [];
foreach ($rows1 as $i => $row)
{
//multiply own occurences by two because we are going to subtract them
$row['sort'] = $row['post_count'] * 2;
//subtract global occurencecount
$row['sort'] -= isset($rows2[$i]) ? $rows2[$i]['post_count'] : 0;
if ($row['id'] != $parentTagId)
$rows []= $row;
}
usort($rows, function($a, $b) { return intval($b['sort']) - intval($a['sort']); });
$rows = array_filter($rows, function($row) use ($context) { return !in_array($row['name'], $context); });
$rows = array_slice($rows, 0, $limit);
return $rows;
}
public static function getMostUsedTag()
{
$stmt = new Sql\SelectStatement();
$stmt->setTable('post_tag');
$stmt->addColumn('tag_id');
$stmt->addColumn(new Sql\AliasFunctor(new Sql\CountFunctor('post_tag.post_id'), 'post_count'));
$stmt->setGroupBy('post_tag.tag_id');
$stmt->setOrderBy('post_count', Sql\SelectStatement::ORDER_DESC);
$stmt->setLimit(1, 0);
$stmt = (new Sql\SelectStatement)
->setTable('post_tag')
->addColumn('tag_id')
->addColumn(new Sql\AliasFunctor(new Sql\CountFunctor('post_tag.post_id'), 'post_count'))
->setGroupBy('post_tag.tag_id')
->setOrderBy('post_count', Sql\SelectStatement::ORDER_DESC)
->setLimit(1, 0);
return Database::fetchOne($stmt);
}
}

View File

@ -156,6 +156,7 @@ class UserModel extends AbstractCrudModel
$stmt->setTable('favoritee');
$stmt->setColumn('post_id', new Sql\Binding($post->id));
$stmt->setColumn('user_id', new Sql\Binding($user->id));
$stmt->setColumn('fav_date', time());
Database::exec($stmt);
});
}

View File

@ -0,0 +1,17 @@
ALTER TABLE favoritee ADD COLUMN fav_date INTEGER DEFAULT NULL;
ALTER TABLE post ADD COLUMN fav_date INTEGER DEFAULT NULL;
CREATE TRIGGER favoritee_update_date AFTER UPDATE ON favoritee FOR EACH ROW
BEGIN
UPDATE post SET fav_date = (SELECT MAX(fav_date) FROM favoritee WHERE favoritee.post_id = post.id);
END;
CREATE TRIGGER favoritee_insert_date AFTER INSERT ON favoritee FOR EACH ROW
BEGIN
UPDATE post SET fav_date = (SELECT MAX(fav_date) FROM favoritee WHERE favoritee.post_id = post.id);
END;
CREATE TRIGGER favoritee_delete_date AFTER DELETE ON favoritee FOR EACH ROW
BEGIN
UPDATE post SET fav_date = (SELECT MAX(fav_date) FROM favoritee WHERE favoritee.post_id = post.id);
END;

View File

@ -0,0 +1,17 @@
ALTER TABLE favoritee ADD COLUMN fav_date INTEGER DEFAULT NULL;
ALTER TABLE post ADD COLUMN fav_date INTEGER DEFAULT NULL;
CREATE TRIGGER favoritee_update_date AFTER UPDATE ON favoritee FOR EACH ROW
BEGIN
UPDATE post SET fav_date = (SELECT MAX(fav_date) FROM favoritee WHERE favoritee.post_id = post.id);
END;
CREATE TRIGGER favoritee_insert_date AFTER INSERT ON favoritee FOR EACH ROW
BEGIN
UPDATE post SET fav_date = (SELECT MAX(fav_date) FROM favoritee WHERE favoritee.post_id = post.id);
END;
CREATE TRIGGER favoritee_delete_date AFTER DELETE ON favoritee FOR EACH ROW
BEGIN
UPDATE post SET fav_date = (SELECT MAX(fav_date) FROM favoritee WHERE favoritee.post_id = post.id);
END;

View File

@ -2,24 +2,41 @@
CustomAssetViewDecorator::setSubTitle('posts');
$tabs = [];
$activeTab = 0;
if (PrivilegesHelper::confirm(Privilege::ListPosts))
$tabs []= ['All posts', \Chibi\UrlHelper::route('post', 'list')];
if (PrivilegesHelper::confirm(Privilege::ListPosts))
{
$tabs []= ['Random', \Chibi\UrlHelper::route('post', 'random')];
if ($this->context->route->simpleActionName == 'random')
$activeTab = count($tabs) - 1;
}
if (PrivilegesHelper::confirm(Privilege::ListPosts))
{
$tabs []= ['Favorites', \Chibi\UrlHelper::route('post', 'favorites')];
if ($this->context->route->simpleActionName == 'favorites')
$activeTab = count($tabs) - 1;
}
if (PrivilegesHelper::confirm(Privilege::ListPosts))
{
$tabs []= ['Upvoted', \Chibi\UrlHelper::route('post', 'upvoted')];
if ($this->context->route->simpleActionName == 'upvoted')
$activeTab = count($tabs) - 1;
}
if (PrivilegesHelper::confirm(Privilege::MassTag))
{
$tabs []= ['Mass tag', \Chibi\UrlHelper::route('post', 'list', [
'source' => 'mass-tag',
'query' => isset($this->context->transport->searchQuery) ? htmlspecialchars($this->context->transport->searchQuery) : '',
'page' => isset($this->context->transport->paginator) ? $this->context->transport->paginator->page : 1])];
if ($this->context->source == 'mass-tag')
$activeTab = count($tabs) - 1;
}
$activeTab = 0;
if ($this->context->route->simpleActionName == 'random') $activeTab = 1;
if ($this->context->route->simpleActionName == 'favorites') $activeTab = 2;
if ($this->context->source == 'mass-tag') $activeTab = 3;
?>
<nav class="tabs">

View File

@ -65,6 +65,7 @@ if ($this->context->user->hasEnabledEndlessScrolling())
</div>
<?php endforeach ?>
</div>
<div class="clear"></div>
</div>
<?php $this->renderFile('paginator') ?>

View File

@ -9,7 +9,7 @@ CustomAssetViewDecorator::setSubTitle('registration form');
CustomAssetViewDecorator::addStylesheet('auth.css');
?>
<form action="<?php echo \Chibi\UrlHelper::route('auth', 'register') ?>" class="auth" method="post">
<form action="<?php echo \Chibi\UrlHelper::route('auth', 'register') ?>" class="auth register" method="post">
<p>Registered users can view more content,<br/>upload files and add posts to favorites.</p>
<div class="form-row">

View File

@ -1,5 +1,5 @@
<?php
define('SZURU_VERSION', '0.7.0');
define('SZURU_VERSION', '0.7.1');
define('SZURU_LINK', 'http://github.com/rr-/szurubooru');
//basic settings and preparation
@ -12,6 +12,7 @@ ini_set('memory_limit', '128M');
//basic include calls, autoloader init
require_once $rootDir . 'lib' . DS . 'php-markdown' . DS . 'Michelf' . DS . 'Markdown.php';
require_once $rootDir . 'lib' . DS . 'php-markdown' . DS . 'Michelf' . DS . 'MarkdownExtra.php';
require_once $rootDir . 'lib' . DS . 'chibi-core' . DS . 'Facade.php';
\Chibi\AutoLoader::init([__DIR__, $rootDir . 'lib' . DS . 'chibi-sql']);