summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2019-09-27 11:06:56 +0200
committerYorhel <git@yorhel.nl>2019-09-27 11:07:05 +0200
commitda2ee73149aade8bc88bc4f87a424dc60348c7fc (patch)
tree457d8512112c58c45ccb72822e7bc617228db111
parent092158fee02c3516ae08f7efcef8f1774bf823e5 (diff)
v2rw: Convert history listings
And I changed the filter selection into a more form-like thing. It's slightly more powerful, but not sure it's such a huge improvement in terms of UI. Everything should be identical apart from that.
-rw-r--r--data/style.css7
-rw-r--r--lib/VNDB/DB/Docs.pm43
-rw-r--r--lib/VNDB/Handler/Misc.pm100
-rw-r--r--lib/VNDB/Util/CommonHTML.pm1
-rw-r--r--lib/VNWeb/DB.pm8
-rw-r--r--lib/VNWeb/HTML.pm56
-rw-r--r--lib/VNWeb/Misc/History.pm213
-rw-r--r--lib/VNWeb/Prelude.pm4
-rw-r--r--lib/VNWeb/Validation.pm1
9 files changed, 282 insertions, 151 deletions
diff --git a/data/style.css b/data/style.css
index afa16fb1..eb1830a0 100644
--- a/data/style.css
+++ b/data/style.css
@@ -42,7 +42,7 @@ table.stripe tbody tr:nth-child(odd):not(.nostripe),
#debug h2 { color: #f00!important; font-size: 20px; }
#debug, #debug a { color: #fff!important; }
-.visuallyhidden {
+.visuallyhidden, p.linkradio input {
position: absolute !important;
left: 0;
height: 1px; width: 1px;
@@ -153,6 +153,11 @@ table.formtable td { padding: 0; }
table.formtable tr.newfield td { padding-top: 5px; }
table.formtable tr.newpart td { padding-top: 20px; font-weight: bold; }
+p.linkradio { padding: 2px }
+p.linkradio label { color: $link$; cursor: pointer }
+p.linkradio input:checked + label { color: $maintext$ }
+p.linkradio em { font-weight: normal; font-style: normal; color: $grayedout$ }
+
div.spinner { content: ''; border: 3px solid #9eaebd; border-bottom-color: transparent; border-radius: 100%; animation: spin 1s infinite linear; width: 14px; height: 14px; display: inline-block; margin: auto }
@keyframes spin { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
diff --git a/lib/VNDB/DB/Docs.pm b/lib/VNDB/DB/Docs.pm
deleted file mode 100644
index d7c220ff..00000000
--- a/lib/VNDB/DB/Docs.pm
+++ /dev/null
@@ -1,43 +0,0 @@
-
-package VNDB::DB::Docs;
-
-use strict;
-use warnings;
-use Exporter 'import';
-
-our @EXPORT = qw|dbDocGet dbDocGetRev|;
-
-
-# Can only fetch a single document.
-# $doc = $self->dbDocGet(id => $id);
-sub dbDocGet {
- my $self = shift;
- my %o = @_;
-
- my $r = $self->dbAll('SELECT id, title, content FROM docs WHERE id = ?', $o{id});
- return wantarray ? ($r, 0) : $r;
-}
-
-
-# options: id, rev
-sub dbDocGetRev {
- my $self = shift;
- my %o = @_;
-
- $o{rev} ||= $self->dbRow('SELECT MAX(rev) AS rev FROM changes WHERE type = \'d\' AND itemid = ?', $o{id})->{rev};
-
- my $r = $self->dbAll(q|
- SELECT de.id, d.title, d.content, de.hidden, de.locked,
- extract('epoch' from c.added) as added, c.requester, c.comments, u.username, c.rev, c.ihid, c.ilock, c.id AS cid,
- NOT EXISTS(SELECT 1 FROM changes c2 WHERE c2.type = c.type AND c2.itemid = c.itemid AND c2.rev = c.rev+1) AS lastrev
- FROM changes c
- JOIN docs de ON de.id = c.itemid
- JOIN docs_hist d ON d.chid = c.id
- JOIN users u ON u.id = c.requester
- WHERE c.type = 'd' AND c.itemid = ? AND c.rev = ?|,
- $o{id}, $o{rev}
- );
- return wantarray ? ($r, 0) : $r;
-}
-
-1;
diff --git a/lib/VNDB/Handler/Misc.pm b/lib/VNDB/Handler/Misc.pm
index 666e71bd..9b4073ce 100644
--- a/lib/VNDB/Handler/Misc.pm
+++ b/lib/VNDB/Handler/Misc.pm
@@ -11,7 +11,6 @@ use VNDB::Types;
TUWF::register(
qr{}, \&homepage,
- qr{(?:([upvrcsd])([1-9]\d*)/)?hist},\&history,
qr{nospam}, \&nospam,
qr{xml/prefs\.xml}, \&prefs,
qr{opensearch\.xml}, \&opensearch,
@@ -194,105 +193,6 @@ sub homepage {
}
-sub history {
- my($self, $type, $id) = @_;
- $type ||= '';
- $id ||= 0;
-
- my $f = $self->formValidate(
- { get => 'p', required => 0, default => 1, template => 'page' },
- { get => 'm', required => 0, default => !$type, enum => [ 0, 1 ] },
- { get => 'h', required => 0, default => 0, enum => [ -1..1 ] },
- { get => 't', required => 0, default => '', enum => [qw|v r p c s d a|] },
- { get => 'e', required => 0, default => 0, enum => [ -1..1 ] },
- { get => 'r', required => 0, default => 0, enum => [ 0, 1 ] },
- );
- return $self->resNotFound if $f->{_err};
-
- # get item object and title
- my $obj = $type eq 'u' ? $self->dbUserGet(uid => $id, what => 'hide_list')->[0] :
- $type eq 'p' ? $self->dbProducerGet(id => $id)->[0] :
- $type eq 'r' ? $self->dbReleaseGet(id => $id)->[0] :
- $type eq 'c' ? $self->dbCharGet(id => $id)->[0] :
- $type eq 's' ? $self->dbStaffGet(id => $id)->[0] :
- $type eq 'd' ? $self->dbDocGet(id => $id)->[0] :
- $type eq 'v' ? $self->dbVNGet(id => $id)->[0] : undef;
- return $self->resNotFound if $type && !$obj->{id};
- my $title = $type ? 'Edit history of '.($obj->{title} || $obj->{name} || $obj->{username}) : 'Recent changes';
-
- # get the edit history
- my($list, $np) = $self->dbRevisionGet(
- $type && $type ne 'u' ? ( type => $type, itemid => $id ) : (),
- $type eq 'u' ? ( uid => $id ) : (),
- $f->{t} ? ( type => $f->{t} eq 'a' ? [qw|v r p s d|] : $f->{t} ) : (),
- page => $f->{p},
- results => 50,
- auto => $f->{m},
- hidden => $type && $type ne 'u' ? 0 : $f->{h},
- edit => $f->{e},
- releases => $f->{r},
- );
-
- $self->htmlHeader(title => $title, noindex => 1, feeds => [ 'changes' ]);
- $self->htmlMainTabs($type, $obj, 'hist') if $type;
-
- # url generator
- my $u = sub {
- my($n, $v) = @_;
- $n ||= '';
- local $_ = ($type ? "/$type$id" : '').'/hist';
- $_ .= '?m='.($n eq 'm' ? $v : $f->{m});
- $_ .= ';h='.($n eq 'h' ? $v : $f->{h});
- $_ .= ';t='.($n eq 't' ? $v : $f->{t});
- $_ .= ';e='.($n eq 'e' ? $v : $f->{e});
- $_ .= ';r='.($n eq 'r' ? $v : $f->{r});
- };
-
- # filters
- div class => 'mainbox';
- h1 $title;
- if($type ne 'u') {
- p class => 'browseopts';
- a !$f->{m} ? (class => 'optselected') : (), href => $u->(m => 0), 'Show automated edits';
- a $f->{m} ? (class => 'optselected') : (), href => $u->(m => 1), 'Hide automated edits';
- end;
- }
- if(!$type || $type eq 'u') {
- if($self->authCan('dbmod')) {
- p class => 'browseopts';
- a $f->{h} == 1 ? (class => 'optselected') : (), href => $u->(h => 1), 'Hide deleted items';
- a $f->{h} == -1 ? (class => 'optselected') : (), href => $u->(h => -1), 'Show deleted items';
- end;
- }
- p class => 'browseopts';
- a !$f->{t} ? (class => 'optselected') : (), href => $u->(t => ''), 'Show all items';
- a $f->{t} eq 'v' ? (class => 'optselected') : (), href => $u->(t => 'v'), 'Only visual novels';
- a $f->{t} eq 'r' ? (class => 'optselected') : (), href => $u->(t => 'r'), 'Only releases';
- a $f->{t} eq 'p' ? (class => 'optselected') : (), href => $u->(t => 'p'), 'Only producers';
- a $f->{t} eq 's' ? (class => 'optselected') : (), href => $u->(t => 's'), 'Only staff';
- a $f->{t} eq 'c' ? (class => 'optselected') : (), href => $u->(t => 'c'), 'Only characters';
- a $f->{t} eq 'd' ? (class => 'optselected') : (), href => $u->(t => 'd'), 'Only docs';
- a $f->{t} eq 'a' ? (class => 'optselected') : (), href => $u->(t => 'a'), 'All except characters';
- end;
- p class => 'browseopts';
- a !$f->{e} ? (class => 'optselected') : (), href => $u->(e => 0), 'Show all changes';
- a $f->{e} == 1 ? (class => 'optselected') : (), href => $u->(e => 1), 'Only edits';
- a $f->{e} == -1 ? (class => 'optselected') : (), href => $u->(e => -1), 'Only newly created pages';
- end;
- }
- if($type eq 'v') {
- p class => 'browseopts';
- a !$f->{r} ? (class => 'optselected') : (), href => $u->(r => 0), 'Exclude edits of releases';
- a $f->{r} ? (class => 'optselected') : (), href => $u->(r => 1), 'Include edits of releases';
- end;
- }
- end 'div';
-
- $self->htmlBrowseHist($list, $f, $np, $u->());
- $self->htmlFooter;
-}
-
-
sub nospam {
my $self = shift;
$self->htmlHeader(title => 'Could not send form', noindex => 1);
diff --git a/lib/VNDB/Util/CommonHTML.pm b/lib/VNDB/Util/CommonHTML.pm
index 4b80eb21..7a6e8620 100644
--- a/lib/VNDB/Util/CommonHTML.pm
+++ b/lib/VNDB/Util/CommonHTML.pm
@@ -123,7 +123,6 @@ sub htmlHiddenMessage {
: $type eq 'r' ? $self->dbReleaseGetRev(id => $obj->{id})->[0]{comments}
: $type eq 'c' ? $self->dbCharGetRev(id => $obj->{id})->[0]{comments}
: $type eq 's' ? $self->dbStaffGetRev(id => $obj->{id})->[0]{comments}
- : $type eq 'd' ? $self->dbDocGetRev(id => $obj->{id})->[0]{comments}
: $self->dbProducerGetRev(id => $obj->{id})->[0]{comments};
div class => 'mainbox';
h1 $obj->{title}||$obj->{name};
diff --git a/lib/VNWeb/DB.pm b/lib/VNWeb/DB.pm
index 7ef2e161..0d5616e0 100644
--- a/lib/VNWeb/DB.pm
+++ b/lib/VNWeb/DB.pm
@@ -10,7 +10,7 @@ use VNDB::Schema;
our @EXPORT = qw/
sql
- sql_join sql_comma sql_and sql_array sql_func sql_fromhex sql_tohex sql_fromtime sql_totime
+ sql_join sql_comma sql_and sql_or sql_array sql_func sql_fromhex sql_tohex sql_fromtime sql_totime
enrich enrich_merge enrich_flatten
db_entry db_edit
/;
@@ -49,15 +49,15 @@ $Carp::Internal{ (__PACKAGE__) }++;
sub sql_join {
my $sep = shift;
my @args = map +($sep, $_), @_;
- shift @args;
- return @args;
+ sql @args[1..$#args];
}
# Join multiple arguments together with a comma, for use in a SELECT or IN
# clause or function arguments.
sub sql_comma { sql_join ',', @_ }
-sub sql_and { sql_join 'AND', map sql('(', $_, ')'), @_ }
+sub sql_and { @_ ? sql_join 'AND', map sql('(', $_, ')'), @_ : sql '1=1' }
+sub sql_or { @_ ? sql_join 'OR', map sql('(', $_, ')'), @_ : sql '1=0' }
# Construct a PostgreSQL array type from the function arguments.
sub sql_array { 'ARRAY[', sql_join(',', map \$_, @_), ']' }
diff --git a/lib/VNWeb/HTML.pm b/lib/VNWeb/HTML.pm
index d1498c1e..2c89d3b9 100644
--- a/lib/VNWeb/HTML.pm
+++ b/lib/VNWeb/HTML.pm
@@ -8,6 +8,7 @@ use Encode 'encode_utf8';
use JSON::XS;
use TUWF ':html5_', 'uri_escape', 'html_escape', 'mkclass';
use Exporter 'import';
+use POSIX 'ceil';
use JSON::XS;
use VNDB::Config;
use VNDB::BBCode;
@@ -24,6 +25,7 @@ our @EXPORT = qw/
elm_
framework_
revision_
+ paginate_
/;
@@ -249,8 +251,18 @@ sub _maintabs_ {
WHERE}, { 'tb.type' => $t, 'tb.iid' => $o->{id}, 't.hidden' => 0, 't.private' => 0 });
t disc => "/t/$id", "discussions ($cnt)";
};
-
- # TODO: User lists
+ t posts => "/$id/posts", 'posts' if $t eq 'u';
+
+ do {
+ t wish => "/$id/wish", 'wishlist';
+ t votes => "/$id/votes", 'votes';
+ t list => "/$id/list", 'list';
+ } if $t eq 'u' && (
+ auth->permUsermod || (auth && auth->uid == $o->{id})
+ || !(exists $o->{hide_list}
+ ? $o->{hide_list}
+ : tuwf->dbVali('SELECT value FROM users_prefs WHERE', { uid => $o->{id}, key => 'hide_list' }))
+ );
t tagmod => "/$id/tagmod", 'modify tags' if $t eq 'v' && auth->permTag && !$o->{entry_hidden};
t copy => "/$id/copy", 'copy' if $t =~ /[rc]/ && can_edit $t, $o;
@@ -525,4 +537,44 @@ sub revision_ {
};
}
+
+# Creates next/previous buttons (tabs), if needed.
+# Arguments:
+# url generator (code reference that takes $_ and returns a url for that page).
+# current page number (1..n),
+# nextpage (0/1 or, if the full count is known: [$total, $perpage]),
+# alignment (t/b)
+sub paginate_ {
+ my($url, $p, $np, $al) = @_;
+ my($cnt, $pp) = ref($np) ? @$np : ($p+$np, 1);
+ return if $p == 1 && $cnt <= $pp;
+
+ my sub tab_ {
+ my($left, $page, $label) = @_;
+ li_ mkclass(left => $left), sub {
+ local $_ = $page;
+ my $u = $url->();
+ a_ href => $u, $label;
+ }
+ }
+ my sub ell_ {
+ my($left) = @_;
+ li_ mkclass(ellipsis => 1, left => $left), sub { b_ '⋯' };
+ }
+ my $nc = 5; # max. number of buttons on each side
+
+ ul_ class => 'maintabs browsetabs ' . ($al eq 't' ? 'notfirst' : 'bottom'), sub {
+ $p > 2 and ref $np and tab_ 1, 1, '« first';
+ $p > $nc+1 and ref $np and ell_ 1;
+ $p > $_ and ref $np and tab_ 1, $p-$_, $p-$_ for (reverse 2..($nc>$p-2?$p-2:$nc-1));
+ $p > 1 and tab_ 1, $p-1, '‹ previous';
+
+ my $l = ceil($cnt/$pp)-$p+1;
+ $l > 2 and tab_ 0, $l+$p-1, 'last »';
+ $l > $nc+1 and ell_ 0;
+ $l > $_ and tab_ 0, $p+$_, $p+$_ for (reverse 2..($nc>$l-2?$l-2:$nc-1));
+ $l > 1 and tab_ 0, $p+1, 'next ›';
+ }
+}
+
1;
diff --git a/lib/VNWeb/Misc/History.pm b/lib/VNWeb/Misc/History.pm
new file mode 100644
index 00000000..8c1fc8bc
--- /dev/null
+++ b/lib/VNWeb/Misc/History.pm
@@ -0,0 +1,213 @@
+package VNWeb::Misc::History;
+
+use VNWeb::Prelude;
+
+
+sub fetch {
+ my($type, $id, $filt) = @_;
+
+ my $where = sql_and
+ !$type ? ()
+ : $type eq 'u' ? sql 'c.requester =', \$id
+ : sql_or(
+ sql('c.type =', \$type, ' AND c.itemid =', \$id),
+
+ # This may need an index on releases_vn_hist.vid
+ $type eq 'v' && $filt->{r} ?
+ sql 'c.id IN(SELECT chid FROM releases_vn_hist WHERE vid =', \$id, ')' : ()
+ ),
+
+ $filt->{t} && $filt->{t}->@* ? sql 'c.type IN', \$filt->{t} : (),
+ $filt->{m} ? sql 'c.requester <> 1' : (),
+
+ $filt->{e} && $filt->{e} == 1 ? sql 'c.rev <> 1' : (),
+ $filt->{e} && $filt->{e} ==-1 ? sql 'c.rev = 1' : (),
+
+ $filt->{h} ? sql $filt->{h} == 1 ? 'NOT' : '',
+ 'EXISTS(SELECT 1 FROM changes c_i
+ WHERE c_i.type = c.type AND c_i.itemid = c.itemid AND c_i.ihid
+ AND c_i.rev = (SELECT MAX(c_ii.rev) FROM changes c_ii WHERE c_ii.type = c.type AND c_ii.itemid = c.itemid))' : ();
+
+ my($lst, $np) = tuwf->dbPagei({ page => $filt->{p}, results => 50 }, q{
+ SELECT c.id, c.type, c.itemid, c.comments, c.rev,}, sql_totime('c.added'), q{ AS added
+ , c.requester, u.username
+ FROM changes c
+ JOIN users u ON c.requester = u.id
+ WHERE}, $where, q{
+ ORDER BY c.id DESC
+ });
+
+ # Fetching the titles in a separate query is faster, for some reason.
+ enrich_merge id => sql(q{
+ SELECT id, title, original FROM (
+ SELECT chid, title, original FROM vn_hist
+ UNION ALL SELECT chid, title, original FROM releases_hist
+ UNION ALL SELECT chid, name, original FROM producers_hist
+ UNION ALL SELECT chid, name, original FROM chars_hist
+ UNION ALL SELECT chid, title, '' AS original FROM docs_hist
+ UNION ALL SELECT sh.chid, name, original FROM staff_hist sh JOIN staff_alias_hist sah ON sah.chid = sh.chid AND sah.aid = sh.aid
+ ) t(id, title, original)
+ WHERE id IN}), $lst;
+ ($lst, $np)
+}
+
+
+sub _filturl {
+ my($filt) = @_;
+ return '?'.join '&', map {
+ my $k = $_;
+ ref $filt->{$k} ? map "$k=$_", sort $filt->{$k}->@* : "$k=$filt->{$k}"
+ } sort keys %$filt;
+}
+
+
+sub tablebox_ {
+ my($type, $id, $filt) = @_;
+
+ my($lst, $np) = fetch $type, $id, $filt;
+
+ my sub url { _filturl {%$filt, p => $_} }
+
+ paginate_ \&url, $filt->{p}, $np, 't';
+ div_ class => 'mainbox browse history', sub {
+ table_ class => 'stripe', sub {
+ thead_ sub { tr_ sub {
+ td_ class => 'tc1_1', 'Rev.';
+ td_ class => 'tc1_2', '';
+ td_ class => 'tc2', 'Date';
+ td_ class => 'tc3', 'User';
+ td_ class => 'tc4', sub { txt_ 'Page'; debug_ $lst; };
+ }};
+ tr_ sub {
+ my $i = $_;
+ my $revurl = "/$i->{type}$i->{itemid}.$i->{rev}";
+
+ tr_ sub {
+ td_ class => 'tc1_1', sub { a_ href => $revurl, "$i->{type}$i->{itemid}" };
+ td_ class => 'tc1_2', sub { a_ href => $revurl, ".$i->{rev}" };
+ td_ class => 'tc2', fmtdate $i->{added}, 'full';
+ td_ class => 'tc3', sub { user_ $i->{requester}, $i->{username} };
+ td_ class => 'tc4', sub {
+ a_ href => $revurl, title => $i->{original}, shorten $i->{title}, 80;
+ b_ class => 'grayedout', sub { lit_ bb2html $i->{comments}, 150 };
+ };
+ }
+ } for @$lst;
+ };
+ };
+ paginate_ \&url, $filt->{p}, $np, 'b';
+}
+
+
+sub filters_ {
+ my($type) = @_;
+
+ my @types = (
+ [ v => 'Visual novels' ],
+ [ r => 'Releases' ],
+ [ p => 'Producers' ],
+ [ s => 'Staff' ],
+ [ c => 'Characters' ],
+ [ d => 'Docs' ]
+ );
+
+ state $schema = tuwf->compile({ type => 'hash', keys => {
+ # Types
+ t => { type => 'array', scalar => 1, required => 0, default => [map $_->[0], @types], values => { enum => [(map $_->[0], @types), 'a'] } },
+ m => { required => 0, enum => [ 0, 1 ] }, # Automated edits
+ h => { required => 0, default => 0, enum => [ -1..1 ] }, # Hidden items
+ e => { required => 0, default => 0, enum => [ -1..1 ] }, # Existing/new items
+ r => { required => 0, default => 0, enum => [ 0, 1 ] }, # Include releases
+ p => { page => 1 },
+ }});
+ my $filt = tuwf->validate(get => $schema)->data;
+
+ $filt->{m} //= $type ? 0 : 1; # Exclude automated edits by default on the main 'recent changes' view.
+
+ # For compat with old URLs, 't=a' means "everything except characters". Let's also weed out duplicates
+ my %t = map +($_, 1), map $_ eq 'a' ? (qw|v r p s d|) : ($_), $filt->{t}->@*;
+ $filt->{t} = keys %t == @types ? [] : [ keys %t ];
+
+ # Not all filters apply everywhere
+ delete @{$filt}{qw/ t e h /} if $type && $type ne 'u';
+ delete $filt->{m} if $type eq 'u';
+ delete $filt->{r} if $type ne 'v';
+
+ my sub opt_ {
+ my($key, $val, $label) = @_;
+ input_ type => 'radio', name => $key, id => "form_${key}{$val}", value => $val,
+ $filt->{$key} eq $val ? (checked => 'checked') : ();
+ label_ for => "form_${key}{$val}", $label;
+ };
+
+ form_ method => 'get', action => tuwf->reqPath(), sub {
+ table_ style => 'margin: 0 auto', sub { tr_ sub {
+ td_ style => 'padding: 10px', sub {
+ p_ class => 'linkradio', sub {
+ join_ \&br_, sub {
+ input_ type => 'checkbox', name => 't', value => $_->[0], id => "form_t$_->[0]", $t{$_->[0]}? (checked => 'checked') : ();
+ label_ for => "form_t$_->[0]", ' '.$_->[1]
+ }, @types;
+ }
+ } if exists $filt->{t};
+
+ td_ style => 'padding: 10px', sub {
+ p_ class => 'linkradio', sub {
+ opt_ e => 0, 'All'; em_ ' | ';
+ opt_ e => 1, 'Only changes to existing items'; em_ ' | ';
+ opt_ e =>-1, 'Only newly created items';
+ } if exists $filt->{e};
+ p_ class => 'linkradio', sub {
+ opt_ h => 0, 'All'; em_ ' | ';
+ opt_ h => 1, 'Only non-deleted items'; em_ ' | ';
+ opt_ h =>-1, 'Only deleted';
+ } if exists $filt->{h};
+ p_ class => 'linkradio', sub {
+ opt_ m => 0, 'Show'; em_ ' | ';
+ opt_ m => 1, 'Hide'; txt_ ' automated edits';
+ } if exists $filt->{m};
+ p_ class => 'linkradio', sub {
+ opt_ r => 0, 'Exclude'; em_ ' | ';
+ opt_ r => 1, 'Include'; txt_ ' releases';
+ } if exists $filt->{r};
+ input_ type => 'submit', class => 'submit', value => 'Update';
+ debug_ $filt;
+ };
+ }};
+ };
+ $filt;
+}
+
+
+TUWF::get qr{/(?:([upvrcsd])([1-9]\d*)/)?hist} => sub {
+ my($type, $id) = (tuwf->capture(1)||'', tuwf->capture(2));
+
+ my sub dbitem {
+ my($table, $title) = @_;
+ tuwf->dbRowi('SELECT id,', $title, ' AS title, hidden AS entry_hidden, locked AS entry_locked FROM', $table, 'WHERE id =', \$id);
+ };
+
+ my $obj = !$type ? undef :
+ $type eq 'u' ? tuwf->dbRowi('SELECT id, username AS title FROM users WHERE id =', \$id) :
+ $type eq 'p' ? dbitem producers => 'name' :
+ $type eq 'v' ? dbitem vn => 'title' :
+ $type eq 'r' ? dbitem releases => 'title' :
+ $type eq 'c' ? dbitem chars => 'name' :
+ $type eq 's' ? dbitem staff => '(SELECT name FROM staff_alias WHERE aid = staff.aid)' :
+ $type eq 'd' ? dbitem docs => 'title' : die;
+
+ return tuwf->resNotFound if $type && !$obj->{id};
+
+ my $title = $type ? "Edit history of $obj->{title}" : 'Recent changes';
+ framework_ title => $title, index => 0, type => $type, dbobj => $obj, tab => 'hist',
+ sub {
+ my $filt;
+ div_ class => 'mainbox', sub {
+ h1_ $title;
+ $filt = filters_ $type;
+ };
+ tablebox_ $type, $id, $filt;
+ };
+};
+
+1;
diff --git a/lib/VNWeb/Prelude.pm b/lib/VNWeb/Prelude.pm
index 26d0763e..506d0592 100644
--- a/lib/VNWeb/Prelude.pm
+++ b/lib/VNWeb/Prelude.pm
@@ -9,8 +9,10 @@
# use Time::HiRes 'time';
#
# use VNDBUtil;
+# use VNDB::BBCode;
# use VNDB::Types;
# use VNDB::Config;
+# use VNDB::Func 'fmtdate';
# use VNWeb::Auth;
# use VNWeb::HTML;
# use VNWeb::DB;
@@ -48,8 +50,10 @@ sub import {
use Time::HiRes 'time';
use VNDBUtil;
+ use VNDB::BBCode;
use VNDB::Types;
use VNDB::Config;
+ use VNDB::Func 'fmtdate';
use VNWeb::Auth;
use VNWeb::HTML;
use VNWeb::DB;
diff --git a/lib/VNWeb/Validation.pm b/lib/VNWeb/Validation.pm
index 80af1d34..c649c7a3 100644
--- a/lib/VNWeb/Validation.pm
+++ b/lib/VNWeb/Validation.pm
@@ -15,6 +15,7 @@ our @EXPORT = qw/
TUWF::set custom_validations => {
id => { uint => 1, max => 1<<40 },
editsum => { required => 1, length => [ 2, 5000 ] },
+ page => { uint => 1, min => 1, max => 1000, required => 0, default => 1 },
};