summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2008-12-20 10:04:01 +0100
committerYorhel <git@yorhel.nl>2008-12-20 10:04:01 +0100
commit74d22f8b7ede972f458f3177c87d66795424176c (patch)
treed390c75bb3f6af904fd8e6a574fdb1f0d8b9b45a
parent9f85f50c26796f62946ac30f5f60a1a30e098add (diff)
parentf574d1678ff813ffd664bbf9b648fe19737142a1 (diff)
Merge branch '2.0-rewrite'
Conflicts: data/docs/1 data/tpl/main lib/global.pl
-rw-r--r--.gitignore1
-rw-r--r--.gitmodules3
-rw-r--r--data/docs/13
-rw-r--r--data/docs/22
-rw-r--r--data/docs/64
-rw-r--r--data/docs/72
-rw-r--r--data/docs/notfinished4
-rw-r--r--data/global.pl243
-rw-r--r--data/tpl/defs.pl562
-rw-r--r--data/tpl/docs4
-rw-r--r--data/tpl/error10
-rw-r--r--data/tpl/hist102
-rw-r--r--data/tpl/home66
-rw-r--r--data/tpl/main163
-rw-r--r--data/tpl/pbrowse45
-rw-r--r--data/tpl/pedit44
-rw-r--r--data/tpl/ppage55
-rw-r--r--data/tpl/redit69
-rw-r--r--data/tpl/rlist90
-rw-r--r--data/tpl/rpage68
-rw-r--r--data/tpl/tedit33
-rw-r--r--data/tpl/tindex49
-rw-r--r--data/tpl/ttag60
-rw-r--r--data/tpl/tthread81
-rw-r--r--data/tpl/useredit36
-rw-r--r--data/tpl/userlist50
-rw-r--r--data/tpl/userlogin14
-rw-r--r--data/tpl/userpage13
-rw-r--r--data/tpl/userpass21
-rw-r--r--data/tpl/userreg38
-rw-r--r--data/tpl/vnbrowse108
-rw-r--r--data/tpl/vnedit120
-rw-r--r--data/tpl/vnlist63
-rw-r--r--data/tpl/vnpage204
-rw-r--r--data/tpl/vnpage_rel59
-rw-r--r--data/tpl/vnpage_rg11
-rw-r--r--data/tpl/vnpage_scr37
-rw-r--r--data/tpl/vnpage_stats39
-rw-r--r--data/tpl/wlist55
-rw-r--r--lib/ChangeLog33
-rw-r--r--lib/Multi/Anime.pm15
-rw-r--r--lib/Multi/Core.pm13
-rw-r--r--lib/Multi/IRC.pm14
-rw-r--r--lib/Multi/Image.pm13
-rw-r--r--lib/Multi/Maintenance.pm47
-rw-r--r--lib/Multi/RG.pm15
-rw-r--r--lib/Multi/Sitemap.pm27
-rw-r--r--lib/VNDB.pm247
-rw-r--r--lib/VNDB/DB/Discussions.pm221
-rw-r--r--lib/VNDB/DB/Misc.pm179
-rw-r--r--lib/VNDB/DB/Producers.pm114
-rw-r--r--lib/VNDB/DB/Releases.pm168
-rw-r--r--lib/VNDB/DB/ULists.pm282
-rw-r--r--lib/VNDB/DB/Users.pm102
-rw-r--r--lib/VNDB/DB/VN.pm282
-rw-r--r--lib/VNDB/Discussions.pm193
-rw-r--r--lib/VNDB/Func.pm220
-rw-r--r--lib/VNDB/Handler/Discussions.pm399
-rw-r--r--lib/VNDB/Handler/Misc.pm382
-rw-r--r--lib/VNDB/Handler/Producers.pm234
-rw-r--r--lib/VNDB/Handler/Releases.pm422
-rw-r--r--lib/VNDB/Handler/ULists.pm336
-rw-r--r--lib/VNDB/Handler/Users.pm467
-rw-r--r--lib/VNDB/Handler/VNBrowse.pm183
-rw-r--r--lib/VNDB/Handler/VNEdit.pm413
-rw-r--r--lib/VNDB/Handler/VNPage.pm484
-rw-r--r--lib/VNDB/HomePages.pm306
-rw-r--r--lib/VNDB/Producers.pm176
-rw-r--r--lib/VNDB/Releases.pm188
-rw-r--r--lib/VNDB/Users.pm238
-rw-r--r--lib/VNDB/Util/Auth.pm147
-rw-r--r--lib/VNDB/Util/CommonHTML.pm497
-rw-r--r--lib/VNDB/Util/DB.pm1582
-rw-r--r--lib/VNDB/Util/FormHTML.pm259
-rw-r--r--lib/VNDB/Util/LayoutHTML.pm162
-rw-r--r--lib/VNDB/Util/Misc.pm28
-rw-r--r--lib/VNDB/Util/Request.pm46
-rw-r--r--lib/VNDB/Util/Response.pm220
-rw-r--r--lib/VNDB/Util/Template.pm235
-rw-r--r--lib/VNDB/Util/Tools.pm172
-rw-r--r--lib/VNDB/VN.pm419
-rw-r--r--lib/VNDB/VNLists.pm230
-rw-r--r--lib/global.pl687
-rw-r--r--static/f/bg.jpgbin0 -> 36419 bytes
-rw-r--r--static/f/bgright.jpgbin0 -> 4366 bytes
-rw-r--r--static/f/blank.css (renamed from static/files/blank.css)0
-rw-r--r--static/f/boxbg.pngbin0 -> 83 bytes
-rw-r--r--static/f/forms.js871
-rw-r--r--static/f/icons.pngbin0 -> 7422 bytes
-rw-r--r--static/f/script.js396
-rw-r--r--static/f/select.pngbin0 -> 1196 bytes
-rw-r--r--static/f/style.css1108
-rw-r--r--static/files/def.js479
-rw-r--r--static/files/dyna.js815
-rw-r--r--static/files/footer.gifbin91 -> 0 bytes
-rw-r--r--static/files/graph.pngbin601 -> 0 bytes
-rw-r--r--static/files/headerbg.jpgbin6068 -> 0 bytes
-rw-r--r--static/files/headerbot.pngbin2343 -> 0 bytes
-rw-r--r--static/files/icons.pngbin3208 -> 0 bytes
-rw-r--r--static/files/rss.pngbin735 -> 0 bytes
-rw-r--r--static/files/select.pngbin1165 -> 0 bytes
-rw-r--r--static/files/sidebarbg.jpgbin3035 -> 0 bytes
-rw-r--r--static/files/sidebarbot.jpgbin1642 -> 0 bytes
-rw-r--r--static/files/sidebg.jpgbin553 -> 0 bytes
-rw-r--r--static/files/style.css923
-rw-r--r--static/files/uicons.pngbin4404 -> 0 bytes
-rw-r--r--static/files/warning.pngbin2348 -> 0 bytes
-rwxr-xr-xutil/dbgraph.pl92
-rw-r--r--util/dump.sql102
-rw-r--r--util/fcgi.pl124
-rwxr-xr-xutil/init.pl70
-rwxr-xr-xutil/multi.pl25
-rw-r--r--util/updates/update_2.0.sql140
-rwxr-xr-xutil/vndb.pl75
m---------yawf0
115 files changed, 9129 insertions, 9819 deletions
diff --git a/.gitignore b/.gitignore
index 47fa605d..4ce94ab6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
/data/config.pl
/data/log/
-/data/tplcompiled.pm
/static/cv/
/static/rg/
/static/sf/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..325cb406
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "yawf"]
+ path = yawf
+ url = git://git.theappleman.me.uk/yawf.git
diff --git a/data/docs/1 b/data/docs/1
index 7e5d3aae..835d2070 100644
--- a/data/docs/1
+++ b/data/docs/1
@@ -87,7 +87,7 @@
<i>Linear</i>.
</p>
<dl>
- <dt>Linear</d><dd>
+ <dt>Linear</dt><dd>
A game with a linear plot has a static story; it is not possible to get different paths
or endings. Many games in this category do not prompt the player with choices and simply
tell the story as it is. This is, however, not a rule: it is also possible for a game
@@ -146,6 +146,7 @@
A combination of male and female protagonists is possible, e.g. multiple
protagonists.
In cases of sex change or transsexuality, use both.
+</p>
<dl>
<dt>Male</dt><dd>
The protagonist is male.
diff --git a/data/docs/2 b/data/docs/2
index e0c7d619..c6428c55 100644
--- a/data/docs/2
+++ b/data/docs/2
@@ -16,7 +16,7 @@
The title should be based upon the title of the original release.<br />
If the title uses the Latin alphabet, use our <a href="/d5.2">capitalisation guidelines</a>.<br />
Otherwise, <a href="/d5.1">romanise</a> according to our guidelines.
- </dt><dt>Original title</dt><dd>
+ </dd><dt>Original title</dt><dd>
If the name is officially under a different title (usually because of different
character sets), put the original title here.
</dd><dt>Aliases</dt><dd>
diff --git a/data/docs/6 b/data/docs/6
index 69116665..02a40469 100644
--- a/data/docs/6
+++ b/data/docs/6
@@ -14,7 +14,7 @@
<br />
For more information see <a href="http://en.wikipedia.org/wiki/Visual_Novel">
the Wikipedia article on visual novels</a> or the description on
- <a href="http://visual-novels.net/vn/index.php?option=com_content&task=view&id=259&Itemid=47">Visual-Novels.net</a>.
+ <a href="http://visual-novels.net/vn/index.php?option=com_content&amp;task=view&amp;id=259&amp;Itemid=47">Visual-Novels.net</a>.
To get a general idea of the genre, try one of the free short visual novels from
<a href="http://at2006.haeleth.net/release.php">al|together 2006</a>.
</p>
@@ -49,7 +49,7 @@
:SUB:How can I help VNDB?
-</p>
+<p>
There are many ways to contribute to VNDB. First of all you can freely
edit all information found on this website, so if you find any errors
just click the "edit" link on the top right of the page. You can also
diff --git a/data/docs/7 b/data/docs/7
index fa6fb2d2..ab9fe945 100644
--- a/data/docs/7
+++ b/data/docs/7
@@ -28,7 +28,7 @@
Founder of VNDB and still active as the main developer.
</dd><dt><a href="/u93">applehq</a></dt><dd>
Moderator for both the discussion board and database entries.
- </dd><dt><a href="/u4" class="retired">hillie</a> <b>[retired]</b><dt><dd>
+ </dd><dt><a href="/u4" class="retired">hillie</a> <b>[retired]</b></dt><dd>
Responsible for beta testing of the first few versions of the site and
moderating user submitted content.
</dd>
diff --git a/data/docs/notfinished b/data/docs/notfinished
index 8fc2669a..8b6b379f 100644
--- a/data/docs/notfinished
+++ b/data/docs/notfinished
@@ -1,4 +1,4 @@
-<span class="warning">
+<div class="warning">
This page is not yet finished!<br />
If you are interested in helping us finish it off, please join us on <a href="irc://irc.synirc.net/vndb">IRC</a>.
-</span>
+</div>
diff --git a/data/global.pl b/data/global.pl
new file mode 100644
index 00000000..bb4bfe4a
--- /dev/null
+++ b/data/global.pl
@@ -0,0 +1,243 @@
+
+package VNDB;
+
+our(%O, %S, $ROOT);
+
+
+# options for YAWF
+our %O = (
+ db_login => [ 'dbi:Pg:dbname=vndb', 'vndb', 'passwd' ],
+ debug => 1,
+ logfile => $ROOT.'/data/log/vndb.log',
+);
+
+
+# VNDB-specific options (object_data)
+our %S = (
+ version => `cd $VNDB::ROOT; git describe` =~ /^(.+)$/ && $1,
+ url => 'http://vndb.org',
+ url_static => 'http://s.vndb.org',
+ site_title => 'Yet another VNDB clone',
+ cookie_domain => '.vndb.org',
+ cookie_key => 'any-private-string-here',
+ sharedmem_key => 'VNDB',
+ user_ranks => [
+ # rankname allowed actions # DB number
+ [qw| visitor hist |], # 0
+ [qw| banned hist |], # 1
+ [qw| loser hist board |], # 2
+ [qw| user hist board edit |], # 3
+ [qw| mod hist board boardmod edit mod lock del |], # 4
+ [qw| admin hist board boardmod edit mod lock del usermod |], # 5
+ ],
+ languages => {
+ cs => q|Czech|,
+ da => q|Danish|,
+ de => q|German|,
+ en => q|English|,
+ es => q|Spanish|,
+ fi => q|Finnish|,
+ fr => q|French|,
+ it => q|Italian|,
+ ja => q|Japanese|,
+ ko => q|Korean|,
+ nl => q|Dutch|,
+ no => q|Norwegian|,
+ pl => q|Polish|,
+ pt => q|Portuguese|,
+ ru => q|Russian|,
+ sv => q|Swedish|,
+ tr => q|Turkish|,
+ zh => q|Chinese|,
+ },
+ producer_types => {
+ co => 'Company',
+ in => 'Individual',
+ ng => 'Amateur group',
+ },
+ discussion_tags => {
+ an => 'Announcements', # 0 - usage restricted to boardmods
+ db => 'VNDB Discussions', # 0
+ v => 'Visual novels', # vid
+ p => 'Producers', # pid
+ u => 'Users', # uid
+ },
+ vn_lengths => [
+ [ 'Unkown', '', '' ],
+ [ 'Very short', '< 2 hours', 'OMGWTFOTL, A Dream of Summer' ],
+ [ 'Short', '2 - 10 hours', 'Narcissu, Planetarian' ],
+ [ 'Medium', '10 - 30 hours', 'Kana: Little Sister' ],
+ [ 'Long', '30 - 50 hours', 'Tsukihime' ],
+ [ 'Very long', '> 50 hours', 'Clannad' ],
+ ],
+ categories => {
+ g => [ 'Gameplay', {
+ aa => 'NVL', # 0..1
+ ab => 'ADV', # 0..1
+ ac => "Act\x{200B}ion", # Ugliest. Hack. Ever.
+ rp => 'RPG',
+ st => 'Strategy',
+ si => 'Simulation',
+ }, 2 ],
+ p => [ 'Plot', { # 0..1
+ li => 'Linear',
+ br => 'Branching',
+ }, 3 ],
+ e => [ 'Elements', {
+ ac => 'Action',
+ co => 'Comedy',
+ dr => 'Drama',
+ fa => 'Fantasy',
+ ho => 'Horror',
+ my => 'Mystery',
+ ro => 'Romance',
+ sc => 'School Life',
+ sf => 'SciFi',
+ sj => 'Shoujo Ai',
+ sn => 'Shounen Ai',
+ }, 1 ],
+ t => [ 'Time', { # 0..1
+ fu => 'Future',
+ pa => 'Past',
+ pr => 'Present',
+ }, 4 ],
+ l => [ 'Place', { # 0..1
+ ea => 'Earth',
+ fa => "Fant\x{200B}asy world",
+ sp => 'Space',
+ }, 5 ],
+ h => [ 'Protagonist', { # 0..1
+ fa => 'Male',
+ fe => "Fem\x{200B}ale",
+ }, 6 ],
+ s => [ 'Sexual content', {
+ aa => 'Sexual content',
+ be => 'Bestiality',
+ in => 'Incest',
+ lo => 'Lolicon',
+ sh => 'Shotacon',
+ ya => 'Yaoi',
+ yu => 'Yuri',
+ ra => 'Rape',
+ }, 7 ],
+ },
+ anime_types => [
+ # VNDB AniDB
+ [ 'unknown', 'unknown', ],
+ [ 'TV', 'TV Series' ],
+ [ 'OVA', 'OVA' ],
+ [ 'Movie', 'Movie' ],
+ [ 'unknown', 'Other' ],
+ [ 'unknown', 'Web' ],
+ [ 'TV Special', 'TV Special' ],
+ [ 'unknown', 'Music Video' ],
+ ],
+ vn_relations => [
+ # Name, Reverse--
+ [ 'Sequel', 0 ],
+ [ 'Prequel', 1 ],
+ [ 'Same setting', 0 ],
+ [ 'Alternative setting', 0 ],
+ [ 'Alternative version', 0 ],
+ [ 'Same characters', 0 ],
+ [ 'Side story', 0 ],
+ [ 'Parent story', 1 ],
+ [ 'Summary', 0 ],
+ [ 'Full story', 1 ],
+ [ 'Other', 0 ],
+ ],
+ age_ratings => {
+ -1 => 'Unknown',
+ 0 => 'All ages',
+ map { $_ => $_.'+' } 6..18
+ },
+ release_types => [
+ 'Complete',
+ 'Partial',
+ 'Trial'
+ ],
+ platforms => {
+ win => 'Windows',
+ lin => 'Linux',
+ mac => 'Mac OS',
+ dvd => 'DVD Player',
+ gba => 'Game Boy Advance',
+ msx => 'MSX',
+ nds => 'Nintendo DS',
+ nes => 'Famicom',
+ psp => 'Playstation Portable',
+ ps1 => 'Playstation 1',
+ ps2 => 'Playstation 2',
+ ps3 => 'Playstation 3',
+ drc => 'Dreamcast',
+ sfc => 'Super Nintendo',
+ wii => 'Nintendo Wii',
+ xb3 => 'Xbox 360',
+ oth => 'Other'
+ },
+ media => {
+ #DB display qty
+ cd => [ 'CD', 1 ],
+ dvd => [ 'DVD', 1 ],
+ gdr => [ 'GD-ROM', 1 ],
+ blr => [ 'Blu-Ray disk', 1 ],
+ in => [ 'Internet download', 0 ],
+ pa => [ 'Patch', 0 ],
+ otc => [ 'Other (console)', 0 ],
+ },
+ votes => [
+ 'worst ever',
+ 'awful',
+ 'bad',
+ 'weak',
+ 'so-so',
+ 'decent',
+ 'good',
+ 'very good',
+ 'excellent',
+ 'masterpiece',
+ ],
+ wishlist_status => [
+ 'high',
+ 'medium',
+ 'low',
+ 'blacklist',
+ ],
+ vn_rstat => [
+ 'Unknown',
+ 'Pending',
+ 'Obtained', # hardcoded
+ 'On loan',
+ 'Deleted',
+ ],
+ vn_vstat => [
+ 'Unknown',
+ 'Playing',
+ 'Finished', # hardcoded
+ 'Stalled',
+ 'Dropped',
+ ],
+);
+
+
+# Multi-specific options (Multi also uses some options in %S and %O)
+our %M = (
+ log_dir => $ROOT.'/data/log',
+ log_level => 3, # 3: dbg, 2: wrn, 1: err
+ modules => {
+ RG => {},
+ Image => {},
+ Sitemap => {},
+ #Anime => {}, # disabled by default, requires AniDB username/pass
+ Maintenance => {},
+ #IRC => {}, # disabled by default, no need to run an IRC bot when debugging
+ },
+);
+
+
+# allow the settings to be overwritten in config.pl
+require $ROOT.'/data/config.pl' if -f $ROOT.'/data/config.pl';
+
+1;
+
+
diff --git a/data/tpl/defs.pl b/data/tpl/defs.pl
deleted file mode 100644
index 9135adb1..00000000
--- a/data/tpl/defs.pl
+++ /dev/null
@@ -1,562 +0,0 @@
-[[!
-
-use Time::CTime ();
-use Algorithm::Diff 'sdiff';
-use POSIX ('ceil', 'floor');
-
-my %p; # $X->{page} global page data
-my %d; # $X->{page}->{$p} local page data
-
-# redefine _hchar - usually a bad idea, but who cares
-sub _hchar {local$_=shift||return'';s/&/&amp;/g;s/</&lt;/g;s/>/&gt;/g;s/"/&quot;/g;s/\r?\n/ <br \/>\n/g;return$_;}
-
-sub formatdate {return _hchar(Time::CTime::strftime($_[0],gmtime($_[1]||0)))||'';}
-sub txt {local$_=shift||return'';s/&/&amp;/g;s/</&lt;/g;s/>/&gt;/g;return$_;}
-sub art2str {my$r='';$r.=($r?' & ':'').$_->{name}foreach (@{$_[0]->{artists}});return $_[1]?$r:_hchar($r);}
-sub calctime {my$r=shift;return'0:00:00'if!$r;my$x=sprintf'%d:%02d:%02d',int($r/3600),int(($r%3600)/60),($r%3600)%60;return $x;}
-sub shorten {local$_=shift||return'';return length>$_[0]?substr($_,0,$_[0]-3).'...':$_};
-
-# Date string format: yyyy-mm-dd
-# y = 0 -> Unknown
-# y = 9999 -> TBA (To Be Announced)
-# m = 99 -> Month + day unknown, year known
-# d = 99 -> Day unknown, month + year known
-sub datestr {
- my $d = sprintf '%08d', $_[0]||0;
- my $b = $d > Time::CTime::strftime("%Y%m%d", gmtime());
- my @d = map int, $1, $2, $3 if $d =~ /^([0-9]{4})([0-9]{2})([0-9]{2})$/;
- return 'unknown' if $d[0] == 0;
- my $r = sprintf $d[1] == 99 ? '%04d' : $d[2] == 99 ? '%04d-%02d' : '%04d-%02d-%02d', @d;
- $r = 'TBA' if $d[0] == 9999;
- $r =~ s/^[0-9]{4}-// if $_[1];
- return ($b&&!$_[1]?'<b class="future">':'').$r.($b?'</b>':'');
-}
-sub mediastr {
- return join(', ', map {
- $_->{medium} =~ /^(cd|dvd|gdr|blr)$/
- ? sprintf('%d %s%s', $_->{qty}, $VNDB::MED->{$_->{medium}}, $_->{qty}>1?'s':'')
- : $VNDB::MED->{$_->{medium}}
- } @{$_[0]});
-}
-sub sortbut { # url, col
- my $r=' '; my $u = _hchar($_[0]);
- $u .= $u =~ /\?/ ? ';' : '?';
- for ('a', 'd') {
- my $chr = $_ eq 'd' ? "\x{25BE}" : "\x{25B4}";
- $r .= $d{order}[0] eq $_[1] && $d{order}[1] eq $_ ? $chr :
- sprintf '<a href="%ss=%s;o=%s">%s</a>', $u, $_[1], $_, $chr;
- }
- return $r;
-}
-sub pagebut { # url
- my @br; my $ng = $_[0] =~ /\?/ ? ';' : '?';
- push @br, sprintf '<a href="%s">&lt;- previous</a>', $_[0].($d{page}-2 ? $ng.'p='.($d{page}-1) : '') if $d{page} > 1;
- push @br, sprintf '<a href="%s">next -&gt;</a>', $_[0].$ng.'p='.($d{page}+1) if $d{npage};
- return $#br >= 0 ? ('<p class="browse">( '.join(' | ', @br).' )</p>') : '';
-}
-sub wraplong { # text, margin
- local $_ = $_[0];
- my $m = $_[1]/2;
- s/([^\s\r\n]{$m})([^\s\r\n])/$1 $2/g;
- return $_;
-}
-sub age {
- my $a = time-$_[0];
- return sprintf '%d %s%s',
- $a > 60*60*24*365*2 ? ( $a/60/60/24/365, 'years' ) :
- $a > 60*60*24*(365/12)*2 ? ( $a/60/60/24/(365/12), 'months' ) :
- $a > 60*60*24*7*2 ? ( $a/60/60/24/7, 'weeks' ) :
- $a > 60*60*24*2 ? ( $a/60/60/24, 'days' ) :
- $a > 60*60*2 ? ( $a/60/60, 'hours' ) :
- $a > 60*2 ? ( $a/60, 'min' ) :
- ( $a, 'sec' ),
- $_[1]?'':' ago';
-}
-sub userstr { # [ uid, username ] or a hashref containing those keys
- my($id,$n) = ref($_[0])eq'HASH'?($_[0]{uid}||$_[0]{requester}, $_[0]{username}):@_;
- return !$id ? '[deleted]' : '<a href="/u'.$id.'">'.$n.'</a>';
-}
-
-
-sub wordsplit { # split a string into an array of words, but make sure to not split HTML tags
-# return [ split //, $_[0] ];
- my @a;
- my $in='';
- for (split /\s+/, $_[0]) {
- my $gt = () = />/g;
- my $lt = () = /</g;
- if($in && $gt > $lt) {
- push @a, $in.$_;
- $in='';
- } elsif($lt > $gt || $in) {
- $in .= $_.' ';
- } else {
- push @a, $_;
- };
- }
- push @a, $in if $in;
- return \@a;
-}
-
-sub cdiff { # obj1, obj2, @items->[ short, name, serialise, diff, [parsed_x, parsed_y] ]
- my($x, $y, @items, @c) = @_;
- # serialise = 0 -> integer, 1 -> string, CODEref -> code
-
- my $type = defined $$y{minage} ? 'r' : defined $$y{length} ? 'v' : 'p';
- my $pre = '<div id="revbrowse">'.
- ($$y{next} ? qq|<a href="/$type$$y{id}.$$y{next}" id="revnext">later revision -&gt;</a>| : '').
- ($x ? qq|<a href="/$type$$y{id}.$$x{rev}" id="revprev">&lt;- earlier revision</a>| : '').
- qq|<a href="/$type$$y{id}" id="revmain">$type$$y{id}</a>&nbsp;</div>|;
-
- if(!$x) { # just show info about the revision if there is no previous edit
- return $pre.qq|<div id="tmc"><b>Revision $$y{rev}</b> (<a href="/$type$$y{id}/edit?rev=$$y{rev}">edit</a>)<br />By |.userstr($y).q| on |.
- formatdate('%Y-%m-%d at %R', $$y{added}).'<br /><b>Edit summary:</b><br /><br />'.
- summary($$y{comments}, 0, '[no summary]').'</div>';
- }
- for (@items) {
- $_->[4] = !$_->[2] ? $x->{$_->[0]}||'0' : !ref($_->[2]) ? _hchar(wraplong($x->{$_->[0]}||'[empty]',60)) : &{$_->[2]}($x->{$_->[0]})||'[empty]';
- $_->[5] = !$_->[2] ? $y->{$_->[0]}||'0' : !ref($_->[2]) ? _hchar(wraplong($y->{$_->[0]}||'[empty]',60)) : &{$_->[2]}($y->{$_->[0]})||'[empty]';
- push(@c, $_) if $_->[4] ne $_->[5];
- if($_->[3] && $_->[4] ne $_->[5]) {
- my($rx,$ry,$ch) = ('','','u');
- for (sdiff(wordsplit($_->[4]), wordsplit($_->[5]))) {
- if($ch ne $_->[0]) {
- if($ch ne 'u') {
- $rx .= '</b>';
- $ry .= '</b>';
- }
- $rx .= '<b class="diff_del">' if $_->[0] eq '-' || $_->[0] eq 'c';
- $ry .= '<b class="diff_add">' if $_->[0] eq '+' || $_->[0] eq 'c';
- }
- $ch = $_->[0];
- $rx .= $_->[1].' ' if $ch ne '+';
- $ry .= $_->[2].' ' if $ch ne '-';
- }
- $_->[4] = $rx;
- $_->[5] = $ry;
- }
- }
- return $pre.'<table id="tmc"><thead><tr><td class="tc1">&nbsp;</td>'.
- qq|<td class="tc2"><b>Revision $$x{rev}</b> (<a href="/$type$$y{id}/edit?rev=$$x{rev}">edit</a>)<br />By |.userstr($x).' on '.formatdate('%Y-%m-%d at %R', $$x{added}).'</td>'.
- qq|<td class="tc3"><b>Revision $$y{rev}</b> (<a href="/$type$$y{id}/edit?rev=$$y{rev}">edit</a>)<br />By |.userstr($y).' on '.formatdate('%Y-%m-%d at %R', $$y{added}).'</td>'.
- '</tr><tr></tr><tr><td>&nbsp;</td><td colspan="2"><b>Edit summary of revision '.$$y{rev}.'</b><br /><br />'.summary($$y{comments}, 0, '[no summary]').'<br /><br /></td></tr></thead>'.
- join('',map{
- '<tr><td class="tc1">'.$_->[1].'</td><td class="tc2">'.$_->[4].'</td><td class="tc3">'.$_->[5].'</td></tr>'
- } @c).'</table>';
-}
-
-
-sub summary { # cmd, len, def
- return $_[2]||'' if !$_[0];
- my $res = '';
- my $len = 0;
- my $as = 0;
- my $raw = 0;
- (my $txt = $_[0]) =~ s/\r?\n/\n /g;
- for (split / /, $txt) {
- next if !defined $_ || $_ eq '';
- my $l = length;
- s/\&/&amp;/g;
- s/>/&gt;/g;
- s/</&lt;/g;
- if(!$raw && s/^\[raw\]//) {
- $l -= 5;
- $raw++;
- }
- if(!$raw) {
- $l -= 9 while(s/\[spoiler\]/<b class="spoiler">/i);
- $l -= 10 while(s/\[\/spoiler\]/<\/b>/i);
- while(s/\[url=((https?:\/\/|\/)[^\]>]+)\]/<a href="$1" rel="nofollow">/i) {
- $l -= length($1)+6;
- $as++;
- }
- if(!$as && s/(http|https):\/\/(.+[0-9a-zA-Z=\/])/<a href="$1:\/\/$2" rel="nofollow">link<\/a>/) {
- $l = 4;
- } elsif(!$as) {
- s/^(.*[^\w]|)([tdvpr][1-9][0-9]*)\.([1-9][0-9]*)([^\w].*|)$/$1<a href="\/$2.$3">$2.$3<\/a>$4/ ||
- s/^(.*[^\w]|)([tduvpr][1-9][0-9]*)([^\w].*|)$/$1<a href="\/$2">$2<\/a>$3/;
- }
- while(s/\[\/url\]/<\/a>/i) {
- $l -= 6;
- $as--;
- }
- }
- if(s/\[\/raw\]//) {
- $l -= 6;
- $raw=0;
- }
- $len += $l + 1;
- last if $_[1] && $len > $_[1];
- $res .= "$_ ";
- }
- $res =~ y/\n/ / if $_[1];
- $res =~ s/\n/<br \/>/g if !$_[1];
- $res =~ s/ +$//;
- $res .= '</a>' x $as if $as;
- $res .= '...' if $_[1] && $len > $_[1];
- return $res;
-}
-
-
-sub ttabs { # [vrpu], obj, sel
- my($t, $o, $s) = @_;
- $s||='';
- my @act = (
- !$s?'%s':'<a href="/%s">%1$s</a>',
- $$o{locked} ?
- '<b>locked for editing</b>' : (),
- $p{Authlock} && $t ne 'u' ?
- sprintf('<a href="/%%s/lock">%s</a>', $$o{locked} ? 'unlock' : 'lock') : (),
- $p{Authdel} && $t ne 'u' ? (
- sprintf('<a href="/%%s/hide"%s>%s</a>', $t eq 'v' ? ' id="vhide"' : '', $$o{hidden} ? 'unhide' : 'hide') ) : (),
- $t eq 'u' && $p{Authusermod} ? (
- '<a href="/%s/del" id="userdel">del</a>' ) : (),
- ($t eq 'u' && $p{Authusermod}) || ($t ne 'u' && (!$$o{locked} && !$$o{hidden}) || ($p{Authedit} && $p{Authlock})) ?
- ($s eq 'edit' ? 'edit' : '<a href="'.($p{Authedit}?'/%s/edit':'/u/register?n=1').'" '.($t eq 'v' || $t eq 'r' ? 'class="dropdown" rel="nofollow editDD"':'').'>edit</a>') : (),
-
- $t eq 'u' ? (
- $o->{flags} & $VNDB::UFLAGS->{list} ? ( $s eq 'list' ? 'list' : '<a href="/%s/list">list</a>', ) : (),
- $o->{flags} & $VNDB::UFLAGS->{list} ? ( $s eq 'wish' ? 'wishlist' : '<a href="/%s/wish">wishlist</a>', ) : (),
- ) : (),
-
- $t ne 'r' ? (
- $s eq 'disc' ? 'discussions' : '<a href="/t/%s">discussions</a>', ) : (),
-
- $p{Authhist} ?
- ($s eq 'hist' ? 'history' : '<a href="/%s/hist">history</a>') : (),
- );
- return '<p class="mod">&lt; '.join(' - ', map { sprintf $_, $t.$$o{id} } @act).' &gt;</p>'.(
- !$p{Authedit} ? qq|
-<div id="editDD" class="dropdown">
- <ul>
- <li><b>Not logged in</b></li>
- <li><a href="/u/login">Login</a></li>
- <li><a href="/u/register">Register</a></li>
- </ul>
-</div>
- | : $t eq 'v' ? qq|
-<div id="editDD" class="dropdown">
- <ul>
- <li><a href="/v$$o{id}/edit" rel="nofollow">Edit all</a></li>
- <li><a href="/v$$o{id}/edit?fh=info" rel="nofollow">General info</a></li>
- <li><a href="/v$$o{id}/edit?fh=cat" rel="nofollow">Categories</a></li>
- <li><a href="/v$$o{id}/edit?fh=rel" rel="nofollow">Relations</a></li>
- <li><a href="/v$$o{id}/edit?fh=img" rel="nofollow">Image</a></li>
- <li><a href="/v$$o{id}/edit?fh=scr" rel="nofollow">Screenshots</a></li>
- <li><a href="/v$$o{id}/add" rel="nofollow">Add release</a></li>
- </ul>
-</div>| : $t eq 'r' ? qq|
-<div id="editDD" class="dropdown">
- <ul>
- <li><a href="/r$$o{id}/edit" rel="nofollow">Edit all</a></li>
- <li><a href="/r$$o{id}/edit?fh=info" rel="nofollow">General info</a></li>
- <li><a href="/r$$o{id}/edit?fh=pnm" rel="nofollow">Platforms &amp; media</a></li>
- <li><a href="/r$$o{id}/edit?fh=prod" rel="nofollow">Producers</a></li>
- <li><a href="/r$$o{id}/edit?fh=rel" rel="nofollow">Relations</a></li>
- </ul>
-</div>| : ''
- );
-}
-
-
-# Uwaaaa~ ugly function!
-sub rlist_dd {
- my $r = shift;
- return
- qq|<div class="dropdown rlistdd" id="rlistDD$$r{id}"><ul><li><b>Release status</b></li>|.
- join('', map {
- $r->{rlist} && $_ == $r->{rlist}{rstat} ? qq|<li><b><acronym class="uicons r$_">&nbsp;</acronym>&nbsp;$$VNDB::RSTAT[$_]</b></li>|
- : qq|<li><a href="/r$$r{id}/list?r=$_"><acronym class="uicons r$_">&nbsp;</acronym>&nbsp;$$VNDB::RSTAT[$_]</a></li>|
- } 0..$#$VNDB::RSTAT).
- qq|</ul><ul><li><b>Play status</b></li>|.
- join('', map {
- $r->{rlist} && $_ == $r->{rlist}{vstat} ? qq|<li><b><acronym class="uicons v$_">&nbsp;</acronym>&nbsp;$$VNDB::VSTAT[$_]</b></li>|
- : qq|<li><a href="/r$$r{id}/list?v=$_"><acronym class="uicons v$_">&nbsp;</acronym>&nbsp;$$VNDB::VSTAT[$_]</a></li>|
- } 0..$#$VNDB::VSTAT).
- qq|</ul><ul class="full">|.
- ($r->{rlist} ? qq|<li class="center"><a href="/r$$r{id}/list?d=1">remove from my list</a></li>|
- : qq|<li class="center"><b>not in your list</b></li>|).
- qq|</ul></div>|;
-}
-
-
-my %pagetitles = (
- faq => 'Frequently Asked Questions',
- userlogin => 'Login',
- userreg => 'Register a new account',
- userpass => 'Forgot your password?',
- home => 'Visual Novel Database',
- pbrowse => 'Browse producers',
- userlist => 'Browse users',
- tindex => 'Discussion board index',
- ttag => sub {
- return ($p{ttag}{obj} ? 'Related discussions for ' : '').$p{ttag}{title} },
- tthread => sub {
- return $p{tthread}{t}{title} },
- tedit => sub {
- return $p{tedit}{p} ? 'Edit post' :
- $p{tedit}{t} ? 'Reply to thread' : 'Start a new thread' },
- userpage => sub {
- return 'User: '.$p{userpage}{user}{username} },
- vnlist => 'My visual novel list (old)',
- wlist => sub {
- return $p{wlist}{user}{username} eq $p{AuthUsername} ? 'My wishlist' : ($p{wlist}{user}{username}.'\'s wishlist'); },
- rlist => sub {
- return $p{rlist}{user}{username} eq $p{AuthUsername} ? 'My visual novel list' : ($p{rlist}{user}{username}.'\'s visual novel list'); },
- useredit => sub {
- return !$p{useredit}{adm} ? 'My account' : 'Edit '.$p{useredit}{form}{username}.'\'s account'; },
- ppage => sub {
- return $p{ppage}{prod}{name} },
- pedit => sub {
- return $p{pedit}{id} ? sprintf('Edit %s', $p{pedit}{form}{name}) : 'Add a new producer'; },
- vnedit => sub {
- return $p{vnedit}{id} ? sprintf('Edit %s', $p{vnedit}{form}{title}) : 'Add a new visual novel'; },
- redit => sub {
- return $p{redit}{id} ? sprintf('Edit %s', $p{redit}{rel}{title}) : sprintf('Add release to %s', $p{redit}{vn}{title}); },
- vnpage => sub { return $p{vnpage}{vn}{title}; },
- vnrg => sub { return 'Relations for '.$p{vnrg}{vn}{title} },
- vnstats => sub { return 'User statistics for '.$p{vnstats}{vn}{title} },
- vnbrowse => sub {
- return $p{vnbrowse}{chr} eq 'search' ? 'Visual novel search' :
- $p{vnbrowse}{chr} eq 'mod' ? 'Visual Novels awaiting moderation' :
- $p{vnbrowse}{chr} eq 'all' ? 'Browse all visual novels' :
- $p{vnbrowse}{chr} eq '0' ? 'Browse by char: Other' :
- sprintf 'Browse by char: %s', uc $p{vnbrowse}{chr}; },
- rpage => sub {
- return $p{rpage}{rel}{romaji} || $p{rpage}{rel}{title} },
- hist => sub {
- return !$p{hist}{id} || !$p{hist}{type} ? 'Recent changes' :
- $p{hist}{type} eq 'u' ? 'Recent changes by '.$p{hist}{title} : 'Edit history of '.$p{hist}{title}; },
- docs => sub { $p{docs}{title} },
- error => sub {
- $p{error}{err} eq 'notfound' ? '404 Page Not Found' : 'Error Parsing Form' },
-);
-sub gettitle{$p{$_}&&($p{PageTitle}=ref($pagetitles{$_}) eq 'CODE' ? &{$pagetitles{$_}} : $pagetitles{$_}) for (keys%pagetitles);}
-
-
-#
-# F O R M E R R O R H A N D L I N G
-#
-my %formerr_names = (
- # this list is rather incomplete...
- mail => 'Email',
- username => 'Username',
- userpass => 'Password',
- pass1 => 'Password',
- pass2 => 'Password (second)',
- title => 'Title',
- desc => 'Description',
- rel => 'Relation',
- romaji => 'Romanized title',
- lang => 'Language',
- web => 'Website',
- released => 'Release date',
- platforms => 'Platforms',
- media => 'Media',
- name => 'Name',
- vn => 'Visual novel relations',
- l_vnn => 'Visual-novels.net link',
- comm => 'Edit summary',
- msg => 'Message',
-);
-my @formerr_msgs = (
- sub { return sprintf 'Field "%s" is required.', @_ },
- sub { return sprintf '%s should have at least %d characters.', @_ },
- sub { return sprintf '%s is too large! Only %d characters allowed.', @_ },
- sub { return
- $_[1] eq 'mail' ? 'Invalid email address' :
- $_[1] eq 'url' ? 'Invalid URL' :
- $_[1] eq 'pname' ? sprintf('%s can only contain alfanumeric characters!', $_[0]) :
- $_[1] eq 'asciiprint' ? sprintf('Only ASCII characters are allowed at %s', $_[0]) :
- $_[1] eq 'int' ? sprintf('%s should be a number!', $_[0]) :
- $_[1] eq 'gtin' ? 'Not a valid JAN, UPC or EAN code!' : '';
- },
- sub { return sprintf '%s: invalid item selected', @_ },
- sub { return 'Invalid unicode, are you sure your browser works fine?' },
-);
-my %formerr_exeptions = (
- loginerr => 'Invalid username or password',
- badpass => 'Passwords do not match',
- usrexists => 'Username already exists, please choose an other one',
- mailexists => 'There already is a user with that email address, please request a new password if you forgot it',
- nomail => 'No user found with that email address',
- nojpeg => 'Image is not in JPEG or PNG format!',
- toolarge => 'Image is too large (in filesize), try to compress it a little',
- wrongtag => 'Wrong tag selected!',
-);
-sub formerr {
- my @err = ref $_[0] eq 'ARRAY' ? @{$_[0]} : ();
- return '' if $#err < 0;
- my @msgs;
- my $ret = '<span class="warning">
- Error:<ul>';
- $ret .= sprintf " <li>%s</li>\n",
- /^([a-z0-9_]+)-([0-9]+)(?:-(.+))?$/ ? &{$formerr_msgs[$2-1]}($formerr_names{$1}||$1, $3||'') : $formerr_exeptions{$_}
- foreach (@err);
- $ret .= "</ul>\n</span>\n";
-}
-
-#
-# F O R M C R E A T I N G
-#
-
-# args = [
-# {
-# type => $type,
-# %options
-# }, ...
-# ], $formobj
-#
-# $type $formobj %options ( required, [ optional ] )
-# error X ( )
-# startform ( action, [ upload ] )
-# endform ( )
-# input X ( short, name, [ class, default ] )
-# pass ( short, name )
-# upload ( short, name, [ class ] )
-# hidden X ( short, [ value ] )
-# textarea X ( short, name, [ rows, cols, class ] )
-# select X ( short, name, options, [ class ] ) # options = arrayref of hashes with keys: short, name
-# as X ( name )
-# trans X ( )
-# submit ( [ text, short ] )
-# sub ( title )
-# check X ( short, name, [ value ] )
-# static ( text, raw [ name, class ] )
-# date X ( short, name )
-#
-sub cform {
- my $obj = shift;
- my $frm = shift;
- my $ret = '';
- my $csub = '';
- for (@$obj) {
- $_->{class} ||= '';
- $_->{class} .= ' sf_'.$csub if $csub && $_->{class} !~ /nohid/;
- $_->{class} .= ' formhid' if $csub && $frm->{_hid} && !$frm->{_hid}{$csub} && $_->{class} !~ /nohid/;
- $_->{name} = '<i>*</i> '.$_->{name} if $_->{r};
-
- # error
- if($_->{type} eq 'error') {
- $ret .= formerr($frm->{_err});
- # startform
- } elsif($_->{type} eq 'startform') {
- $ret .= sprintf qq|<form action="/nospam?%s" method="post" accept-charset="utf-8"%s>\n|,
- $_->{action}, $_->{upload} ? ' enctype="multipart/form-data"' : '';
- $ret .= sprintf qq| <input type="hidden" class="hidden" name="fh" id="_hid" value="%s" />\n|,
- $frm->{_hid} ? _hchar(join(',', keys %{$frm->{_hid}})) : '' if $_->{fh};
- $ret .= qq|<p class="formnotice">Items denoted by a red asterisk (<i>*</i>) are required.</p>\n|
- if scalar grep { $_->{r} } @$obj;
- $ret .= "<ul>\n";
- # endform
- } elsif($_->{type} eq 'endform') {
- $ret .= qq|</ul></form>\n|;
- # input
- } elsif($_->{type} eq 'input') {
- $ret .= sprintf qq|<li%s>\n <label for="%s">%s</label>\n %s<input type="text" class="text" name="%2\$s" id="%2\$s" value="%s" />%s\n</li>\n|,
- $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{short}, $_->{name}, $_->{pre} ? '<i>'.$_->{pre}.'</i>' : '',
- _hchar($frm->{$_->{short}}?$frm->{$_->{short}}:$_->{default}), $_->{post} ? '<i>'.$_->{post}.'</i>' : '';
- # pass
- } elsif($_->{type} eq 'pass') {
- $ret .= sprintf qq|<li%s>\n <label for="%s">%s</label>\n <input type="password" class="text" name="%2\$s" id="%2\$s" />\n</li>\n|,
- $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{short}, $_->{name};
- # upload
- } elsif($_->{type} eq 'upload') {
- $ret .= sprintf qq|<li%s>\n <label for="%s">%s</label>\n <input type="file" class="text" name="%2\$s" id="%2\$s" />\n</li>\n|,
- $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{short}, $_->{name};
- # hidden
- } elsif($_->{type} eq 'hidden') {
- $ret .= sprintf qq| <input type="hidden" class="hidden" name="%s" id="%1\$s" value="%s" />\n|,
- $_->{short}, _hchar($_->{value} || $frm->{$_->{short}});
- # textarea
- } elsif($_->{type} eq 'textarea') {
- $ret .= sprintf qq|<li%s>\n <label for="%s">%s</label>\n <textarea name="%2\$s" id="%2\$s" rows="%s" cols="%s">%s</textarea>\n</li>\n|,
- $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{short}, $_->{name}, $_->{rows}||15, $_->{cols}||70, txt($frm->{$_->{short}});
- # select
- } elsif($_->{type} eq 'select') {
- $ret .= sprintf qq|<li%s>\n <label for="%s">%s</label>\n <select name="%2\$s" id="%2\$s">\n%s</select>\n</li>\n|,
- $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{short}, $_->{name}, eval {
- my $r='';
- for my $s (@{$_->{options}}) {
- $r .= sprintf qq| <option value="%s"%s>%s</option>\n|,
- $s->{short}, defined $frm->{$_->{short}} && $frm->{$_->{short}} eq $s->{short} ? ' selected="selected"' : '', $s->{name};
- }
- return $r;
- };
- # jssel
- } elsif($_->{type} eq 'jssel') {
- (my $oname = $_->{name}) =~ s/^<i>\*<\/i>//;
- $ret .= sprintf
- qq|<li%s>\n|
- .qq| <label for="%s_select">%s</label>\n|
- .qq| <select name="%s_select" id="%s_select" multiple="multiple" size="5" class="multiple">\n|
- .qq| <option value="0_new" style="font-style: italic">Add %s...</option>\n|
- .qq| </select>\n|
- .qq| <div id="%s_conts">\n|
- .qq| Loading...\n|
- .qq| </div>\n|
- .qq| <input type="hidden" name="%s" id="%s" class="hidden" value="%s" />\n|
- .qq|</li>\n|,
- $_->{class} ? ' class="'.$_->{class}.'"' : '',
- $_->{sh}, $_->{name}, $_->{sh}, $_->{sh}, $oname, $_->{sh}, $_->{short}, $_->{short}, _hchar($frm->{$_->{short}});
- # submit
- } elsif($_->{type} eq 'submit') {
- $ret .= sprintf qq|<li class="nolabel">\n <br /><input type="submit" class="submit" value="%s"%s />\n </li>\n|,
- $_->{text} || 'Verstuur', $_->{short} ? sprintf(' name="%s" id="%1$s"', $_->{short}) : '';
- # sub
- } elsif($_->{type} eq 'sub') {
- $ret .= sprintf qq|<li class="subform">\n <a href="#" class="s_%s">%s %s</a>\n</li>\n|,
- $_->{short}, $frm->{_hid} && !$frm->{_hid}{$_->{short}} ? '&#9656;' : '&#9662;', $_->{title};
- $csub = $_->{short};
- # check
- } elsif($_->{type} eq 'check') {
- $ret .= sprintf qq|<li class="nolabel%s">\n <input type="checkbox" name="%s" id="%2\$s" value="%s"%s />\n <label for="%2\$s" class="checkbox">%s</label>\n</li>\n|,
- $_->{class} ? ' '.$_->{class} : '',
- $_->{short}, $_->{value} || 'true', $frm->{$_->{short}} ? ' checked="checked"' : '', $_->{name};
- # static
- } elsif($_->{type} eq 'static') {
- $ret .= $_->{name}
- ? sprintf qq|<li%s>\n <label>%s</label>\n <p>%s</p>\n</li>|, $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{name}, $_->{text}
- : $_->{raw}
- ? sprintf qq|<li%s>\n %s\n</li>|, $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{text}
- : sprintf qq|<li class="nolabel%s">\n %s\n</li>|, $_->{class} ? ' '.$_->{class} : '', $_->{text};
- # date
- } elsif($_->{type} eq 'date') {
- $ret .= sprintf qq|<li class="date%s">\n <label for="%s">%s</label>\n|,
- $_->{class} ? ' '.$_->{class} : '', $_->{short}, $_->{name};
- $ret .= sprintf qq| <select name="%s" id="%s">\n%s</select>\n|,
- $_->{short}, $_->{short}, eval {
- my $r='';
- for my $s (0, 1980..((localtime())[5]+1905), 9999) {
- $r .= sprintf qq| <option value="%s"%s>%s</option>\n|,
- $s, $frm->{$_->{short}} && ($frm->{$_->{short}}[0]||0) == $s ? ' selected="selected"' : '',
- !$s ? '-year-' : $s < 9999 ? $s : 'TBA';
- }
- return $r;
- };
- $ret .= sprintf qq| <select name="%s" id="%s_m">\n%s</select>\n|,
- $_->{short}, $_->{short}, eval {
- my $r='';
- for my $s (0..12) {
- $r .= sprintf qq| <option value="%s"%s>%s</option>\n|,
- $s, $frm->{$_->{short}} && ($frm->{$_->{short}}[1]||0) == $s ? ' selected="selected"' : '',
- $s ? $Time::CTime::MonthOfYear[$s-1] : '-month-';
- }
- return $r;
- };
- $ret .= sprintf qq| <select name="%s" id="%s_d">\n%s</select>\n</li>\n|,
- $_->{short}, $_->{short}, eval {
- my $r='';
- for my $s (0..31) {
- $r .= sprintf qq| <option value="%s"%s>%s</option>\n|,
- $s, $frm->{$_->{short}} && ($frm->{$_->{short}}[2]||0) == $s ? ' selected="selected"' : '',
- $s ? $s : '-day-';
- }
- return $r;
- };
- }
- }
- return $ret;
-}
-
-]]
diff --git a/data/tpl/docs b/data/tpl/docs
deleted file mode 100644
index 6aeefe81..00000000
--- a/data/tpl/docs
+++ /dev/null
@@ -1,4 +0,0 @@
-<h2>[[: $p{PageTitle} ]]</h2>
-<div id="dpage">
-[[= $d{content} ]]
-</div>
diff --git a/data/tpl/error b/data/tpl/error
deleted file mode 100644
index 995293d1..00000000
--- a/data/tpl/error
+++ /dev/null
@@ -1,10 +0,0 @@
-<h2>[[: $p{PageTitle} ]]</h2>
-[[ if($d{err} eq 'notfound') { ]]
- The page you were looking for could not be found!
-
-[[ } elsif($d{err} eq 'formerr') { ]]
-<span class="warning">
- <b>Error:</b> The form could not be sent, please make sure you have Javascript
- enabled in your browser!
-</span>
-[[ } ]]
diff --git a/data/tpl/hist b/data/tpl/hist
deleted file mode 100644
index fd8863b4..00000000
--- a/data/tpl/hist
+++ /dev/null
@@ -1,102 +0,0 @@
-[[= $d{type} ? ttabs($d{type}, $d{obj}, 'hist') : '' ]]-
-<h2 class="rss">[[: $p{PageTitle} ]]</h2>
-[[ if($d{type} eq 'u' && $#{$d{hist}} < 0) { ]]
-<p>
- You haven't made any changes yet.
-</p>
-[[ } ]]
-<br /><br />
-
-[[
- my $url = !$d{type} ? '/hist' : '/'.$d{type}.$d{id}.'/hist';
- my $furl = $url.'?e='.$d{sele}.';t=';
- my $eurl = $url.'?t='.$d{selt}.';e=';
- my $purl = !$d{type}?$eurl.$d{sele}:$d{type} eq 'v' && $d{seli} ? $url.'?i=1' : $url;
- my $rurl = $url.'/rss.xml'.(!$d{type}?'?t='.$d{selt}.';e='.$d{sele}:$d{type} eq 'v' && $d{seli} ? '?i=1' : '');
- local $_ = $d{selt};
- my @fil = (
- /a/ ? 'all items' : '<a href="%sa">all items</a>',
- /v/ ? 'visual novels' : '<a href="%sv">visual novels</a>',
- /r/ ? 'releases' : '<a href="%sr">releases</a>',
- /p/ ? 'producers' : '<a href="%sp">producers</a>',
- );
- local $_ = $d{sele};
- my @edi = (
- /0/ ? 'all changes' : '<a href="%s0">all changes</a>',
- /2/ ? 'edits only' : '<a href="%s2">edits only</a>',
- /1/ ? 'newly created pages only' : '<a href="%s1">newly created pages only</a>',
- );
- local $_ = $d{seli};
- my @inc = (
- /0/ ? 'exclude' : '<a href="'.$url.'">exclude</a>',
- /1/ ? 'include' : '<a href="'.$url.'?i=1">include</a>',
- );
-]]
-
-[[ if(!$d{type}) { ]]-
-<p class="browse">
- [[= join(' | ', map { sprintf $_, $furl } @fil) ]]<br />
- [[= join(' | ', map { sprintf $_, $eurl } @edi) ]]<br /><br /><br />
-</p>
-[[ } if($d{type} eq 'v') { ]]-
-<p class="browse">
- ([[= join(' | ', @inc) ]]) edits of releases.
-</p>
-[[ } ]]
-
-[[ if($d{act} eq 'r') { ]]
-<span class="msg">
- Performed the mass-revert, please see the following list for details.
-</span>
-[[ } elsif($d{act} eq 'd') { ]]
-<span class="msg">
- The following edits have been completely deleted.
-</span>
-[[ } ]]-
-
-
-<a class="rss" href="[[= $rurl ]]">RSS</a>
-[[= pagebut($purl) ]]
-[[ if(0 and $p{Authmod} || $p{Authdel}) { ]]
-<form method="post" action="[[= $purl ]]" class="tblf">
-[[ } ]]
-<table id="thi">
- <thead><tr>
- <td class="tc1" colspan="2">Rev.</td>
- <td class="tc2">Date</td>
- [[ if($d{type} ne 'u' || $d{act}) { ]]-
- <td class="tc3">User</td>[[ } ]]-
- [[ if(!$d{type} || $d{type} eq 'u' || $d{act}) { ]]-
- <td class="tc4">Page</td>[[ } ]]-
- [[ if($d{type} && !$d{act}) { ]]-
- <td class="tc5">Summary</td>[[ } ]]-
- [[ if($d{act} eq 'r') { ]]-
- <td class="tc5">Action</td>[[ } ]]-
- [[ if(0 and $p{Authmod}) { ]]-
- <td class="tc6"><input type="checkbox" id="checkall" name="sel" value="all" /></td>[[ } ]]-
- </tr></thead>
-
- [[ for (@{$d{hist}}) { my $t = (qw|v r p|)[$_->{type}]; ]]-
- <tr>
- <td class="tc1_1"><a href="/[[= $t.$_->{iid}.'.'.$_->{rev} ]]">[[= $_->{rev} == 1 ? "<b>$t$_->{iid}</b>" : $t.$_->{iid} ]]</a></td>
- <td class="tc1_2"><a href="/[[= $t.$_->{iid}.'.'.$_->{rev} ]]">.[[= $_->{rev} == 1 ? '<b>'.$_->{rev}.'</b>' : $_->{rev} ]]</a></td>
- <td class="tc2">[[= formatdate('%Y-%m-%d %R', $_->{added}, 'dh') ]]</td>
- [[ if($d{type} ne 'u' || $d{act}) { ]]-
- <td class="tc3">[[= userstr $_ ]]</td>[[ } ]]-
- [[ if(!$d{type} || $d{type} eq 'u' || $d{act}) { ]]-
- <td class="tc4"><a href="/[[= $t.$_->{iid} ]].[[= $_->{rev} ]]" title="[[: $_->{ioriginal}||$_->{ititle} ]]">[[: shorten $_->{ititle}, 30 ]]</a></td>[[ } ]]-
- [[ if($d{type} && !$d{act}) { ]]-
- <td class="tc5">[[= summary($_->{comments}, $d{type} eq 'u' ? 40 : 60)||'[empty]' ]]</td>[[ } ]]-
- [[ if($d{act} eq 'r') { ]]-
- <td class="tc5">[[: $_->{_status} ]]</td>[[ } ]]-
- [[ if(0 and $p{Authmod} && !$d{act}) { ]]-
- <td class="tc6"><input type="checkbox" name="sel" value="[[= $_->{id} ]]" /></td>[[ } ]]-
- </tr>
- [[ } ]]
-
-</table>
-[[ if(0 and $p{Authmod}) { ]]<input type="submit" class="right" name="post" value="Mass revert" />[[ } ]]
-[[ if(0 and $p{Authdel}) { ]]<input type="submit" class="right" name="post" value="Mass delete" id="massdel" />[[ } ]]
-[[ if(0 and $p{Authmod} || $p{Authdel}) { ]]</form>[[ } ]]
-[[= pagebut($purl) ]]
-
diff --git a/data/tpl/home b/data/tpl/home
deleted file mode 100644
index 3b36246d..00000000
--- a/data/tpl/home
+++ /dev/null
@@ -1,66 +0,0 @@
-<h2>Welcome to VNDB - The Visual Novel Database!</h2>
-<p class="desc">
- <br />
- VNDB.org strives to be a comprehensive database for information about visual novels and
- eroge.<br />
- This website is built as a wiki, meaning that anyone can freely add and contribute information
- to the database, allowing us to create the largest, most accurate and most up-to-date visual novel
- database on the web.<br />
- Registered users are also able to keep track of a personal list of games they want to play or have finished
- and they can vote on all visual novels.<br /><br />
-
- Feel free to <a href="/v">browse around</a>, <a href="/u/register">register an account</a>
- or to participate in the discussions about visual novels or VNDB on our <a href="/t">discussion board</a>.
-</p>
-
-[[ if($d{an}{title}) { ]]-
-<h3 class="home">[[: $d{an}{title} ]]-
- <p class="actions">[[= age $d{anpost}{date} ]]</p></h3>
-<p class="desc">
- [[= summary $d{anpost}{msg}, 200 ]]
- <br />
- <a href="/t[[= $d{an}{id} ]]">Read more...</a> - <a href="/t/an">news archive</a>.
-</p>
-[[ } ]]-
-
-<ul class="home">
- <li><b>Recent changes</b></li>
- [[ for (@{$d{recentedits}}) { my $t = (qw|v r p|)[$_->{type}]; ]]-
- <li>[[= $t ]]:<a href="/[[= $t.$_->{iid}.'.'.$_->{rev} ]]" title="[[: $_->{ioriginal}||$_->{ititle} ]]">[[: shorten $_->{ititle}, 30 ]]</a></li>
- [[ } ]]-
-</ul>
-
-<ul class="home">
- <li><b>Recent posts</b></li>
- [[ for (@{$d{recentposts}}) { ]]-
- <li><a href="/t[[= $_->{id}.'.'.$_->{count} ]]" title="[[: $_->{original}||$_->{title} ]]">[[: shorten $_->{title}, 25 ]]</a> <i>[[= age $_->{ldate}, 1 ]]</i></li>
- [[ } ]]-
-</ul>
-
-<ul class="home">
- <li><b>Upcoming releases</b></li>
- [[ for (@{$d{upcomingrel}}) { ]]-
- <li>[[= datestr $_->{released}, 1 ]]- <a href="/r[[= $_->{id} ]]" title="[[: $_->{original}||$_->{title} ]]">[[: shorten $_->{title}, 25 ]]</a></i></li>
- [[ } ]]-
-</ul>
-
-<ul class="home break">
- <li><b>Recently added visual novels</b></li>
- [[ for (@{$d{recentvns}}) { ]]-
- <li><a href="/v[[= $_->{iid} ]]" title="[[: $_->{ioriginal}||$_->{ititle} ]]">[[: shorten $_->{ititle}, 30 ]]</a></li>
- [[ } ]]-
-</ul>
-
-<ul class="home">
- <li><b>Random visual novels</b></li>
- [[ for (@{$d{randomvns}}) { ]]-
- <li><a href="/v[[= $_->{id} ]]" title="[[: $_->{original}||$_->{title} ]]">[[: shorten $_->{title}, 30 ]]</a></li>
-[[ } ]]-
-</ul>
-
-<ul class="home">
- <li><b>Just released</b></li>
- [[ for (@{$d{justrel}}) { ]]-
- <li>[[= datestr $_->{released}, 1 ]]- <a href="/r[[= $_->{id} ]]" title="[[: $_->{original}||$_->{title} ]]">[[: shorten $_->{title}, 25 ]]</a></i></li>
- [[ } ]]-
-</ul>
diff --git a/data/tpl/main b/data/tpl/main
deleted file mode 100644
index 7f1b65f4..00000000
--- a/data/tpl/main
+++ /dev/null
@@ -1,163 +0,0 @@
-[[+ defs.pl ]]
-[[ %p = %{$X->{page}}; gettitle(); ]]
-
-<!DOCTYPE html
- PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-
-<head>
- <title>[[: $p{PageTitle} ]]- :: VNDB</title>
- <link href="[[: $p{st} ]]/files/style.css?[[= $VNDB::VERSION ]]" rel="stylesheet" type="text/css" media="screen" />
- <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
-[[ if($p{redit} || $p{vnedit}) { ]]-
- <script src="[[: $p{st} ]]/files/dyna.js?[[= $VNDB::VERSION ]]" type="text/javascript"></script>
-[[ } ]]-
- <script src="[[: $p{st} ]]/files/def.js?[[= $VNDB::VERSION ]]" type="text/javascript"></script>
-[[ if(0 && $p{devshit}) { ]]-
- <link rel="icon" href="/favicon.gif" type="image/gif" />
- <meta name="robots" content="noindex, nofollow" />
-[[ } elsif(
- grep($p{$_}, qw| userlist userpage userlogin userreg userpass vnlist rlist wlist hist |)
- || ($p{ttag} && $p{ttag}{iid})
- || ($p{vnpage} && $p{vnpage}{page} eq 'stats')
- || grep { $p{$_} && $p{$_}{change} } qw|vnpage ppage rpage|
- ) { ]]-
- <meta name="robots" content="noindex, follow" />
-[[ }]]-
-
-[[if($p{hist}){ ]]
- <link rel="alternate" type="application/rss+xml" title="Recent changes" href="
- [[= (!$p{hist}{type}?'/hist':'/'.$p{hist}{type}.$p{hist}{id}.'/hist').'/rss.xml'.(!$p{hist}{type}?'?t='.$p{hist}{selt}.';e='.$p{hist}{sele}:$p{hist}{type} eq 'v' && $p{hist}{seli} ? '?i=1':'') ]]" />
-[[ } ]]-
-</head>
-
-<body>
-<div id="header">
- <form id="search" method="get" action="/v/search">
- <fieldset>
- <legend>Search</legend>
- <input id="searchfield" type="text" name="sq" value="search" style="color: #999" />
- <input id="searchsubmit" type="submit" value="Search" />
- </fieldset>
- </form>
- <h1><a href="http://vndb.org/">vndb.org</a> / #vndb @ irc.synirc.net <a href="/">
- [[ if($p{devshit}) { ]]<b style="color: red">The VNDB.org Testing Grounds</b>[[ } else { ]]<b>The Visual Novel Database</b>[[ } ]]</a></h1>
-</div>
-
-
-<div id="page">
-
-<div id="content">
-[[ # = noindex-tag (see above) ]]
-[[ if($p{home}) { %d = %{$p{home}}; ]] [[+ home ]][[ } ]]
-[[ if($p{userlogin}) { %d = %{$p{userlogin}}; ]] [[+ userlogin ]][[ } ]]
-[[ if($p{userreg}) { %d = %{$p{userreg}}; ]] [[+ userreg ]][[ } ]]
-[[ if($p{userpass}) { %d = %{$p{userpass}}; ]] [[+ userpass ]][[ } ]]
-[[ if($p{useredit}) { %d = %{$p{useredit}}; ]] [[+ useredit ]][[ } ]]
-[[ if($p{userlist}) { %d = %{$p{userlist}}; ]] [[+ userlist ]][[ }# ]]
-[[ if($p{userpage}) { %d = %{$p{userpage}}; ]] [[+ userpage ]][[ }# ]]
-[[ if($p{vnpage}) { %d = %{$p{vnpage}}; ]] [[+ vnpage ]][[ } ]]
-[[ if($p{vnedit}) { %d = %{$p{vnedit}}; ]] [[+ vnedit ]][[ } ]]
-[[ if($p{redit}) { %d = %{$p{redit}}; ]] [[+ redit ]][[ } ]]
-[[ if($p{vnbrowse}) { %d = %{$p{vnbrowse}}; ]] [[+ vnbrowse ]][[ } ]]
-[[ if($p{pbrowse}) { %d = %{$p{pbrowse}}; ]] [[+ pbrowse ]][[ } ]]
-[[ if($p{pedit}) { %d = %{$p{pedit}}; ]] [[+ pedit ]][[ } ]]
-[[ if($p{ppage}) { %d = %{$p{ppage}}; ]] [[+ ppage ]][[ } ]]
-[[ if($p{vnlist}) { %d = %{$p{vnlist}}; ]] [[+ vnlist ]][[ }# ]]
-[[ if($p{rlist}) { %d = %{$p{rlist}}; ]] [[+ rlist ]][[ }# ]]
-[[ if($p{wlist}) { %d = %{$p{wlist}}; ]] [[+ wlist ]][[ }# ]]
-[[ if($p{hist}) { %d = %{$p{hist}}; ]] [[+ hist ]][[ }# ]]
-[[ if($p{rpage}) { %d = %{$p{rpage}}; ]] [[+ rpage ]][[ } ]]
-[[ if($p{docs}) { %d = %{$p{docs}}; ]] [[+ docs ]][[ } ]]
-[[ if($p{tindex}) { %d = %{$p{tindex}}; ]] [[+ tindex ]][[ } ]]
-[[ if($p{ttag}) { %d = %{$p{ttag}}; ]] [[+ ttag ]][[ }# ]]
-[[ if($p{tthread}) { %d = %{$p{tthread}}; ]] [[+ tthread ]][[ } ]]
-[[ if($p{tedit}) { %d = %{$p{tedit}}; ]] [[+ tedit ]][[ } ]]
-[[ if($p{error}) { %d = %{$p{error}}; ]] [[+ error ]][[ } ]]
-</div>
-
-
-<div id="side"><div><div>
-
- <h2>Menu</h2>
- <ul>
- <li><a href="/">Home</a></li>
- <li><a href="/v">Browse</a> | <a href="/v/search">Search</a></li>
- <li><a href="/p">Producers</a></li>
- <li><a href="/u/list">Users</a></li>
- <li><a href="/hist">Recent changes</a></li>
- <li><a href="/t">Discussion board</a></li>
- <li><a href="/d6">FAQ</a></li>
- </ul>
-
--[[ if(!$p{AuthLoggedin}) { ]]-
- <h2>Login</h2>
- <form method="post" action="/nospam?/u/login" id="loginform">
- <fieldset>
- <legend>Login</legend>
- <input type="text" id="usrname" name="username" />
- <input type="password" id="usrpass" name="userpass" />
- <input type="submit" value="Login" />
- </fieldset>
- </form>
- <p>
- <a href="/u/register">register</a> or <a href="/u/newpass">forgot password?</a>
- </p>
-[[ } else { ]]-
- <h2>User menu</h2>
- <ul>
- <li><a href="/u[[= $p{AuthId} ]]">[[: $p{AuthUsername} ]]</a> ([[: $p{AuthRankname} ]])</li>
- [[ if($p{AuthId} && $p{AuthId} == 364) { ]]
- <li><a href="/t61" style="color: red; font-size: 16px; font-weight: bold">READ THIS!</a></li>[[ } ]]
- <li><a href="/u[[= $p{AuthId} ]]/edit">My profile</a></li>
- <li><a href="/u[[= $p{AuthId} ]]/list">My visual novel list</a></li>
- [[ if($p{AuthOldList}) { ]]-
- <li><a href="/u[[= $p{AuthId} ]]/vlist">My visual novel list (old)</a></li>[[ } ]]-
- <li><a href="/u[[= $p{AuthId} ]]/wish">My wishlist</a></li>
- <li><a href="/t/u[[= $p{AuthId} ]]">My messages</a></li>
- <li><a href="/u[[= $p{AuthId} ]]/hist">My recent changes</a></li>
- [[ if($p{Authedit}) { ]]-
- <li>&nbsp;</li>
- <li><a href="/v/new">Add visual novel</a></li>
- <li><a href="/p/add">Add producer</a></li>
- [[ } ]]
- <li>&nbsp;</li>
- <li><a href="/u/logout">Logout</a></li>
- </ul>
-[[ } ]]-
-
--[[ #</div></div><div><div> ]]
- <h2>Statistics</h2>
- <ul>
- <li><b>[[= $p{Statvn}||0 ]]</b> visual novels</li>
- <li><b>[[= $p{Statproducers}||0 ]]</b> producers</li>
- <li><b>[[= $p{Statreleases}||0 ]]</b> releases</li>
- <li><b>[[= $p{Statusers}||0 ]]</b> users</li>
- </ul>
-[[ if(0) { ]] <h2>Most popular</h2>
- <ul>[[ for (@{$p{popular}}) { ]]-
- <li><a href="/v[[: $_->{id} ]]" title="[[: $_->{title} ]]">[[: length($_->{title})>30 ? (substr($_->{title}, 0, 27).'...') : $_->{title} ]]</a></li>[[ } ]]-
- <li class="more"><a href="/v/all?s=votes&amp;o=d">More...</a></li>
- </ul>[[ } ]]-
-</div></div></div>
-
-</div>
-
-<div id="footer">
- <p>
- vndb v[[: $VNDB::VERSION ]]- |
- <a href="/d7">about us</a> |
- <a href="mailto:contact@vndb.org">contact@vndb.org</a> (english only) |
- designed by <a href="http://www.freecsstemplates.org/">free css templates</a>.
- </p>
-</div>
-
--[[ if(0 && $p{devshit}) { ]]-
- <pre id="debug">SQL Queries used:<br />
-[[= $p{devshit} ]]
-</pre>
-[[ } ]]-
-
-</body>
-</html>
diff --git a/data/tpl/pbrowse b/data/tpl/pbrowse
deleted file mode 100644
index 8d2604c3..00000000
--- a/data/tpl/pbrowse
+++ /dev/null
@@ -1,45 +0,0 @@
-<h2>[[: $p{PageTitle} ]]</h2>
-<p class="chr">
- -[[= $d{chr} ne 'all' ? '<a href="/p/all">all</a>' : 'all' ]]- |
- [[ for('a'..'z', 0) { ]]-
- -[[ if($d{chr} eq $_) { ]][[= $_?$_:'#' ]][[ } else { ]]<a href="/p/[[= $_ ]]">[[= $_?$_:'#' ]]</a>[[ } ]]
- [[ } ]]-
- <form id="psearch" method="get" action="/p" accept-charset="UTF-8">
- <fieldset>
- <input type="text" name="q" id="q" value="[[: $d{query} ]]" class="text"
- /><input type="submit" value="Search!" />
- </fieldset>
- </form>
-</p>
-
--[[ if($#{$d{prods}} < 0) { ]]
-<p>
- No results again, life sucks... :'(
-</p>
-[[ } else {
- my $url = sprintf '/p/%s', $d{chr};
- $url .= '?q='.$d{query} if $d{query};
-]]
-[[= pagebut($url) ]]
-<table id="tpd">
- <thead><tr>
- <td class="tc1">Name</td>
- <td class="tc2">Type</td>
- <td class="tc3">Website</td>
- </tr></thead>
-[[ for (@{$d{prods}}) { ]]-
- <tr>
- <td class="tc1">
- <acronym class="icons lang -[[= $_->{lang} ]]" title="[[: $VNDB::LANG->{$_->{lang}} ]]">&nbsp;</acronym><a
- href="/p[[= $_->{id} ]]" title="[[: $_->{original}||$_->{name} ]]">[[: $_->{name} ]]</a></td>
- <td class="tc2">[[: $VNDB::PROT->{$_->{type}} ]]</td>
- <td class="tc3">
- [[ if($_->{website}) { ]]
- <a href="[[: $_->{website} ]]">[[: shorten $_->{website}, 50 ]]
- [[ } else { ]]---[[ } ]]
- </td>
- </tr>
-[[ } ]]-
-</table>
-[[= pagebut($url) ]]
-[[ } ]]
diff --git a/data/tpl/pedit b/data/tpl/pedit
deleted file mode 100644
index 56c06b92..00000000
--- a/data/tpl/pedit
+++ /dev/null
@@ -1,44 +0,0 @@
-[[= $d{id} ? ttabs('p', $d{prod}, 'edit') : '' ]]
-<h2>[[: $p{PageTitle} ]]</h2>
--[[ if(!$d{id}) { ]]
- <span class="msg">
- Please search the database before adding a new producer in order to prevent duplicate entries.
- </span>
-[[ } else { ]]
- <span class="msg">
- Please check the <a href="/t/p[[= $d{id} ]]">discussion board</a> <b>before</b> making
- any changes!
- </span>
-[[ } if($d{id} && $d{prod}{cid} != $d{prod}{latest}) { ]]
- <span class="warning">
- You are editing an old revision of this producer. If you save it, all changes made after
- -[[= formatdate('%Y-%m-%d %R', $d{prod}{added}) ]]- will be removed!
- </span>
-[[ } ]]
-
--[[= cform([
- { type => 'error' },
- { type => 'startform', action => $d{id} ? '/p'.$d{id}.'/edit' : '/p/add' },
-
- { type => 'sub', title => 'General info', short => 'info' },
- { type => 'select', name => 'Type', short => 'type', r=>1, options => [ map {
- { short => $_, name => $VNDB::PROT->{$_} } } sort keys %$VNDB::PROT ] },
- { type => 'input', name => 'Name (romaji)', short => 'name', r=>1 },
- { type => 'input', name => 'Original name', short => 'original' },
- { type => 'static', text => q|
- The original name of the producer, leave blank if it is already in the Latin alphabet.<br /><br />| },
-
- { type => 'select', name => 'Primary language', short => 'lang', r=>1, options => [ map {
- ({ short => $_, name => sprintf '%s (%s)', $_, $VNDB::LANG->{$_} }) } sort keys %{$VNDB::LANG} ] },
-
- { type => 'input', name => 'Website', short => 'website' },
- { type => 'textarea', name => 'Description', short => 'desc', rows => 7, cols => 60 },
-
- { type => 'sub', title => 'Edit summary', short => 'com' },
- { type => 'textarea', name => 'Edit summary', short => 'comm', rows => 3, cols => 60 },
- { type => 'static', text => 'Please explain your modifications and cite all sources.' },
-
- { type => 'submit', text => $d{id} ? 'Edit' : 'Add' },
- { type => 'endform' },
-
-], $d{form}) ]]
diff --git a/data/tpl/ppage b/data/tpl/ppage
deleted file mode 100644
index e0a1fa08..00000000
--- a/data/tpl/ppage
+++ /dev/null
@@ -1,55 +0,0 @@
-[[= ttabs('p', $d{prod}) ]]
-<h2>[[: $p{PageTitle} ]]</h2>
-[[ if($d{prod}{original}) { ]]<h3 class="alttitle">[[: $d{prod}{original} ]]</h3>[[ } ]]
-
-[[ if($d{prod}{hidden}) { ]]-
- <span class="warning">
- This item has been deleted from the database. File a request on the
- <a href="/t/p[[= $d{prod}{id} ]]">discussion board</a> to undelete this page.
- </span>
-[[ } ]]
-[[ if(!$d{prod}{hidden} || $p{Authdel}) { ]]-
-
-
-
-[[ if($d{change}) { ]]
-[[= cdiff($d{prev}, $d{prod},
- [ type => 'Type', sub { $VNDB::PROT->{$_[0]} } ],
- [ name => 'Name (romaji)', 1 ],
- [ original => 'Original name', 1 ],
- [ lang => 'Language', sub { $VNDB::LANG->{$_[0]} } ],
- [ website => 'Website', 1 ],
- [ desc => 'Description', 1, 1 ],
- ) ]]
-[[ } ]]
-
-<dl>
- <dt>Type</dt><dd>[[: $VNDB::PROT->{$d{prod}{type}} ]]</dd>
- <dt>Primary lang.</dt><dd>[[: $VNDB::LANG->{$d{prod}{lang}} ]]</dd>
-[[ if($d{prod}{website}) { ]]-
- <dt>Links</dt><dd><a href="[[: $d{prod}{website} ]]">Official homepage</a></dd>[[ } ]]-
-</dl>
-
--[[ if($d{prod}{desc}) { ]]
-<p>[[= summary($d{prod}{desc}) ]]<br /><br /></p>
-[[ } ]]
-
-
-<h3>Visual novel relations</h3>
-[[ if($#{$d{vn}} < 0) { ]]-
-<p>
- We have currently no visual novels related to this producer.
-</p>
-[[ } else { ]]-
-<ul>
- [[ for (@{$d{vn}}) { ]]-
- <li><a href="/v[[= $_->{id} ]]">[[: $_->{title} ]]</a>
- [[ if($_->{date} ne "0000-00-00") { ]]- ([[= datestr($_->{date}) ]])[[ } ]]
- </li>
- [[ } ]]-
-</ul>
-[[ } ]]
-
-
-
-[[ } ]]
diff --git a/data/tpl/redit b/data/tpl/redit
deleted file mode 100644
index 32d618e3..00000000
--- a/data/tpl/redit
+++ /dev/null
@@ -1,69 +0,0 @@
-[[= $d{id} ? ttabs('r', $d{rel}, 'edit') : ttabs('v', $d{vn}, 'edit') ]]-
-<h2>[[: $p{PageTitle} ]]</h2>
-
-[[ if($d{id}) { ]]
- <span class="msg">
- Please check the <a href="/t/v[[= $d{rel}{vn}[0]{vid} ]]">discussion board</a> <b>before</b> making
- any changes!
- </span>
-[[ } if($d{id} && $d{rel}{cid} != $d{rel}{latest}) { ]]
- <span class="warning">
- You are editing an old revision of this producer. If you save it, all changes made after
- -[[= formatdate('%Y-%m-%d %R', $d{rel}{added}) ]]- will be removed!
- </span>
-[[ } ]]
-
-[[= cform( [
- { type => 'error' },
- { type => 'startform', action => $d{id} ? sprintf('/r%d/edit', $d{rel}{id}) : '/v'.$d{vn}{id}.'/add', fh => 1 },
-
- { type => 'sub', title => 'General info', short => 'info' },
- { type => 'select', name => 'Type', short => 'type', r=>1, options => [ map {
- ({ short => $_, name => $VNDB::RTYP->[$_] }) } 0..$#{$VNDB::RTYP} ] },
-
- { type => 'input', name => 'Title (romaji)', short => 'title', r=>1 },
- { type => 'input', name => 'Original title', short => 'original' },
- { type => 'static', text => q|
- The original title of this release, leave blank if it already is in the Latin alphabet.<br /><br />| },
-
- { type => 'select', name => 'Language', short => 'language', r=>1, options => [ map {
- ({ short => $_, name => sprintf '%s (%s)', $_, $VNDB::LANG->{$_} }) } sort keys %{$VNDB::LANG} ] },
-
- { type => 'input', name => 'JAN/UPC/EAN', short => 'gtin' },
- { type => 'input', name => 'Official website', short => 'website' },
- { type => 'date', name => 'Release date', short => 'released' },
- { type => 'static', text => 'Leave month or day blank if they are unknown<br /><br />' },
- { type => 'select', name => 'Age rating', short => 'minage', options => [ map
- { { name => $VNDB::VRAGES->{$_}, short => $_ } } sort { $a <=> $b } keys %$VNDB::VRAGES ] },
- { type => 'textarea', name => 'Notes', short => 'notes', rows => 3, cols => 50 },
- { type => 'static', text => 'Miscellaneous notes/comments, information that does not fit in the above fields. E.g.: Censored/uncensored or for which releases this patch applies. Max. 250 characters.' },
-
- { type => 'sub', title => 'Platforms & Media', short => 'pnm' },
- { type => 'static', raw => 1, text => '<label>Platforms</label><ul class="platforms">'.join('', map { my $p = $_;
- '<li><input type="checkbox" name="platforms" value="'.$_.'" id="'.$_.'" '.
- (($d{form}{platforms} && grep { $p eq $_ } @{$d{form}{platforms}}) ? 'checked="checked" ':'').'/>'.
- '<label for="'.$_.'"><acronym class="icons '.$_.'" title="'.$VNDB::PLAT->{$_}.'">&nbsp;</acronym>'.$VNDB::PLAT->{$_}.'</label></li>'
- } sort { $VNDB::PLAT->{$a} cmp $VNDB::PLAT->{$b} } keys %$VNDB::PLAT).'</ul>' },
-
- { type => 'static', text => '<br />' },
- { type => 'jssel', name => 'Media', sh => 'md', short => 'media' },
-
- { type => 'sub', title => 'Producers', short => 'prod' },
- { type => 'jssel', name => 'Producers', sh => 'pd', short => 'producers' },
-
- { type => 'sub', title => 'Visual novel relations', short => 'rel'},
- { type => 'jssel', name => 'Relations', sh => 'vn', short => 'vn', r=>1 },
- { type => 'static', text => q|
- Although a release usually contains only one visual novel, it is also possible
- for one release to include several games. Use this field to specify which
- visual novels are included in this release.| },
-
-
- { type => 'sub', title => 'Edit summary', short => 'com' },
- { type => 'textarea', name => 'Edit summary', short => 'comm', rows => 3, cols => 60 },
- { type => 'static', text => 'Please explain your modifications and cite all sources.' },
-
- { type => 'submit', text => $d{id} ? 'Edit' : 'Add' },
- { type => 'endform' },
-
-], $d{form}) ]]
diff --git a/data/tpl/rlist b/data/tpl/rlist
deleted file mode 100644
index f76eb65d..00000000
--- a/data/tpl/rlist
+++ /dev/null
@@ -1,90 +0,0 @@
-[[= ttabs('u', $d{user}, 'list') ]]
-<h2>[[: $p{PageTitle} ]]</h2>
-
-[[
- my $url = '/u'.$d{user}{id}.'/list';
- my $surl = sprintf '%s?s=%s;o=%s', $url, $d{order}[0], $d{order}[1]; # main URL + order
- my $purl = "$surl;c=$d{char}"; # full URL - page
- my $sourl = "$url?c=$d{char}"; # full URL - page & order
- my $curl = $surl; # full URL - character & page
- my $furl = "$surl;c=$d{char};p=$d{page}"; # full URL
-
- my $i=0;my $j=0;
-
-]]-
-
-<p class="chr">
- -[[= $d{char} ne 'all' ? '<a href="'.$curl.';c=all">all</a>' : 'all' ]]- |
- [[ for('a'..'z', 0) { ]]-
- -[[ if($d{char} eq $_) { ]][[= $_?$_:'#' ]][[ } else { ]]<a href="[[= $curl.';c='.$_ ]]">[[= $_?$_:'#' ]]</a>[[ } ]]
- [[ } ]]-
- <br /><br />
-</p>
-
-
--[[ if(@{$d{list}}) { ]]
-[[= pagebut($surl) ]]
-[[ if($d{user}{username} eq $p{AuthUsername}) { ]]
-<form method="post" action="[[= $furl ]]" class="tblf">
-[[ } ]]-
-<table id="rli">
- <thead><tr>
- <td colspan="3" class="tc1">Title -[[= sortbut($sourl, 'title') ]]</td>
- <td class="tc2" id="relhidpar"><b id="relhidparb">&#9656;</b>Releases*</td>
- <td class="tc3">Vote -[[= sortbut($sourl, 'vote') ]]</td>
- </tr></thead>
-[[ for (@{$d{list}}) { $j=0; my $c = ' style="background-color: #'.($i++%2?'fff':'f5f5f5').'"'; ]]-
- <tr[[=$c]]>
- <td colspan="3" class="tc1"><a href="/v[[= $_->{vid} ]]" title="[[: $_->{original}||$_->{title} ]]">[[= shorten $_->{title}, 50 ]]</a></td>
- <td class="tc2[[= @{$_->{rels}} ? ' relhid" id="rh'.$_->{vid}.'"' : ' relnone"' ]]><b[[= @{$_->{rels}} ? ' id="rhd'.$_->{vid}.'"' : '' ]]>&#9656;</b>
- [[= grep $_->{rstat}==2, @{$_->{rels}} ]]/[[= grep $_->{vstat}==2, @{$_->{rels}} ]]/[[= @{$_->{rels}} ]]
- </td>
- <td class="tc3">[[= $_->{vote} || '-' ]]</td>
- </tr>
- [[ for (@{$_->{rels}}) { ]]-
- <tr class="relhid" id="rr[[= $_->{vid}.'-'.++$j ]]">
- <td class="tc1_1">[[= datestr $_->{released} ]]</td>
- <td class="tc1_2">
- <acronym class="icons lang -[[= $_->{language} ]]" title="[[: $$VNDB::LANG{$_->{language}} ]]">&nbsp;</acronym><acronym
- title="[[= $VNDB::RTYP->[$_->{type}] ]]- release" class="icons -[[= lc substr($VNDB::RTYP->[$_->{type}],0,3) ]]">&nbsp;</acronym>
- </td>
- <td class="tc1_3"><a href="/r[[= $_->{rid} ]]" title="[[: $_->{original}||$_->{title} ]]">[[= shorten $_->{title}, 60 ]]</a></td>
- <td class="tc1_4">
- <acronym title="[[: $VNDB::RSTAT->[$_->{rstat}] ]]" class="uicons r[[= $_->{rstat} ]]">&nbsp;</acronym><acronym
- title="[[: $VNDB::VSTAT->[$_->{vstat}] ]]" class="uicons v[[= $_->{vstat} ]]">&nbsp;</acronym>
- </td>
- <td class="tc1_5">[[ if($d{user}{username} eq $p{AuthUsername}) { ]]<input type="checkbox" name="rsel" value="[[= $_->{rid} ]]" />[[ } else { ]]&nbsp;[[ } ]]</td>
- </tr>
- [[ } ]]
-[[ } ]]-
-</table>
-[[ if($d{user}{username} eq $p{AuthUsername}) { ]]
-<select id="vnlistchange" name="vnlistchange" class="right">
- <option value="n">- with selected -</option>
- <option value="d">Delete</option>
- <optgroup label="Update release status:">
- [[ for (0..$#$VNDB::RSTAT) { ]]-
- <option value="r[[= $_ ]]">[[: $VNDB::RSTAT->[$_] ]]</option>
- [[ } ]]
- </optgroup>
- <optgroup label="Update play status:">
- [[ for (0..$#$VNDB::VSTAT) { ]]-
- <option value="v[[= $_ ]]">[[: $VNDB::VSTAT->[$_] ]]</option>
- [[ } ]]
- </optgroup>
-</select>
-</form>
-[[ } ]]
-[[= pagebut($surl) ]]
-<p>
- <br />
- <b>*</b> Obtained/finished/total.
-</p>
-
-
-
-[[ } else { ]]-
-<p>
- No results found.
-</p>
-[[ } ]]
diff --git a/data/tpl/rpage b/data/tpl/rpage
deleted file mode 100644
index d31ffe24..00000000
--- a/data/tpl/rpage
+++ /dev/null
@@ -1,68 +0,0 @@
-[[= ttabs('r', $d{rel}) ]]
-<h2>[[: $p{PageTitle} ]]</h2>
-[[ if($d{rel}{original}) { ]]<h3 class="alttitle">[[: $d{rel}{original} ]]</h3>[[ } ]]
-
-[[ if($d{rel}{hidden}) { ]]-
- <span class="warning">
- This item has been deleted from the database. File a request on the
- <a href="/t/v[[= $d{rel}{vn}[0]{vid} ]]">discussion board</a> to undelete this page.
- </span>
-[[ } ]]
-[[ if(!$d{rel}{hidden} || $p{Authdel}) { ]]-
-
--[[ if($p{AuthLoggedin}) { ]]
-<p class="mod">&lt;
-<a href="/u[[= $p{AuthId} ]]/list" rel="rlistDD[[= $d{rel}{id} ]]" class="dropdown">
-[[= !$d{rel}{rlist} ? 'not in your list' : "$$VNDB::RSTAT[$d{rel}{rlist}{rstat}] / $$VNDB::VSTAT[$d{rel}{rlist}{vstat}]" ]]
-</a> &gt;</p>
-[[= rlist_dd($d{rel}) ]]
-[[ } ]]-
-
-
--[[ if($d{change}) { ]]
-[[= cdiff($d{prev}, $d{rel},
- [ vn => 'Relations', sub { join("<br />\n", map { $_->{title} } @{$_[0]}) } ],
- [ type => 'Type', sub { $VNDB::RTYP->[$_[0] ] } ],
- [ title => 'Title', 1 ],
- [ original => 'Orig. title', 1 ],
- [ gtin => 'JAN/UPC/EAN', 1 ],
- [ language => 'Language', sub { $VNDB::LANG->{$_[0]} } ],
- [ website => 'Website', \&summary ],
- [ released => 'Release date', \&datestr ],
- [ minage => 'Age rating', sub { $VNDB::VRAGES->{$_[0]} } ],
- [ notes => 'Notes', 1, 1 ],
- [ platforms => 'Platforms', sub { join(', ', sort @{$_[0]}) } ],
- [ media => 'Media', \&mediastr ],
- [ producers => 'Producers', sub { join(', ', map { _hchar($_->{name}) } sort { $a->{name} cmp $b->{name} } @{$_[0]}) } ],
- ) ]]
-[[ } ]]
-
-<dl>
- <dt>Relation</dt><dd>[[= join('<br />', map { '<a href="/v'.$_->{vid}.'" title="'._hchar($_->{original}||$_->{title}).'">'._hchar($_->{title}).'</a>' } @{$d{rel}{vn}}) ]]</dd>
- <dt>Type</dt><dd>[[: $VNDB::RTYP->[$d{rel}{type}] ]]</dd>
- <dt>Language</dt><dd>[[: $VNDB::LANG->{$d{rel}{language}} ]]</dd>
- <dt>Release date</dt><dd>[[= datestr($d{rel}{released}) ]]</dd>
-[[ if($d{rel}{gtin}) { ]]-
- <dt>[[: VNDB::GTINType($d{rel}{gtin}) ]]- code</dt><dd>[[= $d{rel}{gtin} ]]</dd>[[ } ]]-
-[[ if($d{rel}{minage} >= 0) { ]]-
- <dt>Age rating</dt><dd>[[: $VNDB::VRAGES->{$d{rel}{minage}} ]]</dd>[[ } ]]-
-[[ if($#{$d{rel}{producers}} >= 0) { ]]-
- <dt>Producer[[: $#{$d{rel}{producers}} > 0 ? 's' : '' ]]</dt><dd>[[= join(', ', map {
- sprintf('<a href="/p%d" title="'._hchar($_->{original}||$_->{name}).'">%s</a>', $_->{id}, _hchar($_->{name})) } @{$d{rel}{producers}})
- ]]</dd>[[ } ]]-
-[[ if($#{$d{rel}{platforms}} >= 0) { ]]-
- <dt>Platform[[: $#{$d{rel}{platforms}} > 0 ? 's' : '' ]]</dt><dd>[[: join(', ', map {
- $VNDB::PLAT->{$_} } @{$d{rel}{platforms}}) ]]</dd>[[ } ]]-
-[[ if($#{$d{rel}{media}} >= 0) { ]]-
- <dt>Medi[[: $#{$d{rel}{media}} > 0 ? 'a' : 'um' ]]</dt><dd>[[: mediastr($d{rel}{media}) ]]</dd>[[ } ]]-
-[[ if($d{rel}{website}) { ]]-
- <dt>Links</dt><dd><a href="[[: $d{rel}{website} ]]">Official website</a></dd>[[ } ]]-
-</dl>
-
-[[ if($d{rel}{notes}) { ]]-
-<p>[[= summary($d{rel}{notes}) ]]<br /><br /></p>
-[[ } ]]-
-
-
-
-[[ } ]]
diff --git a/data/tpl/tedit b/data/tpl/tedit
deleted file mode 100644
index 5953ead0..00000000
--- a/data/tpl/tedit
+++ /dev/null
@@ -1,33 +0,0 @@
-<h2>[[: $p{PageTitle} ]]</h2>
-[[ if($d{tag}) { my($type, $iid) = ($1, $2||0) if $d{tag} =~ /^([a-z]{1,2})([0-9]*)$/; ]]-
-<p>
- <a href="/t">Discussion board</a>
- &gt; <a href="/t/[[= $type ]]">[[: $VNDB::DTAGS->{$type} ]]</a>
- [[ if($iid) { ]]-
- &gt; <a href="/t/[[= $d{tag} ]]">[[: $d{tag} ]]</a>[[ } ]]-
-</p>
-[[ } ]]
-
--[[= cform( [
- { type => 'error' },
- { type => 'startform', action => $d{p} ? '/t'.$d{t}{id}.'.'.$d{p}{num}.'/edit' : $d{t} ? '/t'.$d{t}{id}.'/reply' : '/t/'.$d{tag}.'/new' },
- { type => 'static', name => 'Username', text => '<a href="/u'.($d{p}?$d{p}{uid}:$p{AuthId}).'">'.($d{p}?$d{p}{username}:$p{AuthUsername}).'</a>' },
- $d{t} && !($d{p} && $d{p}{num} == 1) ? (
- { type => 'static', name => 'Topic', text => '<a href="/t'.$d{t}{id}.'">'.$d{t}{title}.'</a>.' }
- ) : (
- { type => 'input', short => 'title', name => 'Thread title' },
- { type => 'input', short => 'tags', name => 'Tags' },
- { type => 'static', text => 'Read <a href="/d9.2">d9.2</a> for information about how to use tags' },
- $p{Authboardmod} ? (
- { type => 'check', short => 'lock', name => 'Locked' },
- ) : (),
- ),
- $p{Authboardmod} ? (
- { type => 'check', short => 'hide', name => 'Hidden' },
- ) : (),
- { type => 'textarea', short => 'msg', name => 'Message', rows => 10, cols => 60 },
- { type => 'static', text => 'It is possible to include formatting and ID codes, see <a href="/d9.3">d9.3</a> for more information.' },
- { type => 'submit', text => 'Submit' },
- { type => 'endform' },
-], $d{form}) ]]-
-
diff --git a/data/tpl/tindex b/data/tpl/tindex
deleted file mode 100644
index cb9cd154..00000000
--- a/data/tpl/tindex
+++ /dev/null
@@ -1,49 +0,0 @@
-<h2>[[: $p{PageTitle} ]]</h2>
-
-[[
- my %desc = (
- an => 'Yorhel\'s place to make useless announcements...',
- db => 'General discussions about VNDB.org. This is the place to be for feature requests and bug reports.',
- v => 'Discussions about the visual novels in the database',
- p => '...Producers',
- r => '...Releases',
- u => 'Messages to or discussions about other users on this site.'
- );
-]]
-
-
-[[ for my $tag (qw|an db v p u|) { ]]-
-
-<br />
-<h3>[[: $VNDB::DTAGS->{$tag} ]]</h3>
-[[ if(@{$d{$tag}}) { ]]-
-<table id="tin">
-[[ for (@{$d{$tag}}) { ]]-
- <tr>
- <td class="tc1">
- <a href="/t[[= $_->{id} ]]" title="[[= join ', ', sort map $$_[1]?$$_[0].$$_[1]:$$_[0], @{$_->{tags}} ]]">[[: $_->{title} ]]</a>
- [[ if($_->{locked}) { ]]- <b>[locked]</b>[[ } ]]
- </td>
- <td class="tc2">[[= $_->{count}-1 ]]</td>
- <td class="tc3">[[= userstr $_ ]]</td>
- <td class="tc4">[[= userstr $_->{luid}, $_->{lusername} ]]- @
- <a href="/t[[= $_->{id}.($_->{count}>$d{ppp}?'/'.ceil($_->{count}/$d{ppp}):'').'#'.$_->{count} ]]">
- [[= age $_->{ldate} ]]</a></td>
- </tr>
-[[ } ]]
-</table>
-<a class="right" href="/t/[[= $tag ]]">more...</a>
-[[ if($tag eq 'db') { ]]-
- <a class="right" href="/t/[[= $tag ]]/new" style="padding-right: 10px">start new thread</a>
-[[ } ]]
-
-[[ } else { ]]-
- <p>No threads yet.
- -[[ if($tag eq 'db') { ]]
- <a href="/t/[[= $tag ]]/new">Start a new thread.</a>
- [[ } ]]
- </p>
-[[ } ]]
-<br />
-
-[[ } ]]
diff --git a/data/tpl/ttag b/data/tpl/ttag
deleted file mode 100644
index 0d46ddf1..00000000
--- a/data/tpl/ttag
+++ /dev/null
@@ -1,60 +0,0 @@
-[[= $d{obj} ? ttabs($d{type}, $d{obj}, 'disc') : '' ]]-
-<h2>[[: $p{PageTitle} ]]</h2>
-<p>
- <a href="/t">Discussion board</a>
- &gt; <a href="/t/[[= $d{type} ]]">[[: $VNDB::DTAGS->{$d{type}} ]]</a>
- [[ if($d{obj}) { ]]-
- &gt; <b>[[= $d{tag} ]]</b>:<a href="/t/[[= $d{tag} ]]" title="[[: $d{original}||$d{title} ]]">[[: $d{title} ]]</a>[[ } ]]-
-</p>
-
-[[ if(@{$d{t}}) { ]]-
-
-[[ if(!$d{obj} && $d{tag} !~ /(an|db)/) {
- my @tags = grep $$_[0] eq $d{type}, map @{$_->{tags}}, @{$d{t}}; my %tags; my $i=0; ]]-
-<dl>
- <dt>Recent tags</dt>
- <dd>
- [[ for (@tags) { next if $tags{$$_[1]}++; last if $i++ == 5; ]]-
- <b>[[= $$_[0].$$_[1] ]]</b>:<a href="/t/[[= $$_[0].$$_[1] ]]">[[: $$_[2] ]]</a><br />[[ } ]]
- </dd>
-</dl>
-[[ } else { ]]
-<br />
-[[ } ]]
-
-
-[[= pagebut('/t/'.$d{tag}) ]]
-<table id="tin">
- <thead><tr>
- <td class="tc1">Topic</td>
- <td class="tc2">#</td>
- <td class="tc3">Starter</td>
- <td class="tc4">Last post</td>
- </tr></thead>
-[[ for (@{$d{t}}) { ]]-
- <tr>
- <td class="tc1">
- <a href="/t[[= $_->{id} ]]" title="[[= join ', ', sort map $$_[1]?$$_[0].$$_[1]:$$_[0], @{$_->{tags}} ]]">[[: $_->{title} ]]</a>
- [[ if($_->{locked}) { ]]- <b>[locked]</b>[[ } ]]
- </td>
- <td class="tc2">[[= $_->{count}-1 ]]</td>
- <td class="tc3">[[= userstr $_ ]]</td>
- <td class="tc4">[[= userstr $_->{luid}, $_->{lusername} ]]- @
- <a href="/t[[= $_->{id}.($_->{count}>$d{ppp}?'/'.ceil($_->{count}/$d{ppp}):'').'#'.$_->{count} ]]">
- [[= age $_->{ldate} ]]</a></td>
- </tr>
-[[ } ]]
-</table>
-[[= pagebut('/t/'.$d{tag}) ]]
-
-[[ } else { ]]
-<p><br />
- <b>No related threads found.</b>
-</p>
-[[ } ]]-
-
--[[ if($p{Authboard} && $d{tag} =~ /^(?:db|[vpru][0-9]+)$/) { ]]-
-<br />
-<a class="right" href="/t/[[= $d{tag} ]]/new">create a new thread</a>
-[[ } ]]
-
diff --git a/data/tpl/tthread b/data/tpl/tthread
deleted file mode 100644
index 0b67345c..00000000
--- a/data/tpl/tthread
+++ /dev/null
@@ -1,81 +0,0 @@
-<h2>[[: $p{PageTitle} ]]</h2>
-[[ if($d{t}{hidden}) { ]]
-<span class="warning">This thread has been deleted!</span>
-[[ } ]]
-
-<dl>
- <dt>Posted in</dt>
- <dd>
- [[ for (sort { $$a[0].$$a[1] cmp $$b[0].$$b[1] } @{$d{t}{tags}}) { ]]-
- <a href="/t/[[= $$_[0] ]]">[[: $VNDB::DTAGS->{$$_[0]} ]]</a>
- [[ if($$_[1]) { ]]-
- &gt; <b>[[= $$_[0].$$_[1] ]]</b>:<a href="/t/[[= $$_[0].$$_[1] ]]" title="[[: $$_[3]||$$_[2] ]]">[[: $$_[2] ]]</a>[[ } ]]-
- <br />
- [[ } ]]
- </dd>
-</dl>
-
-<br />
-
-[[
- my $pages='';
- my $lp = ceil($d{t}{count}/$d{ppp});
- if($d{t}{count} > $d{ppp}) {
- my @pages = (
- $d{page} == 1 ? '&lt;&lt;' : '<a href="/t%d">&lt;&lt;</a>',
- $lp > 2 ? (map
- $d{page} == $_ ? $_ : '<a href="/t%d/'.$_.'">'.$_.'</a>',
- 2..($lp-1) ) : (),
- $d{page} == $lp ? '&gt;&gt;' : '<a href="/t%d/'.$lp.'">&gt;&gt;</a>'
- );
- $pages = '<p class="browse">'.join(' &nbsp;', map sprintf($_,$d{t}{id}), @pages).'</p>';
- }
-]]
-
-[[= $pages ]]
-<table id="tth">
- [[ for (@{$d{p}}) { ]]-
- <tr>
- <td class="tc1">
- <a href="/t[[= $d{t}{id} ]].[[= $_->{num} ]]" name="[[= $_->{num} ]]">#[[= $_->{num} ]]</a>
- [[ if(!$_->{hidden}) { ]]-
- by -[[= userstr $_ ]]<br />
- <i>[[= formatdate('%Y-%m-%d %R', $_->{date}) ]]</i>
- [[ } ]]
- </td>
- <td class="tc2">
- [[ if($p{AuthId} == $_->{uid} && !$_->{hidden} || $p{Authboardmod}) { ]]-
- <p class="mod">&lt; <a href="/t[[= $d{t}{id}.'.'.$_->{num} ]]/edit">edit</a> &gt;</p>
- [[ } ]]
- [[ if(!$_->{hidden}) { ]]-
- [[= summary $_->{msg} ]]
- [[ if($_->{edited}) { ]]<br />
- <i>last modified -[[= formatdate('%Y-%m-%d %R', $_->{edited}) ]]</i>
- [[ } ]]
- [[ } else { ]]
- <b class="hidden">Post deleted.</b>
- [[ } ]]
- </td>
- </tr>
- [[ } ]]-
-</table>
-[[= $pages ]]
-
-<br />
-[[ if($lp == $d{page}) { ]]
- [[ if($d{t}{locked}) { ]]-
- <p>
- This thread has been locked, you can't reply anymore.
- </p>
- [[ } elsif(!$p{AuthLoggedin}) { ]]-
- <p>
- You need to be <a href="/u/login">logged in</a> to reply to this thread.
- </p>
- [[ } elsif($p{Authboard} && $lp == $d{page}) { ]]-
- <form action="/nospam?/t[[= $d{t}{id} ]]/reply" method="post" accept-charset="utf-8" id="qreply">
- <h3>Quick reply</h3>
- <textarea name="msg" id="msg" rows="5" cols="70"></textarea>
- <input type="submit" value="Post reply" />
- </form>
- [[ } ]]
-[[ } ]]
diff --git a/data/tpl/useredit b/data/tpl/useredit
deleted file mode 100644
index e6243fb5..00000000
--- a/data/tpl/useredit
+++ /dev/null
@@ -1,36 +0,0 @@
-[[= ttabs('u', $d{u}, 'edit') ]]
-<h2>[[: $p{PageTitle} ]]</h2>
-
--[[ if($d{done}) { ]]
-<span class="msg">
- Settings succesfully saved.
-</span>
-[[ } ]]
--[[= cform( [
- { type => 'error' },
- { type => 'startform', action => '/u'.$d{user}.'/edit' },
-
- { type => 'sub', title => 'General info', short => 'info' },
- { type => 'static', name => 'Username', text => _hchar($d{form}{username}) },
- { type => 'input', name => 'Email', short => 'mail' },
-
- { type => 'sub', title => 'Change password', short => 'pass' },
- { type => 'static', text => 'Leave blank to keep your current password.' },
- { type => 'pass', name => 'Password', short => 'pass1' },
- { type => 'pass', name => 'Confirm', short => 'pass2' },
-
- { type => 'sub', title => 'Miscellaneous options', short => 'misc' },
- { type => 'check', short => 'plist', name => sprintf
- 'Allow other people to see my visual novel list (<a href="/u%d/list">/u%1$d/list</a>) and wishlist (<a href="/u%1$d/wish">/u%1$d/wish</a>)', $d{user} },
- { type => 'check', short => 'pign_nsfw', name => 'Disable warnings for images that are not safe for work.' },
-
- $d{adm} ? (
- { type => 'sub', title => 'Admin', short => 'adm' },
- { type => 'input', name => 'Username', short => 'username' },
- { type => 'select', name => 'Rank', short => 'rank', options => [
- map { { name => $VNDB::VNDBopts{ranks}[0][0][$_], short => $_ } } 1..($#{$VNDB::VNDBopts{ranks}}-1) ] },
- ) : (),
-
- { type => 'submit', text => 'Save' },
- { type => 'endform' },
-], $d{form}) ]]
diff --git a/data/tpl/userlist b/data/tpl/userlist
deleted file mode 100644
index 578d320e..00000000
--- a/data/tpl/userlist
+++ /dev/null
@@ -1,50 +0,0 @@
-<h2>[[: $p{PageTitle} ]]</h2>
-<p class="chr">
- -[[= $d{chr} ne 'all' ? '<a href="/u/list/all">all</a>' : 'all' ]]- |
- [[ for('a'..'z', 0) { ]]-
- -[[ if($d{chr} eq $_) { ]][[= $_?$_:'#' ]][[ } else { ]]<a href="/u/list/[[= $_ ]]">[[= $_?$_:'#' ]]</a>[[ } ]]
- [[ } ]]-
- <br /><br />
-</p>
-
-[[ if($#{$d{users}} < 0) { ]]-
-<p>
- No users found...
-</p>
-[[ } else {
- my $url = sprintf '/u/list/%s', $d{chr};
- my $surl = sprintf '%s?s=%s&amp;o=%s', $url, $d{order}[0], $d{order}[1];
-]]
-[[= pagebut($surl) ]]-
-<table id="tul">
- <thead><tr>
- <td class="tc1">Username [[= sortbut($url, 'username') ]]</td>
-[[ if($p{Authusermod}) { ]]-
- <td class="tc2">Mail [[= sortbut($url, 'mail') ]]</td>
- <td class="tc3">Rank [[= sortbut($url, 'rank') ]]</td>[[ } ]]-
- <td class="tc4">Registered [[= sortbut($url, 'registered') ]]</td>
- <td class="tc6">Votes</td>
- <td class="tc7">Changes</td>
-[[ if($p{Authusermod}) { ]]-
- <td class="tc8">&nbsp;</td>[[ } ]]-
- </tr></thead>
- [[ for (@{$d{users}}) { ]]-
- <tr>
- <td class="tc1"><a href="/u[[= $_->{id} ]]">[[: $_->{username} ]]</a></td>
-[[ if($p{Authusermod}) { ]]-
- <td class="tc2">[[: $_->{mail} ]]</td>
- <td class="tc3">[[: $VNDB::VNDBopts{ranks}[0][0][$_->{rank}] ]]</td>[[ } ]]-
- <td class="tc4">[[= formatdate('%Y-%m-%d', $_->{registered}, 'wd') ]]</td>
- <td class="tc6">[[ if($_->{flags} & $VNDB::UFLAGS->{list} && $_->{votes}) { ]]
- <a href="/u[[= $_->{id} ]]/list" title="[[: $_->{username} ]]'s votes">[[= $_->{votes} ]]</a>
- [[ } else { ]][[= $_->{flags} & $VNDB::UFLAGS->{list} ? 0 : '-' ]][[ } ]]</td>
- <td class="tc7">[[ if($_->{changes}) { ]]
- <a href="/u[[= $_->{id} ]]/hist" title="Recent changes by -[[: $_->{username} ]]">[[= $_->{changes} ]]</a>
- [[ } else { ]]0[[ } ]]</td>
-[[ if($p{Authusermod}) { ]]-
- <td class="tc8">( <a href="/u[[= $_->{id} ]]/edit">edit</a> )</td>[[ } ]]-
- </tr>
- [[ } ]]-
-</table>
--[[= pagebut($surl) ]]-
-[[ } ]]
diff --git a/data/tpl/userlogin b/data/tpl/userlogin
deleted file mode 100644
index b4af29d2..00000000
--- a/data/tpl/userlogin
+++ /dev/null
@@ -1,14 +0,0 @@
-<h2>[[: $p{PageTitle} ]]</h2>
--[[= cform( [
- { type => 'error' },
- { type => 'startform', action => '/u/login' },
- { type => 'input', short => 'username', name => 'Username' },
- { type => 'pass', short => 'userpass', name => 'Password' },
- { type => 'submit', text => 'Login!' },
- { type => 'endform' },
-], $d{log}) ]]-
-
-<p>
- <br /><br />
- <a href="/u/register">No account yet</a>, or <a href="/u/newpass">forgot your username or password?</a>
-</p>
diff --git a/data/tpl/userpage b/data/tpl/userpage
deleted file mode 100644
index aa02062e..00000000
--- a/data/tpl/userpage
+++ /dev/null
@@ -1,13 +0,0 @@
-[[= ttabs('u', $d{user}) ]]
-[[
- $d{pl} = $d{user}{flags} & $VNDB::UFLAGS->{list};
-]]
-<h2>[[: $p{PageTitle} ]]</h2>
-<dl>
- <dt>Username</dt><dd>[[: $d{user}{username} ]]- (<a href="/u[[= $d{user}{id} ]]">u[[= $d{user}{id} ]]</a>)</dd>
- <dt>Registered</dt><dd>[[= formatdate('%Y-%m-%d', $d{user}{registered}) ]]</dd>
- <dt>Votes</dt><dd>[[= $d{pl} ? $d{user}{votes}.' (<a href="/u'.$d{user}{id}.'/list">view all</a>)' : '(hidden)' ]]</dd>
- <dt>Changes</dt><dd>[[= $d{user}{changes}.($d{user}{changes}>0?' (<a href="/u'.$d{user}{id}.'/hist">recent changes</a>)':'') ]]</dd>
-</dl>
-
-[[= T_vnpage_stats($X) ]]
diff --git a/data/tpl/userpass b/data/tpl/userpass
deleted file mode 100644
index c3b04840..00000000
--- a/data/tpl/userpass
+++ /dev/null
@@ -1,21 +0,0 @@
-<h2>[[: $p{PageTitle} ]]</h2>
-<p>
- You're lucky that vndb has a very advanced password recovery tool! Just
- type your email address (the same one you used for your account), and
- wait for an email!
-</p>
-
--[[ if(!$d{done}) { ]]
-[[= cform( [
- { type => 'error', },
- { type => 'startform', action => '/u/newpass' },
- { type => 'input', short => 'mail', name => 'Email' },
- { type => 'submit', text => 'Gimme my password!' },
- { type => 'endform' },
-], $d{pas} ) ]]
-
-[[ } else { ]]
-<span class="msg">
- Your password succesfully been reset. Check your mail for instructions.
-</span>
-[[ } ]]
diff --git a/data/tpl/userreg b/data/tpl/userreg
deleted file mode 100644
index 31b2b622..00000000
--- a/data/tpl/userreg
+++ /dev/null
@@ -1,38 +0,0 @@
-<h2>[[: $p{PageTitle} ]]</h2>
-
--[[ if($d{denied}) { ]]
-[[ } ]]-
-
-<br /><br />
-<h3>Why should I register?</h3>
-<p>
- Registered users have access to special features on this site:
-</p>
-<ul>
- <li>You can keep track of the visual novels you'd like to play or have
- finished playing,</li>
- <li>Vote on visual novels,</li>
- <li>And more importantly: you can add and edit all information on the
- website!</li>
-</ul>
-<p>
- <br />
- And of course, registering an account is (and will always remain)
- completely free!
- <br /><br />
-</p>
-
--[[= cform( [
- { type => 'error' },
- { type => 'startform', action => '/u/register' },
- { type => 'input', short => 'username', name => 'Username' },
- { type => 'input', short => 'mail', name => 'Email' },
- { type => 'static', text => q|
- Your email address will only be used in case you lose your password, at least for now.
- We will never send spam or newsletters unless you explicitly ask us for it.
- | },
- { type => 'pass', short => 'pass1', name => 'Password' },
- { type => 'pass', short => 'pass2', name => 'Confirm pass.' },
- { type => 'submit', text => 'Register!' },
- { type => 'endform' },
-], $d{reg}) ]]
diff --git a/data/tpl/vnbrowse b/data/tpl/vnbrowse
deleted file mode 100644
index fa4ba1ac..00000000
--- a/data/tpl/vnbrowse
+++ /dev/null
@@ -1,108 +0,0 @@
-<h2>[[: $p{PageTitle} ]]</h2>
-
-
-[[ if($d{chr} eq 'search') { ]]
- <form id="vsearch" method="get" action="/v/search" accept-charset="UTF-8">
- <fieldset>
- <input type="text" name="q" id="q" value="[[: $p{searchquery} ]]" class="text"
- /><input type="submit" value="Search!" />
- <br />
- <b id="adsearchclick">[[= !$p{searchquery} ? '&#9662;' : '&#9656;' ]]- advanced options</b>
- </fieldset>
- </form>
-
- <div id="adsearch" [[= !$p{searchquery} ? '' : ' style="display: none"' ]]>
- <b>Categories</b> (boolean and, selecting more gives less results. The categories are explained on <a href="/d1">this page</a>)
- <ul id="cat">
- [[ for my $c (qw| e g t p h l s |) { ]]-
- -[[= $c !~ /[thl]/ ? '<li>' : '<br />' ]][[: $VNDB::CAT->{$c}[0] ]]-
- <ul>
- [[ for (sort keys %{$VNDB::CAT->{$c}[1]}) { my $ca = $c.$_; ]]-
- <li id="cat_[[= $ca ]]">
- [[: $VNDB::CAT->{$c}[1]{$_} ]]- ([[= $d{cat}{$ca} || 0 ]])</li>
- [[ } ]]
- </ul>[[= $c !~ /[gph]/ ? '</li>' : '' ]]-
- [[ } ]]-
- </ul>
- <br style="clear: both" />
- <br />
-
- <b>Languages</b> (boolean or, selecting more gives more results)<br />
- <ul class="filter" id="lfilter">
- [[ for (sort keys %{$d{langc}}) { next if !$d{langc}{$_}; my $l=$_; ]]-
- <li><input type="checkbox" name="lang_[[= $l ]]" id="lang_[[= $l ]]" value="[[: $VNDB::LANG->{$l} ]]" />
- <label for="lang_[[= $l ]]"><acronym class="icons lang -[[= $l ]]" title="[[: $VNDB::LANG->{$l} ]]">&nbsp;</acronym>([[= $d{langc}{$l} ]])</label></li>
- [[ } ]]-
- </ul>
- <br style="clear: both" />
- <br />
-
- <b>Platforms</b> (boolean or, selecting more gives more results)<br />
- <ul class="filter" id="pfilter">
- [[ for (sort keys %$VNDB::PLAT) { next if /oth/; my $l=$_; ]]-
- <li><input type="checkbox" name="plat_[[= $l ]]" id="plat_[[= $l ]]" value="[[: $VNDB::PLAT->{$l} ]]" />
- <label for="plat_[[= $l ]]"><acronym class="icons -[[= $l ]]" title="[[: $VNDB::PLAT->{$l} ]]">&nbsp;</acronym></label></li>
- [[ } ]]-
- </ul>
- <br style="clear: both" />
- <input type="button" class="right" id="vsearch_sub" name="vsearch_sub" value="Search!" />
- <br style="clear: left" />
- <br /><br />
- </div>
-
-[[ } else { ]]-
-<p class="chr">
- -[[= $d{chr} ne 'all' ? '<a href="/v/all">all</a>' : 'all' ]]- |
- [[ for('a'..'z', 0) { ]]-
- -[[ if($d{chr} eq $_) { ]][[= $_?$_:'#' ]][[ } else { ]]<a href="/v/[[= $_ ]]">[[= $_?$_:'#' ]]</a>[[ } ]]
- [[ } ]]-
- <br /><br />
-</p>
-[[ } ]]-
-
-
--[[ if($#{$d{vn}} < 0) { ]]
- -[[ if($d{chr} eq 'search' && $p{searchquery} || $d{chr} ne 'search') { ]]
-<p>
- No results again, life sucks... :'(
-</p>
- [[ } ]]
-[[ } else {
- my %url = (
- $p{searchquery} ? ( q => $p{searchquery} ) : (),
- );
- my %urls = ( %url,
- $d{order}[0] ne 'title' ? ( s => $d{order}[0] ) : (),
- $d{order}[1] ne 'a' ? ( o => $d{order}[1] ) : (),
- );
- my $url = sprintf '/v/%s', $d{chr};
- my $urls = $url;
- $urls .= '?'.join(';', map { $_.'='.$urls{$_} } keys %urls) if keys %urls;
- $url .= '?'.join(';', map { $_.'='.$url{$_} } keys %url) if keys %url;
-]]
-
-[[= pagebut($urls) ]]
-<table id="tbv">
- <thead><tr>
- <td class="tc1">Title [[= sortbut($url, 'title') ]]</td>
- <td class="tc2">&nbsp;</td>
- <td class="tc3">&nbsp;</td>
- <td class="tc4">Released [[= sortbut($url, 'released') ]]</td>
- </tr></thead>
- [[ for (@{$d{vn}}) {
- $_->{c_released} =~ s#^([0-9]{4})([0-9]{2}).+#$1==0?'N/A':$1==9999?'TBA':(($2&&$2<13?($Time::CTime::MoY[$2-1].' '):'').$1)#e;
- $_->{c_platforms} = join '', map {
- $_ ne 'oth' ? '<acronym class="icons '.$_.'" title="'._hchar($VNDB::PLAT->{$_}).'">&nbsp;</acronym>' : ()
- } split /\//, $_->{c_platforms};
- $_->{c_languages} = join '', map qq|<acronym class="icons lang $_" title="$$VNDB::LANG{$_}">&nbsp;</acronym>|, reverse sort split /\//, $_->{c_languages};
- ]]-
- <tr>
- <td class="tc1"><a href="/v[[= $_->{id} ]]" title="[[: $_->{original}||$_->{title} ]]">[[: shorten $_->{title}, 50 ]]</a></td>
- <td class="tc2">[[= $_->{c_platforms} ]]</td>
- <td class="tc3">[[= $_->{c_languages} ]]</td>
- <td class="tc4">[[: $_->{c_released} ]]</td>
- </tr>
- [[ } ]]-
-</table>
-[[= pagebut($urls) ]]
-[[ } ]]
diff --git a/data/tpl/vnedit b/data/tpl/vnedit
deleted file mode 100644
index a9801674..00000000
--- a/data/tpl/vnedit
+++ /dev/null
@@ -1,120 +0,0 @@
-[[= $d{id} ? ttabs('v', $d{vn}, 'edit') : '' ]]-
-<h2>[[: $p{PageTitle} ]]</h2>
-
-[[ if(!$d{id}) { ]]
- <span class="msg">Please search the database before adding a new visual novel
- in order to prevent duplicate entries.</span>
-[[ } else { ]]
- <span class="msg">
- Please check the <a href="/t/v[[= $d{id} ]]">discussion board</a> <b>before</b> making
- any changes!
- </span>
-[[ } if($d{id} && $d{vn}{cid} != $d{vn}{latest}) { ]]
- <span class="warning">
- You are editing an old revision of this visual novel. If you save it, all changes made after
- -[[= formatdate('%Y-%m-%d %R', $d{vn}{added}) ]]- will be removed!
- </span>
-[[ } ]]
-
-
--[[= cform([
- { type => 'error' },
- { type => 'startform', action => $d{id} ?( '/v'.$d{id}.'/edit') : '/v/new', upload => 1, fh => 1 },
-
- { type => 'sub', title => 'General info', short => 'info' },
- { type => 'input', name => 'Title (romaji)', short => 'title', r=>1 },
- { type => 'input', name => 'Original title', short => 'original' },
- { type => 'static', text => q|
- The original title of this visual novel, leave blank if it already is in the Latin alphabet.<br /><br />| },
-
- { type => 'textarea', name => 'Aliases', short => 'alias', rows => 2, cols => 60 },
- { type => 'static', text => q|
- Comma seperated list of alternative titles or abbreviations. Can include both official
- (japanese/english) titles and unofficial titles used around net. <b>Titles that are listed in the releases do not have to be added here.</b><br /><br />| },
-
- { type => 'textarea', name => 'Description', short => 'desc', rows => 7, cols => 70, r=>1 },
- { type => 'static', text => q|
- Short description of the main story. Please do not include spoilers, and don't forget to list the source
- in case you didn't write the description yourself. ([url] BBCode tag is allowed)<br /><br />| },
-
- { type => 'select', name => 'Length', short => 'length', class => 'longopts', options => [ map {
- { short => $_,
- name => !$_?$VNDB::VNLEN->[$_][0]:($VNDB::VNLEN->[$_][0].', '.$VNDB::VNLEN->[$_][1].' ('.$VNDB::VNLEN->[$_][2].')') } } 0..$#$VNDB::VNLEN
- ] },
- { type => 'static', text => '<br />' },
- { type => 'input', name => 'External links', short => 'l_wp', pre => 'http://en.wikipedia.org/wiki/' },
- { type => 'input', name => '&nbsp;', short => 'l_encubed', pre => 'http://novelnews.net/tag/', post => '/' },
- { type => 'input', name => '&nbsp;', short => 'l_renai', pre => 'http://renai.us/game/', post => '.shtml' },
- { type => 'input', name => '&nbsp;', short => 'l_vnn', pre => 'http://visual-novels.net/vn/index.php?option=com_content&amp;task=view&amp;id=', class => 'shortopts' },
-
- { type => 'static', text => '<br />' },
- { type => 'input', name => 'Related anime', short => 'anime' },
- { type => 'static', text => q|
- Whitespace seperated list of <a href="http://anidb.net/">AniDB</a> anime IDs.
- E.g. "1015 3348" will add <a href="http://anidb.net/a1015">Shingetsutan Tsukihime</a>
- and <a href="http://anidb.net/a3348">Fate/stay night</a> as related anime.<br />
- <b>Note:</b> It can take a few minutes for the anime titles to appear on the VN page.| },
-
- { type => 'sub', title => 'Categories', short => 'cat' },
- { type => 'hidden', short => 'categories' },
- { type => 'static', raw => 1, text => eval {
- my $r = 'Please read the <a href="/d1">category descriptions</a> before modifying categories!<br /><br />'
- .'<ul id="cat">';
- for my $c (qw| e g t p h l s |) {
- $r .= ($c !~ /[thl]/ ? '<li>' : '<br />').$VNDB::CAT->{$c}[0].'<a href="/d1#'.$VNDB::CAT->{$c}[2].'" class="help">?</a><ul>';
- for (sort keys %{$VNDB::CAT->{$c}[1]}) {
- $r .= sprintf '<li><a href="#" id="cat_%1$s"><b id="b_%1$s">-</b> %2$s</a></li>',
- $c.$_, $VNDB::CAT->{$c}[1]{$_};
- }
- $r .= '</ul>'.($c !~ /[gph]/ ? '</li>' : '');
- }
- $r.'</ul>';
- } },
-
- { type => 'sub', title => 'Image', short => 'img' },
- $d{id} ? (
- { type => 'static', text => $d{vn}{image} > 0 ?
- sprintf '<img src="%s/cv/%02d/%d.jpg" style="float: right" />', $p{st}, $d{vn}{image}%100, $d{vn}{image} :
- $d{vn}{image} < 0 ? '[processing]' : 'No image uploaded yet...' },
- ) : (),
- { type => 'upload', name => $d{vn}{image} ? 'Change' : 'Upload', short => 'img' },
- { type => 'static', text => q|
- Preferably the cover of the CD/DVD/package. Image must be in JPEG or PNG format and at most 500kB. Images larger than 256x400 will automatically be resized.<br /><br />| },
- { type => 'check', short => 'img_nsfw', name => '<b>NSFW.</b> Please check this option if the image contains nudity, gore, or is otherwise not safe in a work-friendly environment.' },
-
- { type => 'sub', title => 'Visual novel relations', short => 'rel' },
- { type => 'jssel', name => 'Relations', short => 'relations', sh => 'rl' },
- { type => 'static', text => q|
- <b>Direct relations:</b> Please only add direct relations. E.g. the sequel of a sequel does not have to be listed
- here because it's already listed on an other visual novel that is in turn listed here. VNDB will handle these
- relations automatically.<br />
- <b>Reverse relations:</b> If you add a relation with an other visual novel here, the same (or "reverse") relation
- will automatically be added to the other visual novel. For example: if you add Tsukihime as a prequel of Kagetsu Tohya,
- Kagetsu Tohya will automatically be added as a sequel for Tsukihime.
- |},
-
- { type => 'sub', title => 'Screenshots', short => 'scr' },
- { type => 'hidden', short => 'screenshots' },
- { type => 'static', raw => 1, text => qq|
- <span class="warning">
- <b style="float: none; display: inline; font-weight: bold">Please keep the following in mind when uploading screenshots:</b><br />
- * Screenshots have to be in the native resolution of the game,<br />
- * Remove any window borders and make sure the image is unmarked,<br />
- * Don't only upload event CGs.<br />
- Please read the <a href="/d2#6">guidelines</a> for more information.<br />
- <b style="float: none; display: inline; font-weight: bold">Make sure to submit the form after the upload has finished!</b>
- </span><br />
- <div id="scrfrm" class="$p{st}">...make sure to enable Javascript...</div>
- <script type="text/javascript">
- var scrRel = [|.join(', ', map { ($$_{title} = _hchar($$_{title})) =~ s/\\/\\\\/g; $$_{title}=~s/'/\\'/g; "[ $$_{id}, '$$_{language}', '$$_{title}' ]" } @{$d{rel}}).q|];
- </script>
- |},
-
- { type => 'sub', title => 'Edit summary', short => 'com' },
- { type => 'textarea', name => 'Edit summary', short => 'comm', rows => 3, cols => 60 },
- { type => 'static', text => 'Please explain your modifications and cite all sources.' },
-
- { type => 'submit', text => $d{id} ? 'Edit' : 'Add' },
- { type => 'endform' },
-
-], $d{form}) ]]
diff --git a/data/tpl/vnlist b/data/tpl/vnlist
deleted file mode 100644
index d2763b39..00000000
--- a/data/tpl/vnlist
+++ /dev/null
@@ -1,63 +0,0 @@
-[[= ttabs('u', $d{user}) ]]
-<h2>[[: $p{PageTitle} ]]</h2>
-[[
- my $url = sprintf '/u%d/vlist', $d{user}{id};
- my $surl = sprintf '%s?s=%s;o=%s', $url, $d{order}[0], $d{order}[1];
- my $purl = $surl . ';t='.$d{status};
- my $sourl = $url . '?t='.$d{status};
- my $furl = $purl . ';p='.$d{page};
-]]
-
-<span class="warning">
- This visual novel list is read-only, only visible to you, and may be
- deleted in future versions of the site. You are highly encouraged to
- move everything in this list to the new <a href="/u[[= $d{user}{id} ]]/list">visual novel list</a>.
-</span>
-
-<p class="chr">
- status: -[[ for (-1..$#$VNDB::LSTAT) { if($_ >= 0) { ]]- | -[[ }
- if($d{status} == $_) { ]]<b>[[= $_ eq -1 ? 'all' : lc $VNDB::LSTAT->[$_] ]]</b>[[ }
- else { ]]<a href="[[= $surl ]]&amp;t=[[= $_ ]]">[[= $_ eq -1 ? 'all' : lc $VNDB::LSTAT->[$_] ]]</a>[[ } } ]]
- <br /><br />
-</p>
-
-
-[[ if($#{$d{list}} < 0) { ]]-
-<p>
-[[ if($d{status} >= 0) { ]]
- No results found...
-[[ } elsif($d{user}{username} eq $p{AuthUsername}) { ]]
- Your visual novel list is empty. You can keep track of all the visual novels
- you'd like to play, you're currently playing, or you've finished. Just go to
- a visual novel page and add it to your VN list!
-[[ } else { ]]
- [[: $d{user}{username} ]]'s visual novel list is empty...
-[[ } ]]
-</p>
-
-[[ } else { ]]
-[[= pagebut($purl) ]]-
-<form method="post" action="[[= $furl ]]" class="tblf">
-<table id="tvl">
- <thead><tr>
- <td class="tc1">Title [[= sortbut($sourl, 'title') ]]</td>
- <td class="tc2">Status</td>
- <td class="tc3">Added [[= sortbut($sourl, 'date') ]]</td>
- <td class="tc4">Personal note</td>
- <td class="tc5">&nbsp;</td>
- </tr></thead>
- [[ for (@{$d{list}}) { ]]-
- <tr>
- <td class="tc1"><a href="/v[[= $_->{vid} ]]" title="[[: $_->{title} ]]">[[: length($_->{title})>40 ? substr($_->{title},0, 37).'...' : $_->{title} ]]</a></td>
- <td class="tc2">[[= $VNDB::LSTAT->[$_->{status}] ]]</td>
- <td class="tc3">[[= formatdate('%Y-%m-%d', $_->{date}, 'dh') ]]</td>
- <td class="tc4">[[: $_->{comments}||'-' ]]</td>
- <td class="tc5"><input type="checkbox" name="sel" value="[[= $_->{vid} ]]" /></td>
- </tr>
- [[ } ]]-
-</table>
-<input type="submit" value="Delete selected items" class="right" />
-</form>
--[[= pagebut($purl) ]]
-[[ } ]]-
-
diff --git a/data/tpl/vnpage b/data/tpl/vnpage
deleted file mode 100644
index bca1bf35..00000000
--- a/data/tpl/vnpage
+++ /dev/null
@@ -1,204 +0,0 @@
-[[= ttabs('v', $d{vn}) ]]
-
-<h2>[[: $d{vn}{title} ]]</h2>
-[[ if($d{vn}{original}) { ]]<h3 class="alttitle">[[: $d{vn}{original} ]]</h3>[[ } ]]
-
-
-[[ if($d{vn}{hidden}) { ]]-
- <span class="warning">
- This item has been deleted from the database. File a request on the
- <a href="/t/v[[= $d{vn}{id} ]]">discussion board</a> to undelete this page.
- </span>
-[[ } ]]
-[[ if(!$d{vn}{hidden} || $p{Authdel}) { ]]-
-
--[[ if($p{AuthLoggedin}) { ]]
-<p class="mod">&lt;
- user options -
- <a href="/u[[= $p{AuthId} ]]/list" rel="voteDD" class="dropdown">[[= $d{vote}{vid} ? 'your vote: '.$d{vote}{vote} : 'vote' ]]</a>
- - <a href="/u[[= $p{AuthId} ]]/wish" rel="wishDD" class="dropdown">[[= $d{wlist}{vid} ? 'wishlist: '.lc($$VNDB::WSTAT[$d{wlist}{wstat}]) : 'wishlist' ]]</a>
-&gt;</p>
-[[ } ]]-
-
-
-
-[[ if($d{change}) { ]]
-[[= cdiff($d{prev}, $d{vn},
- [ title => 'Title (romaji)', 1 ],
- [ original => 'Original title', 1 ],
- [ alias => 'Alias', 1, 1 ],
- [ desc => 'Description', 1, 1 ],
- [ length => 'Length', sub { $VNDB::VNLEN->[$_[0] ][0] } ],
- [ l_wp => 'Wikipedia link', sub { $_[0] ? '<a href="http://en.wikipedia.org/wiki/'.$_[0].'">'.$_[0].'</a>' : 'No link' } ],
- [ l_encubed => 'Encubed tag', sub { $_[0] ? '<a href="http://novelnews.net/tag/'._huri($_[0]).'/">'.$_[0].'</a>' : 'No link' } ],
- [ l_renai => 'Renai.us link', sub { $_[0] ? '<a href="http://renai.us/game/'._huri($_[0]).'.shtml">'.$_[0].'</a>' : 'No link' } ],
- [ l_vnn => 'V-N.net link', sub { $_[0] ? '<a href="http://visual-novels.net/vn/index.php?option=com_content&amp;task=view&amp;id='.$_[0].'">'.$_[0].'</a>' : 'No link' } ],
- [ anime => 'Related anime', sub { join(' ', map qq|<a href="http://anidb.net/a$$_{id}">$$_{id}</a>|, sort { $a->{id} <=> $b->{id} } @{$_[0]}) } ],
- [ categories => 'Categories', sub { join(' ', map { my $l=$VNDB::CAT->{substr($_->[0],0,1)}[1]{substr($_->[0],1,2)}; $l?$l.'('.$_->[1].')':() } sort { $a->[0] cmp $b->[0] } @{$_[0]}) || 'No categories selected' }, 1 ],
- [ relations => 'Relations', sub { join("<br />\n", map { $VNDB::VREL->[$_->{relation}].': '._hchar($_->{title}) } sort { $a->{id} <=> $b->{id} } @{$_[0]}) } ],
- [ image => 'Image', sub { $_[0] > 0 ? sprintf '<img src="%s/cv/%02d/%d.jpg" />', $p{st}, $_[0]%100, $_[0] : $_[0] < 0 ? '[processing]' : 'No image'; } ],
- [ screenshots => 'Screenshots', sub { join "<br />\n", map sprintf('[%s] <a href="%s/sf/%02d/%d.jpg">%4$d</a> (%s)',$$_{rid}?qq|<a href="/r$$_{rid}">r$$_{rid}</a>|:'no release',$p{st},$$_{id}%100,$$_{id},$$_{nsfw}?'NSFW':'Safe'), @{$_[0]} } ],
- [ img_nsfw => 'NSFW', sub { $_[0] ? 'Not safe' : 'Safe' } ]
- ) ]]
-[[ } ]]-
-
-[[
- my @lang;
- for (@{$d{rel}}) {
- my $l = $_->{language};
- next if grep { $_ eq $l } @lang;
- push @lang, $l;
- }
-
-]]
-
-
-<div id="vnheader">
-<div>
-[[ if($d{vn}{image} > 0) { ]]
- [[ if($d{vn}{img_nsfw} && !$p{AuthNsfw}) { ]]
- <img src="[[: $p{st} ]]/cv/nsfw.png" id="nsfw" class="[[: $p{st} ]]/cv/[[= sprintf '%02d/%d', $d{vn}{image}%100, $d{vn}{image} ]].jpg" />
- [[ } else { ]]
- <img src="[[: $p{st} ]]/cv/[[= sprintf '%02d/%d', $d{vn}{image}%100, $d{vn}{image} ]].jpg" alt="[[: $p{PageTitle} ]]" />
- [[ if($d{vn}{img_nsfw}) { ]]
- <p class="nsfw">[ flagged as NSFW ]</p>
- [[ } ]]
- [[ } ]]
-[[ } elsif($d{vn}{image} < 0) { ]]-
- [processing image, please return in a few minutes]
-[[ } else { ]]-
- No image uploaded yet...
-[[ } ]]-
-</div>
-
--[[
- my @links = (
- $d{vn}{l_wp} ? [ 'Wikipedia', 'http://en.wikipedia.org/wiki/%s', $d{vn}{l_wp} ] : (),
- $d{vn}{l_encubed} ? [ 'Encubed', 'http://novelnews.net/tag/%s/', _huri $d{vn}{l_encubed} ] : (),
- $d{vn}{l_renai} ? [ 'Renai.us', 'http://renai.us/game/%s.shtml', _huri $d{vn}{l_renai} ] : (),
- $d{vn}{l_vnn} ? [ 'V-N.net', 'http://visual-novels.net/vn/index.php?option=com_content&amp;task=view&amp;id=%d', $d{vn}{l_vnn} ] : (),
- );
-
- my $prod = @lang && grep { @{$_->{producers}} } @{$d{rel}};
-
-if($d{vn}{length} || $d{vn}{alias} || @links || $prod) { ]]
- <h3>General info</h3>
- <dl>
- [[ if($d{vn}{length}) { ]]-
- <dt>Length</dt><dd>[[: $VNDB::VNLEN->[$d{vn}{length}][0] ]]- ([[: $VNDB::VNLEN->[$d{vn}{length}][1] ]])</dd>[[ } ]]-
- [[ if($d{vn}{alias}) { ]]-
- <dt>Aliases</dt><dd>[[: $d{vn}{alias} ]]</dd>[[ } ]]-
- [[ if(@links > 0) { ]]-
- <dt>Links</dt><dd>[[= join(', ', map { '<a href="'.sprintf($_->[1],$_->[2]).'">'.$_->[0].'</a>' } @links) ]]</dd>[[ } ]]-
- [[ if($prod) { ]]-
- <dt>Producers</dt><dd>
- [[ for my $l (@lang) { my %l;
- $_->{language} eq $l && (%l = ( %l, map {
- sprintf('<a href="/p%d" title="%s">%s</a>',
- $_->{id}, _hchar($_->{original}||$_->{name}), _hchar shorten $_->{name}, 30) => 1
- } @{$_->{producers}} )) for (@{$d{rel}});
- if(keys %l) { ]]-
- <acronym class="icons lang -[[= $l ]]" title="[[: $VNDB::LANG->{$l} ]]">&nbsp;</acronym>[[= join(' &amp; ', keys %l) ]]<br />
- [[ } } ]]
- </dd>[[ } ]]-
- </dl>
-[[ } ]]-
-
- [[ if(@{$d{vn}{categories}}) { my %nolvl = (map {$_=>1} qw| pli pbr gaa gab hfa hfe lea lfa lsp tfu tpa tpr |); ]]-
- <h3>Categories</h3>
- <dl class="vnrel">
- [[ for (qw|e s g p h|) {
- my $c = $_;
- my @c = map { my $s=$_;
- my ($cs) = grep { $_->[0] eq $c.$s } @{$d{vn}{categories}};
- $cs ? sprintf('<i class="crgn%d">%s</i>', $nolvl{$c.$_}?0:$cs->[1], $VNDB::CAT->{$c}[1]{$s})
- : ()
- } sort keys %{$VNDB::CAT->{$c}[1]};
- if(@c) { ]]-
- <dt>[[: $VNDB::CAT->{$c}[0] ]]</dt><dd>[[= join(', ', @c) ]]</dd>
- [[ } } ]]
- [[ if(grep $_->[0] =~ /^[tl]/, @{$d{vn}{categories}}) { ]]-
- <dt>Place/Time</dt><dd>[[= join ', ', map $VNDB::CAT->{substr($_->[0],0,1)}[1]{substr($_->[0],1,2)},
- sort { $a->[0] cmp $b->[0] } grep $_->[0] =~ /^[tl]/, @{$d{vn}{categories}} ]]</dd>
- [[ } ]]-
- </dl>
- [[ } ]]-
-
- [[ if($#{$d{vn}{relations}} >= 0) { ]]-
- <h3>[[= $d{page} eq 'rg' ? 'Relations' : '<a href="/v'.$d{vn}{id}.'/rg">Relations</a>' ]]</h3>
- <dl class="vnrel">
- [[ my $lrel = -1; my $i=0; for (sort { $a->{relation} <=> $b->{relation} } @{$d{vn}{relations}}) {
- if($_->{relation} != $lrel) { $lrel=$_->{relation}; if($i) { ]]</dd>[[ } ]]-
- <dt>[[: $VNDB::VREL->[$lrel] ]]</dt><dd>[[ } else { ]]<br />[[ } ]]
- <a href="/v[[= $_->{id} ]]" title="[[: $_->{original}||$_->{title} ]]">[[: shorten $_->{title}, 40 ]]</a>
- [[ ++$i;} ]]</dd>
- </dl>
- [[ } ]]-
-
- [[ if(@{$d{vn}{anime}}) { ]]-
- <h3>Related anime</h3>
- <ul class="vnani">
- [[ for (sort { $a->{year} < 1 ? 1 : $b->{year} < 1 ? -1 : $a->{year} <=> $b->{year} } @{$d{vn}{anime}}) { ]]-
- <li>
- -[[ if($_->{lastfetch} < 1) { ]]
- <b>[[= $_->{lastfetch} < 0 ? '[unknown anidb id: ' : '[no information available at this time: ' ]]<a href="http://anidb.net/a[[= $_->{id} ]]">[[= $_->{id} ]]</a>]</b>
- [[ } else {
- my $l = join '-', qq|<a href="http://anidb.net/a$_->{id}" title="AniDB">DB</a>|,
- $_->{nfo_id} ? qq|<a href="http://animenfo.com/animetitle,$_->{nfo_id},vndb.html" title="AnimeNFO">NFO</a>| : (),
- $_->{ann_id} ? qq|<a href="http://www.animenewsnetwork.com/encyclopedia/anime.php?id=$_->{ann_id}" title="Anime News Network">ANN</a>| : ();
- ]]
- <b>[-[[= $l ]]-]</b> <acronym title="[[: $_->{title_kanji} ]]">[[: shorten $_->{title_romaji}, 40 ]]</acronym>
- <b>([[: $VNDB::ANITYPE->[$_->{type}][0] eq 'unknown' ? '' : $VNDB::ANITYPE->[$_->{type}][0].', ' ]][[= $_->{year} ]])</b>
- [[ } ]]
- </li>
- [[ } ]]
- </ul>
- [[ } ]]-
-</div>
-
--[[
- my @lnks = (
- !$d{page} ? '<b>description &amp; releases</b>' : '<a href="/v'.$d{vn}{id}.'">description &amp; releases</a>',
- $d{page} eq 'stats' ? '<b>stats</b>' : '<a href="/v'.$d{vn}{id}.'/stats">stats</a>',
- @{$d{vn}{screenshots}} ? (
- $d{page} eq 'scr' ? '<b>screenshots</b>' : '<a href="/v'.$d{vn}{id}.'/scr">screenshots</a>',
- ) : (),
- @{$d{vn}{relations}} ? (
- $d{page} eq 'rg' ? '<b>relations</b>' : '<a href="/v'.$d{vn}{id}.'/rg">relations</a>',
- ) : (),
- );
-]]
-<p class="opts">- -[[= join(' - ', @lnks) ]]- -</p>
-
-[[ if(!$d{page}) { ]][[+ vnpage_rel ]][[ } ]]
-[[ if($d{page} eq 'stats') { ]][[+ vnpage_stats ]][[ } ]]
-[[ if($d{page} eq 'rg') { ]][[+ vnpage_rg ]][[ } ]]
-[[ if($d{page} eq 'scr') { ]][[+ vnpage_scr ]][[ } ]]
-
-[[ if($p{AuthLoggedin}) { ]]-
-<div class="dropdown" id="voteDD">
- <ul>
- [[ if($d{vote}{vid}) { ]]-
- <li><a href="/v[[= $d{vn}{id} ]]/vote?v=-1">revoke</a></li>
- [[ } for (reverse 1..10) { ]]-
- <li class="center"><a href="/v[[= $d{vn}{id} ]]/vote?v=[[= $_ ]]" id="dovote_[[= $_ ]]">[[= $_ ]]</a></li>
- [[ } ]]
- </ul>
-</div>
-
-<div class="dropdown" id="wishDD">
- <ul>
- [[ for (0..$#$VNDB::WSTAT) {
- if($d{wlist}{vid} && $d{wlist}{wstat} == $_) { ]]-
- <li><b>[[: $$VNDB::WSTAT[$_] ]]</b></li>
- [[ } else { ]]-
- <li><a href="/v[[= $d{vn}{id} ]]/wish?w=[[= $_ ]]">[[: $$VNDB::WSTAT[$_] ]]</a></li>
- [[ } } if($d{wlist}{vid}) { ]]-
- <li><a href="/v[[= $d{vn}{id} ]]/wish?w=-1">remove</a></li>
- [[ } ]]
- </ul>
-</div>
-[[ } ]]
-
-
-[[ } ]]
diff --git a/data/tpl/vnpage_rel b/data/tpl/vnpage_rel
deleted file mode 100644
index 4e5d29b2..00000000
--- a/data/tpl/vnpage_rel
+++ /dev/null
@@ -1,59 +0,0 @@
-<h3>Description</h3>
-<p class="desc">
- [[= summary($d{vn}{desc}) ]]
- <br /><br /><br />
-</p>
-
-
-
-[[
- my @lang;
- for (@{$d{rel}}) {
- my $l = $_->{language};
- next if grep { $_ eq $l } @lang;
- push @lang, $l;
- }
-
-]]
-
-
-<h3>Releases
-[[ if((!$d{vn}{locked} && $p{Authedit}) || $p{Authlock}) { ]]- <p class="actions">(<a href="/v[[= $d{vn}{id} ]]/add">add release</a>)</p>[[ } ]]</h3>
-[[ if(@{$d{rel}}) { ]]-
-<table id="tre">
-[[ for(@lang) { my $l = $_; ]]-
-<tr class="lang">
- <td colspan="6">[[: $VNDB::LANG->{$l} ]]</td>
-</tr>
-[[ for (@{$d{rel}}) { next if $l ne $_->{language}; ]]-
- <tr>
- <td class="tc1">[[= datestr($_->{released}) ]]</td>
- <td class="tc2">[[= $_->{minage}<0 ? '' : $VNDB::VRAGES->{$_->{minage}} ]]</td>
- <td class="tc3">
- [[= join('', map { $_ ne 'oth' ? '<acronym class="icons '.$_.'" title="'._hchar($VNDB::PLAT->{$_}).'">&nbsp;</acronym>' : () } sort @{$_->{platforms}}) ]]
- <acronym title="[[= $VNDB::RTYP->[$_->{type}] ]]- release" class="icons -[[= lc substr($VNDB::RTYP->[$_->{type}],0,3) ]]">&nbsp;</acronym>
- </td>
- <td class="tc4"><a href="/r[[= $_->{id} ]]" title="[[: $_->{original} || $_->{title} ]]">[[: shorten $_->{title},60 ]]</a></td>
- <td class="tc5">[[ if($p{AuthId}) { ]]
- [[= sprintf '<a href="/r%d" class="dropdown above" rel="rlistDD%1$d">%s</a>', $_->{id}, $_->{rlist} ?
- ('<acronym title="'.$VNDB::RSTAT->[$_->{rlist}{rstat}].'" class="uicons r'.$_->{rlist}{rstat}.'">&nbsp;</acronym>'.
- '<acronym title="'.$VNDB::VSTAT->[$_->{rlist}{vstat}].'" class="uicons v'.$_->{rlist}{vstat}.'">&nbsp;</acronym>')
- : '<acronym title="Add to your visual novel list" class="uicons no">&nbsp;</acronym>' ]]
- [[ } else { ]]&nbsp;[[ } ]]</td>
- <td class="tc6">[[ if($_->{website}) { ]]<a href="[[: $_->{website} ]]" class="icons ext" title="WWW">&nbsp;</a>[[ } ]]</td>
- </tr>
-[[ } ]]-
-[[ } ]]-
-</table>
-[[ } else { ]]-
-<p>
- This game has either not been released yet, or we just don't have information about
- any releases.
-</p>
-[[ } ]]
-
-
-[[ if($p{AuthId}) { ]]
-[[ for my $r (@{$d{rel}}) { ]]-
--[[= rlist_dd($r) ]]
-[[ } } ]]
diff --git a/data/tpl/vnpage_rg b/data/tpl/vnpage_rg
deleted file mode 100644
index deae6124..00000000
--- a/data/tpl/vnpage_rg
+++ /dev/null
@@ -1,11 +0,0 @@
-<h3>Relations</h3>
-[[ if(!$d{vn}{rgraph}) { ]]
- <p>
- Relation graph has not been generated yet...
- </p>
-[[ } else { ]]
- [[= $d{vn}{cmap} ]]
- <p id="relations">
- <img src="[[= sprintf "%s/rg/%02d/%d.png", $p{st}, $d{vn}{rgraph}%100, $d{vn}{rgraph} ]]" usemap="#rgraph" alt="Relation graph for -[[: $d{vn}{title} ]]" />
- </p>
-[[ } ]]
diff --git a/data/tpl/vnpage_scr b/data/tpl/vnpage_scr
deleted file mode 100644
index f0c4ae9f..00000000
--- a/data/tpl/vnpage_scr
+++ /dev/null
@@ -1,37 +0,0 @@
-<h3>Screenshots
-[[ if((!$d{vn}{locked} && $p{Authedit}) || $p{Authlock}) { ]]- <p class="actions">(<a href="/v[[= $d{vn}{id} ]]/edit?fh=scr">manage screenshots</a>)</p>[[ } ]]</h3>
-
-[[ if(@{$d{vn}{screenshots}}) {
- my $tot = @{$d{vn}{screenshots}};
- my $nsfw = grep $$_{nsfw}, @{$d{vn}{screenshots}};
-]]-
-
-<div id="screenshots">
-[[ for my $r (undef, @{$d{rel}}) {
- my @s = grep { !$r && !$_->{rid} || $r && $_->{rid} && $_->{rid} == $r->{id} } @{$d{vn}{screenshots}};
- next if !@s;
-]]-
-[[ if($r) { ]]
-<b><acronym class="icons lang -[[= $r->{language} ]]" title="[[: $VNDB::LANG->{$r->{language}} ]]">&nbsp;</acronym>
-[[: $r->{title} ]]</b>
-[[ } ]]
-[[ for(@s) { ]]
- <a href="[[= sprintf '%s/sf/%02d/%d.jpg', $p{st}, $$_{id}%100, $$_{id} ]]"
- class="shot [[= $$_{nsfw} ? ' scr_nsfw':'' ]]" [[= !$p{AuthNsfw}&&$$_{nsfw}?' style="display: none"':'' ]]-
- rel="[[= $$_{width}.'x'.$$_{height} ]]"><img src="[[= sprintf '%s/st/%02d/%d.jpg', $p{st}, $$_{id}%100, $$_{id} ]]"
- /><b>[[= $$_{nsfw} ? 'x' : '&nbsp;' ]]</b></a>
-[[ } ]]-
-<br style="clear: left" /><br />
-[[ } ]]
-
-</div>
-[[ if($nsfw) { ]]-
-<p id="scrNsfwHid">&nbsp;</p>
-<i style="font-size: 10px;">Items marked with a red X are flagged as NSFW.</i>
-[[ } ]]-
-
-[[ } else { ]]-
-<p>
- No screenshots have been uploaded yet for this visual novel.
-</p>
-[[ } ]]
diff --git a/data/tpl/vnpage_stats b/data/tpl/vnpage_stats
deleted file mode 100644
index ab159f14..00000000
--- a/data/tpl/vnpage_stats
+++ /dev/null
@@ -1,39 +0,0 @@
-<ul id="stats">
-
-[[
- my $max = 1; my $total = 0; my $sum = 0;
- for (0..$#{$d{votes}{graph}}) {
- $total += $d{votes}{graph}[$_];
- $max = $d{votes}{graph}[$_] if $d{votes}{graph}[$_] > $max;
- $sum += ($_+1) * $d{votes}{graph}[$_];
- }
-]]
-[[ if(!$d{user} || ($d{pl} && $d{user}{votes})) { ]]-
-<li><h3>Vote graph <p class="actions">[[= $total ]]- vote[[= $total==1?'':'s' ]]- total
- [[= $total && $d{user} ? sprintf(', average: %.1f.', $sum/$total) : '' ]]</p></h3>
-<table id="tvg">
-[[ for (reverse 0..$#{$d{votes}{graph}}) { ]]-
- <tr>
- <td class="tc1">[[= $_+1 ]]</td>
- <td class="tc2"><div style="width: -[[= ($d{votes}{graph}[$_]/$max)*270 + 5 ]]px">&nbsp;</div>[[= $d{votes}{graph}[$_] ]]</td>
- </tr>
-[[ } ]]-
-</table></li>
-
-[[ if($#{$d{votes}{latest}} >= 0) { ]]
-<li><h3>Recent votes</h3>
-<table id="tvr">
-[[ for (@{$d{votes}{latest}}) { ]]-
- <tr>
- [[ if(!$d{user}) { ]]-
- <td class="tc1">[[= userstr $_ ]]</td>
- [[ } else { ]]-
- <td class="tc1"><a href="/v[[= $_->{vid} ]]" title="[[: $_->{original}||$_->{title} ]]">[[: shorten $_->{title}, 30 ]]</a></td>
- [[ } ]]-
- <td class="tc2">[[= $_->{vote} ]]</td>
- <td class="tc3">[[= formatdate('%Y-%m-%d %R', $_->{date}, 'dh') ]]</td>
- </tr>
-[[ } ]]-
-</table></li>
-[[ } } ]]-
-</ul>
diff --git a/data/tpl/wlist b/data/tpl/wlist
deleted file mode 100644
index 049d28ca..00000000
--- a/data/tpl/wlist
+++ /dev/null
@@ -1,55 +0,0 @@
-[[= ttabs('u', $d{user}, 'wish') ]]
-<h2>[[: $p{PageTitle} ]]</h2>
-[[
- my $url = sprintf '/u%d/wish', $d{user}{id};
- my $surl = sprintf '%s?s=%s;o=%s', $url, $d{order}[0], $d{order}[1];
- my $furl = $surl . ';p='.$d{page};
-]]
-
-
-[[ if($#{$d{list}} < 0) { ]]-
-<p>
-[[ if($d{user}{username} eq $p{AuthUsername}) { ]]
- Your wishlist is empty. You can keep track of all the visual novels
- you'd like to play. Just go to a visual novel page and add it to your wishlist!
-[[ } else { ]]
- [[: $d{user}{username} ]]'s wishlist is empty...
-[[ } ]]
-</p>
-
-[[ } else { ]]
-[[= pagebut($surl) ]]-
-[[ if($d{user}{username} eq $p{AuthUsername}) { ]]
-<form method="post" action="[[= $furl ]]" class="tblf">
-[[ } ]]
-<table id="twl">
- <thead><tr>
- <td class="tc1">Title [[= sortbut($url, 'title') ]]</td>
- <td class="tc2">Priority [[= sortbut($url, 'wstat') ]]</td>
- <td class="tc3">Added [[= sortbut($url, 'added') ]]</td>
- <td class="tc4">&nbsp;</td>
- </tr></thead>
- [[ for (@{$d{list}}) { ]]-
- <tr>
- <td class="tc1"><a href="/v[[= $_->{vid} ]]" title="[[: $_->{original}||$_->{title} ]]">[[: shorten $_->{title}, 40 ]]</a></td>
- <td class="tc2">[[= $VNDB::WSTAT->[$_->{wstat}] ]]</td>
- <td class="tc3">[[= formatdate('%Y-%m-%d', $_->{added}) ]]</td>
- <td class="tc4">[[ if($d{user}{username} eq $p{AuthUsername}) { ]]<input type="checkbox" name="sel" value="[[= $_->{vid} ]]" />[[ } else { ]]&nbsp;[[ } ]]</td>
- </tr>
- [[ } ]]-
-</table>
-[[ if($d{user}{username} eq $p{AuthUsername}) { ]]
-<select id="vnlistchange" name="vnlistchange" class="right">
- <option value="n">- with selected -</option>
- <option value="d">Delete</option>
- <optgroup label="Update priority:">
- [[ for (0..$#$VNDB::WSTAT) { ]]-
- <option value="[[= $_ ]]">[[: $VNDB::WSTAT->[$_] ]]</option>
- [[ } ]]
- </optgroup>
-</select>
-</form>
-[[ } ]]
--[[= pagebut($surl) ]]
-[[ } ]]-
-
diff --git a/lib/ChangeLog b/lib/ChangeLog
index 8933b6c0..e2b5bd35 100644
--- a/lib/ChangeLog
+++ b/lib/ChangeLog
@@ -5,7 +5,38 @@ TODO:
+ Remove all references to an item when it's hidden
(preferably with the option to re-add them when unhiding)
+ Add a link for the hidden 'h' option at /hist
- + Rewrite the revision diff code, it's horrible...
+
+2.0 - ?
+ - New layout
+ - Massive code rewrite:
+ - Switched to YAWF
+ - Removed template system
+ - Split DB functions in several files
+ - Converted absolute paths to be relative to the root directory
+ - Database changes:
+ - Added caching of edit and vote counts in users.c_votes and .c_changes
+ - Split users.flags into users.show_nsfw and .show_list (boolean type)
+ - Global statistics are cached in stats_cache
+ - URL changes:
+ - /p and /v don't work anymore, use /[pv]/all
+ - /u/list/* -> /u/* and /u/list -> /u/list/all
+ - Revert URL changed from /x99/edit?rev=1 to /x99.1/edit
+ - /v+/stats and /v+/scr moved into /v+
+ - Functionality changes:
+ - Ability to sort the userlist on vote and change counts
+ - Added threads and posts counts to the global statistics
+ - Improved diff calculation
+ - Whitespace around input fields are removed
+ - Automated edits filter to history browser
+ - Number of threads is shown in the discussion tab for each item
+ - Boardmods can edit threads without updating the last edited field
+ - No more RSS feeds for changes (will be replaced with a notification
+ system in the future)
+ - Improved formsub interface
+ - Improved VN relation editor interface
+ - Voted/non-voted filter to user's VNLists
+ - VNList status can only be changed from release pages
+ - More stats + recent changes on user pages
1.23 - 2008-10-22 (r117)
- Removed redirects for old revision URLs (the code wasn't very secure...)
diff --git a/lib/Multi/Anime.pm b/lib/Multi/Anime.pm
index 42bd52bf..1215a4a1 100644
--- a/lib/Multi/Anime.pm
+++ b/lib/Multi/Anime.pm
@@ -125,7 +125,7 @@ sub cmd_anime { # cmd, arg
}
if(@push) {
- my $s = tie my %s, 'Tie::ShareLite', @VNDB::SHMOPTS;
+ my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
$s->lock(LOCK_EX);
my @q = $s{anime} ? @{$s{anime}} : ();
push @q, grep {
@@ -143,7 +143,7 @@ sub cmd_anime { # cmd, arg
sub nextcmd {
return if $_[HEAP]{lm};
- my $s = tie my %s, 'Tie::ShareLite', @VNDB::SHMOPTS;
+ my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
my @q = $s{anime} ? @{$s{anime}} : ();
undef $s;
@@ -188,8 +188,8 @@ sub nextcmd {
$_.'='.$cmd{$_}
} keys %cmd);
$_[HEAP]{w}->put({ payload => [ $cmd ]});
- $VNDB::DEBUG && printf " > %s\n", $cmd;
-
+
+ #$_[KERNEL]->call(core => log => 3, '> %s', $cmd);
$_[KERNEL]->delay(receivepacket => $_[HEAP]{timeout}, { payload => [ $_[HEAP]{tag}.' 100 TIMEOUT' ] });
$_[HEAP]{lm} = time;
}
@@ -208,7 +208,6 @@ sub receivepacket { # input, wheelid
} else {
$_[KERNEL]->call(core => log => 3, 'Received from AniDB after %.2fs: %d %s',
time-$_[HEAP]{lm}, $code, $msg);
- $VNDB::DEBUG && print ' < '.join("\n < ", @r)."\n";
}
# just handle anime data, even if the tag is not correct
@@ -263,8 +262,8 @@ sub updateanime { # aid, data|'notfound'
$_ =~ s/`/'/g;
}
$col[3] = $1 if $col[3] =~ /^([0-9]+)/; # remove multi-year stuff
- for(0..$#$VNDB::ANITYPE) {
- $col[4] = $_ if lc($VNDB::ANITYPE->[$_][1]) eq lc($col[4]);
+ for(0..$#{$VNDB::S{anime_types}}) {
+ $col[4] = $_ if lc($VNDB::S{anime_types}[$_][1]) eq lc($col[4]);
}
$col[4] = 0 if $col[4] !~ /^[0-9]+$/;
$col[2] = '' if $col[2] =~ /^0,/;
@@ -286,7 +285,7 @@ sub updateanime { # aid, data|'notfound'
undef, @col) if $r < 1;
# remove from queue
- my $s = tie my %s, 'Tie::ShareLite', @VNDB::SHMOPTS;
+ my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
$s->lock(LOCK_EX);
my @q = grep $_ != $_[ARG0], ($s{anime} ? @{$s{anime}} : ());
$s{anime} = \@q;
diff --git a/lib/Multi/Core.pm b/lib/Multi/Core.pm
index 74706910..7b2ae24b 100644
--- a/lib/Multi/Core.pm
+++ b/lib/Multi/Core.pm
@@ -56,7 +56,7 @@ sub heartbeat { # last beat
sub queue { # cmd
- my $s = tie my %s, 'Tie::ShareLite', @VNDB::SHMOPTS;
+ my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
$s->lock(LOCK_EX);
my @q = ( ($s{queue} ? @{$s{queue}} : ()), $_[ARG0] );
$s{queue} = \@q;
@@ -70,7 +70,7 @@ sub queue { # cmd
sub prepare { # determines whether to execute a new cmd
return if $Multi::STOP || $_[HEAP]{running};
- my $s = tie my %s, 'Tie::ShareLite', @VNDB::SHMOPTS;
+ my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
$s->lock(LOCK_SH);
if($s{queue} && @{$s{queue}}) {
$_[KERNEL]->yield(execute => $s{queue}[0]);
@@ -100,7 +100,7 @@ sub finish { # cmd
$_[KERNEL]->call(core => log => 2, "Unqueuing '%s' after %.2fs.",
$_[ARG0], tv_interval($_[HEAP]{starttime}));
- my $s = tie my %s, 'Tie::ShareLite', @VNDB::SHMOPTS;
+ my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
$s->lock(LOCK_EX);
my @q = grep { $_ ne $_[ARG0] } $s{queue} ? @{$s{queue}} : ();
$s{queue} = \@q;
@@ -111,19 +111,16 @@ sub finish { # cmd
sub log { # level, msg
- return if $_[ARG0] > $Multi::LOGLVL;
+ return if $_[ARG0] > $VNDB::M{log_level};
(my $p = eval { $_[SENDER][2]{$_[CALLER_STATE]}[0] } || '') =~ s/^Multi:://;
my $msg = sprintf '(%s) %s::%s: %s',
(qw|WRN ACT DBG|)[$_[ARG0]-1], $p, $_[CALLER_STATE],
$_[ARG2] ? sprintf($_[ARG1], @_[ARG2..$#_]) : $_[ARG1];
- open(my $F, '>>', $Multi::LOGDIR.'/multi.log');
+ open(my $F, '>>', $VNDB::M{log_dir}.'/multi.log');
printf $F "[%s] %s\n", scalar localtime, $msg;
close $F;
-
- # (debug) log to stdout as well...
- $VNDB::DEBUG && printf "[%s] %s\n", scalar localtime, $msg;
}
diff --git a/lib/Multi/IRC.pm b/lib/Multi/IRC.pm
index 4a60f324..224b35d4 100644
--- a/lib/Multi/IRC.pm
+++ b/lib/Multi/IRC.pm
@@ -61,7 +61,7 @@ sub _start {
$_[HEAP]{irc}->plugin_add(
Logger => POE::Component::IRC::Plugin::Logger->new(
- Path => $Multi::LOGDIR,
+ Path => $VNDB::M{log_dir},
Private => 0,
Public => 1,
));
@@ -70,7 +70,7 @@ sub _start {
);
$_[HEAP]{irc}->plugin_add(
CTCP => POE::Component::IRC::Plugin::CTCP->new(
- version => $_[HEAP]{o}{ircname}.' v'.$VNDB::VERSION,
+ version => $_[HEAP]{o}{ircname}.' v'.$VNDB::S{version},
userinfo => $_[HEAP]{o}{ircname},
));
if($_[HEAP]{o}{pass}) {
@@ -175,9 +175,9 @@ sub vndbid { # dest, msg, force
# nf (normal format): x+ : x, id, title
# sf (sub format): x+.+ : x, id, subid, title, action2, title2
# ef (extended format): x+.+ : x, id, subid, action, title, action2, title2
- my $nf = BOLD.RED.'['.NORMAL.BOLD.'%s%d' .RED.']' .NORMAL.' %s ' .RED.'@'.NORMAL.LIGHT_GREY.' '.$VNDB::VNDBopts{root_url}.'/%1$s%2$d'.NORMAL;
- my $sf = BOLD.RED.'['.NORMAL.BOLD.'%s%d.%d'.RED.']' .NORMAL.' %s '.RED.'%s'.NORMAL.' %s '.RED.'@'.NORMAL.LIGHT_GREY.' '.$VNDB::VNDBopts{root_url}.'/%1$s%2$d.%3$d'.NORMAL;
- my $ef = BOLD.RED.'['.NORMAL.BOLD.'%s%d.%d'.RED.']'.NORMAL.RED.' %s'.NORMAL.' %s '.RED.'%s'.NORMAL.' %s '.RED.'@'.NORMAL.LIGHT_GREY.' '.$VNDB::VNDBopts{root_url}.'/%1$s%2$d.%3$d'.NORMAL;
+ my $nf = BOLD.RED.'['.NORMAL.BOLD.'%s%d' .RED.']' .NORMAL.' %s ' .RED.'@'.NORMAL.LIGHT_GREY.' '.$VNDB::S{url}.'/%1$s%2$d'.NORMAL;
+ my $sf = BOLD.RED.'['.NORMAL.BOLD.'%s%d.%d'.RED.']' .NORMAL.' %s '.RED.'%s'.NORMAL.' %s '.RED.'@'.NORMAL.LIGHT_GREY.' '.$VNDB::S{url}.'/%1$s%2$d.%3$d'.NORMAL;
+ my $ef = BOLD.RED.'['.NORMAL.BOLD.'%s%d.%d'.RED.']'.NORMAL.RED.' %s'.NORMAL.' %s '.RED.'%s'.NORMAL.' %s '.RED.'@'.NORMAL.LIGHT_GREY.' '.$VNDB::S{url}.'/%1$s%2$d.%3$d'.NORMAL;
# get a list of possible IDs (a la sub summary in defs.pl)
my @id; # [ type, id, ref ]
@@ -272,7 +272,7 @@ sub shutdown {
sub cmd_info {
$_[KERNEL]->post(circ => privmsg => $_[DEST],
- 'Hello, I am HMX-12 Multi v'.$VNDB::VERSION.' made by the great Yorhel!');
+ 'Hello, I am HMX-12 Multi v'.$VNDB::S{version}.' made by the great Yorhel!');
}
@@ -316,7 +316,7 @@ sub cmd_vn { # $arg = search string
sprintf 'No results found for %s', $_[ARG]) if !@$res;
return $_[KERNEL]->post(circ => privmsg => $_[DEST],
sprintf 'Too many results found, see %s/v/search?q=%s',
- $VNDB::VNDBopts{root_url}, uri_escape_utf8($_[ARG])) if @$res > 5;
+ $VNDB::S{url}, uri_escape_utf8($_[ARG])) if @$res > 5;
$_[KERNEL]->yield(vndbid => $_[DEST], join(' ', map 'v'.$_->[0], @$res), 1);
}
diff --git a/lib/Multi/Image.pm b/lib/Multi/Image.pm
index fb712afd..fe3d411e 100644
--- a/lib/Multi/Image.pm
+++ b/lib/Multi/Image.pm
@@ -25,6 +25,9 @@ sub spawn {
heap => {
cvsize => [ 256, 400 ],
scrsize => [ 136, 102 ],
+ cvpath => $VNDB::ROOT.'/static/cv',
+ sfpath => $VNDB::ROOT.'/static/sf',
+ stpath => $VNDB::ROOT.'/static/st',
},
);
}
@@ -60,7 +63,7 @@ sub cmd_coverimage {
sub cv_process { # id
my $start = time;
- my $img = sprintf '%s/%02d/%d.jpg', $VNDB::VNDBopts{imgpath}, $_[ARG0]%100, $_[ARG0];
+ my $img = sprintf '%s/%02d/%d.jpg', $_[HEAP]{cvpath}, $_[ARG0]%100, $_[ARG0];
my $os = -s $img;
my $im = Image::Magick->new;
@@ -150,8 +153,8 @@ sub cmd_screenshot {
sub scr_process { # id
my $start = time;
- my $sf = sprintf '%s/%02d/%d.jpg', $VNDB::VNDBopts{sfpath}, $_[ARG0]%100, $_[ARG0];
- my $st = sprintf '%s/%02d/%d.jpg', $VNDB::VNDBopts{stpath}, $_[ARG0]%100, $_[ARG0];
+ my $sf = sprintf '%s/%02d/%d.jpg', $_[HEAP]{sfpath}, $_[ARG0]%100, $_[ARG0];
+ my $st = sprintf '%s/%02d/%d.jpg', $_[HEAP]{stpath}, $_[ARG0]%100, $_[ARG0];
# convert/compress full-size image
my $os = -s $sf;
@@ -208,8 +211,8 @@ sub scr_clean {
my($bytes, $items, $id) = (0, 0, 0);
while(($id) = $q->fetchrow_array) {
- my $f = sprintf '%s/%02d/%d.jpg', $VNDB::VNDBopts{stpath}, $id%100, $id;
- my $t = sprintf '%s/%02d/%d.jpg', $VNDB::VNDBopts{stpath}, $id%100, $id;
+ my $f = sprintf '%s/%02d/%d.jpg', $_[HEAP]{sfpath}, $id%100, $id;
+ my $t = sprintf '%s/%02d/%d.jpg', $_[HEAP]{stpath}, $id%100, $id;
$bytes += -s $f;
$bytes += -s $t;
$items++;
diff --git a/lib/Multi/Maintenance.pm b/lib/Multi/Maintenance.pm
index b5d51ea3..2fb7b2bd 100644
--- a/lib/Multi/Maintenance.pm
+++ b/lib/Multi/Maintenance.pm
@@ -17,7 +17,7 @@ sub spawn {
my $p = shift;
POE::Session->create(
package_states => [
- $p => [qw| _start cmd_maintenance vncache revcache integrity unkanime logrotate |],
+ $p => [qw| _start cmd_maintenance vncache usercache statscache revcache integrity unkanime logrotate |],
],
);
}
@@ -25,12 +25,12 @@ sub spawn {
sub _start {
$_[KERNEL]->alias_set('maintenance');
- $_[KERNEL]->call(core => register => qr/^maintenance((?: (?:vncache|revcache|integrity|unkanime|logrotate))+)$/, 'cmd_maintenance');
+ $_[KERNEL]->call(core => register => qr/^maintenance((?: (?:vncache|revcache|usercache|statscache|integrity|unkanime|logrotate))+)$/, 'cmd_maintenance');
# Perform some maintenance functions every day on 0:00
$_[KERNEL]->post(core => addcron => '0 0 * * *', 'maintenance vncache integrity unkanime');
# update caches and rotate logs every 1st day of the month at 0:05
- $_[KERNEL]->post(core => addcron => '5 0 1 * *' => 'maintenance revcache logrotate');
+ $_[KERNEL]->post(core => addcron => '5 0 1 * *' => 'maintenance usercache statscache revcache logrotate');
}
@@ -49,6 +49,39 @@ sub vncache {
}
+sub usercache {
+ $_[KERNEL]->call(core => log => 3 => 'Updating c_* columns in the users table...');
+ $Multi::SQL->do(q|UPDATE users SET
+ c_votes = COALESCE(
+ (SELECT COUNT(vid)
+ FROM votes
+ WHERE uid = users.id
+ GROUP BY uid
+ ), 0),
+ c_changes = COALESCE(
+ (SELECT COUNT(id)
+ FROM changes
+ WHERE requester = users.id
+ GROUP BY requester
+ ), 0)
+ |);
+}
+
+
+sub statscache {
+ $_[KERNEL]->call(core => log => 3 => 'Updating the stats_cache table...');
+ $Multi::SQL->do($_) for(
+ q|UPDATE stats_cache SET count = (SELECT COUNT(*) FROM users)-1 WHERE section = 'users'|,
+ q|UPDATE stats_cache SET count = (SELECT COUNT(*) FROM vn WHERE hidden = FALSE) WHERE section = 'vn'|,
+ q|UPDATE stats_cache SET count = (SELECT COUNT(*) FROM releases WHERE hidden = FALSE) WHERE section = 'releases'|,
+ q|UPDATE stats_cache SET count = (SELECT COUNT(*) FROM producers WHERE hidden = FALSE) WHERE section = 'producers'|,
+ q|UPDATE stats_cache SET count = (SELECT COUNT(*) FROM threads WHERE hidden = FALSE) WHERE section = 'threads'|,
+ q|UPDATE stats_cache SET count = (SELECT COUNT(*) FROM threads_posts WHERE hidden = FALSE
+ AND EXISTS(SELECT 1 FROM threads WHERE threads.id = tid AND threads.hidden = FALSE)) WHERE section = 'threads_posts'|
+ );
+}
+
+
sub revcache {
$_[KERNEL]->call(core => log => 3 => 'Updating rev column in the changes table...');
# this can take a while, maybe split these up in 3 queries?
@@ -114,10 +147,10 @@ sub unkanime {
sub logrotate {
- my $dir = sprintf '%s/old', $Multi::LOGDIR;
+ my $dir = sprintf '%s/old', $VNDB::M{log_dir};
mkdir $dir if !-d $dir;
- for (glob sprintf '%s/*', $Multi::LOGDIR) {
+ for (glob sprintf '%s/*', $VNDB::M{log_dir}) {
next if /^\./ || /~$/ || !-f;
my $f = /([^\/]+)$/ ? $1 : $_;
my $n = sprintf '%s/%s.%04d-%02d-%02d.gz', $dir, $f, (localtime)[5]+1900, (localtime)[4]+1, (localtime)[3];
@@ -125,12 +158,12 @@ sub logrotate {
$_[KERNEL]->call(core => log => 1, 'Logs already rotated earlier today!');
return;
}
- open my $I, '<', sprintf '%s/%s', $Multi::LOGDIR, $f;
+ open my $I, '<', sprintf '%s/%s', $VNDB::M{log_dir}, $f;
open my $O, '>:gzip', $n;
print $O $_ while <$I>;
close $O;
close $I;
- open $I, '>', sprintf '%s/%s', $Multi::LOGDIR, $f;
+ open $I, '>', sprintf '%s/%s', $VNDB::M{log_dir}, $f;
close $I;
}
}
diff --git a/lib/Multi/RG.pm b/lib/Multi/RG.pm
index 37037a96..d16d63b5 100644
--- a/lib/Multi/RG.pm
+++ b/lib/Multi/RG.pm
@@ -127,10 +127,11 @@ sub builddot {
my $gv =
qq|graph rgraph {\n|.
qq|\tratio = "compress"\n|.
+ qq|\tgraph [ bgcolor="#ffffff00" ]\n|.
qq|\tnode [ fontname = "$_[HEAP]{font}", shape = "plaintext",|.
- qq| fontsize = $_[HEAP]{fsize}[0], style = "setlinewidth(0.5)" ]\n|.
+ qq| fontsize = $_[HEAP]{fsize}[0], style = "setlinewidth(0.5)", fontcolor = "#cccccc", color = "#225588" ]\n|.
qq|\tedge [ labeldistance = 2.5, labelangle = -20, labeljust = 1, minlen = 2, dir = "both",|.
- qq| fontname = $_[HEAP]{font}, fontsize = $_[HEAP]{fsize}[1], arrowsize = 0.7, color = "#69a89a" ]\n|;
+ qq| fontname = $_[HEAP]{font}, fontsize = $_[HEAP]{fsize}[1], arrowsize = 0.7, color = "#225588", fontcolor = "#cccccc" ]\n|;
# insert all nodes, ordered by release date
for (sort { $a->[2] <=> $b->[2] } values %{$_[HEAP]{nodes}}) {
@@ -149,7 +150,7 @@ sub builddot {
$gv .= sprintf
qq|\tv%d [ URL = "/v%d", tooltip = "%s" label=<|.
- q|<TABLE CELLSPACING="0" CELLPADDING="1" BORDER="0" CELLBORDER="1" BGCOLOR="#f0f0f0">|.
+ q|<TABLE CELLSPACING="0" CELLPADDING="1" BORDER="0" CELLBORDER="1" BGCOLOR="#00000033">|.
q|<TR><TD COLSPAN="2" ALIGN="CENTER" CELLPADDING="2"><FONT POINT-SIZE="%d"> %s </FONT></TD></TR>|.
q|<TR><TD> %s </TD><TD> %s </TD></TR>|.
qq|</TABLE>> ]\n|,
@@ -171,9 +172,9 @@ sub builddot {
}
my $label =
- $VNDB::VRELW->{$_->[2]} ? qq|headlabel = "$VNDB::VREL->[$_->[2]]", taillabel = "$VNDB::VREL->[$_->[2]-1]"| :
- $VNDB::VRELW->{$_->[2]+1} ? qq|headlabel = "$VNDB::VREL->[$_->[2]]", taillabel = "$VNDB::VREL->[$_->[2]+1]"|
- : qq|label = " $VNDB::VREL->[$_->[2]]"|;
+ $VNDB::S{vn_relations}[$_->[2]][1] ? qq|headlabel = "$VNDB::S{vn_relations}[$_->[2]][0]", taillabel = "$VNDB::S{vn_relations}[$_->[2]-1][0]"| :
+ $VNDB::S{vn_relations}[$_->[2]+1][1] ? qq|headlabel = "$VNDB::S{vn_relations}[$_->[2]][0]", taillabel = "$VNDB::S{vn_relations}[$_->[2]+1][0]"|
+ : qq|label = " $VNDB::S{vn_relations}[$_->[2]][0]"|;
$gv .= qq|\tv$$_[1] -- v$$_[0] [ $label ]\n|;
}
@@ -269,7 +270,7 @@ sub proc_child {
# Not a POE handler, just a small macro
sub reverserel { # relation
- return $VNDB::VRELW->{$_[0]} ? $_[0]-1 : $VNDB::VRELW->{$_[0]+1} ? $_[0]+1 : $_[0];
+ return $VNDB::S{vn_relations}[$_[0]][1] ? $_[0]-1 : $VNDB::S{vn_relations}[$_[0]+1][1] ? $_[0]+1 : $_[0];
}
diff --git a/lib/Multi/Sitemap.pm b/lib/Multi/Sitemap.pm
index ded836ea..5ae305c1 100644
--- a/lib/Multi/Sitemap.pm
+++ b/lib/Multi/Sitemap.pm
@@ -20,8 +20,7 @@ sub spawn {
$p => [qw| _start cmd_sitemap staticpages vnpages releasepages producerpages finish addurl |],
],
heap => {
- output => '/www/vndb/www/sitemap.xml.gz',
- baseurl => $VNDB::VNDBopts{root_url},
+ output => $VNDB::ROOT.'/www/sitemap.xml.gz',
@_,
}
);
@@ -69,11 +68,11 @@ sub staticpages {
$_[KERNEL]->call(sitemap => addurl => '', 'daily');
- /([0-9]+)$/ && $_[KERNEL]->call(sitemap => addurl => 'd'.$1, 'monthly')
- for ( (</www/vndb/data/docs/*>) );
+ /([0-9]+)$/ && $_[KERNEL]->call(sitemap => addurl => 'd'.$1, 'monthly', [stat $_]->[9])
+ for (glob "$VNDB::ROOT/data/docs/*");
$_[KERNEL]->call(sitemap => addurl => $_, 'weekly')
- for ( (map { 'v/'.$_ } 'a'..'z'), 'v/all', 'v/search', (map { 'p/'.$_ } 'a'..'z'), 'p/all');
+ for (map { 'v/'.$_, 'p/'.$_ } 'a'..'z', 0, 'all');
$_[KERNEL]->yield('vnpages');
}
@@ -89,10 +88,8 @@ sub vnpages {
JOIN changes c ON vr.id = c.id
|);
$q->execute;
- while(local $_ = $q->fetchrow_arrayref) {
- $_[KERNEL]->call(sitemap => addurl => 'v'.$_->[0], 'weekly', $_->[1], 0.7);
- $_[KERNEL]->call(sitemap => addurl => 'v'.$_->[0].'/rg', 'weekly', $_->[1], 0.7) if $_->[2];
- }
+ $_[KERNEL]->call(sitemap => addurl => 'v'.$_->[0], 'weekly', $_->[1], 0.7)
+ while(local $_ = $q->fetchrow_arrayref);
$_[KERNEL]->yield('releasepages');
}
@@ -108,9 +105,8 @@ sub releasepages {
JOIN changes c ON c.id = rr.id
|);
$q->execute;
- while(local $_ = $q->fetchrow_arrayref) {
- $_[KERNEL]->call(sitemap => addurl => 'r'.$_->[0], 'weekly', $_->[1], 0.3);
- }
+ $_[KERNEL]->call(sitemap => addurl => 'r'.$_->[0], 'weekly', $_->[1], 0.3)
+ while(local $_ = $q->fetchrow_arrayref);
$_[KERNEL]->yield('producerpages');
}
@@ -126,9 +122,8 @@ sub producerpages {
JOIN changes c ON c.id = pr.id
|);
$q->execute;
- while(local $_ = $q->fetchrow_arrayref) {
- $_[KERNEL]->call(sitemap => addurl => 'p'.$_->[0], 'weekly', $_->[1]);
- }
+ $_[KERNEL]->call(sitemap => addurl => 'p'.$_->[0], 'weekly', $_->[1])
+ while(local $_ = $q->fetchrow_arrayref);
$_[KERNEL]->yield('finish');
}
@@ -146,7 +141,7 @@ sub finish {
sub addurl { # loc, changefreq, lastmod, priority
$_[HEAP]{xml}->startTag('url');
- $_[HEAP]{xml}->dataElement(loc => $_[HEAP]{baseurl}.'/'.$_[ARG0]);
+ $_[HEAP]{xml}->dataElement(loc => $VNDB::S{url}.'/'.$_[ARG0]);
$_[HEAP]{xml}->dataElement(changefreq => $_[ARG1]) if defined $_[ARG1];
$_[HEAP]{xml}->dataElement(lastmod => DateTime->from_epoch(epoch => $_[ARG2])->ymd) if defined $_[ARG2];
$_[HEAP]{xml}->dataElement(priority => $_[ARG3]) if defined $_[ARG3];
diff --git a/lib/VNDB.pm b/lib/VNDB.pm
deleted file mode 100644
index 94edf82e..00000000
--- a/lib/VNDB.pm
+++ /dev/null
@@ -1,247 +0,0 @@
-package VNDB;
-
-use strict;
-use warnings;
-
-BEGIN { require 'global.pl'; }
-our $DEBUG;
-
-require Time::HiRes if $DEBUG;
-require Data::Dumper if $DEBUG;
-use VNDB::Util::Template;
-use VNDB::Util::Request;
-use VNDB::Util::Response;
-use VNDB::Util::DB;
-use VNDB::Util::Tools;
-use VNDB::Util::Auth;
-use VNDB::Discussions;
-use VNDB::HomePages;
-use VNDB::Producers;
-use VNDB::Releases;
-use VNDB::VNLists;
-use VNDB::Users;
-use VNDB::VN;
-
-
-my %VNDBuris = ( # wildcards: * -> (.+), + -> ([0-9]+)
- '/' => sub { shift->HomePage },
- 'd+' => sub { shift->DocPage(shift) },
- 'd+.+' => sub { shift->ResRedirect('/d'.$_[0][0].'#'.$_[0][1]) },
- nospam => sub { shift->ResAddTpl(error => { err => 'formerr' }) },
- hist => {'*'=> sub { shift->History(undef, undef, $_[1]) } },
- # users
- u => {
- login => sub { shift->UsrLogin },
- logout => sub { shift->UsrLogout },
- register => sub { shift->UsrReg },
- newpass => sub { shift->UsrPass },
- list => {
- '/' => sub { shift->UsrList },
- '*' => sub { $_[3] =~ /^([a-z0]|all)$/ ? shift->UsrList($_[2]) : shift->ResNotFound },
- },
- },
- 'u+' => {
- '/' => sub { shift->UsrPage(shift) },
- edit => sub { shift->UsrEdit(shift) },
- del => sub { shift->UsrDel(shift) },
- list => sub { shift->RList(shift) },
- vlist => sub { shift->VNMyList(shift) },
- wish => sub { shift->WList(shift) },
- hist => {'*'=> sub { shift->History('u', shift, $_[1]) } },
- },
- # visual novels
- v => {
- '/' => sub { shift->VNBrowse },
- new => sub { shift->VNEdit(0); },
- '*' => sub { $_[2] =~ /^([a-z0]|all|search)$/ ? shift->VNBrowse($_[1]) : shift->ResNotFound; },
- },
- 'v+' => {
- '/' => sub { shift->VNPage(shift) },
- stats => sub { shift->VNPage(shift, shift) },
- rg => sub { shift->VNPage(shift, shift) },
- scr => sub { shift->VNPage(shift, shift) },
- edit => sub { shift->VNEdit(shift) },
- vote => sub { shift->VNVote(shift) },
- wish => sub { shift->WListMod(shift) },
- add => sub { shift->REdit('v', shift) },
- lock => sub { shift->VNLock(shift) },
- hide => sub { shift->VNHide(shift) },
- hist => {'*'=> sub { shift->History('v', shift, $_[1]) } },
- },
- 'v+.+' => sub { shift->VNPage($_[0][0], '', $_[0][1]) },
- # releases
- 'r+' => {
- '/' => sub { shift->RPage(shift) },
- edit => sub { shift->REdit('r', shift) },
- lock => sub { shift->RLock(shift) },
- hide => sub { shift->RHide(shift) },
- list => sub { shift->RListMod(shift) },
- hist => {'*'=> sub { shift->History('r', shift, $_[1]) } },
- },
- 'r+.+' => sub { shift->RPage($_[0][0], $_[0][1]) },
- # producers
- p => {
- '/' => sub { shift->PBrowse },
- add => sub { shift->PEdit(0) },
- '*' => sub { $_[2] =~ /^([a-z0]|all)$/ ? shift->PBrowse($_[1]) : shift->ResNotFound; }
- },
- 'p+' => {
- '/' => sub { shift->PPage(shift) },
- edit => sub { shift->PEdit(shift) },
- lock => sub { shift->PLock(shift) },
- hide => sub { shift->PHide(shift) },
- hist => {'*'=> sub { shift->History('p', shift, $_[1]) } },
- },
- 'p+.+' => sub { shift->PPage($_[0][0], $_[0][1]) },
- # discussions
- t => {
- '/' => sub { shift->TIndex },
- '*' => {
- '/' => sub { shift->TTag($_[1]) },
- new => sub { shift->TEdit(0, 0, $_[1]) },
- },
- },
- 't+' => {
- '/' => sub { shift->TThread(shift) },
- reply => sub { shift->TEdit(shift) },
- '+' => sub { shift->TThread(shift, shift) },
- },
- 't+.+' => {
- edit => sub { shift->TEdit($_[0][0], $_[0][1]) },
- '/' => sub { $_[0]->ResRedirect('/t'.$_[1][0].($_[1][1]>$_[0]->{postsperpage}?'/'.ceil($_[1][1]/$_[0]->{postsperpagee}):'').'#'.$_[1][1], 'perm') },
- },
- # stuff (.xml extension to make sure they aren't counted as pageviews)
- xml => {
- 'producers.xml' => sub { shift->PXML },
- 'vn.xml' => sub { shift->VNXML },
- 'screenshots.xml' => sub { shift->VNScrXML },
- },
-);
-
-
-# provide redirects for old URIs
-my %OLDuris = (
- faq => sub { shift->ResRedirect('/d6', 'perm') },
- notes => sub { shift->ResRedirect('/d8', 'perm') },
- vn => {
- rss => sub { shift->ResRedirect('/hist/rss?t=v&e=1', 'perm') },
- '*' => sub { shift->ResRedirect('/v/'.$_[1], 'perm') },
- },
- v => {
- cat => sub {
- my $f = $_[0]->FormCheck({name=>'i',required=>0},{name=>'e',required=>0},{name=>'l',required=>0},
- {name=>'p',required=>0},{name=>'o',required=>0},{name=>'s',required=>0});
- my %f;
- $f{$_} = $f->{$_} for (qw|p o s|);
- $f{q} = join ' ', (map $VNDB::CAT->{substr($_,0,1)}[1]{substr($_,1,2)}, split /,/, $f->{i}),
- (map '-'.$VNDB::CAT->{substr($_,0,1)}[1]{substr($_,1,2)}, split /,/, $f->{e}),
- (map $VNDB::LANG->{$_}, split /,/, $f->{l});
- !$f{$_}&&delete $f{$_} for keys %f;
- $_[0]->ResRedirect('/v/search'.(!(keys %f)?'':'?'.join(';', map $_.'='.$f{$_}, keys %f) ), 'perm');
- },
- },
- 'v+' => {
- votes => sub { shift->ResRedirect('/v'.(shift).'/stats', 'perm') },
- hist=>{rss => sub { shift->ResRedirect('/v'.(shift).'/hist/rss.xml', 'perm') } },
- },
- u => {
- '*' => {
- '*' => sub {
- if($_[2] =~ /^_(login|logout|register|newpass|list)$/) {
- $_[3] eq '/' ? $_[0]->ResRedirect('/u/'.$1, 'perm') : $_[0]->ResRedirect('/u/'.$1.'/'.$_[3], 'perm');
- } else {
- my $id = $_[0]->DBGetUser(username => $_[2])->[0]{id};
- $id ? $_[0]->ResRedirect('/u'.$id.'/'.$_[3], 'perm') : $_[0]->ResNotFound;
- }
- },
- }
- },
- 'u+' => {
- votes => sub { shift->ResRedirect('/u'.(shift).'/list', 'perm') },
- hist=>{rss => sub { shift->ResRedirect('/u'.(shift).'/hist/rss.xml', 'perm') } },
- },
- 'p+' => {
- hist=>{rss => sub { shift->ResRedirect('/p'.(shift).'/hist/rss.xml', 'perm') } },
- },
- 'r+' => {
- hist=>{rss => sub { shift->ResRedirect('/r'.(shift).'/hist/rss.xml', 'perm') } },
- },
- hist=>{rss => sub { shift->ResRedirect('/hist/rss.xml', 'perm') } },
-);
-
-
-
-sub new {
- my $self = shift;
- my $type = ref($self) || $self;
- my %args = @_;
-
- my $me = bless {
- debug => $VNDB::DEBUG,
- %args,
- _DB => VNDB::Util::DB->new(@VNDB::DBLOGIN),
- _TPL => VNDB::Util::Template->new(%{$args{tplopts}}),
- cmds => [],
- }, $type;
-
- return $me;
-}
-
-
-sub get_page {
- my $self = shift;
- my $r = shift;
-
- $self->{_Req} = VNDB::Util::Request->new($r);
- $self->{_Res} = VNDB::Util::Response->new($self->{_TPL});
-
- $self->AuthCheckCookie();
- $self->checkuri();
-
- my $res = $self->ResSetModPerl($r);
- $self->DBCommit();
-
- # commands have to be executed _after_ the call to DBCommit,
- # otherwise Multi can't see the new additions
- $self->RunCmd();
-
- return($self, $res);
-}
-
-
-sub checkuri {
- my $self = shift;
- (my $uri = lc($self->ReqUri)) =~ s/^\/+//;
- $uri =~ s/\?.*$//;
- return $self->ResRedirect("/$uri", 'perm') if $uri =~ s/\/+$//;
- $uri =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; # ugly hack, but we only accept ASCII anyway
- return $self->ResNotFound() if $uri !~ /^[a-z0-9\-\._~\/]*$/; # rfc3986 section 2.3, "Unreserved Characters"
- my @uri;
- defined $_ and push(@uri, $_) for (split(/\/+/, $uri));
- my @ouri = @uri; # items in @uri can be modified by uri2page
- $self->uri2page(\%VNDBuris, \@uri, 0);
- $self->uri2page(\%OLDuris, \@ouri, 0) # provide redirects for old uris
- if $self->{_Res}->{code} == 404;
-}
-
-
-sub uri2page {
- my($s, $o, $u, $i) = @_;
- $u->[$i] = '/' if !defined $u->[$i];
- my $n = $o->{$u->[$i]} ? $u->[$i] : ((map {
- if(/[\*\+]/) {
- (my $t = "^$_\$") =~ s/\./\\./g;
- /\*/ ? ($t =~ s/\*/(.+)/g) : ($t =~ s/\+/([1-9][0-9]*)/g);
- $u->[$i] =~ /$t/ ? ($u->[$i] = $2?[$1,$2]:$1) && $_ : ();
- } else { () } }
- sort { length($b) <=> length($a) } keys %$o)[0] || '*');
- ref($o->{$n}) eq 'HASH' && $n ne '/' ?
- $s->uri2page($o->{$n}, $u, ++$i) :
- ref($o->{$n}) eq 'CODE' && $i == $#$u ?
- &{$o->{$n}}($s, @$u) :
- $s->ResNotFound();
-}
-
-
-1;
-
diff --git a/lib/VNDB/DB/Discussions.pm b/lib/VNDB/DB/Discussions.pm
new file mode 100644
index 00000000..1e626c57
--- /dev/null
+++ b/lib/VNDB/DB/Discussions.pm
@@ -0,0 +1,221 @@
+
+package VNDB::DB::Discussions;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+our @EXPORT = qw|dbThreadGet dbThreadEdit dbThreadAdd dbPostGet dbPostEdit dbPostAdd dbThreadCount|;
+
+
+# Options: id, type, iid, results, page, what
+# What: tags, tagtitles, firstpost, lastpost
+sub dbThreadGet {
+ my($self, %o) = @_;
+ $o{results} ||= 50;
+ $o{page} ||= 1;
+ $o{what} ||= '';
+ $o{order} ||= 't.id DESC';
+
+ my %where = (
+ $o{id} ? (
+ 't.id = ?' => $o{id} ) : (),
+ !$o{id} ? (
+ 't.hidden = FALSE' => 0 ) : (),
+ $o{type} && !$o{iid} ? (
+ 't.id IN(SELECT tid FROM threads_tags WHERE type = ?)' => $o{type} ) : (),
+ $o{type} && $o{iid} ? (
+ 'tt.type = ?' => $o{type}, 'tt.iid = ?' => $o{iid} ) : (),
+ );
+
+ my @select = (
+ qw|t.id t.title t.count t.locked t.hidden|,
+ $o{what} =~ /firstpost/ ? ('tpf.uid AS fuid', 'tpf.date AS fdate', 'uf.username AS fusername') : (),
+ $o{what} =~ /lastpost/ ? ('tpl.uid AS luid', 'tpl.date AS ldate', 'ul.username AS lusername') : (),
+ );
+
+ my @join = (
+ $o{what} =~ /firstpost/ ? (
+ 'JOIN threads_posts tpf ON tpf.tid = t.id AND tpf.num = 1',
+ 'JOIN users uf ON uf.id = tpf.uid'
+ ) : (),
+ $o{what} =~ /lastpost/ ? (
+ 'JOIN threads_posts tpl ON tpl.tid = t.id AND tpl.num = t.count',
+ 'JOIN users ul ON ul.id = tpl.uid'
+ ) : (),
+ $o{type} && $o{iid} ?
+ 'JOIN threads_tags tt ON tt.tid = t.id' : (),
+ );
+
+ my($r, $np) = $self->dbPage(\%o, q|
+ SELECT !s
+ FROM threads t
+ !s
+ !W
+ ORDER BY !s|,
+ join(', ', @select), join(' ', @join), \%where, $o{order}
+ );
+
+ if($o{what} =~ /(tags|tagtitles)/ && $#$r >= 0) {
+ my %r = map {
+ $r->[$_]{tags} = [];
+ ($r->[$_]{id}, $_)
+ } 0..$#$r;
+
+ if($o{what} =~ /tags/) {
+ ($_->{type}=~s/ +//||1) && push(@{$r->[$r{$_->{tid}}]{tags}}, [ $_->{type}, $_->{iid} ]) for (@{$self->dbAll(q|
+ SELECT tid, type, iid
+ FROM threads_tags
+ WHERE tid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+ if($o{what} =~ /tagtitles/) {
+ ($_->{type}=~s/ +//||1) && push(@{$r->[$r{$_->{tid}}]{tags}}, $_) for (@{$self->dbAll(q|
+ SELECT tt.tid, tt.type, tt.iid, COALESCE(u.username, vr.title, pr.name) AS title, COALESCE(u.username, vr.original, pr.original) AS original
+ FROM threads_tags tt
+ LEFT JOIN vn v ON tt.type = 'v' AND v.id = tt.iid
+ LEFT JOIN vn_rev vr ON vr.id = v.latest
+ LEFT JOIN producers p ON tt.type = 'p' AND p.id = tt.iid
+ LEFT JOIN producers_rev pr ON pr.id = p.latest
+ LEFT JOIN users u ON tt.type = 'u' AND u.id = tt.iid
+ WHERE tt.tid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+ }
+
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# id, %options->( title locked hidden tags }
+sub dbThreadEdit {
+ my($self, $id, %o) = @_;
+
+ my %set = (
+ 'title = ?' => $o{title},
+ 'locked = ?' => $o{locked}?1:0,
+ 'hidden = ?' => $o{hidden}?1:0,
+ );
+
+ $self->dbExec(q|
+ UPDATE threads
+ !H
+ WHERE id = ?|,
+ \%set, $id);
+
+ if($o{tags}) {
+ $self->dbExec('DELETE FROM threads_tags WHERE tid = ?', $id);
+ $self->dbExec(q|
+ INSERT INTO threads_tags (tid, type, iid)
+ VALUES (?, ?, ?)|,
+ $id, $_->[0], $_->[1]||0
+ ) for (@{$o{tags}});
+ }
+}
+
+
+# %options->{ title hidden locked tags }
+sub dbThreadAdd {
+ my($self, %o) = @_;
+
+ my $id = $self->dbRow(q|
+ INSERT INTO threads (title, hidden, locked)
+ VALUES (?, ?, ?)
+ RETURNING id|,
+ $o{title}, $o{hidden}?1:0, $o{locked}?1:0
+ )->{id};
+
+ $self->dbExec(q|
+ INSERT INTO threads_tags (tid, type, iid)
+ VALUES (?, ?, ?)|,
+ $id, $_->[0], $_->[1]||0
+ ) for (@{$o{tags}});
+
+ return $id;
+}
+
+
+# Returns thread count of a specific item tag
+# Arguments: type, iid
+sub dbThreadCount {
+ my($self, $type, $iid) = @_;
+ return $self->dbRow(q|
+ SELECT COUNT(*) AS cnt
+ FROM threads_tags
+ WHERE type = ? AND iid = ?|,
+ $type, $iid)->{cnt};
+}
+
+
+# Options: tid, num, what, page, results
+sub dbPostGet {
+ my($self, %o) = @_;
+ $o{results} ||= 50;
+ $o{page} ||= 1;
+
+ my %where = (
+ 'tp.tid = ?' => $o{tid},
+ $o{num} ? (
+ 'tp.num = ?' => $o{num} ) : (),
+ );
+
+ my($r, $np) = $self->dbPage(\%o, q|
+ SELECT tp.num, tp.date, tp.edited, tp.msg, tp.hidden, tp.uid, u.username
+ FROM threads_posts tp
+ JOIN users u ON u.id = tp.uid
+ !W
+ ORDER BY tp.num ASC|,
+ \%where,
+ );
+
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# tid, num, %options->{ num msg hidden lastmod }
+sub dbPostEdit {
+ my($self, $tid, $num, %o) = @_;
+
+ my %set = (
+ 'msg = ?' => $o{msg},
+ 'edited = ?' => $o{lastmod},
+ 'hidden = ?' => $o{hidden}?1:0,
+ );
+
+ $self->dbExec(q|
+ UPDATE threads_posts
+ !H
+ WHERE tid = ?
+ AND num = ?|,
+ \%set, $tid, $num
+ );
+}
+
+
+# tid, %options->{ uid msg }
+sub dbPostAdd {
+ my($self, $tid, %o) = @_;
+
+ my $num = $self->dbRow('SELECT num FROM threads_posts WHERE tid = ? ORDER BY num DESC LIMIT 1', $tid)->{num};
+ $num = $num ? $num+1 : 1;
+ $o{uid} ||= $self->authInfo->{id};
+
+ $self->dbExec(q|
+ INSERT INTO threads_posts (tid, num, uid, msg)
+ VALUES(?, ?, ?, ?)|,
+ $tid, $num, @o{qw| uid msg |}
+ );
+ $self->dbExec(q|
+ UPDATE threads
+ SET count = count+1
+ WHERE id = ?|,
+ $tid);
+
+ return $num;
+}
+
+
+1;
+
diff --git a/lib/VNDB/DB/Misc.pm b/lib/VNDB/DB/Misc.pm
new file mode 100644
index 00000000..14ef45ac
--- /dev/null
+++ b/lib/VNDB/DB/Misc.pm
@@ -0,0 +1,179 @@
+
+package VNDB::DB::Misc;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+our @EXPORT = qw|
+ dbStats dbRevisionInsert dbItemInsert dbRevisionGet dbItemMod dbLanguages
+|;
+
+
+# Returns: hashref, key = section, value = number of (visible) entries
+# Sections: vn, producers, releases, users, threads, posts
+sub dbStats {
+ my $s = shift;
+ return { map {
+ $_->{section} eq 'threads_posts' ? 'posts' : $_->{section}, $_->{count}
+ } @{$s->dbAll('SELECT * FROM stats_cache')}};
+}
+
+
+# Inserts a new revision and updates the item to point to this revision
+# This function leaves the DB in an inconsistent state, the actual revision
+# will have to be inserted directly after calling this function, otherwise
+# the commit will fail.
+# Arguments: type [0..2], item ID, edit summary
+# Returns: local revision, global revision
+sub dbRevisionInsert {
+ my($self, $type, $iid, $editsum, $uid) = @_;
+
+ my $table = [qw|vn releases producers|]->[$type];
+
+ my $c = $self->dbRow(q|
+ INSERT INTO changes (type, requester, ip, comments, rev)
+ VALUES (?, ?, ?, ?, (
+ SELECT c.rev+1
+ FROM changes c
+ JOIN !s_rev ir ON ir.id = c.id
+ WHERE ir.!sid = ?
+ ORDER BY c.id DESC
+ LIMIT 1
+ ))
+ RETURNING id, rev|,
+ $type, $uid||$self->authInfo->{id}, $self->reqIP, $editsum,
+ $table, [qw|v r p|]->[$type], $iid
+ );
+
+ $self->dbExec(q|UPDATE !s SET latest = ? WHERE id = ?|, $table, $c->{id}, $iid);
+
+ return ($c->{rev}, $c->{id});
+}
+
+
+# Comparable to RevisionInsert, but creates a new item with a corresponding
+# change. Same things about inconsistent state, etc.
+# Argumments: type [0..2], edit summary, [uid]
+# Returns: item id, global revision
+sub dbItemInsert {
+ my($self, $type, $editsum, $uid) = @_;
+
+ my $cid = $self->dbRow(q|
+ INSERT INTO changes (type, requester, ip, comments)
+ VALUES (?, ?, ?, ?)
+ RETURNING id|,
+ $type, $uid||$self->authInfo->{id}, $self->reqIP, $editsum
+ )->{id};
+
+ my $iid = $self->dbRow(q|
+ INSERT INTO !s (latest)
+ VALUES (?)
+ RETURNING id|,
+ [qw|vn releases producers|]->[$type], $cid
+ )->{id};
+
+ return ($iid, $cid);
+}
+
+
+# Options: type, iid, uid, auto, hidden, edit, page, results, what, releases
+# what: item user
+sub dbRevisionGet {
+ my($self, %o) = @_;
+ $o{results} ||= 10;
+ $o{page} ||= 1;
+ $o{auto} ||= 0; # 0:show, -1:only, 1:hide
+ $o{hidden} ||= 0;
+ $o{edit} ||= 0; # 0:both, -1:new, 1:edits
+ $o{what} ||= '';
+ $o{releases} = 0 if !$o{type} || $o{type} ne 'v' || !$o{iid};
+
+ my %where = (
+ $o{releases} ? (
+ '((c.type = ? AND vr.vid = ?) OR (c.type = ? AND rv.vid = ?))' => [0, $o{iid}, 1, $o{iid}],
+ ) : (
+ $o{type} ? (
+ 'c.type = ?' => { v=>0, r=>1, p=>2 }->{$o{type}} ) : (),
+ $o{iid} ? (
+ '!sr.!sid = ?' => [ $o{type}, $o{type}, $o{iid} ] ) : (),
+ ),
+ $o{uid} ? (
+ 'c.requester = ?' => $o{uid} ) : (),
+ $o{auto} ? (
+ 'c.requester !s 1' => $o{auto} < 0 ? '=' : '<>' ) : (),
+ $o{hidden} == 1 ? (
+ '(v.hidden IS NOT NULL AND v.hidden = FALSE OR r.hidden IS NOT NULL AND r.hidden = FALSE OR p.hidden IS NOT NULL AND p.hidden = FALSE)' => 1,
+ ) : $o{hidden} == -1 ? (
+ '(v.hidden IS NOT NULL AND v.hidden = TRUE OR r.hidden IS NOT NULL AND r.hidden = TRUE OR p.hidden IS NOT NULL AND p.hidden = TRUE)' => 1,
+ ) : (),
+ $o{edit} ? (
+ 'c.rev !s 1' => $o{edit} < 0 ? '=' : '>' ) : (),
+ );
+
+ my @join = (
+ $o{iid} || $o{what} =~ /item/ || $o{hidden} || $o{releases} ? (
+ 'LEFT JOIN vn_rev vr ON c.type = 0 AND c.id = vr.id',
+ 'LEFT JOIN releases_rev rr ON c.type = 1 AND c.id = rr.id',
+ 'LEFT JOIN producers_rev pr ON c.type = 2 AND c.id = pr.id',
+ ) : (),
+ $o{hidden} || $o{releases} ? (
+ 'LEFT JOIN vn v ON c.type = 0 AND vr.vid = v.id',
+ 'LEFT JOIN releases r ON c.type = 1 AND rr.rid = r.id',
+ 'LEFT JOIN producers p ON c.type = 2 AND pr.pid = p.id',
+ ) : (),
+ $o{what} =~ /user/ ? 'JOIN users u ON c.requester = u.id' : (),
+ $o{releases} ? 'LEFT JOIN releases_vn rv ON c.id = rv.rid' : (),
+ );
+
+ my @select = (
+ qw|c.id c.type c.added c.requester c.comments c.rev c.causedby|,
+ $o{what} =~ /user/ ? 'u.username' : (),
+ $o{what} =~ /item/ ? (
+ 'COALESCE(vr.vid, rr.rid, pr.pid) AS iid',
+ 'COALESCE(vr.title, rr.title, pr.name) AS ititle',
+ 'COALESCE(vr.original, rr.original, pr.original) AS ioriginal',
+ ) : (),
+ );
+
+ my($r, $np) = $self->dbPage(\%o, q|
+ SELECT !s
+ FROM changes c
+ !s
+ !W
+ ORDER BY c.id DESC|,
+ join(', ', @select), join(' ', @join), \%where
+ );
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# Lock or hide a DB item
+# arguments: v/r/p, id, %options ->( hidden, locked )
+sub dbItemMod {
+ my($self, $type, $id, %o) = @_;
+ $self->dbExec('UPDATE !s !H WHERE id = ?',
+ {qw|v vn r releases p producers|}->{$type},
+ { map { ($_.' = ?', int $o{$_}) } keys %o }, $id
+ );
+}
+
+
+# Returns a list of languages actually in use
+sub dbLanguages {
+ my $self = shift;
+ return [
+ map $_->{language}, @{$self->dbAll(q|
+ SELECT DISTINCT rr.language
+ FROM releases r
+ JOIN releases_rev rr ON rr.id = r.latest
+ WHERE r.hidden = FALSE|
+ )}
+ ];
+}
+
+
+
+
+1;
+
diff --git a/lib/VNDB/DB/Producers.pm b/lib/VNDB/DB/Producers.pm
new file mode 100644
index 00000000..edee4a8a
--- /dev/null
+++ b/lib/VNDB/DB/Producers.pm
@@ -0,0 +1,114 @@
+
+package VNDB::DB::Producers;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+our @EXPORT = qw|dbProducerGet dbProducerEdit dbProducerAdd|;
+
+
+# options: results, page, id, search, char, rev
+# what: changes, vn
+sub dbProducerGet {
+ my $self = shift;
+ my %o = (
+ results => 10,
+ page => 1,
+ what => '',
+ @_
+ );
+
+ $o{search} =~ s/%//g if $o{search};
+
+ my %where = (
+ !$o{id} && !$o{rev} ? (
+ 'p.hidden = FALSE' => 1 ) : (),
+ $o{id} ? (
+ 'p.id = ?' => $o{id} ) : (),
+ $o{search} ? (
+ '(pr.name ILIKE ? OR pr.original ILIKE ?)', [ '%%'.$o{search}.'%%', '%%'.$o{search}.'%%' ] ) : (),
+ $o{char} ? (
+ 'LOWER(SUBSTR(pr.name, 1, 1)) = ?' => $o{char} ) : (),
+ defined $o{char} && !$o{char} ? (
+ '(ASCII(pr.name) < 97 OR ASCII(pr.name) > 122) AND (ASCII(pr.name) < 65 OR ASCII(pr.name) > 90)' => 1 ) : (),
+ $o{rev} ? (
+ 'c.rev = ?' => $o{rev} ) : (),
+ );
+
+ my @join;
+ push @join, $o{rev} ? 'JOIN producers p ON p.id = pr.pid' : 'JOIN producers p ON pr.id = p.latest';
+ push @join, 'JOIN changes c ON c.id = pr.id' if $o{what} =~ /changes/ || $o{rev};
+ push @join, 'JOIN users u ON u.id = c.requester' if $o{what} =~ /changes/;
+
+ my $select = 'p.id, p.locked, p.hidden, pr.type, pr.name, pr.original, pr.website, pr.lang, pr.desc';
+ $select .= ', c.added, c.requester, c.comments, p.latest, pr.id AS cid, u.username, c.rev' if $o{what} =~ /changes/;
+
+ my($r, $np) = $self->dbPage(\%o, q|
+ SELECT !s
+ FROM producers_rev pr
+ !s
+ !W
+ ORDER BY pr.name ASC|,
+ $select, join(' ', @join), \%where,
+ );
+
+ if(@$r && $o{what} =~ /vn/) {
+ my %r = map {
+ $r->[$_]{vn} = [];
+ ($r->[$_]{id}, $_)
+ } 0..$#$r;
+
+ push @{$r->[$r{$_->{pid}}]{vn}}, $_ for (@{$self->dbAll(q|
+ SELECT MAX(vp.pid) AS pid, v.id, MAX(vr.title) AS title, MAX(vr.original) AS original, MIN(rr.released) AS date
+ FROM releases_producers vp
+ JOIN releases_rev rr ON rr.id = vp.rid
+ JOIN releases r ON r.latest = rr.id
+ JOIN releases_vn rv ON rv.rid = rr.id
+ JOIN vn v ON v.id = rv.vid
+ JOIN vn_rev vr ON vr.id = v.latest
+ WHERE vp.pid IN(!l)
+ AND v.hidden = FALSE
+ GROUP BY v.id
+ ORDER BY date|,
+ [ keys %r ]
+ )});
+ }
+
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# arguments: id, %options ->( editsum uid + insert_rev )
+# returns: ( local revision, global revision )
+sub dbProducerEdit {
+ my($self, $pid, %o) = @_;
+ my($rev, $cid) = $self->dbRevisionInsert(2, $pid, $o{editsum}, $o{uid});
+ insert_rev($self, $cid, $pid, \%o);
+ return ($rev, $cid);
+}
+
+
+# arguments: %options ->( editsum uid + insert_rev )
+# returns: ( item id, global revision )
+sub dbProducerAdd {
+ my($self, %o) = @_;
+ my($pid, $cid) = $self->dbItemInsert(2, $o{editsum}, $o{uid});
+ insert_rev($self, $cid, $pid, \%o);
+ return ($pid, $cid);
+}
+
+
+# helper function, inserts a producer revision
+# Arguments: global revision, item id, { columns in producers_rev }
+sub insert_rev {
+ my($self, $cid, $pid, $o) = @_;
+ $self->dbExec(q|
+ INSERT INTO producers_rev (id, pid, name, original, website, type, lang, "desc")
+ VALUES (!l)|,
+ [ $cid, $pid, @$o{qw| name original website type lang desc|} ]
+ );
+}
+
+
+1;
diff --git a/lib/VNDB/DB/Releases.pm b/lib/VNDB/DB/Releases.pm
new file mode 100644
index 00000000..b2c78d5e
--- /dev/null
+++ b/lib/VNDB/DB/Releases.pm
@@ -0,0 +1,168 @@
+
+package VNDB::DB::Releases;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+our @EXPORT = qw|dbReleaseGet dbReleaseAdd dbReleaseEdit|;
+
+
+# Options: id vid rev order unreleased page results what
+# What: changes vn producers platforms media
+sub dbReleaseGet {
+ my($self, %o) = @_;
+ $o{results} ||= 50;
+ $o{page} ||= 1;
+ $o{what} ||= '';
+ $o{order} ||= 'rr.released ASC';
+
+ my %where = (
+ !$o{id} && !$o{rev} ? (
+ 'r.hidden = FALSE' => 0 ) : (),
+ $o{id} ? (
+ 'r.id = ?' => $o{id} ) : (),
+ $o{rev} ? (
+ 'c.rev = ?' => $o{rev} ) : (),
+ $o{vid} ? (
+ 'rv.vid = ?' => $o{vid} ) : (),
+ defined $o{unreleased} ? (
+ q|rr.released !s TO_CHAR('today'::timestamp, 'YYYYMMDD')::integer| => $o{unreleased} ? '>' : '<=' ) : (),
+ );
+
+ my @join = (
+ $o{rev} ? 'JOIN releases r ON r.id = rr.rid' : 'JOIN releases r ON rr.id = r.latest',
+ $o{vid} ? 'JOIN releases_vn rv ON rv.rid = rr.id' : (),
+ $o{what} =~ /changes/ || $o{rev} ? (
+ 'JOIN changes c ON c.id = rr.id',
+ 'JOIN users u ON u.id = c.requester'
+ ) : (),
+ );
+
+ my @select = (
+ qw|r.id r.locked r.hidden rr.title rr.original rr.gtin rr.language rr.website rr.released rr.notes rr.minage rr.type|, 'rr.id AS cid',
+ $o{what} =~ /changes/ ? qw|c.added c.requester c.comments r.latest u.username c.rev| : (),
+ );
+
+ my($r, $np) = $self->dbPage(\%o, q|
+ SELECT !s
+ FROM releases_rev rr
+ !s
+ !W
+ ORDER BY !s|,
+ join(', ', @select), join(' ', @join), \%where, $o{order}
+ );
+
+ if(@$r && $o{what} =~ /(vn|producers|platforms|media)/) {
+ my %r = map {
+ $r->[$_]{producers} = [];
+ $r->[$_]{platforms} = [];
+ $r->[$_]{media} = [];
+ $r->[$_]{vn} = [];
+ ($r->[$_]{cid}, $_)
+ } 0..$#$r;
+
+ if($o{what} =~ /vn/) {
+ push(@{$r->[$r{$_->{rid}}]{vn}}, $_) for (@{$self->dbAll(q|
+ SELECT rv.rid, vr.vid, vr.title, vr.original
+ FROM releases_vn rv
+ JOIN vn v ON v.id = rv.vid
+ JOIN vn_rev vr ON vr.id = v.latest
+ WHERE rv.rid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+
+ if($o{what} =~ /producers/) {
+ push(@{$r->[$r{$_->{rid}}]{producers}}, $_) for (@{$self->dbAll(q|
+ SELECT rp.rid, p.id, pr.name, pr.original, pr.type
+ FROM releases_producers rp
+ JOIN producers p ON rp.pid = p.id
+ JOIN producers_rev pr ON pr.id = p.latest
+ WHERE rp.rid IN(!l)
+ ORDER BY pr.name|,
+ [ keys %r ]
+ )});
+ }
+
+ if($o{what} =~ /platforms/) {
+ push(@{$r->[$r{$_->{rid}}]{platforms}}, $_->{platform}) for (@{$self->dbAll(q|
+ SELECT rid, platform
+ FROM releases_platforms
+ WHERE rid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+
+ if($o{what} =~ /media/) {
+ ($_->{medium}=~s/\s+//||1)&&push(@{$r->[$r{$_->{rid}}]{media}}, $_) for (@{$self->dbAll(q|
+ SELECT rid, medium, qty
+ FROM releases_media
+ WHERE rid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+ }
+
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# arguments: id, %options ->( editsum uid + insert_rev )
+# returns: ( local revision, global revision )
+sub dbReleaseEdit {
+ my($self, $rid, %o) = @_;
+ my($rev, $cid) = $self->dbRevisionInsert(1, $rid, $o{editsum}, $o{uid});
+ insert_rev($self, $cid, $rid, \%o);
+ return ($rev, $cid);
+}
+
+
+# arguments: %options ->( editsum uid + insert_rev )
+# returns: ( item id, global revision )
+sub dbReleaseAdd {
+ my($self, %o) = @_;
+ my($rid, $cid) = $self->dbItemInsert(1, $o{editsum}, $o{uid});
+ insert_rev($self, $cid, $rid, \%o);
+ return ($rid, $cid);
+}
+
+
+# helper function, inserts a producer revision
+# Arguments: global revision, item id, { columns in releases_rev + vn + producers + media + platforms }
+sub insert_rev {
+ my($self, $cid, $rid, $o) = @_;
+
+ $self->dbExec(q|
+ INSERT INTO releases_rev (id, rid, title, original, gtin, language, website, released, notes, minage, type)
+ VALUES (!l)|,
+ [ $cid, $rid, @$o{qw| title original gtin language website released notes minage type|} ]);
+
+ $self->dbExec(q|
+ INSERT INTO releases_producers (rid, pid)
+ VALUES (?, ?)|,
+ $cid, $_
+ ) for (@{$o->{producers}});
+
+ $self->dbExec(q|
+ INSERT INTO releases_platforms (rid, platform)
+ VALUES (?, ?)|,
+ $cid, $_
+ ) for (@{$o->{platforms}});
+
+ $self->dbExec(q|
+ INSERT INTO releases_vn (rid, vid)
+ VALUES (?, ?)|,
+ $cid, $_
+ ) for (@{$o->{vn}});
+
+ $self->dbExec(q|
+ INSERT INTO releases_media (rid, medium, qty)
+ VALUES (?, ?, ?)|,
+ $cid, $_->[0], $_->[1]
+ ) for (@{$o->{media}});
+}
+
+
+1;
+
diff --git a/lib/VNDB/DB/ULists.pm b/lib/VNDB/DB/ULists.pm
new file mode 100644
index 00000000..829ee537
--- /dev/null
+++ b/lib/VNDB/DB/ULists.pm
@@ -0,0 +1,282 @@
+
+package VNDB::DB::ULists;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+
+our @EXPORT = qw|
+ dbVNListGet dbVNListList dbVNListAdd dbVNListDel
+ dbVoteGet dbVoteStats dbVoteAdd dbVoteDel
+ dbWishListGet dbWishListAdd dbWishListDel
+|;
+
+
+# Simpler and more efficient version of dbVNListList below
+# %options->{ uid rid }
+sub dbVNListGet {
+ my($self, %o) = @_;
+
+ my %where = (
+ 'uid = ?' => $o{uid},
+ $o{rid} && !ref $o{rid} ? (
+ 'rid = ?' => $o{rid} ) : (),
+ $o{rid} && ref $o{rid} ? (
+ 'rid IN(!l)' => [$o{rid}] ) : (),
+ );
+
+ return $self->dbAll(q|
+ SELECT uid, rid, rstat, vstat
+ FROM rlists
+ !W|,
+ \%where
+ );
+}
+
+
+# %options->{ uid order char voted page results }
+# NOTE: this function is mostly copied from 1.x, may need some rewriting...
+sub dbVNListList {
+ my($self, %o) = @_;
+
+ $o{results} ||= 50;
+ $o{page} ||= 1;
+ $o{order} ||= 'vr.title ASC';
+ $o{voted} ||= 0; # -1: only non-voted, 0: all, 1: only voted
+
+ # construct the global WHERE clause
+ my $where = $o{voted} != -1 ? 'vo.vote IS NOT NULL' : '';
+ $where .= ($where?' OR ':'').q|v.id IN(
+ SELECT irv.vid
+ FROM rlists irl
+ JOIN releases ir ON ir.id = irl.rid
+ JOIN releases_vn irv ON irv.rid = ir.latest
+ WHERE uid = ?
+ )| if $o{voted} != 1;
+ $where = '('.$where.') AND LOWER(SUBSTR(vr.title, 1, 1)) = \''.$o{char}.'\'' if $o{char};
+ $where = '('.$where.') AND (ASCII(vr.title) < 97 OR ASCII(vr.title) > 122) AND (ASCII(vr.title) < 65 OR ASCII(vr.title) > 90)' if defined $o{char} && !$o{char};
+ $where = '('.$where.') AND vo.vote IS NULL' if $o{voted} == -1;
+
+ # execute query
+ my($r, $np) = $self->dbPage(\%o, qq|
+ SELECT vr.vid, vr.title, vr.original, v.c_released, v.c_languages, v.c_platforms, COALESCE(vo.vote, 0) AS vote
+ FROM vn v
+ JOIN vn_rev vr ON vr.id = v.latest
+ !s JOIN votes vo ON vo.vid = v.id AND vo.uid = ?
+ WHERE $where
+ ORDER BY !s|,
+ $o{voted} == 1 ? '' : 'LEFT', $o{uid}, # JOIN if we only want votes, LEFT JOIN if we also want rlist items
+ $o{voted} != 1 ? $o{uid} : (), $o{order},
+ );
+
+ # fetch releases and link to VNs
+ if(@$r) {
+ my %vns = map {
+ $_->{rels}=[];
+ $_->{vid}, $_->{rels}
+ } @$r;
+
+ push @{$vns{$_->{vid}}}, $_ for (@{$self->dbAll(q|
+ SELECT rv.vid, rr.rid, rr.title, rr.original, rr.released, rr.type, rr.language, rr.minage, rl.rstat, rl.vstat
+ FROM rlists rl
+ JOIN releases r ON rl.rid = r.id
+ JOIN releases_rev rr ON rr.id = r.latest
+ JOIN releases_vn rv ON rv.rid = r.latest
+ WHERE rl.uid = ?
+ AND rv.vid IN(!l)
+ ORDER BY rr.released ASC|,
+ $o{uid}, [ keys %vns ]
+ )});
+ }
+
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# %options->{ uid rid rstat vstat }
+sub dbVNListAdd {
+ my($self, %o) = @_;
+
+ my %s = (
+ defined $o{rstat} ? ( 'rstat = ?', $o{rstat} ) : (),
+ defined $o{vstat} ? ( 'vstat = ?', $o{vstat} ) : (),
+ );
+ $o{rstat}||=0;
+ $o{vstat}||=0;
+
+ $self->dbExec(
+ 'UPDATE rlists !H WHERE uid = ? AND rid IN(!l)',
+ \%s, $o{uid}, ref($o{rid}) eq 'ARRAY' ? $o{rid} : [ $o{rid} ]
+ )
+ ||
+ $self->dbExec(
+ 'INSERT INTO rlists (uid, rid, rstat, vstat) VALUES(!l)',
+ [@o{qw| uid rid rstat vstat |}]
+ );
+}
+
+
+# Arguments: uid, rid
+sub dbVNListDel {
+ my($self, $uid, $rid) = @_;
+ $self->dbExec(
+ 'DELETE FROM rlists WHERE uid = ? AND rid IN(!l)',
+ $uid, ref($rid) eq 'ARRAY' ? $rid : [ $rid ]
+ );
+}
+
+
+# %options->{ uid vid hide order results page what }
+# what: user, vn
+sub dbVoteGet {
+ my($self, %o) = @_;
+ $o{order} ||= 'n.date DESC';
+ $o{results} ||= 50;
+ $o{page} ||= 1;
+ $o{what} ||= '';
+
+ my %where = (
+ $o{uid} ? ( 'n.uid = ?' => $o{uid} ) : (),
+ $o{vid} ? ( 'n.vid = ?' => $o{vid} ) : (),
+ $o{hide} ? ( 'u.show_list = TRUE' => 1 ) : (),
+ );
+
+ my @select = (
+ qw|n.vid n.vote n.date n.uid|,
+ $o{what} =~ /user/ ? ('u.username') : (),
+ $o{what} =~ /vn/ ? (qw|vr.title vr.original|) : (),
+ );
+
+ my @join = (
+ $o{what} =~ /vn/ ? (
+ 'JOIN vn v ON v.id = n.vid',
+ 'JOIN vn_rev vr ON vr.id = v.latest'
+ ) : (),
+ $o{what} =~ /user/ || $o{hide} ? (
+ 'JOIN users u ON u.id = n.uid'
+ ) : (),
+ );
+
+ my($r, $np) = $self->dbPage(\%o, q|
+ SELECT !s
+ FROM votes n
+ !s
+ !W
+ ORDER BY !s|,
+ join(',', @select), join(' ', @join), \%where, $o{order}
+ );
+
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# Arguments: (uid|vid), id
+# Returns an arrayref with 10 elements containing the number of votes for index+1
+sub dbVoteStats {
+ my($self, $col, $id) = @_;
+ my $r = [ qw| 0 0 0 0 0 0 0 0 0 0 | ];
+ $r->[$_->{vote}-1] = $_->{votes} for (@{$self->dbAll(q|
+ SELECT vote, COUNT(vote) as votes
+ FROM votes
+ !W
+ GROUP BY vote|,
+ $col ? { '!s = ?' => [ $col, $id ] } : {},
+ )});
+ return $r;
+}
+
+
+# Adds a new vote or updates an existing one
+# Arguments: vid, uid, vote
+sub dbVoteAdd {
+ my($self, $vid, $uid, $vote) = @_;
+ $self->dbExec(q|
+ UPDATE votes
+ SET vote = ?
+ WHERE vid = ?
+ AND uid = ?|,
+ $vote, $vid, $uid
+ ) || $self->dbExec(q|
+ INSERT INTO votes
+ (vid, uid, vote, date)
+ VALUES (!l)|,
+ [ $vid, $uid, $vote, time ]
+ );
+}
+
+
+# Arguments: uid, vid
+sub dbVoteDel {
+ my($self, $uid, $vid) = @_;
+ $self->dbExec('DELETE FROM votes !W',
+ { 'vid = ?' => $vid, 'uid = ?' => $uid }
+ );
+}
+
+
+# %options->{ uid vid wstat what order page results }
+# what: vn
+sub dbWishListGet {
+ my($self, %o) = @_;
+
+ $o{order} ||= 'wl.wstat ASC';
+ $o{page} ||= 1;
+ $o{results} ||= 50;
+ $o{what} ||= '';
+
+ my %where = (
+ 'wl.uid = ?' => $o{uid},
+ $o{vid} ? ( 'wl.vid = ?' => $o{vid} ) : (),
+ defined $o{wstat} ? ( 'wl.wstat = ?' => $o{wstat} ) : (),
+ );
+
+ my $select = 'wl.vid, wl.wstat, wl.added';
+ my @join;
+ if($o{what} =~ /vn/) {
+ $select .= ', vr.title, vr.original';
+ push @join, 'JOIN vn v ON v.id = wl.vid',
+ 'JOIN vn_rev vr ON vr.id = v.latest';
+ }
+
+ my($r, $np) = $self->dbPage(\%o, q|
+ SELECT !s
+ FROM wlists wl
+ !s
+ !W
+ ORDER BY !s|,
+ $select, join(' ', @join), \%where, $o{order},
+ );
+
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# Updates or adds a whishlist item
+# Arguments: vid, uid, wstat
+sub dbWishListAdd {
+ my($self, $vid, $uid, $wstat) = @_;
+ $self->dbExec(
+ 'UPDATE wlists SET wstat = ? WHERE uid = ? AND vid IN(!l)',
+ $wstat, $uid, ref($vid) eq 'ARRAY' ? $vid : [ $vid ]
+ )
+ ||
+ $self->dbExec(
+ 'INSERT INTO wlists (uid, vid, wstat) VALUES(!l)',
+ [ $uid, $vid, $wstat ]
+ );
+}
+
+
+# Arguments: uid, vids
+sub dbWishListDel {
+ my($self, $uid, $vid) = @_;
+ $self->dbExec(
+ 'DELETE FROM wlists WHERE uid = ? AND vid IN(!l)',
+ $uid, ref($vid) eq 'ARRAY' ? $vid : [ $vid ]
+ );
+}
+
+
+1;
+
diff --git a/lib/VNDB/DB/Users.pm b/lib/VNDB/DB/Users.pm
new file mode 100644
index 00000000..501bb8bb
--- /dev/null
+++ b/lib/VNDB/DB/Users.pm
@@ -0,0 +1,102 @@
+
+package VNDB::DB::Users;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+our @EXPORT = qw|dbUserGet dbUserEdit dbUserAdd dbUserDel|;
+
+
+# %options->{ username passwd mail order uid results page what }
+# what: stats
+sub dbUserGet {
+ my $s = shift;
+ my %o = (
+ order => 'username ASC',
+ page => 1,
+ results => 10,
+ what => '',
+ @_
+ );
+
+ my %where = (
+ $o{username} ? (
+ 'username = ?' => $o{username} ) : (),
+ $o{passwd} ? (
+ 'passwd = decode(?, \'hex\')' => $o{passwd} ) : (),
+ $o{firstchar} ? (
+ 'SUBSTRING(username from 1 for 1) = ?' => $o{firstchar} ) : (),
+ !$o{firstchar} && defined $o{firstchar} ? (
+ 'ASCII(username) < 97 OR ASCII(username) > 122' => 1 ) : (),
+ $o{mail} ? (
+ 'mail = ?' => $o{mail} ) : (),
+ $o{uid} ? (
+ 'id = ?' => $o{uid} ) : (),
+ !$o{uid} && !$o{username} ? (
+ 'id > 0' => 1 ) : (),
+ );
+
+ my @select = (
+ 'u.*',
+ $o{what} =~ /stats/ ? (
+ '(SELECT COUNT(*) FROM rlists WHERE uid = u.id) AS releasecount',
+ '(SELECT COUNT(DISTINCT rv.vid) FROM rlists rl JOIN releases r ON rl.rid = r.id JOIN releases_vn rv ON rv.rid = r.latest WHERE uid = u.id) AS vncount',
+ '(SELECT COUNT(*) FROM threads_posts WHERE uid = u.id) AS postcount',
+ '(SELECT COUNT(*) FROM threads_posts WHERE uid = u.id AND num = 1) AS threadcount',
+ ) : (),
+ );
+
+ my($r, $np) = $s->dbPage(\%o, q|
+ SELECT !s
+ FROM users u
+ !W
+ ORDER BY !s|,
+ join(', ', @select), \%where, $o{order}
+ );
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# uid, %options->{ columns in users table }
+sub dbUserEdit {
+ my($s, $uid, %o) = @_;
+
+ my %h;
+ defined $o{$_} && ($h{$_.' = ?'} = $o{$_})
+ for (qw| username mail rank show_nsfw show_list |);
+ $h{'passwd = decode(?, \'hex\')'} = $o{passwd}
+ if defined $o{passwd};
+
+ return if scalar keys %h <= 0;
+ return $s->dbExec(q|
+ UPDATE users
+ !H
+ WHERE id = ?|,
+ \%h, $uid);
+}
+
+
+# username, md5(pass), mail
+sub dbUserAdd {
+ my($s, @o) = @_;
+ $s->dbExec(q|INSERT INTO users (username, passwd, mail, registered) VALUES(?, decode(?, 'hex'), ?, ?)|, @o, time);
+}
+
+
+# uid
+sub dbUserDel {
+ my($s, $id) = @_;
+ $s->dbExec($_, $id) for (
+ q|DELETE FROM vnlists WHERE uid = ?|,
+ q|DELETE FROM rlists WHERE uid = ?|,
+ q|DELETE FROM wlists WHERE uid = ?|,
+ q|DELETE FROM votes WHERE uid = ?|,
+ q|UPDATE changes SET requester = 0 WHERE requester = ?|,
+ q|UPDATE threads_posts SET uid = 0 WHERE uid = ?|,
+ q|DELETE FROM users WHERE id = ?|
+ );
+}
+
+
+1;
diff --git a/lib/VNDB/DB/VN.pm b/lib/VNDB/DB/VN.pm
new file mode 100644
index 00000000..df2e0a03
--- /dev/null
+++ b/lib/VNDB/DB/VN.pm
@@ -0,0 +1,282 @@
+
+package VNDB::DB::VN;
+
+use strict;
+use warnings;
+use Exporter 'import';
+use VNDB::Func 'gtintype';
+
+our @EXPORT = qw|dbVNGet dbVNAdd dbVNEdit dbVNImageId dbVNCache dbScreenshotAdd dbScreenshotGet dbScreenshotRandom|;
+
+
+# Options: id, rev, char, search, cati, cate, lang, platform, results, page, order, what
+# What: extended categories anime relations screenshots relgraph changes
+sub dbVNGet {
+ my($self, %o) = @_;
+ $o{results} ||= 10;
+ $o{page} ||= 1;
+ $o{order} ||= 'vr.title ASC';
+ $o{what} ||= '';
+
+ my %where = (
+ $o{id} ? (
+ 'v.id = ?' => $o{id} ) : (),
+ $o{rev} ? (
+ 'c.rev = ?' => $o{rev} ) : (),
+ $o{char} ? (
+ 'LOWER(SUBSTR(vr.title, 1, 1)) = ?' => $o{char} ) : (),
+ defined $o{char} && !$o{char} ? (
+ '(ASCII(vr.title) < 97 OR ASCII(vr.title) > 122) AND (ASCII(vr.title) < 65 OR ASCII(vr.title) > 90)' => 1 ) : (),
+ $o{cati} && @{$o{cati}} ? ( q|
+ v.id IN(SELECT iv.id
+ FROM vn_categories ivc
+ JOIN vn iv ON iv.latest = ivc.vid
+ WHERE cat IN(!l)
+ GROUP BY iv.id
+ HAVING COUNT(cat) = ?)| => [ $o{cati}, $#{$o{cati}}+1 ] ) : (),
+ $o{cate} && @{$o{cate}} ? ( q|
+ v.id NOT IN(SELECT iv.id
+ FROM vn_categories ivc
+ JOIN vn iv ON iv.latest = ivc.vid
+ WHERE cat IN(!l)
+ GROUP BY iv.id)| => [ $o{cate} ] ) : (),
+ $o{lang} && @{$o{lang}} ? (
+ '('.join(' OR ', map "v.c_languages ILIKE '%%$_%%'", @{$o{lang}}).')' => 1 ) : (),
+ $o{platform} && @{$o{platform}} ? (
+ '('.join(' OR ', map "v.c_platforms ILIKE '%%$_%%'", @{$o{platform}}).')' => 1 ) : (),
+ # don't fetch hidden items unless we ask for an ID
+ !$o{id} && !$o{rev} ? (
+ 'v.hidden = FALSE' => 0 ) : (),
+ );
+
+ if($o{search}) {
+ my @w;
+ for (split /[ -,]/, $o{search}) {
+ s/%//g;
+ next if length($_) < 2;
+ if(/^\d+$/ && gtintype($_)) {
+ push @w, 'irr.gtin = ?', $_;
+ } else {
+ $_ = "%$_%";
+ push @w, '(ivr.title ILIKE ? OR ivr.alias ILIKE ? OR irr.title ILIKE ? OR irr.original ILIKE ?)',
+ [ $_, $_, $_, $_ ];
+ }
+ }
+ $where{ q|
+ v.id IN(SELECT iv.id
+ FROM vn iv
+ JOIN vn_rev ivr ON iv.latest = ivr.id
+ LEFT JOIN releases_vn irv ON irv.vid = iv.id
+ LEFT JOIN releases_rev irr ON irr.id = irv.rid
+ LEFT JOIN releases ir ON ir.latest = irr.id
+ !W
+ GROUP BY iv.id)|
+ } = [ \@w ] if @w;
+ }
+
+ my @join = (
+ $o{rev} ?
+ 'JOIN vn v ON v.id = vr.vid' :
+ 'JOIN vn v ON vr.id = v.latest',
+ $o{rev} || $o{what} =~ /changes/ ?
+ 'JOIN changes c ON c.id = vr.id' : (),
+ $o{what} =~ /changes/ ?
+ 'JOIN users u ON u.id = c.requester' : (),
+ $o{what} =~ /relgraph/ ?
+ 'JOIN relgraph rg ON rg.id = v.rgraph' : (),
+ );
+
+ my @select = (
+ qw|v.id v.locked v.hidden v.c_released v.c_languages v.c_platforms vr.title vr.original v.rgraph|, 'vr.id AS cid',
+ $o{what} =~ /extended/ ? (
+ qw|vr.alias vr.image vr.img_nsfw vr.length vr.desc vr.l_wp vr.l_encubed vr.l_renai vr.l_vnn| ) : (),
+ $o{what} =~ /changes/ ? (
+ qw|c.added c.requester c.comments v.latest u.username c.rev c.causedby|) : (),
+ $o{what} =~ /relgraph/ ? 'rg.cmap' : (),
+ );
+
+ my($r, $np) = $self->dbPage(\%o, q|
+ SELECT !s
+ FROM vn_rev vr
+ !s
+ !W
+ ORDER BY !s|,
+ join(', ', @select), join(' ', @join), \%where, $o{order},
+ );
+
+ if(@$r && $o{what} =~ /(categories|anime|relations|screenshots)/) {
+ my %r = map {
+ $r->[$_]{categories} = [];
+ $r->[$_]{anime} = [];
+ $r->[$_]{relations} = [];
+ $r->[$_]{screenshots} = [];
+ ($r->[$_]{cid}, $_)
+ } 0..$#$r;
+
+ if($o{what} =~ /categories/) {
+ push(@{$r->[$r{$_->{vid}}]{categories}}, [ $_->{cat}, $_->{lvl} ]) for (@{$self->dbAll(q|
+ SELECT vid, cat, lvl
+ FROM vn_categories
+ WHERE vid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+
+ if($o{what} =~ /anime/) {
+ push(@{$r->[$r{$_->{vid}}]{anime}}, $_) && delete $_->{vid} for (@{$self->dbAll(q|
+ SELECT va.vid, a.*
+ FROM vn_anime va
+ JOIN anime a ON va.aid = a.id
+ WHERE va.vid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+
+ if($o{what} =~ /relations/) {
+ push(@{$r->[$r{$_->{vid1}}]{relations}}, {
+ relation => $_->{relation},
+ id => $_->{vid2},
+ title => $_->{title},
+ original => $_->{original}
+ }) for(@{$self->dbAll(q|
+ SELECT rel.vid1, rel.vid2, rel.relation, vr.title, vr.original
+ FROM vn_relations rel
+ JOIN vn v ON rel.vid2 = v.id
+ JOIN vn_rev vr ON v.latest = vr.id
+ WHERE rel.vid1 IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+
+ if($o{what} =~ /screenshots/) {
+ push(@{$r->[$r{$_->{vid}}]{screenshots}}, $_) && delete $_->{vid} for (@{$self->dbAll(q|
+ SELECT vs.vid, s.id, vs.nsfw, vs.rid, s.width, s.height
+ FROM vn_screenshots vs
+ JOIN screenshots s ON vs.scr = s.id
+ WHERE vs.vid IN(!l)
+ ORDER BY vs.scr|,
+ [ keys %r ]
+ )});
+ }
+ }
+
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# arguments: id, %options ->( editsum uid + insert_rev )
+# returns: ( local revision, global revision )
+sub dbVNEdit {
+ my($self, $id, %o) = @_;
+ my($rev, $cid) = $self->dbRevisionInsert(0, $id, $o{editsum}, $o{uid});
+ insert_rev($self, $cid, $id, \%o);
+ return ($rev, $cid);
+}
+
+
+# arguments: %options ->( editsum uid + insert_rev )
+# returns: ( item id, global revision )
+sub dbVNAdd {
+ my($self, %o) = @_;
+ my($id, $cid) = $self->dbItemInsert(0, $o{editsum}, $o{uid});
+ insert_rev($self, $cid, $id, \%o);
+ return ($id, $cid);
+}
+
+
+# helper function, inserts a producer revision
+# Arguments: global revision, item id, { columns in producers_rev + categories + anime + relations + screenshots }
+# categories = [ [ catid, level ], .. ]
+# screenshots = [ [ scrid, nsfw, rid ], .. ]
+# relations = [ [ rel, vid ], .. ]
+# anime = [ aid, .. ]
+sub insert_rev {
+ my($self, $cid, $vid, $o) = @_;
+
+ $o->{img_nsfw} = $o->{img_nsfw}?1:0;
+ $self->dbExec(q|
+ INSERT INTO vn_rev (id, vid, title, original, "desc", alias, image, img_nsfw, length, l_wp, l_encubed, l_renai, l_vnn)
+ VALUES (!l)|,
+ [ $cid, $vid, @$o{qw|title original desc alias image img_nsfw length l_wp l_encubed l_renai l_vnn|} ]);
+
+ $self->dbExec(q|
+ INSERT INTO vn_categories (vid, cat, lvl)
+ VALUES (?, ?, ?)|,
+ $cid, $_->[0], $_->[1]
+ ) for (@{$o->{categories}});
+
+ $self->dbExec(q|
+ INSERT INTO vn_screenshots (vid, scr, nsfw, rid)
+ VALUES (?, ?, ?, ?)|,
+ $cid, $_->[0], $_->[1]?1:0, $_->[2]
+ ) for (@{$o->{screenshots}});
+
+ $self->dbExec(q|
+ INSERT INTO vn_relations (vid1, vid2, relation)
+ VALUES (?, ?, ?)|,
+ $cid, $_->[1], $_->[0]
+ ) for (@{$o->{relations}});
+
+ if(@{$o->{anime}}) {
+ $self->dbExec(q|
+ INSERT INTO vn_anime (vid, aid)
+ VALUES (?, ?)|,
+ $cid, $_
+ ) for (@{$o->{anime}});
+
+ # insert unknown anime
+ my $a = $self->dbAll(q|
+ SELECT id FROM anime WHERE id IN(!l)|,
+ $o->{anime});
+ $self->dbExec(q|
+ INSERT INTO anime (id) VALUES (?)|, $_
+ ) for (grep {
+ my $ia = $_;
+ !(scalar grep $ia == $_->{id}, @$a)
+ } @{$o->{anime}});
+ }
+}
+
+
+# fetches an ID for a new image
+sub dbVNImageId {
+ return shift->dbRow("SELECT nextval('covers_seq') AS ni")->{ni};
+}
+
+
+# Updates the vn.c_ columns
+sub dbVNCache {
+ my($self, @vn) = @_;
+ $self->dbExec('SELECT update_vncache(?)', $_) for (@vn);
+}
+
+
+# insert a new screenshot and return it's ID
+# (no arguments required, as Multi is responsible for filling the entry with information)
+sub dbScreenshotAdd {
+ return shift->dbRow(q|INSERT INTO screenshots (status) VALUES(0) RETURNING id|)->{id};
+}
+
+
+# arrayref of screenshot IDs as argument
+sub dbScreenshotGet {
+ return shift->dbAll(q|SELECT * FROM screenshots WHERE id IN(!l)|, shift);
+}
+
+
+# Fetch random VN + screenshots
+sub dbScreenshotRandom {
+ return shift->dbAll(q|
+ SELECT vs.scr, vr.vid, vr.title
+ FROM vn_screenshots vs
+ JOIN vn v ON v.latest = vs.vid
+ JOIN vn_rev vr ON vr.id = v.latest
+ WHERE vs.nsfw = FALSE
+ ORDER BY RANDOM()
+ LIMIT 4|
+ );
+}
+
+
+1;
+
diff --git a/lib/VNDB/Discussions.pm b/lib/VNDB/Discussions.pm
deleted file mode 100644
index 9f80b0b0..00000000
--- a/lib/VNDB/Discussions.pm
+++ /dev/null
@@ -1,193 +0,0 @@
-
-package VNDB::Discussions;
-
-use strict;
-use warnings;
-use Exporter 'import';
-use POSIX 'ceil';
-
-use vars ('$VERSION', '@EXPORT');
-$VERSION = $VNDB::VERSION;
-@EXPORT = qw| TThread TEdit TIndex TTag |;
-
-
-sub TThread {
- my $self = shift;
- my $id = shift;
- my $page = shift||1;
-
- my $t = $self->DBGetThreads(id => $id, what => 'tagtitles')->[0];
- return $self->ResNotFound if !$t || $t->{hidden} && !$self->AuthCan('boardmod');
-
- my $p = $self->DBGetPosts(tid => $id, results => $self->{postsperpage}, page => $page);
- return $self->ResNotFound if !$p->[0];
-
- $self->ResAddTpl(tthread => {
- t => $t,
- ppp => $self->{postsperpage},
- page => $page,
- p => $p,
- });
-}
-
-
-# tid num action
-# 0 0 Start a new thread
-# x 0 Reply to a thread
-# x 1 Edit thread (and first post)
-# x x Edit post
-sub TEdit {
- my $self = shift;
- my $tid = shift||0;
- my $num = shift||0;
- my $tag = shift||'';
-
- my $t = $tid && $self->DBGetThreads(id => $tid, what => 'tags')->[0];
- return $self->ResNotFound if $tid && !$t;
-
- my $p = $num && $self->DBGetPosts(tid => $tid, num => $num)->[0];
-
- my $frm = {};
- if($self->ReqMethod eq 'POST') {
- $frm = $self->FormCheck(
- { name => 'msg', required => 1, maxlength => 5000 },
- !$tid || $num == 1 ? (
- { name => 'title', required => 1, maxlength => 50 },
- { name => 'tags', required => 1, maxlength => 50 },
- ) : (),
- $self->AuthCan('boardmod') ? (
- { name => 'hide', required => 0 },
- { name => 'lock', required => 0 }
- ) : (),
- );
- $frm->{msg} =~ s/[\r\s\n]$//g;
-
- my %tags = !$frm->{tags} || $frm->{_err} ? () : map {
- $frm->{_err} = [ 'wrongtag' ] if
- !/^([a-z]{1,2})([0-9]*)$/ || !$VNDB::DTAGS->{$1}
- || $1 eq 'v' && (!$2 || !$self->DBGetVN(id => $2)->[0])
- #|| $1 eq 'r' && (!$2 || !$self->DBGetRelease(id => $2)->[0])
- || $1 eq 'p' && (!$2 || !$self->DBGetProducer(id => $2)->[0])
- || $1 eq 'u' && (!$2 || !$self->DBGetUser(id => $2)->[0])
- || $1 eq 'an' && !$self->AuthCan('boardmod');
- $1.($2||0) => [ $1, $2||0 ]
- } split / /, $frm->{tags};
- my @tags = values %tags;
-
- if(!$frm->{_err}) {
- my $otid = $tid;
- if(!$tid || $num == 1) {
- my @tags =
- my %thread = (
- id => $tid,
- title => $frm->{title},
- tags => \@tags,
- hidden => $frm->{hide},
- locked => $frm->{lock},
- );
- $self->DBEditThread(%thread) if $tid; # edit thread
- $tid = $self->DBAddThread(%thread) if !$tid; # create thread
- }
-
- my $onum = $num;
- my %post = (
- tid => $tid,
- num => !$otid ? 1 : $num,
- msg => $frm->{msg},
- hidden => $num != 1 && $frm->{hide},
- );
- $self->DBEditPost(%post) if $num; # edit post
- $num = $self->DBAddPost(%post) if !$num; # add post
-
- $self->RunCmd('ircnotify t'.$tid.'.'.$num) if !$onum && !$frm->{hide};
-
- my $pagenum = ceil($num/$self->{postsperpage});
- $pagenum = $pagenum > 1 ? '/'.$pagenum : '';
- $self->ResRedirect('/t'.$tid.$pagenum.'#'.$num, 'POST');
- }
- }
-
- if($p) {
- $frm->{msg} ||= $p->{msg};
- $frm->{hide} = $p->{hidden};
- if($num == 1) {
- $frm->{tags} ||= join ' ', sort map $_->[1]?$_->[0].$_->[1]:$_->[0], @{$t->{tags}};
- $frm->{title} ||= $t->{title};
- $frm->{lock} = $t->{locked};
- $frm->{hide} = $t->{hidden};
- }
- }
- $frm->{tags} ||= $tag;
-
- $self->ResAddTpl(tedit => {
- t => $t,
- p => $p,
- tag => $tag,
- form => $frm,
- });
-}
-
-
-sub TIndex {
- my $self = shift;
-
- my %opts = (
- results => 6,
- what => 'firstpost lastpost tags',
- order => 'tp2.date DESC',
- );
-
- $self->ResAddTpl(tindex => {
- ppp => $self->{postsperpage},
- map +($_, scalar $self->DBGetThreads(%opts, type => $_)), qw| an db v p u|
- });
-}
-
-
-sub TTag {
- my $self = shift;
- my $tag = shift;
- my($type, $iid) = ($1, $2||0) if $tag =~ /^([a-z]{1,2})([0-9]*)$/;
- return $self->ResNotFound if !$type;
-
- my $f = $self->FormCheck(
- { name => 'p', required => 0, default => 1, template => 'int' },
- );
- return $self->ResNotFound if $f->{_err};
-
- my $o = !$iid ? undef :
- $type eq 'u' ? $self->DBGetUser(uid => $iid)->[0] :
- $type eq 'v' ? $self->DBGetVN(id => $iid)->[0] :
- #$type eq 'r' ? $self->DBGetRelease(id => $iid)->[0] :
- $self->DBGetProducer(id => $iid)->[0];
- return $self->ResNotFound if $iid && !$o || !$VNDB::DTAGS->{$type};
- my $title = $o ? $o->{username} || $o->{romaji} || $o->{title} || $o->{name} : $VNDB::DTAGS->{$type};
- my $original = $o ? $o->{username} || $o->{original} : $VNDB::DTAGS->{$type};
-
- my($t, $np) = $self->DBGetThreads(
- type => $type,
- iid => $iid,
- results => 50,
- page => $f->{p},
- what => 'firstpost lastpost tagtitles',
- order => $tag eq 'an' ? 't.id DESC' : 'tp2.date DESC',
- );
-
- $self->ResAddTpl(ttag => {
- page => $f->{p},
- npage => $np,
- obj => $o,
- type => $type,
- iid => $iid,
- title => $title,
- original => $original,
- tag => $tag,
- t => $t,
- ppp => $self->{postsperpage},
- });
-}
-
-
-
-1;
-
diff --git a/lib/VNDB/Func.pm b/lib/VNDB/Func.pm
new file mode 100644
index 00000000..9abacad0
--- /dev/null
+++ b/lib/VNDB/Func.pm
@@ -0,0 +1,220 @@
+
+package VNDB::Func;
+
+use strict;
+use warnings;
+use YAWF ':html';
+use Exporter 'import';
+use POSIX 'strftime';
+our @EXPORT = qw| shorten age date datestr monthstr userstr bb2html gtintype liststat clearfloat cssicon |;
+
+
+# I would've done this as a #define if this was C...
+sub shorten {
+ my($str, $len) = @_;
+ return length($str) > $len ? substr($str, 0, $len-3).'...' : $str;
+}
+
+
+# Argument: unix timestamp
+# Returns: age
+sub age {
+ my $a = time-$_[0];
+ return sprintf '%d %s ago',
+ $a > 60*60*24*365*2 ? ( $a/60/60/24/365, 'years' ) :
+ $a > 60*60*24*(365/12)*2 ? ( $a/60/60/24/(365/12), 'months' ) :
+ $a > 60*60*24*7*2 ? ( $a/60/60/24/7, 'weeks' ) :
+ $a > 60*60*24*2 ? ( $a/60/60/24, 'days' ) :
+ $a > 60*60*2 ? ( $a/60/60, 'hours' ) :
+ $a > 60*2 ? ( $a/60, 'min' ) :
+ ( $a, 'sec' );
+}
+
+
+# argument: unix timestamp and optional format (compact/full)
+# return value: yyyy-mm-dd
+# (maybe an idea to use cgit-style ages for recent timestamps)
+sub date {
+ my($t, $f) = @_;
+ return strftime '%Y-%m-%d', gmtime $t if !$f || $f eq 'compact';
+ return strftime '%Y-%m-%d at %R', gmtime $t;
+}
+
+
+# argument: database release date format (yyyymmdd)
+# y = 0000 -> unkown
+# y = 9999 -> TBA
+# m = 99 -> month+day unkown
+# d = 99 -> day unknown
+# return value: (unknown|TBA|yyyy|yyyy-mm|yyyy-mm-dd)
+# if date > now: <b class="future">str</b>
+sub datestr {
+ my $date = sprintf '%08d', shift||0;
+ my $future = $date > strftime '%Y%m%d', gmtime;
+ my($y, $m, $d) = ($1, $2, $3) if $date =~ /^([0-9]{4})([0-9]{2})([0-9]{2})$/;
+
+ my $str = $y == 0 ? 'unknown' : $y == 9999 ? 'TBA' :
+ $m == 99 ? sprintf('%04d', $y) :
+ $d == 99 ? sprintf('%04d-%02d', $y, $m) :
+ sprintf('%04d-%02d-%02d', $y, $m, $d);
+
+ return $str if !$future;
+ return qq|<b class="future">$str</b>|;
+}
+
+# same as datestr(), but different output format:
+# e.g.: 'Jan 2009', '2009', 'unknown', 'TBA'
+sub monthstr {
+ my $date = sprintf '%08d', shift||0;
+ my($y, $m) = ($1, $2) if $date =~ /^([0-9]{4})([0-9]{2})/;
+ return 'TBA' if $y == 9999;
+ return 'unknown' if $y == 0;
+ return $y if $m == 99;
+ return strftime '%b %Y', 0, 0, 0, 0, $m-1, $y-1900, 0, 0, 0;
+}
+
+
+# Arguments: (uid, username), or a hashref containing that info
+sub userstr {
+ my($id,$n) = ref($_[0])eq'HASH'?($_[0]{uid}||$_[0]{requester}, $_[0]{username}):@_;
+ return !$id ? '[deleted]' : '<a href="/u'.$id.'">'.$n.'</a>';
+}
+
+
+# Arguments: input, and optionally the maximum length
+# Parses:
+# [url=..] [/url]
+# [raw] .. [/raw]
+# [spoiler] .. [/spoiler]
+# v+, v+.+
+# http://../
+sub bb2html {
+ my $raw = shift;
+ my $maxlength = shift;
+ $raw =~ s/\r//g;
+ return '' if !$raw && $raw ne "0";
+
+ my($result, $length, @open) = ('', 0, 'first');
+
+ my $e = sub {
+ local $_ = shift;
+ tr/A-Za-z/N-ZA-Mn-za-m/ if !@_ && grep /spoiler/, @open;
+ s/&/&amp;/g;
+ s/>/&gt;/g;
+ s/</&lt;/g;
+ s/\n/<br \/>/g if !$maxlength;
+ s/\n/ /g if $maxlength;
+ return $_;
+ };
+
+ for (split /(\s|\n|\[[^\]]+\])/, $raw) {
+ next if !defined $_;
+
+ my $lit = $_;
+ if($open[$#open] ne 'raw') {
+ if ($_ eq '[raw]') { push @open, 'raw'; next }
+ elsif ($_ eq '[spoiler]') { push @open, 'spoiler'; next }
+ elsif ($_ eq '[/spoiler]') { pop @open if $open[$#open] eq 'spoiler'; next }
+ elsif ($_ eq '[/url]') {
+ if($open[$#open] eq 'url') {
+ $result .= '</a>';
+ pop @open;
+ }
+ next;
+ } elsif(s{\[url=((https?://|/)[^\]>]+)\]}{<a href="$1" rel="nofollow">}i) {
+ $result .= $_;
+ push @open, 'url';
+ next;
+ } elsif(!grep(/url/, @open) &&
+ s{(.*)(http|https)://(.+[0-9a-zA-Z=/])(.*)}
+ {$e->($1).qq|<a href="$2://|.$e->($3, 1).'" rel="nofollow">'.$e->('link').'</a>'.$e->($4)}e) {
+ $length += 4;
+ last if $maxlength && $length > $maxlength;
+ $result .= $_;
+ next;
+ } elsif(!grep(/url/, @open) && (
+ s{^(.*[^\w]|)([tdvpr][1-9][0-9]*)\.([1-9][0-9]*)([^\w].*|)$}{$e->($1).qq|<a href="/$2.$3">$2.$3</a>|.$e->($4)}e ||
+ s{^(.*[^\w]|)([tduvpr][1-9][0-9]*)([^\w].*|)$}{$e->($1).qq|<a href="/$2">$2</a>|.$e->($3)}e)) {
+ $length += length $lit;
+ last if $maxlength && $length > $maxlength;
+ $result .= $_;
+ next;
+ }
+ } elsif($_ eq '[/raw]') {
+ pop @open if $open[$#open] eq 'raw';
+ next;
+ }
+
+ # normal text processing
+ $length += length $_;
+ last if $maxlength && $length > $maxlength;
+ $result .= $e->($_);
+ }
+
+ $result .= '</a>'
+ while((local $_ = pop @open) ne 'first');
+ $result .= '...' if $maxlength && $length > $maxlength;
+
+ return $result;
+}
+
+
+# GTIN code as argument,
+# Returns 'JAN', 'EAN', 'UPC' or undef,
+# Also 'normalizes' the first argument in place
+sub gtintype {
+ $_[0] =~ s/[^\d]+//g;
+ $_[0] =~ s/^0+//;
+ my $c = shift;
+ return undef if $c !~ /^[0-9]{12,13}$/; # only gtin-12 and 13
+ $c = ('0'x(13-length $c)) . $c; # pad with zeros
+
+ # calculate check digit according to
+ # http://www.gs1.org/productssolutions/barcodes/support/check_digit_calculator.html#how
+ my @n = reverse split //, $c;
+ my $n = shift @n;
+ $n += $n[$_] * ($_ % 2 != 0 ? 1 : 3) for (0..$#n);
+ return undef if $n % 10 != 0;
+
+ # Do some rough guesses based on:
+ # http://www.gs1.org/productssolutions/barcodes/support/prefix_list.html
+ # and http://en.wikipedia.org/wiki/List_of_GS1_country_codes
+ local $_ = $c;
+ return 'JAN' if /^4[59]/; # prefix code 450-459 & 490-499
+ return 'UPC' if /^(?:0[01]|0[6-9]|13|75[45])/; # prefix code 000-019 & 060-139 & 754-755
+ return undef if /(?:0[2-5]|2|97[789]|9[6-9])/; # some codes we don't want: 020–059 & 200-299 & 977-999
+ return 'EAN'; # let's just call everything else EAN :)
+}
+
+
+# Argument: hashref with rstat and vstat
+# Returns: empty string if not in list, otherwise colour-encoded list status
+sub liststat {
+ my $l = shift;
+ return '' if !$l;
+ my $rs = $YAWF::OBJ->{vn_rstat}[$l->{rstat}];
+ $rs = qq|<b class="done">$rs</b>| if $l->{rstat} == 2; # Obtained
+ $rs = qq|<b class="todo">$rs</b>| if $l->{rstat} < 2; # Unknown/pending
+ my $vs = $YAWF::OBJ->{vn_vstat}[$l->{vstat}];
+ $vs = qq|<b class="done">$vs</b>| if $l->{vstat} == 2; # Finished
+ $vs = qq|<b class="todo">$vs</b>| if $l->{vstat} == 0 || $l->{vstat} == 4; # Unknown/dropped
+ return "$rs / $vs";
+}
+
+
+# Clears a float, to make sure boxes always have the correct height
+sub clearfloat {
+ div class => 'clearfloat', '';
+}
+
+
+# Draws a CSS icon, arguments: class, title
+sub cssicon {
+ acronym class => "icons $_[0]", title => $_[1];
+ lit '&nbsp;';
+ end;
+}
+
+
+1;
+
diff --git a/lib/VNDB/Handler/Discussions.pm b/lib/VNDB/Handler/Discussions.pm
new file mode 100644
index 00000000..286e3ee5
--- /dev/null
+++ b/lib/VNDB/Handler/Discussions.pm
@@ -0,0 +1,399 @@
+
+package VNDB::Handler::Discussions;
+
+use strict;
+use warnings;
+use YAWF ':html', 'xml_escape';
+use POSIX 'ceil';
+use VNDB::Func;
+
+
+YAWF::register(
+ qr{t([1-9]\d*)(?:/([1-9]\d*))?} => \&thread,
+ qr{t([1-9]\d*)\.([1-9]\d*)} => \&redirect,
+ qr{t/(db|an|[vpu])([1-9]\d*)?} => \&tagbrowse,
+ qr{t([1-9]\d*)/reply} => \&edit,
+ qr{t([1-9]\d*)\.([1-9]\d*)/edit} => \&edit,
+ qr{t/(db|an|[vpu])([1-9]\d*)?/new} => \&edit,
+ qr{t} => \&index,
+);
+
+
+sub thread {
+ my($self, $tid, $page) = @_;
+ $page ||= 1;
+
+ my $t = $self->dbThreadGet(id => $tid, what => 'tagtitles')->[0];
+ return 404 if !$t->{id} || $t->{hidden} && !$self->authCan('boardmod');
+
+ my $p = $self->dbPostGet(tid => $tid, results => 25, page => $page);
+ return 404 if !$p->[0];
+
+ $self->htmlHeader(title => $t->{title});
+
+ div class => 'mainbox';
+ h1 $t->{title};
+ h2 'Posted in';
+ ul;
+ for (sort { $a->{type}.$a->{iid} cmp $b->{type}.$b->{iid} } @{$t->{tags}}) {
+ li;
+ a href => "/t/$_->{type}", $self->{discussion_tags}{$_->{type}};
+ if($_->{iid}) {
+ txt ' > ';
+ a style => 'font-weight: bold', href => "/t/$_->{type}$_->{iid}", "$_->{type}$_->{iid}";
+ txt ':';
+ a href => "/$_->{type}$_->{iid}", title => $_->{original}, $_->{title};
+ }
+ end;
+ }
+ end;
+ end;
+
+ $self->htmlBrowseNavigate("/t$tid/", $page, $t->{count} > $page*25, 't', 1);
+ div class => 'mainbox thread';
+ table;
+ for my $i (0..$#$p) {
+ local $_ = $p->[$i];
+ my $class = $i % 2 == 0 ? 'odd ' : '';
+ $class .= 'deleted' if $_->{hidden};
+ Tr class => $class;
+ td class => 'tc1';
+ a href => "/t$tid.$_->{num}", name => $_->{num}, "#$_->{num}";
+ if(!$_->{hidden}) {
+ txt ' by ';
+ lit userstr $_;
+ br;
+ lit date $_->{date}, 'full';
+ }
+ end;
+ td class => 'tc2';
+ if($self->authCan('boardmod') || $self->authInfo->{id} && $_->{uid} == $self->authInfo->{id} && !$_->{hidden}) {
+ i class => 'edit';
+ txt '< ';
+ a href => "/t$tid.$_->{num}/edit", 'edit';
+ txt ' >';
+ end;
+ }
+ if($_->{hidden}) {
+ i class => 'deleted', 'Post deleted.';
+ } else {
+ lit bb2html $_->{msg};
+ i class => 'lastmod', 'Last modified on '.date($_->{edited}, 'full') if $_->{edited};
+ }
+ end;
+ end;
+ }
+ end;
+ end;
+ $self->htmlBrowseNavigate("/t$tid/", $page, $t->{count} > $page*25, 'b', 1);
+
+ if($t->{locked}) {
+ div class => 'mainbox';
+ h1 'Reply';
+ p class => 'center', 'This thread has been locked, you can\'t reply to it anymore.';
+ end;
+ } elsif($t->{count} < $page*25 && $self->authCan('board')) {
+ form action => "/t$tid/reply", method => 'post', 'accept-charset' => 'UTF-8';
+ div class => 'mainbox';
+ fieldset class => 'submit';
+ h2 'Quick reply';
+ textarea name => 'msg', id => 'msg', rows => 4, cols => 50, '';
+ br;
+ input type => 'submit', value => 'Reply', class => 'submit';
+ end;
+ end;
+ end;
+ } elsif(!$self->authCan('board')) {
+ div class => 'mainbox';
+ h1 'Reply';
+ p class => 'center', 'You must be logged in to reply to this thread.';
+ end;
+ }
+
+ $self->htmlFooter;
+}
+
+
+sub redirect {
+ my($self, $tid, $num) = @_;
+ $self->resRedirect("/t$tid".($num > 25 ? '/'.ceil($num/25) : '').'#'.$num, 'perm');
+}
+
+
+# Arguments, action
+# tid reply
+# tid, 1 edit thread
+# tid, num edit post
+# type, (iid) start new thread
+sub edit {
+ my($self, $tid, $num) = @_;
+ $num ||= 0;
+
+ # in case we start a new thread, parse tag
+ my $tag = '';
+ if($tid !~ /^\d+$/) {
+ return 404 if $tid =~ /(db|an)/ && $num || $tid =~ /[vpu]/ && !$num;
+ $tag = $tid.($num||'');
+ $tid = 0;
+ $num = 0;
+ }
+
+ # get thread and post, if any
+ my $t = $tid && $self->dbThreadGet(id => $tid, what => 'tags')->[0];
+ return 404 if $tid && !$t->{id};
+
+ my $p = $num && $self->dbPostGet(tid => $tid, num => $num)->[0];
+ return 404 if $num && !$p->{num};
+
+ # are we allowed to perform this action?
+ return $self->htmlDenied if !$self->authCan('board')
+ || ($tid && ($t->{locked} || $t->{hidden}) && !$self->authCan('boardmod'))
+ || ($num && $p->{uid} != $self->authInfo->{id} && !$self->authCan('boardmod'));
+
+ # check form etc...
+ my $frm;
+ if($self->reqMethod eq 'POST') {
+ $frm = $self->formValidate(
+ !$tid || $num == 1 ? (
+ { name => 'title', maxlength => 50 },
+ { name => 'tags', maxlength => 50 },
+ ) : (),
+ $self->authCan('boardmod') ? (
+ { name => 'locked', required => 0 },
+ { name => 'hidden', required => 0 },
+ { name => 'nolastmod', required => 0 },
+ ) : (),
+ { name => 'msg', maxlenght => 5000 },
+ );
+
+ # parse and validate the tags
+ my @tags;
+ if(!$frm->{_err} && $frm->{tags}) {
+ for (split /[ ,]/, $frm->{tags}) {
+ my($ty, $id) = ($1, $2) if /^([a-z]{1,2})([0-9]*)$/;
+ push @tags, [ $ty, $id ];
+ push @{$frm->{_err}}, [ 'tags', 'wrongtag', $_ ] if
+ !$ty || !$self->{discussion_tags}{$ty}
+ || $ty eq 'an' && ($id || !$self->authCan('boardmod'))
+ || $ty eq 'db' && $id
+ || $ty eq 'v' && (!$id || !$self->dbVNGet(id => $id)->[0]{id})
+ || $ty eq 'p' && (!$id || !$self->dbProducerGet(id => $id)->[0]{id})
+ || $ty eq 'u' && (!$id || !$self->dbUserGet(uid => $id)->[0]{id});
+ }
+ }
+
+ if(!$frm->{_err}) {
+ my($ntid, $nnum) = ($tid, $num);
+
+ # create/edit thread
+ if(!$tid || $num == 1) {
+ my %thread = (
+ title => $frm->{title},
+ tags => \@tags,
+ hidden => $frm->{hidden},
+ locked => $frm->{locked},
+ );
+ $self->dbThreadEdit($tid, %thread) if $tid;
+ $ntid = $self->dbThreadAdd(%thread) if !$tid;
+ }
+
+ # create/edit post
+ my %post = (
+ msg => $frm->{msg},
+ hidden => $num != 1 && $frm->{hidden},
+ lastmod => !$num || $frm->{nolastmod} ? 0 : time,
+ );
+ $self->dbPostEdit($tid, $num, %post) if $num;
+ $nnum = $self->dbPostAdd($ntid, %post) if !$num;
+
+ $self->multiCmd("ircnotify t$ntid.$nnum") if !$num && !$frm->{hidden};
+
+ return $self->resRedirect("/t$ntid".($nnum > 25 ? '/'.ceil($nnum/25) : '').'#'.$nnum, 'post');
+ }
+ }
+
+ # fill out form if we have some data
+ if($p) {
+ $frm->{msg} ||= $p->{msg};
+ $frm->{hidden} = $p->{hidden} if $num != 1 && !exists $frm->{hidden};
+ if($num == 1) {
+ $frm->{tags} ||= join ' ', sort map $_->[1]?$_->[0].$_->[1]:$_->[0], @{$t->{tags}};
+ $frm->{title} ||= $t->{title};
+ $frm->{locked} = $t->{locked} if !exists $frm->{locked};
+ $frm->{hidden} = $t->{hidden} if !exists $frm->{hidden};
+ }
+ }
+ $frm->{tags} ||= $tag;
+ $frm->{nolastmod} = 1 if $num && $self->authCan('boardmod') && !exists $frm->{nolastmod};
+
+ # generate html
+ my $title = !$tid ? 'Start new thread' :
+ !$num ? 'Reply to '.$t->{title} :
+ 'Edit post';
+ my $url = !$tid ? "/t/$tag/new" : !$num ? "/t$tid/reply" : "/t$tid.$num/edit";
+ $self->htmlHeader(title => $title, noindex => 1);
+ $self->htmlForm({ frm => $frm, action => $url }, $title => [
+ [ static => label => 'Username', content => userstr($self->authInfo->{id}, $self->authInfo->{username}) ],
+ !$tid || $num == 1 ? (
+ [ input => short => 'title', name => 'Thread title' ],
+ [ input => short => 'tags', name => 'Tags' ],
+ [ static => content => 'Read <a href="/d9.2">d9.2</a> for information about how to use tags' ],
+ $self->authCan('boardmod') ? (
+ [ check => name => 'Locked', short => 'locked' ],
+ ) : (),
+ ) : (
+ [ static => label => 'Topic', content => qq|<a href="/t$tid">|.xml_escape($t->{title}).'</a>' ],
+ ),
+ $self->authCan('boardmod') ? (
+ [ check => name => 'Hidden', short => 'hidden' ],
+ $num ? (
+ [ check => name => 'Don\'t update last modified field', short => 'nolastmod' ],
+ ) : (),
+ ) : (),
+ [ text => name => 'Message', short => 'msg', rows => 10 ],
+ [ static => content => 'See <a href="/d9.3">d9.3</a> for the allowed formatting codes' ],
+ ]);
+ $self->htmlFooter;
+}
+
+
+sub tagbrowse {
+ my($self, $type, $iid) = @_;
+ $iid ||= '';
+ return 404 if $type =~ /(db|an)/ && $iid;
+
+ my $f = $self->formValidate(
+ { name => 'p', required => 0, default => 1, template => 'int' },
+ );
+ return 404 if $f->{_err};
+
+ my $obj = !$iid ? undef :
+ $type eq 'u' ? $self->dbUserGet(uid => $iid)->[0] :
+ $type eq 'p' ? $self->dbProducerGet(id => $iid)->[0] :
+ $self->dbVNGet(id => $iid)->[0];
+ return 404 if $iid && !$obj;
+ my $ititle = $obj && ($obj->{title}||$obj->{name}||$obj->{username});
+ my $title = !$obj ? $self->{discussion_tags}{$type} : 'Related discussions for '.$ititle;
+
+ my($list, $np) = $self->dbThreadGet(
+ type => $type,
+ $iid ? (iid => $iid) : (),
+ results => 50,
+ page => $f->{p},
+ what => 'firstpost lastpost tagtitles',
+ order => $type eq 'an' ? 't.id DESC' : 'tpl.date DESC',
+ );
+
+ $self->htmlHeader(title => $title, noindex => !@$list);
+
+ $self->htmlMainTabs($type, $obj, 'disc') if $iid;
+ div class => 'mainbox';
+ h1 $title;
+ p;
+ a href => '/t', 'Discussion board';
+ txt ' > ';
+ a href => "/t/$type", $self->{discussion_tags}{$type};
+ if($iid) {
+ txt ' > ';
+ a style => 'font-weight: bold', href => "/t/$type$iid", "$type$iid";
+ txt ':';
+ a href => "/$type$iid", $ititle;
+ }
+ end;
+ p class => 'center';
+ if(!@$list) {
+ b 'No related threads found';
+ br; br;
+ a href => "/t/$type$iid/new", 'Why not create one yourself?';
+ } else {
+ a href => '/t/'.($iid ? $type.$iid : 'db').'/new', 'Start a new thread';
+ }
+ end;
+ end;
+
+ _threadlist($self, $list, $f, $np, "/t/$type$iid") if @$list;
+
+ $self->htmlFooter;
+}
+
+
+sub index {
+ my $self = shift;
+
+ $self->htmlHeader(title => 'Discussion board index');
+ div class => 'mainbox';
+ h1 'Discussion board index';
+ p class => 'browseopts';
+ a href => '/t/'.$_, $self->{discussion_tags}{$_}
+ for (qw|an db v p u|);
+ end;
+ end;
+
+ for (qw|an db v p u|) {
+ my $list = $self->dbThreadGet(
+ type => $_,
+ results => 5,
+ page => 1,
+ what => 'firstpost lastpost tagtitles',
+ order => 'tpl.date DESC',
+ );
+ h1 class => 'boxtitle';
+ a href => "/t/$_", $self->{discussion_tags}{$_};
+ end;
+ _threadlist($self, $list, {p=>1}, 0, "/t");
+ }
+
+ $self->htmlFooter;
+}
+
+
+sub _threadlist {
+ my($self, $list, $f, $np, $url) = @_;
+ $self->htmlBrowse(
+ items => $list,
+ options => $f,
+ nextpage => $np,
+ pageurl => $url,
+ class => 'discussions',
+ header => [
+ [ 'Topic' ], [ 'Replies' ], [ 'Starter' ], [ 'Last post' ]
+ ],
+ row => sub {
+ my($self, $n, $o) = @_;
+ Tr $n % 2 ? ( class => 'odd' ) : ();
+ td class => 'tc1';
+ a $o->{locked} ? ( class => 'locked' ) : (), href => "/t$o->{id}", shorten $o->{title}, 50;
+ end;
+ td class => 'tc2', $o->{count}-1;
+ td class => 'tc3';
+ lit userstr $o->{fuid}, $o->{fusername};
+ end;
+ td class => 'tc4', rowspan => 2;
+ lit userstr $o->{luid}, $o->{lusername};
+ lit ' @ ';
+ a href => "/t$o->{id}.$o->{count}";
+ lit date $o->{ldate};
+ end;
+ end;
+ end;
+ Tr $n % 2 ? ( class => 'odd' ) : ();
+ td colspan => 4, class => 'tags';
+ txt ' > ';
+ my $i = 1;
+ for(sort { $a->{type}.$a->{iid} cmp $b->{type}.$b->{iid} } @{$o->{tags}}) {
+ last if $i++ > 5;
+ txt ', ' if $i > 2;
+ a href => "/t/$_->{type}".($_->{iid}||''),
+ title => $_->{original}||$self->{discussion_tags}{$_->{type}},
+ shorten $_->{title}||$self->{discussion_tags}{$_->{type}}, 30;
+ }
+ txt ', ...' if @{$o->{tags}} > 5;
+ end;
+ end;
+ }
+ );
+}
+
+
+1;
+
diff --git a/lib/VNDB/Handler/Misc.pm b/lib/VNDB/Handler/Misc.pm
new file mode 100644
index 00000000..5a9b9a2a
--- /dev/null
+++ b/lib/VNDB/Handler/Misc.pm
@@ -0,0 +1,382 @@
+
+package VNDB::Handler::Misc;
+
+
+use strict;
+use warnings;
+use YAWF ':html';
+use VNDB::Func;
+
+
+YAWF::register(
+ qr{}, \&homepage,
+ qr{(?:([upvr])([1-9]\d*)/)?hist}, \&history,
+ qr{d([1-9]\d*)}, \&docpage,
+ qr{nospam}, \&nospam,
+ qr{([vrp])([1-9]\d*)/(lock|hide)}, \&itemmod,
+ qr{we-dont-like-ie6}, \&ie6message,
+
+ # redirects for old URLs
+ qr{(.*[^/]+)/+}, sub { $_[0]->resRedirect("/$_[1]", 'perm') },
+ qr{([pv])}, sub { $_[0]->resRedirect("/$_[1]/all", 'perm') },
+ qr{v/search}, sub { $_[0]->resRedirect("/v/all?q=".$_[0]->reqParam('q'), 'perm') },
+ qr{notes}, sub { $_[0]->resRedirect('/d8', 'perm') },
+ qr{faq}, sub { $_[0]->resRedirect('/d6', 'perm') },
+ qr{v([1-9]\d*)/(?:stats|scr|votes)},
+ sub { $_[0]->resRedirect("/v$_[1]", 'perm') },
+ qr{u/list(/[a-z0]|/all)?},
+ sub { my $l = defined $_[1] ? $_[1] : '/all'; $_[0]->resRedirect("/u$l", 'perm') },
+ qr{d([1-9]\d*)\.([1-9]\d*)},
+ sub { $_[0]->resRedirect("/d$_[1]#$_[2]", 'perm') },
+ qr{u([1-9]\d*)/votes},
+ sub { $_[0]->resRedirect("/u$_[1]/list?v=1", 'perm') },
+
+ # rewrite the old category browser to the new-ish search function
+ qr{v/cat}, sub {
+ my $f = $_[0]->formValidate(
+ {name=>'i',required=>0,default=>''},{name=>'e',required=>0,default=>''},{name=>'l',required=>0,default=>''},
+ {name=>'p',required=>0},{name=>'o',required=>0},{name=>'s',required=>0});
+ my %f;
+ $f{$_} = $f->{$_} for (qw|p o s|);
+ $f{q} = join ' ', (map $_[0]{categories}{substr($_,0,1)}[1]{substr($_,1,2)}, split /,/, $f->{i}),
+ (map '-'.$_[0]{categories}{substr($_,0,1)}[1]{substr($_,1,2)}, split /,/, $f->{e}),
+ (map $_[0]{languages}{$_}, split /,/, $f->{l});
+ !$f{$_} && delete $f{$_} for keys %f;
+ $_[0]->resRedirect('/v/all'.(!(keys %f)?'':'?'.join(';', map $_.'='.$f{$_}, keys %f) ), 'perm');
+ },
+
+);
+
+
+sub homepage {
+ my $self = shift;
+ $self->htmlHeader(title => $self->{site_title});
+
+ div class => 'mainbox';
+ h1 $self->{site_title};
+ p class => 'description';
+ lit qq|
+ VNDB.org strives to be a comprehensive database for information about visual novels and
+ eroge.<br />
+ This website is built as a wiki, meaning that anyone can freely add and contribute information
+ to the database, allowing us to create the largest, most accurate and most up-to-date visual novel
+ database on the web.<br />
+ Registered users are also able to keep track of a personal list of games they want to play or have finished
+ and they can vote on all visual novels.<br />
+ <br />
+ Feel free to <a href="/v/all">browse around</a>, <a href="/u/register">register an account</a>
+ or to participate in the discussions about visual novels or VNDB on our <a href="/t">discussion board</a>.
+ |;
+ end;
+
+ my $scr = $self->dbScreenshotRandom;
+ p class => 'screenshots';
+ for (@$scr) {
+ a href => "/v$_->{vid}", title => $_->{title};
+ img src => sprintf("%s/st/%02d/%d.jpg", $self->{url_static}, $_->{scr}%100, $_->{scr}), alt => $_->{title};
+ end;
+ }
+ end;
+ end;
+
+ # Recent changes
+ div class => 'mainbox threelayout';
+ h1 'Recent changes';
+ my $changes = $self->dbRevisionGet(what => 'item user', results => 10, auto => 1, hidden => 1);
+ ul;
+ for (@$changes) {
+ my $t = (qw|v r p|)[$_->{type}];
+ li;
+ b "$t:";
+ a href => "/$t$_->{iid}.$_->{rev}", title => $_->{ioriginal}||$_->{ititle}, shorten $_->{ititle}, 30;
+ txt ' by ';
+ a href => "/u$_->{requester}", $_->{username};
+ end;
+ }
+ end;
+ end;
+
+ # Announcements
+ div class => 'mainbox threelayout';
+ my $an = $self->dbThreadGet(type => 'an', order => 't.id DESC', results => 2);
+ a class => 'right', href => '/t/an', 'News archive';
+ h1 'Announcements';
+ for (@$an) {
+ my $post = $self->dbPostGet(tid => $_->{id}, num => 1)->[0];
+ h2;
+ a href => "/t$_->{id}", $_->{title};
+ end;
+ p;
+ lit bb2html $post->{msg}, 150;
+ end;
+ }
+ end;
+
+ # Recent posts
+ div class => 'mainbox threelayout last';
+ h1 'Recent posts';
+ my $posts = $self->dbThreadGet(what => 'lastpost', results => 10, order => 'tpl.date DESC');
+ ul;
+ for (@$posts) {
+ li;
+ txt age($_->{ldate}).' ';
+ a href => "/t$_->{id}.$_->{count}", title => $_->{title}, shorten $_->{title}, 20;
+ txt ' by ';
+ a href => "/u$_->{luid}", $_->{lusername};
+ end;
+ }
+ end;
+ end;
+
+ # Random visual novels
+ div class => 'mainbox threelayout';
+ h1 'Random visual novels';
+ my $random = $self->dbVNGet(results => 10, order => 'RANDOM()');
+ ul;
+ for (@$random) {
+ li;
+ a href => "/v$_->{id}", title => $_->{original}||$_->{title}, shorten $_->{title}, 40;
+ end;
+ }
+ end;
+ end;
+
+ # Upcoming releases
+ div class => 'mainbox threelayout';
+ h1 'Upcoming releases';
+ my $upcoming = $self->dbReleaseGet(results => 10, unreleased => 1);
+ ul;
+ for (@$upcoming) {
+ li;
+ lit datestr $_->{released};
+ txt ' ';
+ a href => "/r$_->{id}", title => $_->{original}||$_->{title}, shorten $_->{title}, 30;
+ end;
+ }
+ end;
+ end;
+
+ # Just released
+ div class => 'mainbox threelayout last';
+ h1 'Just released';
+ my $justrel = $self->dbReleaseGet(results => 10, order => 'rr.released DESC', unreleased => 0);
+ ul;
+ for (@$justrel) {
+ li;
+ lit datestr $_->{released};
+ txt ' ';
+ a href => "/r$_->{id}", title => $_->{original}||$_->{title}, shorten $_->{title}, 30;
+ end;
+ }
+ end;
+ end;
+
+ clearfloat;
+ $self->htmlFooter;
+}
+
+
+sub history {
+ my($self, $type, $id) = @_;
+ $type ||= '';
+ $id ||= 0;
+
+ my $f = $self->formValidate(
+ { name => 'p', required => 0, default => 1, template => 'int' },
+ { name => 'm', required => 0, default => !$type, enum => [ 0, 1 ] },
+ { name => 'h', required => 0, default => 1, enum => [ -1..1 ] },
+ { name => 't', required => 0, default => '', enum => [ 'v', 'r', 'p' ] },
+ { name => 'e', required => 0, default => 0, enum => [ -1..1 ] },
+ { name => 'r', required => 0, default => 0, enum => [ 0, 1 ] },
+ );
+ return 404 if $f->{_err};
+
+ # get item object and title
+ my $obj = $type eq 'u' ? $self->dbUserGet(uid => $id)->[0] :
+ $type eq 'p' ? $self->dbProducerGet(id => $id)->[0] :
+ $type eq 'r' ? $self->dbReleaseGet(id => $id)->[0] :
+ $self->dbVNGet(id => $id)->[0];
+ my $title = $type ? 'Edit history of '.($obj->{title} || $obj->{name} || $obj->{username}) : 'Recent changes';
+ return 404 if $type && !$obj->{id};
+
+ # get the edit history
+ my($list, $np) = $self->dbRevisionGet(
+ what => 'item user',
+ $type && $type ne 'u' ? ( type => $type, iid => $id ) : (),
+ $type eq 'u' ? ( uid => $id ) : (),
+ $f->{t} ? ( type => $f->{t} ) : (),
+ page => $f->{p},
+ results => 50,
+ auto => $f->{m},
+ hidden => $f->{h},
+ edit => $f->{e},
+ releases => $f->{r},
+ );
+
+ $self->htmlHeader(title => $title, noindex => 1);
+ $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('del')) {
+ 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';
+ 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';
+ a $f->{r} ? (class => 'optselected') : (), href => $u->(r => 1), 'Include edits of releases';
+ end;
+ }
+ end;
+
+ $self->htmlHistory($list, $f, $np, $u->());
+ $self->htmlFooter;
+}
+
+
+sub docpage {
+ my($self, $did) = @_;
+
+ open my $F, '<', sprintf('%s/data/docs/%d', $VNDB::ROOT, $did) or return 404;
+ my @c = <$F>;
+ close $F;
+
+ (my $title = shift @c) =~ s/^:TITLE://;
+ chomp $title;
+
+ my $sec = 0;
+ for (@c) {
+ s{^:SUB:(.+)\r?\n$}{
+ $sec++;
+ qq|<h3><a href="#$sec" name="$sec">$sec. $1</a></h3>\n|
+ }eg;
+ s{^:INC:(.+)\r?\n$}{
+ open $F, '<', sprintf('%s/data/docs/%s', $VNDB::ROOT, $1) or die $!;
+ my $ii = join('', <$F>);
+ close $F;
+ $ii;
+ }eg;
+ }
+
+ $self->htmlHeader(title => $title);
+ div class => 'mainbox';
+ h1 $title;
+ div class => 'docs';
+ lit join '', @c;
+ end;
+ end;
+ $self->htmlFooter;
+}
+
+
+sub nospam {
+ my $self = shift;
+ $self->htmlHeader(title => 'Could not send form', noindex => 1);
+
+ div class => 'mainbox';
+ h1 'Could not send form';
+ div class => 'warning';
+ h2 'Error';
+ p 'The form could not be sent, please make sure you have Javascript enabled in your browser.';
+ end;
+ end;
+
+ $self->htmlFooter;
+}
+
+
+# /hide and /lock for v/r/p+ pages
+sub itemmod {
+ my($self, $type, $iid, $act) = @_;
+ return $self->htmlDenied if !$self->authCan($act eq 'hide' ? 'del' : 'lock');
+
+ my $obj = $type eq 'v' ? $self->dbVNGet(id => $iid)->[0] :
+ $type eq 'r' ? $self->dbReleaseGet(id => $iid)->[0] :
+ $self->dbProducerGet(id => $iid)->[0];
+ return 404 if !$obj->{id};
+
+ $self->dbItemMod($type, $iid, $act eq 'hide' ? (hidden => !$obj->{hidden}) : (locked => !$obj->{locked}));
+
+ $self->resRedirect("/$type$iid", 'temp');
+}
+
+
+sub ie6message {
+ my $self = shift;
+
+ if($self->reqParam('i-still-want-access')) {
+ (my $ref = $self->reqHeader('Referer') || '/') =~ s/^\Q$self->{url}//;
+ $ref = '/' if $ref eq '/we-dont-like-ie6';
+ $self->resRedirect($ref, 'temp');
+ $self->resHeader('Set-Cookie', "ie-sucks=1; path=/; domain=$self->{cookie_domain}");
+ return;
+ }
+
+ html;
+ head;
+ title 'Your browser sucks';
+ style type => 'text/css',
+ q|body { background: black }|
+ .q|div { position: absolute; left: 50%; top: 50%; width: 500px; margin-left: -250px; height: 180px; margin-top: -90px; background-color: #012; border: 1px solid #258; text-align: center; }|
+ .q|p { color: #ddd; margin: 10px; font: 9pt "Tahoma"; }|
+ .q|h1 { color: #258; font-size: 14pt; font-family: "Futura", "Century New Gothic", "Arial", Serif; font-weight: normal; margin: 10px 0 0 0; } |
+ .q|a { color: #fff }|;
+ end;
+ body;
+ div;
+ h1 'Oops, we were too lazy support your browser!';
+ p;
+ lit qq|We decided to stop supporting Internet Explorer 6, as it's a royal pain in |
+ .qq|the ass to make our site look good in a browser that doesn't want to cooperate with us.<br />|
+ .qq|You can try one of the following free alternatives: |
+ .qq|<a href="http://www.mozilla.com/firefox/">Firefox</a>, |
+ .qq|<a href="http://www.opera.com/">Opera</a>, |
+ .qq|<a href="http://www.apple.com/safari/">Safari</a>, or |
+ .qq|<a href="http://www.google.com/chrome">Chrome</a>.<br /><br />|
+ .qq|If you're really stubborn about using Internet Explorer, upgrading to version 7 will also work.<br /><br />|
+ .qq|...and if you're mad, you can also choose to ignore this warning and |
+ .qq|<a href="/we-dont-like-ie6?i-still-want-access=1">open the site anyway</a>.|;
+ end;
+ end;
+ end;
+ end;
+}
+
+
+1;
+
diff --git a/lib/VNDB/Handler/Producers.pm b/lib/VNDB/Handler/Producers.pm
new file mode 100644
index 00000000..0efd2f47
--- /dev/null
+++ b/lib/VNDB/Handler/Producers.pm
@@ -0,0 +1,234 @@
+
+package VNDB::Handler::Producers;
+
+use strict;
+use warnings;
+use YAWF ':html', ':xml';
+use VNDB::Func;
+
+
+YAWF::register(
+ qr{p([1-9]\d*)(?:\.([1-9]\d*))?} => \&page,
+ qr{p(?:([1-9]\d*)(?:\.([1-9]\d*))?/edit|/new)}
+ => \&edit,
+ qr{p/([a-z0]|all)} => \&list,
+ qr{xml/producers\.xml} => \&pxml,
+);
+
+
+sub page {
+ my($self, $pid, $rev) = @_;
+
+ my $p = $self->dbProducerGet(
+ id => $pid,
+ what => 'vn'.($rev ? ' changes' : ''),
+ $rev ? ( rev => $rev ) : ()
+ )->[0];
+ return 404 if !$p->{id};
+
+ $self->htmlHeader(title => $p->{name}, noindex => $rev);
+ $self->htmlMainTabs(p => $p);
+ return if $self->htmlHiddenMessage('p', $p);
+
+ if($rev) {
+ my $prev = $rev && $rev > 1 && $self->dbProducerGet(id => $pid, rev => $rev-1, what => 'changes')->[0];
+ $self->htmlRevision('p', $prev, $p,
+ [ type => 'Type', serialize => sub { $self->{producer_types}{$_[0]} } ],
+ [ name => 'Name (romaji)', diff => 1 ],
+ [ original => 'Original name', diff => 1 ],
+ [ lang => 'Language', serialize => sub { "$_[0] ($self->{languages}{$_[0]})" } ],
+ [ website => 'Website', diff => 1 ],
+ [ desc => 'Description', diff => 1 ],
+ );
+ }
+
+ div class => 'mainbox producerpage';
+ $self->htmlItemMessage('p', $p);
+ h1 $p->{name};
+ h2 class => 'alttitle', $p->{original} if $p->{original};
+ p class => 'center';
+ txt "$self->{languages}{$p->{lang}} \L$self->{producer_types}{$p->{type}}";
+ if($p->{website}) {
+ txt "\n";
+ a href => $p->{website}, $p->{website};
+ }
+ end;
+
+ if($p->{desc}) {
+ p class => 'description';
+ lit bb2html $p->{desc};
+ end;
+ }
+
+ end;
+ div class => 'mainbox producerpage';
+ h1 'Visual Novel Relations';
+ if(!@{$p->{vn}}) {
+ p 'We have currently no visual novels related to this producer.';
+ } else {
+ ul;
+ for (@{$p->{vn}}) {
+ li;
+ i;
+ lit datestr $_->{date};
+ end;
+ a href => "/v$_->{id}", title => $_->{original}, $_->{title};
+ end;
+ }
+ end;
+ }
+ end;
+ $self->htmlFooter;
+}
+
+
+# pid as argument = edit producer
+# no arguments = add new producer
+sub edit {
+ my($self, $pid, $rev) = @_;
+
+ my $p = $pid && $self->dbProducerGet(id => $pid, what => 'changes', $rev ? (rev => $rev) : ())->[0];
+ return 404 if $pid && !$p->{id};
+ $rev = undef if !$p || $p->{cid} == $p->{latest};
+
+ return $self->htmlDenied if !$self->authCan('edit')
+ || $pid && ($p->{locked} && !$self->authCan('lock') || $p->{hidden} && !$self->authCan('del'));
+
+ my %b4 = !$pid ? () : map { $_ => $p->{$_} } qw|type name original lang website desc|;
+ my $frm;
+
+ if($self->reqMethod eq 'POST') {
+ $frm = $self->formValidate(
+ { name => 'type', enum => [ keys %{$self->{producer_types}} ] },
+ { name => 'name', maxlength => 200 },
+ { name => 'original', required => 0, maxlength => 200, default => '' },
+ { name => 'lang', enum => [ keys %{$self->{languages}} ] },
+ { name => 'website', required => 0, template => 'url', default => '' },
+ { name => 'desc', required => 0, maxlength => 5000, default => '' },
+ { name => 'editsum', maxlength => 5000 },
+ );
+ if(!$frm->{_err}) {
+ return $self->resRedirect("/p$pid", 'post')
+ if $pid && !grep $frm->{$_} ne $b4{$_}, keys %b4;
+
+ $rev = 1;
+ if($pid) {
+ ($rev) = $self->dbProducerEdit($pid, %$frm);
+ } else {
+ ($pid) = $self->dbProducerAdd(%$frm);
+ }
+
+ $self->multiCmd("ircnotify p$pid.$rev");
+
+ return $self->resRedirect("/p$pid.$rev", 'post');
+ }
+ }
+
+ !defined $frm->{$_} && ($frm->{$_} = $b4{$_}) for keys %b4;
+ $frm->{lang} = 'ja' if !$pid && !defined $frm->{lang};
+ $frm->{editsum} = sprintf 'Reverted to revision p%d.%d', $pid, $rev if $rev && !defined $frm->{editsum};
+
+ $self->htmlHeader(title => $pid ? 'Edit '.$p->{name} : 'Add new producer', noindex => 1);
+ $self->htmlMainTabs('p', $p, 'edit') if $pid;
+ $self->htmlEditMessage('p', $p);
+ $self->htmlForm({ frm => $frm, action => $pid ? "/p$pid/edit" : '/p/new', editsum => 1 }, "General info" => [
+ [ select => name => 'Type', short => 'type',
+ options => [ map [ $_, $self->{producer_types}{$_} ], sort keys %{$self->{producer_types}} ] ],
+ [ input => name => 'Name (romaji)', short => 'name' ],
+ [ input => name => 'Original name', short => 'original' ],
+ [ static => content => q|The original name of the producer, leave blank if it is already in the Latin alphabet.| ],
+ [ select => name => 'Primary language', short => 'lang',
+ options => [ map [ $_, "$_ ($self->{languages}{$_})" ], sort keys %{$self->{languages}} ] ],
+ [ input => name => 'Website', short => 'website' ],
+ [ text => name => 'Description', short => 'desc', rows => 6 ],
+ ]);
+ $self->htmlFooter;
+}
+
+
+sub list {
+ my($self, $char) = @_;
+
+ my $f = $self->formValidate(
+ { name => 'p', required => 0, default => 1, template => 'int' },
+ { name => 'q', required => 0, default => '' },
+ );
+ return 404 if $f->{_err};
+
+ my($list, $np) = $self->dbProducerGet(
+ $char ne 'all' ? ( char => $char ) : (),
+ $f->{q} ? ( search => $f->{q} ) : (),
+ results => 150,
+ page => $f->{p}
+ );
+
+ $self->htmlHeader(title => 'Browse producers');
+
+ div class => 'mainbox';
+ h1 'Browse producers';
+ form class => 'search', action => '/p/all', 'accept-charset' => 'UTF-8', method => 'get';
+ fieldset;
+ input type => 'text', name => 'q', id => 'q', class => 'text', value => $f->{q};
+ input type => 'submit', class => 'submit', value => 'Search!';
+ end;
+ end;
+ p class => 'browseopts';
+ for ('all', 'a'..'z', 0) {
+ a href => "/p/$_", $_ eq $char ? (class => 'optselected') : (), $_ ? uc $_ : '#';
+ }
+ end;
+ end;
+
+ my $pageurl = "/p/$char" . ($f->{q} ? "?q=$f->{q}" : '');
+ $self->htmlBrowseNavigate($pageurl, $f->{p}, $np, 't');
+ div class => 'mainbox producerbrowse';
+ h1 $f->{q} ? 'Search results' : 'Producer list';
+ if(!@$list) {
+ p 'No results found';
+ } else {
+ # spread the results over 3 equivalent-sized lists
+ my $perlist = @$list/3 < 1 ? 1 : @$list/3;
+ for my $c (0..(@$list < 3 ? $#$list : 2)) {
+ ul;
+ for ($perlist*$c..($perlist*($c+1))-1) {
+ li;
+ cssicon 'lang '.$list->[$_]{lang}, $self->{languages}{$list->[$_]{lang}};
+ a href => "/p$list->[$_]{id}", $list->[$_]{name};
+ end;
+ }
+ end;
+ }
+ }
+ clearfloat;
+ end;
+ $self->htmlBrowseNavigate($pageurl, $f->{p}, $np, 'b');
+ $self->htmlFooter;
+}
+
+
+# peforms a (simple) search and returns the results in XML format
+sub pxml {
+ my $self = shift;
+
+ my $q = $self->formValidate({ name => 'q', maxlength => 500 });
+ return 404 if $q->{_err};
+ $q = $q->{q};
+
+ my($list, $np) = $self->dbProducerGet(
+ $q =~ /^p([1-9]\d*)/ ? (id => $1) : (search => $q),
+ results => 10,
+ page => 1,
+ );
+
+ $self->resHeader('Content-type' => 'text/xml; charset=UTF-8');
+ xml;
+ tag 'producers', more => $np ? 'yes' : 'no', query => $q;
+ for(@$list) {
+ tag 'item', id => $_->{id}, $_->{name};
+ }
+ end;
+}
+
+
+1;
+
diff --git a/lib/VNDB/Handler/Releases.pm b/lib/VNDB/Handler/Releases.pm
new file mode 100644
index 00000000..ff28c64b
--- /dev/null
+++ b/lib/VNDB/Handler/Releases.pm
@@ -0,0 +1,422 @@
+
+package VNDB::Handler::Releases;
+
+use strict;
+use warnings;
+use YAWF ':html';
+use VNDB::Func;
+use POSIX 'strftime';
+
+
+YAWF::register(
+ qr{r([1-9]\d*)(?:\.([1-9]\d*))?} => \&page,
+ qr{(v)([1-9]\d*)/add} => \&edit,
+ qr{r(?:([1-9]\d*)(?:\.([1-9]\d*))?/edit)}
+ => \&edit,
+);
+
+
+sub page {
+ my($self, $rid, $rev) = @_;
+
+ my $r = $self->dbReleaseGet(
+ id => $rid,
+ what => 'vn producers platforms media'.($rev ? ' changes' : ''),
+ $rev ? (rev => $rev) : (),
+ )->[0];
+ return 404 if !$r->{id};
+
+ $self->htmlHeader(title => $r->{title}, noindex => $rev);
+ $self->htmlMainTabs('r', $r);
+ return if $self->htmlHiddenMessage('r', $r);
+
+ if($rev) {
+ my $prev = $rev && $rev > 1 && $self->dbReleaseGet(
+ id => $rid, rev => $rev-1,
+ what => 'vn producers platforms media changes'
+ )->[0];
+ $self->htmlRevision('r', $prev, $r,
+ [ vn => 'Relations', join => '<br />', split => sub {
+ map sprintf('<a href="/v%d" title="%s">%s</a>', $_->{vid}, $_->{original}||$_->{title}, shorten $_->{title}, 50), @{$_[0]};
+ } ],
+ [ type => 'Type', serialize => sub { $self->{release_types}[$_[0]] } ],
+ [ title => 'Title (romaji)', diff => 1 ],
+ [ original => 'Original title', diff => 1 ],
+ [ gtin => 'JAN/UPC/EAN', serialize => sub { $_[0]||'[none]' } ],
+ [ language => 'Language', serialize => sub { $self->{languages}{$_[0]} } ],
+ [ website => 'Website', ],
+ [ released => 'Release date', htmlize => sub { datestr $_[0] } ],
+ [ minage => 'Age rating', serialize => sub { $self->{age_ratings}{$_[0]} } ],
+ [ notes => 'Notes', diff => 1 ],
+ [ platforms => 'Platforms', join => ', ', split => sub { map $self->{platforms}{$_}, @{$_[0]} } ],
+ [ media => 'Media', join => ', ', split => sub {
+ map {
+ my $med = $self->{media}{$_->{medium}};
+ $med->[1] ? sprintf('%d %s%s', $_->{qty}, $med->[0], $_->{qty}>1?'s':'') : $med->[0]
+ } @{$_[0]};
+ } ],
+ [ producers => 'Producers', join => '<br />', split => sub {
+ map sprintf('<a href="/p%d" title="%s">%s</a>', $_->{id}, $_->{original}||$_->{name}, shorten $_->{name}, 50), @{$_[0]};
+ } ],
+ );
+ }
+
+ div class => 'mainbox release';
+ $self->htmlItemMessage('r', $r);
+ h1 $r->{title};
+ h2 class => 'alttitle', $r->{original} if $r->{original};
+
+ _infotable($self, $r);
+
+ if($r->{notes}) {
+ p class => 'description';
+ lit bb2html $r->{notes};
+ end;
+ }
+
+ end;
+ $self->htmlFooter;
+}
+
+
+sub _infotable {
+ my($self, $r) = @_;
+ table;
+ Tr;
+ td class => 'key', ' ';
+ td ' ';
+ end;
+ my $i = 0;
+
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Relation';
+ td;
+ for (@{$r->{vn}}) {
+ a href => "/v$_->{vid}", title => $_->{original}||$_->{title}, shorten $_->{title}, 60;
+ br if $_ != $r->{vn}[$#{$r->{vn}}];
+ }
+ end;
+ end;
+
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Type';
+ td;
+ my $type = $self->{release_types}[$r->{type}];
+ cssicon lc(substr $type, 0, 3), $type;
+ txt ' '.$type;
+ end;
+ end;
+
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Language';
+ td;
+ cssicon "lang $r->{language}", $self->{languages}{$r->{language}};
+ txt ' '.$self->{languages}{$r->{language}};
+ end;
+ end;
+
+ if(@{$r->{platforms}}) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Platform'.($#{$r->{platforms}} ? 's' : '');
+ td;
+ for(@{$r->{platforms}}) {
+ cssicon $_, $self->{platforms}{$_};
+ txt ' '.$self->{platforms}{$_};
+ br if $_ ne $r->{platforms}[$#{$r->{platforms}}];
+ }
+ end;
+ end;
+ }
+
+ if(@{$r->{media}}) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Medi'.($#{$r->{media}} ? 'a' : 'um');
+ td join ', ', map {
+ my $med = $self->{media}{$_->{medium}};
+ $med->[1] ? sprintf('%d %s%s', $_->{qty}, $med->[0], $_->{qty}>1?'s':'') : $med->[0]
+ } @{$r->{media}};
+ end;
+ }
+
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Released';
+ td;
+ lit datestr $r->{released};
+ end;
+ end;
+
+ if($r->{minage} >= 0) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Age rating';
+ td $self->{age_ratings}{$r->{minage}};
+ end;
+ }
+
+ if(@{$r->{producers}}) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Producer'.($#{$r->{producers}} ? 's' : '');
+ td;
+ for (@{$r->{producers}}) {
+ a href => "/p$_->{id}", title => $_->{original}||$_->{name}, shorten $_->{name}, 60;
+ br if $_ != $r->{producers}[$#{$r->{producers}}];
+ }
+ end;
+ end;
+ }
+
+ if($r->{gtin}) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td gtintype $r->{gtin};
+ td $r->{gtin};
+ end;
+ }
+
+ if($r->{website}) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Links';
+ td;
+ a href => $r->{website}, rel => 'nofollow', 'Official website';
+ end;
+ end;
+ }
+
+ if($self->authInfo->{id}) {
+ my $rl = $self->dbVNListGet(uid => $self->authInfo->{id}, rid => $r->{id})->[0];
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'User options';
+ td;
+ Select id => 'listsel', name => 'listsel';
+ option !$rl ? 'not in your list' : "Status: $self->{vn_rstat}[$rl->{rstat}] / $self->{vn_vstat}[$rl->{vstat}]";
+ optgroup label => 'Set release status';
+ option value => "r$_", $self->{vn_rstat}[$_]
+ for (0..$#{$self->{vn_rstat}});
+ end;
+ optgroup label => 'Set play status';
+ option value => "v$_", $self->{vn_vstat}[$_]
+ for (0..$#{$self->{vn_vstat}});
+ end;
+ option value => 'del', 'remove from list' if $rl;
+ end;
+ end;
+ end;
+ }
+
+ end;
+}
+
+
+# rid = \d -> edit release
+# rid = 'v' -> add release to VN with id $rev
+sub edit {
+ my($self, $rid, $rev) = @_;
+
+ my $vid = 0;
+ if($rid eq 'v') {
+ $vid = $rev;
+ $rev = undef;
+ $rid = 0;
+ }
+
+ my $r = $rid && $self->dbReleaseGet(id => $rid, what => 'vn producers platforms media changes', $rev ? (rev => $rev) : ())->[0];
+ return 404 if $rid && !$r->{id};
+ $rev = undef if !$r || $r->{cid} == $r->{latest};
+
+ my $v = $vid && $self->dbVNGet(id => $vid)->[0];
+ return 404 if $vid && !$v->{id};
+
+ return $self->htmlDenied if !$self->authCan('edit')
+ || $rid && ($r->{locked} && !$self->authCan('lock') || $r->{hidden} && !$self->authCan('del'));
+
+ my $vn = $rid ? $r->{vn} : [{ vid => $vid, title => $v->{title} }];
+ my %b4 = !$rid ? () : (
+ (map { $_ => $r->{$_} } qw|type title original gtin language website notes minage platforms|),
+ released => $r->{released} =~ /^([0-9]{4})([0-9]{2})([0-9]{2})$/ ? [ $1, $2, $3 ] : [ 0, 0, 0 ],
+ media => join(',', sort map "$_->{medium} $_->{qty}", @{$r->{media}}),
+ producers => join('|||', map "$_->{id},$_->{name}", sort { $a->{id} <=> $b->{id} } @{$r->{producers}}),
+ );
+ $b4{vn} = join('|||', map "$_->{vid},$_->{title}", sort { $a->{vid} <=> $b->{vid} } @$vn);
+ my $frm;
+
+ if($self->reqMethod eq 'POST') {
+ $frm = $self->formValidate(
+ { name => 'type', enum => [ 0..$#{$self->{release_types}} ] },
+ { name => 'title', maxlength => 250 },
+ { name => 'original', required => 0, default => '', maxlength => 250 },
+ { name => 'gtin', required => 0, default => '0',
+ func => [ \&gtintype, 'Not a valid JAN/UPC/EAN code' ] },
+ { name => 'language', enum => [ keys %{$self->{languages}} ] },
+ { name => 'website', required => 0, default => '', template => 'url' },
+ { name => 'released', required => 0, default => 0, multi => 1, template => 'int' },
+ { name => 'minage' , required => 0, default => -1, enum => [ keys %{$self->{age_ratings}} ] },
+ { name => 'notes', required => 0, default => '', maxlength => 10240 },
+ { name => 'platforms', required => 0, default => '', multi => 1, enum => [ keys %{$self->{platforms}} ] },
+ { name => 'media', required => 0, default => '' },
+ { name => 'producers', required => 0, default => '' },
+ { name => 'vn', maxlength => 5000 },
+ { name => 'editsum', maxlength => 5000 },
+ );
+ if(!$frm->{_err}) {
+ # de-serialize
+ my $released = !$frm->{released}[0] ? 0 : $frm->{released}[0] == 9999 ? 99999999 :
+ sprintf '%04d%02d%02d', $frm->{released}[0], $frm->{released}[1]||99, $frm->{released}[2]||99;
+ my $media = [ map [ split / / ], split /,/, $frm->{media} ];
+ my $producers = [ map { /^([0-9]+)/ ? $1 : () } split /\|\|\|/, $frm->{producers} ];
+ my $new_vn = [ map { /^([0-9]+)/ ? $1 : () } split /\|\|\|/, $frm->{vn} ];
+ $frm->{platforms} = [ grep $_, @{$frm->{platforms}} ];
+
+ return $self->resRedirect("/r$rid", 'post')
+ if $rid && $released == $r->{released} &&
+ (join(',', sort @{$b4{platforms}}) eq join(',', sort @{$frm->{platforms}})) &&
+ (join(',', sort @$producers) eq join(',', sort map $_->{id}, @{$r->{producers}})) &&
+ (join(',', sort @$new_vn) eq join(',', sort map $_->{vid}, @$vn)) &&
+ !grep !/^(released|platforms|producers|vn)$/ && $frm->{$_} ne $b4{$_}, keys %b4;
+
+ my %opts = (
+ (map { $_ => $frm->{$_} } qw| type title original gtin language website notes minage platforms editsum|),
+ vn => $new_vn,
+ producers => $producers,
+ media => $media,
+ released => $released,
+ );
+
+ $rev = 1;
+ ($rev) = $self->dbReleaseEdit($rid, %opts) if $rid;
+ ($rid) = $self->dbReleaseAdd(%opts) if !$rid;
+
+ $self->multiCmd("ircnotify r$rid.$rev");
+ _update_vncache($self, @$new_vn, map $_->{vid}, @$vn);
+
+ return $self->resRedirect("/r$rid.$rev", 'post');
+ }
+ }
+
+ !defined $frm->{$_} && ($frm->{$_} = $b4{$_}) for keys %b4;
+ $frm->{language} = 'ja' if !$rid && !defined $frm->{lang};
+ $frm->{editsum} = sprintf 'Reverted to revision r%d.%d', $rid, $rev if $rev && !defined $frm->{editsum};
+
+ $self->htmlHeader(js => 'forms', title => $rid ? 'Edit '.$r->{title} : 'Add release to '.$v->{title}, noindex => 1);
+ $self->htmlMainTabs('r', $r, 'edit') if $rid;
+ $self->htmlMainTabs('v', $v, 'edit') if $vid;
+ $self->htmlEditMessage('r', $r);
+ _form($self, $r, $v, $frm);
+ $self->htmlFooter;
+}
+
+
+sub _form {
+ my($self, $r, $v, $frm) = @_;
+
+ $self->htmlForm({ frm => $frm, action => $r ? "/r$r->{id}/edit" : "/v$v->{id}/add", editsum => 1 },
+ "General info" => [
+ [ select => short => 'type', name => 'Type',
+ options => [ map [ $_, $self->{release_types}[$_] ], 0..$#{$self->{release_types}} ] ],
+ [ input => short => 'title', name => 'Title (romaji)', width => 300 ],
+ [ input => short => 'original', name => 'Original title', width => 300 ],
+ [ static => content => 'The original title of this release, leave blank if it already is in the Latin alphabet.' ],
+ [ select => short => 'language', name => 'Language',
+ options => [ map [ $_, "$_ ($self->{languages}{$_})" ], sort keys %{$self->{languages}} ] ],
+ [ input => short => 'gtin', name => 'JAN/UPC/EAN' ],
+ [ input => short => 'website', name => 'Official website' ],
+ [ static => label => 'Release date', content => sub {
+ Select id => 'released', name => 'released';
+ option value => $_, $frm->{released} && $frm->{released}[0] == $_ ? (selected => 'selected') : (),
+ !$_ ? '-year-' : $_ < 9999 ? $_ : 'TBA'
+ for (0, 1980..((localtime())[5]+1905), 9999);
+ end;
+ Select id => 'released_m', name => 'released';
+ option value => $_, $frm->{released} && $frm->{released}[1] == $_ ? (selected => 'selected') : (),
+ !$_ ? '-month-' : strftime '%B', 0, 0, 0, 0, $_, 0, 0, 0
+ for(0..12);
+ end;
+ Select id => 'released_d', name => 'released';
+ option value => $_, $frm->{released} && $frm->{released}[2] == $_ ? (selected => 'selected') : (),
+ !$_ ? '-day-' : $_
+ for(0..31);
+ end;
+ }],
+ [ static => content => 'Leave month or day blank if they are unknown' ],
+ [ select => short => 'minage', name => 'Age rating',
+ options => [ map [ $_, $self->{age_ratings}{$_} ], sort { $a <=> $b } keys %{$self->{age_ratings}} ] ],
+ [ textarea => short => 'notes', name => 'Notes' ],
+ [ static => content => 'Miscellaneous notes/comments, information that does not fit in the above fields. '
+ .'E.g.: Censored/uncensored or for which releases this patch applies. Max. 250 characters.' ],
+ ],
+
+ 'Platforms & Media' => [
+ [ hidden => short => 'media' ],
+ [ static => nolabel => 1, content => sub {
+ h2 'Platforms';
+ div class => 'platforms';
+ for my $p (sort keys %{$self->{platforms}}) {
+ span;
+ input type => 'checkbox', name => 'platforms', value => $p, id => $p,
+ $frm->{platforms} && grep($_ eq $p, @{$frm->{platforms}}) ? (checked => 'checked') : ();
+ label for => $p;
+ cssicon $p, $self->{platforms}{$p};
+ txt ' '.$self->{platforms}{$p};
+ end;
+ end;
+ }
+ end;
+
+ h2 'Media';
+ div id => 'media_div';
+ Select;
+ option value => $_, class => $self->{media}{$_}[1] ? 'qty' : 'noqty', $self->{media}{$_}[0]
+ for (sort keys %{$self->{media}});
+ end;
+ end;
+ }],
+ ],
+
+ 'Producers' => [
+ [ hidden => short => 'producers' ],
+ [ static => nolabel => 1, content => sub {
+ h2 'Selected producers';
+ div id => 'producerssel';
+ end;
+ h2 'Add producer';
+ div;
+ input type => 'text', class => 'text';
+ a href => '#', 'add';
+ end;
+ }],
+ ],
+
+ 'Visual novels' => [
+ [ hidden => short => 'vn' ],
+ [ static => nolabel => 1, content => sub {
+ h2 'Selected visual novels';
+ div id => 'vnsel';
+ end;
+ h2 'Add visual novel';
+ div;
+ input type => 'text', class => 'text';
+ a href => '#', 'add';
+ end;
+ }],
+ ],
+ );
+}
+
+
+# Recalculates the vn.c_* columns and regenerates the related relation graphs on any change
+sub _update_vncache {
+ my($self, @vns) = @_;
+
+ my $before = $self->dbVNGet(id => \@vns, order => 'v.id', what => 'relations');
+ $self->dbVNCache(@vns);
+ my $after = $self->dbVNGet(id => \@vns, order => 'v.id');
+
+ my @upd = map {
+ @{$before->[$_]{relations}} && (
+ $before->[$_]{c_released} != $after->[$_]{c_released}
+ || $before->[$_]{c_languages} ne $after->[$_]{c_languages}
+ ) ? $before->[$_]{id} : ();
+ } 0..$#$before;
+ $self->multiCmd('relgraph '.join(' ', @upd)) if @upd;
+}
+
+
+1;
+
diff --git a/lib/VNDB/Handler/ULists.pm b/lib/VNDB/Handler/ULists.pm
new file mode 100644
index 00000000..5353dbc9
--- /dev/null
+++ b/lib/VNDB/Handler/ULists.pm
@@ -0,0 +1,336 @@
+
+package VNDB::Handler::ULists;
+
+use strict;
+use warnings;
+use YAWF ':html';
+use VNDB::Func;
+
+
+YAWF::register(
+ qr{v([1-9]\d*)/vote}, \&vnvote,
+ qr{v([1-9]\d*)/wish}, \&vnwish,
+ qr{r([1-9]\d*)/list}, \&rlist,
+ qr{u([1-9]\d*)/wish}, \&wishlist,
+ qr{u([1-9]\d*)/list}, \&vnlist,
+);
+
+
+sub vnvote {
+ my($self, $id) = @_;
+
+ my $uid = $self->authInfo->{id};
+ return $self->htmlDenied() if !$uid;
+
+ my $f = $self->formValidate(
+ { name => 'v', enum => [ -1, 1..10 ] }
+ );
+ return 404 if $f->{_err};
+
+ $self->dbVoteDel($uid, $id) if $f->{v} == -1;
+ $self->dbVoteAdd($id, $uid, $f->{v}) if $f->{v} > 0;
+
+ $self->resRedirect('/v'.$id, 'temp');
+}
+
+
+sub vnwish {
+ my($self, $id) = @_;
+
+ my $uid = $self->authInfo->{id};
+ return $self->htmlDenied() if !$uid;
+
+ my $f = $self->formValidate(
+ { name => 's', enum => [ -1..$#{$self->{wishlist_status}} ] }
+ );
+ return 404 if $f->{_err};
+
+ $self->dbWishListDel($uid, $id) if $f->{s} == -1;
+ $self->dbWishListAdd($id, $uid, $f->{s}) if $f->{s} != -1;
+
+ $self->resRedirect('/v'.$id, 'temp');
+}
+
+
+sub rlist {
+ my($self, $id) = @_;
+
+ my $uid = $self->authInfo->{id};
+ return $self->htmlDenied() if !$uid;
+
+ my $f = $self->formValidate(
+ { name => 'e', required => 1, enum => [ 'del', map("r$_", 0..$#{$self->{vn_rstat}}), map("v$_", 0..$#{$self->{vn_vstat}}) ] },
+ );
+ return 404 if $f->{_err};
+
+ $self->dbVNListDel($uid, $id) if $f->{e} eq 'del';
+ $self->dbVNListAdd(
+ rid => $id,
+ uid => $uid,
+ $f->{e} =~ /^([rv])(\d+)$/ && $1 eq 'r' ? (rstat => $2) : (vstat => $2)
+ ) if $f->{e} ne 'del';
+
+ $self->resRedirect('/r'.$id, 'temp');
+}
+
+
+sub wishlist {
+ my($self, $uid) = @_;
+
+ my $own = $self->authInfo->{id} && $self->authInfo->{id} == $uid;
+ my $u = $self->dbUserGet(uid => $uid)->[0];
+ return 404 if !$u || !$own && !$u->{show_list};
+
+ my $f = $self->formValidate(
+ { name => 'p', required => 0, default => 1, template => 'int' },
+ { name => 'o', required => 0, default => 'a', enum => [ 'a', 'd' ] },
+ { name => 's', required => 0, default => 'title', enum => [qw|title added|] },
+ { name => 'f', required => 0, default => -1, enum => [ -1..$#{$self->{wishlist_status}} ] },
+ );
+ return 404 if $f->{_err};
+
+ if($own && $self->reqMethod eq 'POST') {
+ my $frm = $self->formValidate(
+ { name => 'sel', required => 0, default => 0, multi => 1, template => 'int' },
+ { name => 'batchedit', required => 1, enum => [ -1..$#{$self->{wishlist_status}} ] },
+ );
+ if(!$frm->{_err} && @{$frm->{sel}} && $frm->{sel}[0]) {
+ $self->dbWishListDel($uid, $frm->{sel}) if $frm->{batchedit} == -1;
+ $self->dbWishListAdd($frm->{sel}, $uid, $frm->{batchedit}) if $frm->{batchedit} >= 0;
+ }
+ }
+
+ my($list, $np) = $self->dbWishListGet(
+ uid => $uid,
+ order => $f->{s}.' '.($f->{o} eq 'a' ? 'ASC' : 'DESC'),
+ $f->{f} != -1 ? (wstat => $f->{f}) : (),
+ what => 'vn',
+ results => 50,
+ page => $f->{p},
+ );
+
+ my $title = $own ? 'My wishlist' : "\u$u->{username}'s wishlist";
+ $self->htmlHeader(title => $title, noindex => 1);
+ $self->htmlMainTabs('u', $u, 'wish');
+ div class => 'mainbox';
+ h1 $title;
+ if(!@$list && $f->{f} == -1) {
+ p 'Wishlist empty...';
+ end;
+ return $self->htmlFooter;
+ }
+ p class => 'browseopts';
+ a $f->{f} == $_ ? (class => 'optselected') : (), href => "/u$uid/wish?f=$_",
+ $_ == -1 ? 'All priorities' : ucfirst $self->{wishlist_status}[$_]
+ for (-1..$#{$self->{wishlist_status}});
+ end;
+ end;
+
+ form action => "/u$uid/wish?f=$f->{f};o=$f->{o};s=$f->{s};p=$f->{p}", method => 'post'
+ if $own;
+
+ $self->htmlBrowse(
+ class => 'wishlist',
+ items => $list,
+ nextpage => $np,
+ options => $f,
+ pageurl => "/u$uid/wish?f=$f->{f};o=$f->{o};s=$f->{s}",
+ sorturl => "/u$uid/wish?f=$f->{f}",
+ header => [
+ [ Title => 'title' ],
+ [ Priority => '' ],
+ [ Added => 'added' ],
+ ],
+ row => sub {
+ my($s, $n, $i) = @_;
+ Tr $n % 2 == 0 ? (class => 'odd') : ();
+ td class => 'tc1';
+ input type => 'checkbox', name => 'sel', value => $i->{vid}
+ if $own;
+ a href => "/v$i->{vid}", title => $i->{original}||$i->{title}, ' '.shorten $i->{title}, 70;
+ end;
+ td class => 'tc2', ucfirst $self->{wishlist_status}[$i->{wstat}];
+ td class => 'tc3', date $i->{added}, 'compact';
+ end;
+ },
+ $own ? (footer => sub {
+ Tr;
+ td colspan => 3;
+ Select name => 'batchedit', id => 'batchedit';
+ option '-- with selected --';
+ optgroup label => 'Change priority';
+ option value => $_, $self->{wishlist_status}[$_]
+ for (0..$#{$self->{wishlist_status}});
+ end;
+ option value => -1, 'remove from wishlist';
+ end;
+ end;
+ end;
+ }) : (),
+ );
+ end if $own;
+ $self->htmlFooter;
+}
+
+
+sub vnlist {
+ my($self, $uid) = @_;
+
+ my $own = $self->authInfo->{id} && $self->authInfo->{id} == $uid;
+ my $u = $self->dbUserGet(uid => $uid)->[0];
+ return 404 if !$u || !$own && !$u->{show_list};
+
+ my $f = $self->formValidate(
+ { name => 'p', required => 0, default => 1, template => 'int' },
+ { name => 'o', required => 0, default => 'a', enum => [ 'a', 'd' ] },
+ { name => 's', required => 0, default => 'title', enum => [ 'title', 'vote' ] },
+ { name => 'c', required => 0, default => 'all', enum => [ 'all', 'a'..'z', 0 ] },
+ { name => 'v', required => 0, default => 0, enum => [ -1..1 ] },
+ );
+ return 404 if $f->{_err};
+
+ if($own && $self->reqMethod eq 'POST') {
+ my $frm = $self->formValidate(
+ { name => 'sel', required => 0, default => 0, multi => 1, template => 'int' },
+ { name => 'batchedit', required => 1, enum => [ 'del', map("r$_", 0..$#{$self->{vn_rstat}}), map("v$_", 0..$#{$self->{vn_vstat}}) ] },
+ );
+ if(!$frm->{_err} && @{$frm->{sel}} && $frm->{sel}[0]) {
+ $self->dbVNListDel($uid, $frm->{sel}) if $frm->{batchedit} eq 'del';
+ $self->dbVNListAdd(
+ rid => $frm->{sel},
+ uid => $uid,
+ $frm->{batchedit} =~ /^([rv])(\d+)$/ && $1 eq 'r' ? (rstat => $2) : (vstat => $2)
+ ) if $frm->{batchedit} ne 'del';
+ }
+ }
+
+
+ my($list, $np) = $self->dbVNListList(
+ uid => $uid,
+ results => 50,
+ page => $f->{p},
+ order => $f->{s}.' '.($f->{o} eq 'd' ? 'DESC' : 'ASC'),
+ voted => $f->{v},
+ $f->{c} ne 'all' ? (char => $f->{c}) : (),
+ );
+
+ my $title = $own ? 'My visual novel list' : "\u$u->{username}'s visual novel list";
+ $self->htmlHeader(title => $title, noindex => 1);
+ $self->htmlMainTabs('u', $u, 'list');
+
+ # url generator
+ my $url = sub {
+ my($n, $v) = @_;
+ $n ||= '';
+ local $_ = "/u$uid/list";
+ $_ .= '?c='.($n eq 'c' ? $v : $f->{c});
+ $_ .= ';v='.($n eq 'v' ? $v : $f->{v});
+ if($n eq 'page') {
+ $_ .= ';o='.($n eq 'o' ? $v : $f->{o});
+ $_ .= ';s='.($n eq 's' ? $v : $f->{s});
+ }
+ return $_;
+ };
+
+ div class => 'mainbox';
+ h1 $title;
+ p class => 'browseopts';
+ for ('all', 'a'..'z', 0) {
+ a href => $url->(c => $_), $_ eq $f->{c} ? (class => 'optselected') : (), $_ ? uc $_ : '#';
+ }
+ end;
+ p class => 'browseopts';
+ a href => $url->(v => 0), 0 == $f->{v} ? (class => 'optselected') : (), 'All';
+ a href => $url->(v => 1), 1 == $f->{v} ? (class => 'optselected') : (), 'Only voted';
+ a href => $url->(v => -1), -1 == $f->{v} ? (class => 'optselected') : (), 'Hide voted';
+ end;
+ end;
+
+ _vnlist_browse($self, $own, $list, $np, $f, $url);
+ $self->htmlFooter;
+}
+
+sub _vnlist_browse {
+ my($self, $own, $list, $np, $f, $url) = @_;
+
+ form action => $url->(), method => 'post'
+ if $own;
+
+ $self->htmlBrowse(
+ class => 'rlist',
+ items => $list,
+ nextpage => $np,
+ options => $f,
+ sorturl => $url->(),
+ pageurl => $url->('page'),
+ header => [
+ [ Title => 'title', 3 ],
+ sub { td class => 'tc2', id => 'relhidall'; lit '<i>&#9656;</i>Releases*'; end; },
+ [ Vote => 'vote' ],
+ ],
+ row => sub {
+
+ my($s, $n, $i) = @_;
+ Tr $n % 2 == 0 ? (class => 'odd') : ();
+ td class => 'tc1', colspan => 3;
+ a href => "/v$i->{vid}", title => $i->{original}||$i->{title}, shorten $i->{title}, 70;
+ end;
+ td class => 'tc2'.(@{$i->{rels}} ? ' relhid_but' : ''), id => 'vid'.$i->{vid};
+ lit '<i>&#9656;</i>';
+ my $obtained = grep $_->{rstat}==2, @{$i->{rels}};
+ my $finished = grep $_->{vstat}==2, @{$i->{rels}};
+ my $txt = sprintf '%d/%d/%d', $obtained, $finished, scalar @{$i->{rels}};
+ $txt = qq|<b class="done">$txt</b>| if $finished > $obtained || $finished && $finished == $obtained;
+ $txt = qq|<b class="todo">$txt</b>| if $obtained > $finished;
+ lit $txt;
+ end;
+ td class => 'tc3', $i->{vote} || '-';
+ end;
+
+ for (@{$i->{rels}}) {
+ Tr class => "relhid vid$i->{vid}";
+ td class => 'tc1'.($own ? ' own' : '');
+ input type => 'checkbox', name => 'sel', value => $_->{rid}
+ if $own;
+ lit datestr $_->{released};
+ end;
+ td class => 'tc2';
+ cssicon "lang $_->{language}", $self->{languages}{$_->{language}};
+ cssicon substr(lc $self->{release_types}[$_->{type}], 0, 3), $self->{release_types}[$_->{type}].' release';
+ end;
+ td class => 'tc3';
+ a href => "/r$_->{rid}", title => $_->{original}||$_->{title}, shorten $_->{title}, 50;
+ end;
+ td colspan => 2, class => 'tc4';
+ lit liststat($_);
+ end;
+ end;
+ }
+ },
+
+ $own ? (footer => sub {
+ Tr;
+ td class => 'tc1', colspan => 3;
+ Select id => 'batchedit', name => 'batchedit';
+ option '- with selected -';
+ optgroup label => 'Change release status';
+ option value => "r$_", $self->{vn_rstat}[$_]
+ for (0..$#{$self->{vn_rstat}});
+ end;
+ optgroup label => 'Change play status';
+ option value => "v$_", $self->{vn_vstat}[$_]
+ for (0..$#{$self->{vn_vstat}});
+ end;
+ option value => 'del', 'remove from list';
+ end;
+ end;
+ td class => 'tc2', colspan => 2, '* Obtained/finished/total';
+ end;
+ }) : (),
+ );
+
+ end if $own;
+}
+
+1;
+
diff --git a/lib/VNDB/Handler/Users.pm b/lib/VNDB/Handler/Users.pm
new file mode 100644
index 00000000..088e5a31
--- /dev/null
+++ b/lib/VNDB/Handler/Users.pm
@@ -0,0 +1,467 @@
+
+package VNDB::Handler::Users;
+
+use strict;
+use warnings;
+use YAWF ':html';
+use Digest::MD5 'md5_hex';
+use VNDB::Func;
+
+
+YAWF::register(
+ qr{u([1-9]\d*)} => \&userpage,
+ qr{u/login} => \&login,
+ qr{u/logout} => \&logout,
+ qr{u/newpass} => \&newpass,
+ qr{u/newpass/sent} => \&newpass_sent,
+ qr{u/register} => \&register,
+ qr{u([1-9]\d*)/edit} => \&edit,
+ qr{u([1-9]\d*)/del(/[od])?} => \&delete,
+ qr{u/(all|[0a-z])} => \&list,
+);
+
+
+sub userpage {
+ my($self, $uid) = @_;
+
+ my $u = $self->dbUserGet(uid => $uid, what => 'stats')->[0];
+ return 404 if !$u->{id};
+
+ my $votes = $u->{c_votes} && $self->dbVoteStats(uid => $uid);
+
+ $self->htmlHeader(title => ucfirst($u->{username})."'s profile");
+ $self->htmlMainTabs('u', $u);
+ div class => 'mainbox userpage';
+ h1 ucfirst($u->{username})."'s profile";
+
+ table;
+ Tr;
+ td class => 'key', ' ';
+ td ' ';
+ end;
+ my $i = 0;
+
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Username';
+ td;
+ txt ucfirst($u->{username}).' (';
+ a href => "/u$uid", "u$uid";
+ txt ')';
+ end;
+ end;
+
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Registered';
+ td date $u->{registered};
+ end;
+
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Edits';
+ td;
+ if($u->{c_changes}) {
+ a href => "/u$uid/hist", $u->{c_changes};
+ } else {
+ txt '-';
+ }
+ end;
+ end;
+
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Votes';
+ td;
+ if(!$u->{show_list}) {
+ txt 'hidden';
+ } elsif($votes) {
+ my($total, $count) = (0, 0);
+ for (1..@$votes) {
+ $total += $_*$votes->[$_-1];
+ $count += $votes->[$_-1];
+ }
+ a href => "/u$uid/list?v=1", $count;
+ txt sprintf ' (%.2f average)', $total/$count;
+ } else {
+ txt '-';
+ }
+ end;
+ end;
+
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'List stats';
+ td !$u->{show_list} ? 'hidden' :
+ sprintf '%d releases of %d visual novels', $u->{releasecount}, $u->{vncount};
+ end;
+
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Forum stats';
+ td sprintf '%d posts, %d new threads', $u->{postcount}, $u->{threadcount};
+ end;
+ end;
+ end;
+
+ if($u->{show_list} && $votes) {
+ div class => 'mainbox';
+ h1 'Vote statistics';
+ $self->htmlVoteStats(u => $u, $votes);
+ end;
+ }
+
+ if($u->{c_changes}) {
+ my $list = $self->dbRevisionGet(what => 'item user', uid => $uid, results => 5);
+ h1 class => 'boxtitle';
+ a href => "/u$uid/hist", 'Recent changes';
+ end;
+ $self->htmlHistory($list, { p => 1 }, 0, "/u$uid/hist");
+ }
+ $self->htmlFooter;
+}
+
+
+sub login {
+ my $self = shift;
+
+ return $self->resRedirect('/') if $self->authInfo->{id};
+
+ my $frm;
+ if($self->reqMethod eq 'POST') {
+ $frm = $self->formValidate(
+ { name => 'usrname', required => 1, minlength => 2, maxlength => 15, template => 'pname' },
+ { name => 'usrpass', required => 1, minlength => 4, maxlength => 15, template => 'asciiprint' },
+ );
+
+ (my $ref = $self->reqHeader('Referer')||'/') =~ s/^\Q$self->{url}//;
+ return if !$frm->{_err} && $self->authLogin($frm->{usrname}, $frm->{usrpass}, $ref);
+ $frm->{_err} = [ 'login_failed' ] if !$frm->{_err};
+ }
+
+ $self->htmlHeader(title => 'Login', noindex => 1);
+ $self->htmlForm({ frm => $frm, action => '/u/login' }, Login => [
+ [ input => name => 'Username', short => 'usrname' ],
+ [ static => content => '<a href="/u/register">No account yet?</a>' ],
+ [ passwd => name => 'Password', short => 'usrpass' ],
+ [ static => content => '<a href="/u/newpass">Forgot your password?</a>' ],
+ ]);
+ $self->htmlFooter;
+}
+
+
+sub logout {
+ shift->authLogout;
+}
+
+
+sub newpass {
+ my $self = shift;
+
+ return $self->resRedirect('/') if $self->authInfo->{id};
+
+ my($frm, $u);
+ if($self->reqMethod eq 'POST') {
+ $frm = $self->formValidate(
+ { name => 'mail', required => 1, template => 'mail' },
+ );
+ if(!$frm->{_err}) {
+ $u = $self->dbUserGet(mail => $frm->{mail})->[0];
+ $frm->{_err} = [ 'nomail' ] if !$u || !$u->{id};
+ }
+ if(!$frm->{_err}) {
+ my @chars = ( 'A'..'Z', 'a'..'z', 0..9 );
+ my $pass = join '', map $chars[int rand $#chars+1], 0..8;
+ $self->dbUserEdit($u->{id}, passwd => md5_hex($pass));
+ $self->mail(
+ sprintf(join('', <DATA>), $u->{username}, $pass),
+ To => $u->{mail},
+ From => 'VNDB <noreply@vndb.org>',
+ Subject => 'New password for '.$u->{username}
+ );
+ return $self->resRedirect('/u/newpass/sent', 'post');
+ }
+ }
+
+ $self->htmlHeader(title => 'Forgot Password', noindex => 1);
+ div class => 'mainbox';
+ h1 'Forgot Password';
+ p "Forgot your password and can't login to VNDB anymore?\n"
+ ."Don't worry! Just give us the email address you used to register on VNDB,\n"
+ ."and we'll send you a new password within a few minutes!";
+ end;
+ $self->htmlForm({ frm => $frm, action => '/u/newpass' }, 'Reset Password' => [
+ [ input => name => 'Email', short => 'mail' ],
+ ]);
+ $self->htmlFooter;
+}
+
+
+sub newpass_sent {
+ my $self = shift;
+ return $self->resRedirect('/') if $self->authInfo->{id};
+ $self->htmlHeader(title => 'New Password', noindex => 1);
+ div class => 'mainbox';
+ h1 'New Password';
+ div class => 'notice';
+ h2 'Password Reset';
+ p;
+ txt "Your password has been reset and your new password should reach your mailbox in a few minutes.\n"
+ ."You can always change your password again after logging in.\n\n";
+ lit '<a href="/u/login">Login</a> - <a href="/">Home</a>';
+ end;
+ end;
+ end;
+ $self->htmlFooter;
+}
+
+
+sub register {
+ my $self = shift;
+ return $self->resRedirect('/') if $self->authInfo->{id};
+
+ my $frm;
+ if($self->reqMethod eq 'POST') {
+ $frm = $self->formValidate(
+ { name => 'usrname', template => 'pname', minlength => 2, maxlength => 15 },
+ { name => 'mail', template => 'mail' },
+ { name => 'usrpass', minlength => 4, maxlength => 15, template => 'asciiprint' },
+ { name => 'usrpass2', minlength => 4, maxlength => 15, template => 'asciiprint' },
+ );
+ push @{$frm->{_err}}, 'passmatch' if $frm->{usrpass} ne $frm->{usrpass2};
+ push @{$frm->{_err}}, 'usrexists' if $frm->{usrname} eq 'anonymous' || !$frm->{_err} && $self->dbUserGet(username => $frm->{usrname})->[0]{id};
+ push @{$frm->{_err}}, 'mailexists' if !$frm->{_err} && $self->dbUserGet(mail => $frm->{mail})->[0]{id};
+
+ if(!$frm->{_err}) {
+ $self->dbUserAdd($frm->{usrname}, md5_hex($frm->{usrpass}), $frm->{mail});
+ return $self->authLogin($frm->{usrname}, $frm->{usrpass}, '/');
+ }
+ }
+
+ $self->htmlHeader(title => 'Create an Account', noindex => 1);
+ div class => 'mainbox';
+ h1 'Create an Account';
+ h2 'Why should I register?';
+ p 'Creating an account is completely painless, the only thing we need to know is your prefered username '
+ .'and a password. You can just use any email address that isn\'t yours, as we don\'t even confirm '
+ .'that the address you gave us is really yours. Keep in mind, however, that you would probably '
+ .'want to remember your password if you do choose to give us an invalid email address...';
+
+ p 'Anyway, having an account here has a few advantages over being just a regular visitor:';
+ ul;
+ li 'You can contribute to the database by editing any entries and adding new ones';
+ li 'Keep track of all visual novels and releases you have, you\'d like to play, are playing, or have finished playing';
+ li 'Vote on the visual novels you liked or disliked';
+ li 'Contribute to the discussions on the boards';
+ li 'And boast about the fact that you have an account on the best visual novel database in the world!';
+ end;
+ end;
+
+ $self->htmlForm({ frm => $frm, action => '/u/register' }, 'New Account' => [
+ [ input => short => 'usrname', name => 'Username' ],
+ [ static => content => 'Requested username. Must be lowercase and can only consist of alphanumeric characters.' ],
+ [ input => short => 'mail', name => 'Email' ],
+ [ static => content => 'Your email address will only be used in case you lose your password. We will never send'
+ .' spam or newsletters unless you explicitly ask us for it.<br /><br />' ],
+ [ passwd => short => 'usrpass', name => 'Password' ],
+ [ passwd => short => 'usrpass2', name => 'Confirm pass.' ],
+ ]);
+ $self->htmlFooter;
+}
+
+
+sub edit {
+ my($self, $uid) = @_;
+
+ # are we allowed to edit this user?
+ return $self->htmlDenied if !$self->authInfo->{id} || $self->authInfo->{id} != $uid && !$self->authCan('usermod');
+
+ # fetch user info (cached if uid == loggedin uid)
+ my $u = $self->authInfo->{id} == $uid ? $self->authInfo : $self->dbUserGet(uid => $uid)->[0];
+ return 404 if !$u->{id};
+
+ # check POST data
+ my $frm;
+ if($self->reqMethod eq 'POST') {
+ $frm = $self->formValidate(
+ $self->authCan('usermod') ? (
+ { name => 'usrname', template => 'pname', minlength => 2, maxlength => 15 },
+ { name => 'rank', enum => [ 1..$#{$self->{user_ranks}} ] },
+ ) : (),
+ { name => 'mail', template => 'mail' },
+ { name => 'usrpass', required => 0, minlength => 4, maxlength => 15, template => 'asciiprint' },
+ { name => 'usrpass2', required => 0, minlength => 4, maxlength => 15, template => 'asciiprint' },
+ { name => 'flags_list', required => 0, default => 0 },
+ { name => 'flags_nsfw', required => 0, default => 0 },
+ );
+ push @{$frm->{_err}}, 'passmatch' if ($frm->{usrpass} || $frm->{usrpass2}) && $frm->{usrpass} ne $frm->{usrpass2};
+ if(!$frm->{_err}) {
+ my %o;
+ $o{username} = $frm->{usrname} if $frm->{usrname};
+ $o{rank} = $frm->{rank} if $frm->{rank};
+ $o{mail} = $frm->{mail};
+ $o{passwd} = md5_hex($frm->{usrpass}) if $frm->{usrpass};
+ $o{show_list} = $frm->{flags_list} ? 1 : 0;
+ $o{show_nsfw} = $frm->{flags_nsfw} ? 1 : 0;
+ $self->dbUserEdit($uid, %o);
+ return $self->resRedirect("/u$uid/edit?d=1", 'post') if $uid != $self->authInfo->{id} || !$frm->{usrpass};
+ return $self->authLogin($frm->{usrname}||$u->{username}, $frm->{usrpass}, "/u$uid/edit?d=1");
+ }
+ }
+
+ # fill out default values
+ $frm->{usrname} ||= $u->{username};
+ $frm->{rank} ||= $u->{rank};
+ $frm->{mail} ||= $u->{mail};
+ $frm->{flags_list} = $u->{show_list} if !defined $frm->{flags_list};
+ $frm->{flags_nsfw} = $u->{show_nsfw} if !defined $frm->{flags_nsfw};
+
+ # create the page
+ my $title = $self->authInfo->{id} != $uid ? "Edit $u->{username}'s Account" : 'My Account';
+ $self->htmlHeader(title => $title, noindex => 1);
+ $self->htmlMainTabs('u', $u, 'edit');
+ if($self->reqParam('d')) {
+ div class => 'mainbox';
+ h1 'Settings saved';
+ div class => 'notice';
+ p 'Settings successfully saved.';
+ end;
+ end
+ }
+ $self->htmlForm({ frm => $frm, action => "/u$uid/edit" }, $title => [
+ [ part => title => 'General Info' ],
+ $self->authCan('usermod') ? (
+ [ input => short => 'usrname', name => 'Username' ],
+ [ select => short => 'rank', name => 'Rank', options => [
+ map [ $_, $self->{user_ranks}[$_][0] ], 1..$#{$self->{user_ranks}} ] ],
+ ) : (
+ [ static => label => 'Username', content => $frm->{usrname} ],
+ ),
+ [ input => short => 'mail', name => 'Email' ],
+
+ [ part => title => 'Change Password' ],
+ [ static => content => 'Leave blank to keep your current password' ],
+ [ input => short => 'usrpass', name => 'Password' ],
+ [ passwd => short => 'usrpass2', name => 'Confirm pass.' ],
+
+ [ part => title => 'Options' ],
+ [ check => short => 'flags_list', name =>
+ qq|Allow other people to see my visual novel list (<a href="/u$uid/list">/u$uid/list</a>) |.
+ qq|and wishlist (<a href="/u$uid/wish">/u$uid/wish</a>)| ],
+ [ check => short => 'flags_nsfw', name => 'Disable warnings for images that are not safe for work.' ],
+ ]);
+ $self->htmlFooter;
+}
+
+
+sub delete {
+ my($self, $uid, $act) = @_;
+ return $self->htmlDenied if !$self->authCan('usermod');
+
+ # confirm
+ if(!$act) {
+ my $u = $self->dbUserGet(uid => $uid)->[0];
+ return 404 if !$u->{id};
+ $self->htmlHeader(title => 'Delete user', noindex => 1);
+ $self->htmlMainTabs('u', $u, 'del');
+ div class => 'mainbox';
+ div class => 'warning';
+ h2 'Delete user';
+ p;
+ lit qq|Are you sure you want to remove <a href="/u$uid">$u->{username}</a>'s account?<br /><br />|
+ .qq|<a href="/u$uid/del/o">Yes, I'm not kidding!</a>|;
+ end;
+ end;
+ end;
+ $self->htmlFooter;
+ }
+ # delete
+ elsif($act eq '/o') {
+ $self->dbUserDel($uid);
+ $self->resRedirect("/u$uid/del/d", 'post');
+ }
+ # done
+ elsif($act eq '/d') {
+ $self->htmlHeader(title => 'Delete user', noindex => 1);
+ div class => 'mainbox';
+ div class => 'notice';
+ p 'User deleted.';
+ end;
+ end;
+ $self->htmlFooter;
+ }
+}
+
+
+sub list {
+ my($self, $char) = @_;
+
+ my $f = $self->formValidate(
+ { name => 's', required => 0, default => 'username', enum => [ qw|username registered votes changes| ] },
+ { name => 'o', required => 0, default => 'a', enum => [ 'a','d' ] },
+ { name => 'p', required => 0, default => 1, template => 'int' },
+ );
+ return 404 if $f->{_err};
+
+ $self->htmlHeader(title => 'Browse users');
+
+ div class => 'mainbox';
+ h1 'Browse users';
+ p class => 'browseopts';
+ for ('all', 'a'..'z', 0) {
+ a href => "/u/$_", $_ eq $char ? (class => 'optselected') : (), $_ ? uc $_ : '#';
+ }
+ end;
+ end;
+
+ my($list, $np) = $self->dbUserGet(
+ order => ($f->{s} eq 'changes' ? 'c_' : $f->{s} eq 'votes' ? 'NOT show_list, c_' : '').$f->{s}.($f->{o} eq 'a' ? ' ASC' : ' DESC'),
+ $char ne 'all' ? (
+ firstchar => $char ) : (),
+ results => 50,
+ page => $f->{p},
+ );
+
+ $self->htmlBrowse(
+ items => $list,
+ options => $f,
+ nextpage => $np,
+ pageurl => "/u/$char?o=$f->{o};s=$f->{s}",
+ sorturl => "/u/$char",
+ header => [
+ [ 'Username', 'username' ],
+ [ 'Registered', 'registered' ],
+ [ 'Votes', 'votes' ],
+ [ 'Edits', 'changes' ],
+ ],
+ row => sub {
+ my($s, $n, $l) = @_;
+ Tr $n % 2 ? (class => 'odd') : ();
+ td class => 'tc1';
+ a href => '/u'.$l->{id}, $l->{username};
+ end;
+ td class => 'tc2', date $l->{registered};
+ td class => 'tc3';
+ lit !$l->{show_list} ? '-' : !$l->{c_votes} ? 0 :
+ qq|<a href="/u$l->{id}/list">$l->{c_votes}</a>|;
+ end;
+ td class => 'tc4';
+ lit !$l->{c_changes} ? 0 : qq|<a href="/u$l->{id}/hist">$l->{c_changes}</a>|;
+ end;
+ end;
+ },
+ );
+ $self->htmlFooter;
+}
+
+
+1;
+
+
+# Contents of the password-reset email
+__DATA__
+Hello %s,
+
+Your password has been reset, you can now login at http://vndb.org/ with the
+following information:
+
+Username: %1$s
+Password: %s
+
+Now don't forget your password again! :-)
+
+vndb.org
diff --git a/lib/VNDB/Handler/VNBrowse.pm b/lib/VNDB/Handler/VNBrowse.pm
new file mode 100644
index 00000000..8af5791e
--- /dev/null
+++ b/lib/VNDB/Handler/VNBrowse.pm
@@ -0,0 +1,183 @@
+
+package VNDB::Handler::VNBrowse;
+
+use strict;
+use warnings;
+use YAWF ':html';
+use VNDB::Func;
+
+
+YAWF::register(
+ qr{v/([a-z0]|all)} => \&list,
+);
+
+
+sub list {
+ my($self, $char) = @_;
+
+ my $f = $self->formValidate(
+ { name => 's', required => 0, default => 'title', enum => [ qw|title rel| ] },
+ { name => 'o', required => 0, default => 'a', enum => [ 'a','d' ] },
+ { name => 'p', required => 0, default => 1, template => 'int' },
+ { name => 'q', required => 0, default => '' },
+ { name => 'sq', required => 0, default => '' },
+ );
+ return 404 if $f->{_err};
+ $f->{q} ||= $f->{sq};
+
+ # NOTE: this entire search thingy can also be done using a PgSQL fulltext search,
+ # which is faster and requires less code. It does require an extra database
+ # column, index and some triggers, though
+
+ my(@cati, @cate, @plat, @lang);
+ my $q = $f->{q};
+ if($q) {
+ # VNDBID
+ return $self->resRedirect('/'.$1.$2.(!$3 ? '' : $1 eq 'd' ? '#'.$3 : '.'.$3), 'temp')
+ if $q =~ /^([vrptud])([0-9]+)(?:\.([0-9]+))?$/;
+
+ if(!($q =~ s/^title://)) {
+ # categories
+ my %catl = map {
+ my $ic = $_;
+ map { $ic.$_ => $self->{categories}{$ic}[1]{$_} } keys %{$self->{categories}{$ic}[1]}
+ } keys %{$self->{categories}};
+
+ $q =~ s/-(?:$catl{$_}|c:$_)//ig && push @cate, $_ for keys %catl;
+ $q =~ s/(?:$catl{$_}|c:$_)//ig && push @cati, $_ for keys %catl;
+
+ # platforms
+ $_ ne 'oth' && $q =~ s/(?:$self->{platforms}{$_}|p:$_)//ig && push @plat, $_ for keys %{$self->{platforms}};
+
+ # languages
+ $q =~ s/($self->{languages}{$_}|l:$_)//ig && push @lang, $_ for keys %{$self->{languages}};
+ }
+ }
+ $q =~ s/ +$//;
+ $q =~ s/^ +//;
+
+ my($list, $np) = $self->dbVNGet(
+ $char ne 'all' ? ( char => $char ) : (),
+ $q ? ( search => $q ) : (),
+ results => 50,
+ page => $f->{p},
+ order => ($f->{s} eq 'rel' ? 'c_released' : 'title').($f->{o} eq 'a' ? ' ASC' : ' DESC'),
+ @cati ? ( cati => \@cati ) : (),
+ @cate ? ( cate => \@cate ) : (),
+ @lang ? ( lang => \@lang ) : (),
+ @plat ? ( platform => \@plat ) : (),
+ );
+
+ $self->resRedirect('/v'.$list->[0]{id}, 'temp')
+ if $q && @$list == 1;
+
+ $self->htmlHeader(title => 'Browse visual novels', search => $f->{q});
+ _filters($self, $f, $char);
+ $self->htmlBrowse(
+ class => 'vnbrowse',
+ items => $list,
+ options => $f,
+ nextpage => $np,
+ pageurl => "/v/$char?o=$f->{o};s=$f->{s};q=$f->{q}",
+ sorturl => "/v/$char?q=$f->{q}",
+ header => [
+ [ 'Title', 'title' ],
+ [ '', 0 ],
+ [ '', 0 ],
+ [ 'Released', 'rel' ],
+ ],
+ row => sub {
+ my($s, $n, $l) = @_;
+ Tr $n % 2 ? (class => 'odd') : ();
+ td class => 'tc1';
+ a href => '/v'.$l->{id}, title => $l->{original}||$l->{title}, shorten $l->{title}, 100;
+ end;
+ td class => 'tc2';
+ $_ ne 'oth' && cssicon $_, $self->{platforms}{$_}
+ for (sort split /\//, $l->{c_platforms});
+ end;
+ td class => 'tc3';
+ cssicon "lang $_", $self->{languages}{$_}
+ for (reverse sort split /\//, $l->{c_languages});
+ end;
+ td class => 'tc4';
+ lit monthstr $l->{c_released};
+ end;
+ end;
+ },
+ );
+ $self->htmlFooter;
+}
+
+
+sub _filters {
+ my($self, $f, $char) = @_;
+
+ div class => 'mainbox';
+ h1 'Browse visual novels';
+ form class => 'search', action => '/v/all', 'accept-charset' => 'UTF-8', method => 'get';
+ fieldset;
+ input type => 'text', name => 'q', id => 'q', class => 'text', value => $f->{q};
+ input type => 'submit', class => 'submit', value => 'Search!';
+ end;
+ end;
+ p class => 'browseopts';
+ for ('all', 'a'..'z', 0) {
+ a href => "/v/$_", $_ eq $char ? (class => 'optselected') : (), $_ ? uc $_ : '#';
+ }
+ end;
+ a id => 'advselect', href => '#';
+ lit '<i>&#9656;</i> advanced search';
+ end;
+ div id => 'advoptions', class => 'hidden';
+
+ h2;
+ lit 'Categories <b>(boolean and, selecting more gives less results. The categories are explained on <a href="/d1">this page</a>)</b>';
+ end;
+ ul id => 'catselect';
+ for my $c (qw| e g t p h l s |) {
+ $c !~ /[thl]/ ? li : br;
+ txt $self->{categories}{$c}[0];
+ ul;
+ li id => "cat_$c$_", $self->{categories}{$c}[1]{$_}
+ for (sort keys %{$self->{categories}{$c}[1]});
+ end;
+ end if $c !~ /[gph]/;
+ }
+ end;
+
+ h2;
+ lit 'Languages <b>(boolean or, selecting more gives more results)</b>';
+ end;
+ for(sort @{$self->dbLanguages}) {
+ span;
+ input type => 'checkbox', id => "lang_$_";
+ label for => "lang_$_";
+ cssicon "lang $_", $self->{languages}{$_};
+ txt $self->{languages}{$_};
+ end;
+ end;
+ }
+
+ h2;
+ lit 'Platforms <b>(boolean or, selecting more gives more results)</b>';
+ end;
+ for(sort keys %{$self->{platforms}}) {
+ next if $_ eq 'oth';
+ span;
+ input type => 'checkbox', id => "plat_$_";
+ label for => "plat_$_";
+ cssicon $_, $self->{platforms}{$_};
+ txt $self->{platforms}{$_};
+ end;
+ end;
+ }
+
+ clearfloat;
+ end;
+ end;
+}
+
+
+1;
+
diff --git a/lib/VNDB/Handler/VNEdit.pm b/lib/VNDB/Handler/VNEdit.pm
new file mode 100644
index 00000000..14859e3a
--- /dev/null
+++ b/lib/VNDB/Handler/VNEdit.pm
@@ -0,0 +1,413 @@
+
+package VNDB::Handler::VNEdit;
+
+use strict;
+use warnings;
+use YAWF ':html', ':xml';
+
+
+YAWF::register(
+ qr{v(?:([1-9]\d*)(?:\.([1-9]\d*))?/edit|/new)}
+ => \&edit,
+ qr{xml/vn\.xml} => \&vnxml,
+ qr{xml/screenshots\.xml} => \&scrxml,
+);
+
+
+sub edit {
+ my($self, $vid, $rev) = @_;
+
+ my $v = $vid && $self->dbVNGet(id => $vid, what => 'extended screenshots relations anime categories changes', $rev ? (rev => $rev) : ())->[0];
+ return 404 if $vid && !$v->{id};
+ $rev = undef if !$vid || $v->{cid} == $v->{latest};
+
+ return $self->htmlDenied if !$self->authCan('edit')
+ || $vid && ($v->{locked} && !$self->authCan('lock') || $v->{hidden} && !$self->authCan('del'));
+
+ my %b4 = !$vid ? () : (
+ (map { $_ => $v->{$_} } qw|title original desc alias length l_wp l_encubed l_renai l_vnn img_nsfw|),
+ anime => join(' ', sort { $a <=> $b } map $_->{id}, @{$v->{anime}}),
+ categories => join(',', map $_->[0].$_->[1], sort { $a->[0] cmp $b->[0] } @{$v->{categories}}),
+ relations => join('|||', map $_->{relation}.','.$_->{id}.','.$_->{title}, sort { $a->{id} <=> $b->{id} } @{$v->{relations}}),
+ screenshots => join(' ', map sprintf('%d,%d,%d', $_->{id}, $_->{nsfw}?1:0, $_->{rid}), @{$v->{screenshots}}),
+ );
+
+ my $frm;
+ if($self->reqMethod eq 'POST') {
+ $frm = $self->formValidate(
+ { name => 'title', maxlength => 250 },
+ { name => 'original', required => 0, maxlength => 250, default => '' },
+ { name => 'alias', required => 0, maxlength => 500, default => '' },
+ { name => 'desc', maxlength => 10240 },
+ { name => 'length', required => 0, default => 0, enum => [ 0..$#{$self->{vn_lengths}} ] },
+ { name => 'l_wp', required => 0, default => '', maxlength => 150 },
+ { name => 'l_encubed', required => 0, default => '', maxlength => 100 },
+ { name => 'l_renai', required => 0, default => '', maxlength => 100 },
+ { name => 'l_vnn', required => 0, default => 0, template => 'int' },
+ { name => 'anime', required => 0, default => '' },
+ { name => 'categories', required => 0, default => '', maxlength => 1000 },
+ { name => 'img_nsfw', required => 0, default => 0 },
+ { name => 'relations', required => 0, default => '', maxlength => 5000 },
+ { name => 'screenshots', required => 0, default => '', maxlength => 1000 },
+ { name => 'editsum', maxlength => 5000 },
+ );
+
+ # handle image upload
+ my $image = _uploadimage($self, $v, $frm);
+
+ if(!$frm->{_err}) {
+ # parse and re-sort fields that have multiple representations of the same information
+ my $anime = [ grep /^[0-9]+$/, split /[ ,]+/, $frm->{anime} ];
+ my $categories = [ map { [ substr($_,0,3), substr($_,3,1) ] } split /,/, $frm->{categories} ];
+ my $relations = [ map { /^([0-9]+),([0-9]+),(.+)$/ && $2 != $vid ? [ $1, $2, $3 ] : () } split /\|\|\|/, $frm->{relations} ];
+ my $screenshots = [ map /^[0-9]+,[01],[0-9]+$/ ? [split /,/] : (), split / +/, $frm->{screenshots} ];
+
+ $frm->{anime} = join ' ', sort { $a <=> $b } @$anime;
+ $frm->{relations} = join '|||', map $_->[0].','.$_->[1].','.$_->[2], sort { $a->[1] <=> $b->[1]} @{$relations};
+ $frm->{img_nsfw} = $frm->{img_nsfw} ? 1 : 0;
+ $frm->{screenshots} = join ' ', map sprintf('%d,%d,%d', $_->[0], $_->[1]?1:0, $_->[2]), sort { $a->[0] <=> $b->[0] } @$screenshots;
+
+ # nothing changed? just redirect
+ return $self->resRedirect("/v$vid", 'post')
+ if $vid && !$self->reqUploadFileName('img') && !grep $frm->{$_} ne $b4{$_}, keys %b4;
+
+ # execute the edit/add
+ my %args = (
+ (map { $_ => $frm->{$_} } qw|title original alias desc length l_wp l_encubed l_renai l_vnn editsum img_nsfw|),
+ anime => $anime,
+ categories => $categories,
+ relations => $relations,
+ image => $image,
+ screenshots => $screenshots,
+ );
+
+ my($nvid, $nrev, $cid) = ($vid, 1);
+ ($nrev, $cid) = $self->dbVNEdit($vid, %args) if $vid;
+ ($nvid, $cid) = $self->dbVNAdd(%args) if !$vid;
+
+ # update reverse relations & relation graph
+ if(!$vid && $#$relations >= 0 || $vid && $frm->{relations} ne $b4{relations}) {
+ my %old = $vid ? (map { $_->{id} => $_->{relation} } @{$v->{relations}}) : ();
+ my %new = map { $_->[1] => $_->[0] } @$relations;
+ _updreverse($self, \%old, \%new, $nvid, $cid, $nrev);
+ } elsif($vid && @$relations && $frm->{title} ne $b4{title}) {
+ $self->multiCmd("relgraph $vid");
+ }
+
+ $self->multiCmd("ircnotify v$nvid.$nrev");
+ $self->multiCmd('anime') if $vid && $frm->{anime} ne $b4{anime} || !$vid && $frm->{anime};
+
+ return $self->resRedirect("/v$nvid.$nrev", 'post');
+ }
+ }
+
+ !exists $frm->{$_} && ($frm->{$_} = $b4{$_}) for (keys %b4);
+ $frm->{editsum} = sprintf 'Reverted to revision v%d.%d', $vid, $rev if $rev && !defined $frm->{editsum};
+
+ $self->htmlHeader(js => 'forms', title => $vid ? "Edit $v->{title}" : 'Add a new visual novel', noindex => 1);
+ $self->htmlMainTabs('v', $v, 'edit') if $vid;
+ $self->htmlEditMessage('v', $v);
+ _form($self, $v, $frm);
+ $self->htmlFooter;
+}
+
+
+sub _uploadimage {
+ my($self, $v, $frm) = @_;
+ return $v ? $v->{image} : 0 if $frm->{_err} || !$self->reqUploadFileName('img');
+
+ # save to temporary location
+ my $tmp = sprintf '%s/static/cv/00/tmp.%d.jpg', $VNDB::ROOT, $$*int(rand(1000)+1);
+ $self->reqSaveUpload('img', $tmp);
+
+ # perform some checks
+ my $l;
+ open(my $T, '<:raw:bytes', $tmp) || die $1;
+ read $T, $l, 2;
+ close($T);
+
+ $frm->{_err} = [ 'noimage' ] if $l ne pack('H*', 'ffd8') && $l ne pack('H*', '8950');
+ $frm->{_err} = [ 'toolarge' ] if -s $tmp > 512*1024;
+
+ if($frm->{_err}) {
+ unlink $tmp;
+ return undef;
+ }
+
+ # store the file and let multi handle it
+ my $imgid = $self->dbVNImageId;
+ my $new = sprintf '%s/static/cv/%02d/%d.jpg', $VNDB::ROOT, $imgid%100, $imgid;
+ rename $tmp, $new or die $!;
+ chmod 0666, $new;
+ $self->multiCmd("coverimage $imgid");
+
+ return -1*$imgid;
+}
+
+
+sub _form {
+ my($self, $v, $frm) = @_;
+ my $r = $v ? $self->dbReleaseGet(vid => $v->{id}) : [];
+ $self->htmlForm({ frm => $frm, action => $v ? "/v$v->{id}/edit" : '/v/new', editsum => 1, upload => 1 },
+ 'General info' => [
+ [ input => short => 'title', name => 'Title (romaji)' ],
+ [ input => short => 'original', name => 'Original title' ],
+ [ static => content => 'The original title of this visual novel, leave blank if it already is in the Latin alphabet.' ],
+ [ textarea => short => 'alias', name => 'Aliases', rows => 4 ],
+ [ static => content => q|
+ Comma seperated list of alternative titles or abbreviations. Can include both official
+ (japanese/english) titles and unofficial titles used around net.<br />
+ <b>Titles that are listed in the releases do not have to be added here.</b>
+ |],
+ [ textarea => short => 'desc', name => 'Description', rows => 10 ],
+ [ static => content => q|
+ Short description of the main story. Please do not include spoilers, and don't forget to list
+ the source in case you didn't write the description yourself. (formatting codes are allowed)
+ |],
+ [ select => short => 'length', name => 'Length', width => 300, options =>
+ [ map [ $_ => $self->{vn_lengths}[$_][0].($_ ? " ($self->{vn_lengths}[$_][2])" : '') ], 0..$#{$self->{vn_lengths}} ] ],
+
+ [ input => short => 'l_wp', name => 'External links', pre => 'http://en.wikipedia.org/wiki/' ],
+ [ input => short => 'l_encubed', pre => 'http://novelnews.net/tag/', post => '/' ],
+ [ input => short => 'l_renai', pre => 'http://renai.us/game/', post => '.shtml' ],
+ [ input => short => 'l_vnn', pre => 'http://visual-novels.net/vn/index.php?option=com_content&amp;task=view&amp;id=', width => 40 ],
+
+ [ input => short => 'anime', name => 'Anime' ],
+ [ static => content => q|
+ Whitespace seperated list of <a href="http://anidb.net/">AniDB</a> anime IDs.
+ E.g. "1015 3348" will add <a href="http://anidb.net/a1015">Shingetsutan Tsukihime</a>
+ and <a href="http://anidb.net/a3348">Fate/stay night</a> as related anime.<br />
+ <b>Note:</b> It can take a few minutes for the anime titles to appear on the VN page.
+ |],
+ ],
+
+ 'Categories' => [
+ [ hidden => short => 'categories' ],
+ [ static => nolabel => 1, content => sub {
+ lit 'Please read the <a href="/d1">category descriptions</a> before modifying categories!<br /><br />';
+ ul;
+ for my $c (qw| e g t p h l s |) {
+ $c !~ /[thl]/ ? li : br;
+ txt $self->{categories}{$c}[0];
+ a href => "/d1#$self->{categories}{$c}[2]", class => 'help', '?';
+ ul;
+ for (sort keys %{$self->{categories}{$c}[1]}) {
+ li;
+ a href => "#", id => "cat_$c$_";
+ b id => "b_$c$_", '-';
+ txt ' '.$self->{categories}{$c}[1]{$_};
+ end;
+ end;
+ }
+ end;
+ end if $c !~ /[gph]/;
+ }
+ end;
+ }],
+ ],
+
+ 'Image' => [
+ [ static => nolabel => 1, content => sub {
+ div class => 'img';
+ p 'No image uploaded yet' if !$v || !$v->{image};
+ p '[processing image, please return in a few minutes]' if $v && $v->{image} < 0;
+ img src => sprintf("%s/cv/%02d/%d.jpg", $self->{url_static}, $v->{image}%100, $v->{image}), alt => $v->{title} if $v && $v->{image} > 0;
+ end;
+ div;
+
+ h2 'Upload new image';
+ input type => 'file', class => 'text', name => 'img', id => 'img';
+ p 'Preferably the cover of the CD/DVD/package. Image must be in JPEG or PNG format'
+ ." and at most 500kB. Images larger than 256x400 will automatically be resized.\n\n\n";
+
+ h2 'NSFW';
+ input type => 'checkbox', class => 'checkbox', id => 'img_nsfw', name => 'img_nsfw',
+ $frm->{img_nsfw} ? (checked => 'checked') : ();
+ label class => 'checkbox', for => 'img_nsfw', "Not Safe For Work.\n";
+ p 'Please check this option if the image contains nudity, gore, or is otherwise not safe in a work-friendly environment.';
+ end;
+ }],
+ ],
+
+ 'Relations' => [
+ [ hidden => short => 'relations' ],
+ [ static => nolabel => 1, content => sub {
+ h2 'Selected relations';
+ table;
+ tbody id => 'relation_tbl';
+ # to be filled using javascript
+ end;
+ end;
+
+ h2 'Add relation';
+ table;
+ Tr id => 'relation_new';
+ td class => 'tc1';
+ input type => 'text', class => 'text';
+ end;
+ td class => 'tc2';
+ txt ' is a ';
+ Select;
+ option value => $_, $self->{vn_relations}[$_][0] for (0..$#{$self->{vn_relations}});
+ end;
+ txt ' of';
+ end;
+ td class => 'tc3', $v ? $v->{title} : '';
+ td class => 'tc4';
+ a href => '#', 'add';
+ end;
+ end;
+ end;
+ }],
+ ],
+
+ !@$r ? () : ( 'Screenshots' => [
+ [ hidden => short => 'screenshots' ],
+ [ static => nolabel => 1, content => sub {
+ div class => 'warning';
+ b 'Please keep the following in mind when uploading screenshots:';
+ ul;
+ li 'Screenshots have to be in the native resolution of the game,';
+ li 'Remove any window borders and make sure the image is unmarked,';
+ li 'Don\'t only upload event CGs.';
+ end;
+ lit 'Please read the <a href="/d2#6">guidelines</a> for more information.';
+ br;
+ b 'Make sure to submit the form after the upload has finished!';
+ end;
+ br;
+ table;
+ tbody id => 'scr_table', '';
+ end;
+ Select id => 'scr_rel', class => $self->{url_static};
+ option value => $_->{id}, sprintf '[%s] %s (r%d)', $_->{language}, $_->{title}, $_->{id} for (@$r);
+ end;
+ }],
+ ])
+
+ );
+}
+
+
+# Update reverse relations and regenerate relation graph
+# Arguments: %old. %new, vid, cid, rev
+# %old,%new -> { vid2 => relation, .. }
+# from the perspective of vid
+# cid, rev are of the related edit
+# !IMPORTANT!: Don't forget to update this function when
+# adding/removing fields to/from VN entries!
+sub _updreverse {
+ my($self, $old, $new, $vid, $cid, $rev) = @_;
+ my %upd;
+
+ # compare %old and %new
+ for (keys %$old, keys %$new) {
+ if(exists $$old{$_} and !exists $$new{$_}) {
+ $upd{$_} = -1;
+ } elsif((!exists $$old{$_} and exists $$new{$_}) || ($$old{$_} != $$new{$_})) {
+ $upd{$_} = $$new{$_};
+ if ($self->{vn_relations}[$upd{$_} ][1]) { $upd{$_}-- }
+ elsif($self->{vn_relations}[$upd{$_}+1][1]) { $upd{$_}++ }
+ }
+ }
+
+ return if !keys %upd;
+
+ # edit all related VNs
+ for my $i (keys %upd) {
+ my $r = $self->dbVNGet(id => $i, what => 'extended relations categories anime screenshots')->[0];
+ my @newrel = map $_->{id} != $vid ? [ $_->{relation}, $_->{id} ] : (), @{$r->{relations}};
+ push @newrel, [ $upd{$i}, $vid ] if $upd{$i} != -1;
+ $self->dbVNEdit($i,
+ relations => \@newrel,
+ editsum => "Reverse relation update caused by revision v$vid.$rev",
+ causedby => $cid,
+ uid => 1, # Multi - hardcoded
+ anime => [ map $_->{id}, @{$r->{anime}} ],
+ screenshots => [ map [ $_->{id}, $_->{nsfw}, $_->{rid} ], @{$r->{screenshots}} ],
+ ( map { $_ => $r->{$_} } qw| title original desc alias categories img_nsfw length l_wp l_encubed l_renai l_vnn image | )
+ );
+ }
+
+ $self->multiCmd('relgraph '.join(' ', $vid, keys %upd));
+}
+
+
+# peforms a (simple) search and returns the results in XML format
+sub vnxml {
+ my $self = shift;
+
+ my $q = $self->formValidate({ name => 'q', maxlength => 500 });
+ return 404 if $q->{_err};
+ $q = $q->{q};
+
+ my($list, $np) = $self->dbVNGet(
+ $q =~ /^v([1-9]\d*)/ ? (id => $1) : (search => $q),
+ results => 10,
+ page => 1,
+ );
+
+ $self->resHeader('Content-type' => 'text/xml; charset=UTF-8');
+ xml;
+ tag 'vns', more => $np ? 'yes' : 'no', query => $q;
+ for(@$list) {
+ tag 'item', id => $_->{id}, $_->{title};
+ }
+ end;
+}
+
+
+# handles uploading screenshots and fetching information about them
+sub scrxml {
+ my $self = shift;
+ return $self->htmlDenied if !$self->authCan('edit');
+ $self->resHeader('Content-type' => 'text/xml; charset=UTF-8');
+
+ # fetch information about screenshots
+ if($self->reqMethod ne 'POST') {
+ my $ids = $self->formValidate(
+ { name => 'id', required => 1, template => 'int', multi => 1 }
+ );
+ return 404 if $ids->{_err};
+ my $r = $self->dbScreenshotGet($ids->{id});
+
+ xml;
+ tag 'screenshots';
+ tag 'item', %$_, undef for (@$r);
+ end;
+ return;
+ }
+
+ # upload new screenshot
+ my $tmp = sprintf '%s/static/sf/00/tmp.%d.jpg', $VNDB::ROOT, $$*int(rand(1000)+1);
+ $self->reqSaveUpload('scr_upload', $tmp);
+
+ my $id = 0;
+ $id = -2 if !-s $tmp;
+ if(!$id) {
+ my $l;
+ open(my $T, '<:raw:bytes', $tmp) || die $1;
+ read $T, $l, 2;
+ close($T);
+ $id = -1 if $l ne pack('H*', 'ffd8') && $l ne pack('H*', '8950');
+ }
+
+ if($id) {
+ unlink $tmp;
+ } else {
+ $id = $self->dbScreenshotAdd;
+ my $new = sprintf '%s/static/sf/%02d/%d.jpg', $VNDB::ROOT, $id%100, $id;
+ rename $tmp, $new or die $!;
+ chmod 0666, $new;
+ $self->multiCmd('screenshot');
+ }
+
+ xml;
+ # blank stylesheet because some browsers don't allow JS access otherwise
+ lit qq|<?xml-stylesheet href="$self->{url_static}/f/blank.css" type="text/css" ?>|;
+ tag 'image', id => $id, undef;
+}
+
+
+1;
+
diff --git a/lib/VNDB/Handler/VNPage.pm b/lib/VNDB/Handler/VNPage.pm
new file mode 100644
index 00000000..35a9ac8c
--- /dev/null
+++ b/lib/VNDB/Handler/VNPage.pm
@@ -0,0 +1,484 @@
+
+package VNDB::Handler::VNPage;
+
+use strict;
+use warnings;
+use YAWF ':html', 'xml_escape';
+use VNDB::Func;
+
+
+YAWF::register(
+ qr{v([1-9]\d*)/rg} => \&rg,
+ qr{v([1-9]\d*)(?:\.([1-9]\d*))?} => \&page,
+);
+
+
+sub rg {
+ my($self, $vid) = @_;
+
+ my $v = $self->dbVNGet(id => $vid, what => 'relgraph')->[0];
+ return 404 if !$v->{id} || !$v->{rgraph};
+
+ $self->htmlHeader(title => 'Relation graph for '.$v->{title});
+ $self->htmlMainTabs('v', $v, 'rg');
+ div class => 'mainbox';
+ h1 'Relation graph for '.$v->{title};
+ lit $v->{cmap};
+ p class => 'center';
+ img src => sprintf('%s/rg/%02d/%d.png', $self->{url_static}, $v->{rgraph}%100, $v->{rgraph}),
+ alt => 'Relation graph for '.$v->{title}, usemap => '#rgraph';
+ end;
+ end;
+}
+
+
+sub page {
+ my($self, $vid, $rev) = @_;
+
+ my $v = $self->dbVNGet(
+ id => $vid,
+ what => 'extended categories anime relations screenshots'.($rev ? ' changes' : ''),
+ $rev ? (rev => $rev) : (),
+ )->[0];
+ return 404 if !$v->{id};
+
+ my $r = $self->dbReleaseGet(vid => $vid, what => 'producers platforms');
+
+ $self->htmlHeader(title => $v->{title}, noindex => $rev);
+ $self->htmlMainTabs('v', $v);
+ return if $self->htmlHiddenMessage('v', $v);
+
+ _revision($self, $v, $rev);
+
+ div class => 'mainbox';
+ $self->htmlItemMessage('v', $v);
+ h1 $v->{title};
+ h2 class => 'alttitle', $v->{original} if $v->{original};
+
+ div class => 'vndetails';
+
+ # image
+ div class => 'vnimg';
+ if(!$v->{image}) {
+ p 'No image uploaded yet';
+ } elsif($v->{image} < 0) {
+ p '[processing image, please return in a few minutes]';
+ } elsif($v->{img_nsfw} && !$self->authInfo->{show_nsfw}) {
+ img id => 'nsfw_hid', src => sprintf("%s/cv/%02d/%d.jpg", $self->{url_static}, $v->{image}%100, $v->{image}), alt => $v->{title};
+ p id => 'nsfw_show';
+ txt "This image has been flagged\nas Not Safe For Work.\n\n";
+ a href => '#', 'Show me anyway';
+ txt "\n\n(This warning can be disabled in your account)";
+ end;
+ } else {
+ img src => sprintf("%s/cv/%02d/%d.jpg", $self->{url_static}, $v->{image}%100, $v->{image}), alt => $v->{title};
+ i 'Flagged as NSFW' if $v->{img_nsfw} && $self->authInfo->{show_nsfw};
+ }
+ end;
+
+ # general info
+ table;
+ Tr;
+ td class => 'key', ' ';
+ td ' ';
+ end;
+ my $i = 0;
+ if($v->{length}) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Length';
+ td "$self->{vn_lengths}[$v->{length}][0] ($self->{vn_lengths}[$v->{length}][1])";
+ end;
+ }
+ if($v->{alias}) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Aliases';
+ td $v->{alias};
+ end;
+ }
+ my @links = (
+ $v->{l_wp} ? [ 'Wikipedia', 'http://en.wikipedia.org/wiki/%s', $v->{l_wp} ] : (),
+ $v->{l_encubed} ? [ 'Encubed', 'http://novelnews.net/tag/%s/', $v->{l_encubed} ] : (),
+ $v->{l_renai} ? [ 'Renai.us', 'http://renai.us/game/%s.shtml', $v->{l_renai} ] : (),
+ $v->{l_vnn} ? [ 'V-N.net', 'http://visual-novels.net/vn/index.php?option=com_content&task=view&id=%d', $v->{l_vnn} ] : (),
+ );
+ if(@links) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'Links';
+ td;
+ for(@links) {
+ a href => sprintf($_->[1], $_->[2]), $_->[0];
+ txt ', ' if $_ ne $links[$#links];
+ }
+ end;
+ end;
+ }
+
+ _producers($self, \$i, $r);
+ _categories($self, \$i, $v) if @{$v->{categories}};
+ _relations($self, \$i, $v) if @{$v->{relations}};
+ _anime($self, \$i, $v) if @{$v->{anime}};
+
+ # User options
+ if($self->authInfo->{id}) {
+ my $vote = $self->dbVoteGet(uid => $self->authInfo->{id}, vid => $v->{id})->[0];
+ my $wish = $self->dbWishListGet(uid => $self->authInfo->{id}, vid => $v->{id})->[0];
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td 'User options';
+ td;
+ Select id => 'votesel';
+ option $vote ? "your vote: $vote->{vote}" : 'not voted yet';
+ optgroup label => $vote ? 'Change vote' : 'Vote';
+ option value => $_, "$_ ($self->{votes}[$_-1])" for (reverse 1..10);
+ end;
+ option value => -1, 'revoke' if $vote;
+ end;
+ if(!$vote || $wish) {
+ br;
+ Select id => 'wishsel';
+ option $wish ? "wishlist: $self->{wishlist_status}[$wish->{wstat}]" : 'not on your wishlist';
+ optgroup label => $wish ? 'Change status' : 'Add to wishlist';
+ option value => $_, $self->{wishlist_status}[$_] for (0..$#{$self->{wishlist_status}});
+ end;
+ option value => -1, 'remove from wishlist';
+ end;
+ }
+ end;
+ end;
+ }
+
+ end;
+ end;
+
+ # description
+ div class => 'vndescription';
+ h2 'Description';
+ p;
+ lit bb2html $v->{desc};
+ end;
+ end;
+ end;
+
+ _releases($self, $v, $r);
+ _stats($self, $v);
+ _screenshots($self, $v, $r) if @{$v->{screenshots}};
+
+ $self->htmlFooter;
+}
+
+
+sub _revision {
+ my($self, $v, $rev) = @_;
+ return if !$rev;
+
+ my $prev = $rev && $rev > 1 && $self->dbVNGet(
+ id => $v->{id}, rev => $rev-1, what => 'extended categories anime relations screenshots changes'
+ )->[0];
+
+ $self->htmlRevision('v', $prev, $v,
+ [ title => 'Title (romaji)', diff => 1 ],
+ [ original => 'Original title', diff => 1 ],
+ [ alias => 'Alias', diff => 1 ],
+ [ desc => 'Description', diff => 1 ],
+ [ length => 'Length', serialize => sub { $self->{vn_lengths}[$_[0]][0] } ],
+ [ l_wp => 'Wikipedia link', htmlize => sub {
+ $_[0] ? sprintf '<a href="http://en.wikipedia.org/wiki/%s">%1$s</a>', xml_escape $_[0] : '[no link]'
+ }],
+ [ l_encubed => 'Encubed tag', htmlize => sub {
+ $_[0] ? sprintf '<a href="http://novelnews.net/tag/%s/">%1$s</a>', xml_escape $_[0] : '[no link]'
+ }],
+ [ l_renai => 'Renai.us link', htmlize => sub {
+ $_[0] ? sprintf '<a href="http://renai.us/game/%s.shtml">%1$s</a>', xml_escape $_[0] : '[no link]'
+ }],
+ [ l_vnn => 'V-N.net link', htmlize => sub {
+ $_[0] ? sprintf '<a href="http://visual-novels.net/vn/index.php?option=com_content&amp;task=view&amp;id=%d">%1$d</a>', xml_escape $_[0] : '[no link]'
+ }],
+ [ categories => 'Categories', join => ', ', split => sub {
+ my @r = map $self->{categories}{substr($_->[0],0,1)}[1]{substr($_->[0],1,2)}."($_->[1])", sort { $a->[0] cmp $b->[0] } @{$_[0]};
+ return @r ? @r : ('[no categories selected]');
+ }],
+ [ relations => 'Relations', join => '<br />', split => sub {
+ my @r = map sprintf('%s: <a href="/v%d" title="%s">%s</a>',
+ $self->{vn_relations}[$_->{relation}][0], $_->{id}, xml_escape($_->{original}||$_->{title}), xml_escape shorten $_->{title}, 40
+ ), sort { $a->{id} <=> $b->{id} } @{$_[0]};
+ return @r ? @r : ('[none]');
+ }],
+ [ anime => 'Anime', join => ', ', split => sub {
+ my @r = map sprintf('<a href="http://anidb.net/a%d">a%1$d</a>', $_->{id}), sort { $a->{id} <=> $b->{id} } @{$_[0]};
+ return @r ? @r : ('[none]');
+ }],
+ [ screenshots => 'Screenshots', join => '<br />', split => sub {
+ my @r = map sprintf('[%s] <a href="%s/sf/%02d/%d.jpg" rel="iv:%dx%d">%4$d</a> (%s)',
+ $_->{rid} ? qq|<a href="/r$_->{rid}">r$_->{rid}</a>| : 'no release',
+ $self->{url_static}, $_->{id}%100, $_->{id}, $_->{width}, $_->{height}, $_->{nsfw} ? 'NSFW' : 'Safe'
+ ), @{$_[0]};
+ return @r ? @r : ('[no screenshots]');
+ }],
+ [ image => 'Image', htmlize => sub {
+ $_[0] > 0 ? sprintf '<img src="%s/cv/%02d/%d.jpg" />', $self->{url_static}, $_[0]%100, $_[0] : $_[0] < 0 ? '[processing]' : 'No image';
+ }],
+ [ img_nsfw => 'Image NSFW', serialize => sub { $_[0] ? 'Not safe' : 'Safe' } ],
+ );
+}
+
+
+sub _producers {
+ my($self, $i, $r) = @_;
+ return if !grep @{$_->{producers}}, @$r;
+
+ my @lang;
+ for my $l (@$r) {
+ push @lang, $l->{language} if !grep $l->{language} eq $_, @lang;
+ }
+
+ Tr ++$$i % 2 ? (class => 'odd') : ();
+ td 'Producers';
+ td;
+ for my $l (@lang) {
+ my %p = map { $_->{id} => $_ } map @{$_->{producers}}, grep $_->{language} eq $l, @$r;
+ my @p = values %p;
+ next if !@p;
+ cssicon "lang $l", $self->{languages}{$l};
+ for (@p) {
+ a href => "/p$_->{id}", title => $_->{original}||$_->{name}, shorten $_->{name}, 30;
+ txt ' & ' if $_ != $p[$#p];
+ }
+ txt "\n";
+ }
+ end;
+ end;
+}
+
+
+sub _categories {
+ my($self, $i, $v) = @_;
+
+ # create an ordered list of selected categories in the form of: [ parent, [ p, sub, lvl ], .. ], ..
+ my @cat;
+ my %nolvl = (map {$_=>1} qw| pli pbr gaa gab hfa hfe |);
+ for my $cp (qw|e s g p h|) {
+ my $thisparent = 0;
+ my @sel = sort { $a->[0] cmp $b->[0] } grep substr($_->[0], 0, 1) eq $cp, @{$v->{categories}};
+ if(@sel) {
+ push @cat, [ $self->{categories}{$cp}[0] ];
+ push @{$cat[$#cat]}, map [ $cp, substr($_->[0],1,2), $nolvl{$_->[0]} ? 0 : $_->[1] ], @sel;
+ }
+ }
+ my @placetime = grep $_->[0] =~ /^[tl]/, @{$v->{categories}};
+ if(@placetime) {
+ push @cat, [ 'Place/Time' ];
+ push @{$cat[$#cat]}, map [ substr($_->[0],0,1), substr($_->[0],1,2), 0], sort { $a->[0] cmp $b->[0] } @placetime;
+ }
+
+ # format & output categories
+ Tr ++$$i % 2 ? (class => 'odd') : ();
+ td 'Categories';
+ td;
+ dl;
+ for (@cat) {
+ dt shift(@$_).':';
+ dd;
+ lit join ', ', map qq|<i class="catlvl_$_->[2]">$self->{categories}{$_->[0]}[1]{$_->[1]}</i>|, @$_;
+ end;
+ }
+ end;
+ end;
+ end;
+}
+
+
+sub _relations {
+ my($self, $i, $v) = @_;
+
+ my %rel;
+ push @{$rel{$_->{relation}}}, $_
+ for (sort { $a->{title} cmp $b->{title} } @{$v->{relations}});
+
+
+ Tr ++$$i % 2 ? (class => 'odd') : ();
+ td 'Relations';
+ td class => 'relations';
+ dl;
+ for(sort keys %rel) {
+ dt $self->{vn_relations}[$_][0];
+ dd;
+ for (@{$rel{$_}}) {
+ a href => "/v$_->{id}", title => $_->{original}||$_->{title}, shorten $_->{title}, 40;
+ br;
+ }
+ end;
+ }
+ end;
+ end;
+ end;
+}
+
+
+sub _anime {
+ my($self, $i, $v) = @_;
+
+ Tr ++$$i % 2 ? (class => 'odd') : ();
+ td 'Related anime';
+ td class => 'anime';
+ for (sort { ($a->{year}||9999) <=> ($b->{year}||9999) } @{$v->{anime}}) {
+ if($_->{lastfetch} < 1) {
+ b;
+ txt $_->{lastfetch} < 0 ? '[unknown anidb id: ' : '[no information available at this time: ';
+ a href => "http://anidb.net/a$_->{id}", $_->{id};
+ txt ']';
+ end;
+ } else {
+ b;
+ txt '[';
+ a href => "http://anidb.net/a$_->{id}", title => 'AniDB', 'DB';
+ if($_->{nfo_id}) {
+ txt '-';
+ a href => "http://animenfo.com/animetitle,$_->{nfo_id},a.html", title => 'AnimeNFO', 'NFO';
+ }
+ if($_->{ann_id}) {
+ txt '-';
+ a href => "http://www.animenewsnetwork.com/encyclopedia/anime.php?id=$_->{ann_id}", title => 'Anime News Network', 'ANN';
+ }
+ txt '] ';
+ end;
+ acronym title => $_->{title_kanji}, shorten $_->{title_romaji}, 50;
+ b ' ('.($self->{anime_types}[$_->{type}][0] eq 'unknown' ? '' : $self->{anime_types}[$_->{type}][0].', ').$_->{year}.')';
+ txt "\n";
+ }
+ }
+ end;
+ end;
+}
+
+
+sub _releases {
+ my($self, $v, $r) = @_;
+
+ div class => 'mainbox releases';
+ a class => 'addnew', href => "/v$v->{id}/add", 'add release';
+ h1 'Releases';
+ if(!@$r) {
+ p 'We don\'t have any information about releases of this visual novel yet...';
+ end;
+ return;
+ }
+
+ if($self->authInfo->{id}) {
+ my $l = $self->dbVNListGet(uid => $self->authInfo->{id}, rid => [map $_->{id}, @$r]);
+ for my $i (@$l) {
+ [grep $i->{rid} == $_->{id}, @$r]->[0]{ulist} = $i;
+ }
+ }
+
+ my @lang;
+ for my $l (@$r) {
+ push @lang, $l->{language} if !grep $l->{language} eq $_, @lang;
+ }
+
+ table;
+ for my $l (@lang) {
+ Tr class => 'lang';
+ td colspan => 6;
+ cssicon "lang $l", $self->{languages}{$l};
+ txt $self->{languages}{$l};
+ end;
+ end;
+ for my $rel (grep $l eq $_->{language}, @$r) {
+ Tr;
+ td class => 'tc1'; lit datestr $rel->{released}; end;
+ td class => 'tc2', $rel->{minage} < 0 ? '' : $self->{age_ratings}{$rel->{minage}};
+ td class => 'tc3';
+ for (sort @{$rel->{platforms}}) {
+ next if $_ eq 'oth';
+ cssicon $_, $self->{platforms}{$_};
+ }
+ cssicon lc(substr($self->{release_types}[$rel->{type}],0,3)), $self->{release_types}[$rel->{type}];
+ end;
+ td class => 'tc4';
+ a href => "/r$rel->{id}", title => $rel->{original}||$rel->{title}, $rel->{title};
+ end;
+ td class => 'tc5';
+ if($rel->{ulist}) {
+ a href => "/r$rel->{id}";
+ lit liststat $rel->{ulist};
+ end;
+ } else {
+ txt ' ';
+ }
+ end;
+ td class => 'tc6';
+ if($rel->{website}) {
+ a href => $rel->{website}, rel => 'nofollow';
+ cssicon 'ext', 'External link';
+ end;
+ } else {
+ txt ' ';
+ }
+ end;
+ end;
+ }
+ }
+ end;
+ end;
+}
+
+
+sub _screenshots {
+ my($self, $v, $r) = @_;
+ div class => 'mainbox', id => 'screenshots';
+
+ if(grep $_->{nsfw}, @{$v->{screenshots}}) {
+ p class => 'nsfwtoggle';
+ lit sprintf 'Showing <i id="nsfwshown">%d</i> out of %d screenshots, ',
+ $self->authInfo->{show_nsfw} ? scalar @{$v->{screenshots}} : scalar grep(!$_->{nsfw}, @{$v->{screenshots}}),
+ scalar @{$v->{screenshots}};
+ a href => '#', id => "nsfwhide", 'show/hide NSFW';
+ txt '.';
+ end;
+ }
+
+ h1 'Screenshots';
+ table;
+ for my $rel (@$r) {
+ my @scr = grep $_->{rid} && $rel->{id} == $_->{rid}, @{$v->{screenshots}};
+ next if !@scr;
+ Tr class => 'rel';
+ td colspan => 5;
+ cssicon 'lang '.$rel->{language}, $self->{languages}{$rel->{language}};
+ txt $rel->{title};
+ end;
+ end;
+ Tr;
+ td class => 'scr';
+ for (@scr) {
+ div $_->{nsfw} ? (class => 'nsfw'.(!$self->authInfo->{show_nsfw} ? ' hidden' : '')) : ();
+ a href => sprintf('%s/sf/%02d/%d.jpg', $self->{url_static}, $_->{id}%100, $_->{id}),
+ rel => "iv:$_->{width}x$_->{height}:scr", $_->{nsfw} && !$self->authInfo->{show_nsfw} ? (class => 'hidden') : ();
+ img src => sprintf('%s/st/%02d/%d.jpg', $self->{url_static}, $_->{id}%100, $_->{id}), alt => "Screenshot #$_->{id}";
+ end;
+ end;
+ }
+ end;
+ end;
+ }
+ end;
+ end;
+}
+
+
+sub _stats {
+ my($self, $v) = @_;
+
+ my $stats = $self->dbVoteStats(vid => $v->{id});
+ div class => 'mainbox';
+ h1 'User stats';
+ if(!grep $_ > 0, @$stats) {
+ p "Nobody has voted on this visual novel yet...";
+ } else {
+ $self->htmlVoteStats(v => $v, $stats);
+ }
+ end;
+}
+
+
+1;
+
diff --git a/lib/VNDB/HomePages.pm b/lib/VNDB/HomePages.pm
deleted file mode 100644
index 63adcab7..00000000
--- a/lib/VNDB/HomePages.pm
+++ /dev/null
@@ -1,306 +0,0 @@
-
-package VNDB::HomePages;
-
-use strict;
-use warnings;
-use Exporter 'import';
-
-use vars ('$VERSION', '@EXPORT');
-$VERSION = $VNDB::VERSION;
-@EXPORT = qw| HomePage DocPage History HistRevert HistDelete |;
-
-
-sub HomePage {
- my $self = shift;
-
- my $an = $self->DBGetThreads(type => 'an', order => 't.id DESC', results => 1)->[0];
- $self->ResAddTpl(home => {
- an => $an,
- anpost => $self->DBGetPosts(tid => $an->{id}, num => 1)->[0],
- recentedits => scalar $self->DBGetHist( results => 10, what => 'iid ititle'),
- recentvns => scalar $self->DBGetHist( results => 10, what => 'iid ititle', edits => 0, type => 'v'),
- randomvns => scalar $self->DBGetVN( results => 10, order => 'RANDOM()'),
- recentposts => scalar $self->DBGetThreads(results => 10, what => 'lastpost', order => 'tp2.date DESC'),
- # cache this shit when performance is going to be problematic
- upcomingrel => scalar $self->DBGetRelease(results => 10, unreleased => 1),
- justrel => scalar $self->DBGetRelease(results => 10, order => 'rr.released DESC', unreleased => 0),
- });
-}
-
-
-sub DocPage {
- my($s,$p) = @_;
-
- open my $F, '<', sprintf('%s/%d', $s->{docpath}, $p) or return $s->ResNotFound();
- my @c = <$F>;
- close $F;
-
- (my $title = shift @c) =~ s/^:TITLE://;
- chomp $title;
-
- my $sec = 0;
- for (@c) {
- s{^:SUB:(.+)\r?\n$}{
- $sec++;
- qq|<h3><a href="#$sec" name="$sec">$sec. $1</a></h3>\n|
- }eg;
- s{^:INC:(.+)\r?\n$}{
- open $F, '<', sprintf('%s/%s', $s->{docpath}, $1) or die $!;
- my $ii = join('', <$F>);
- close $F;
- $ii;
- }eg;
- }
-
- $s->ResAddTpl(docs => {
- title => $title,
- content => join('', @c),
- });
-}
-
-
-sub History { # type(p,v,r,u), id, [rss.xml|/]
- my($self, $type, $id, $fmt) = @_;
- $type ||= '';
- $id ||= 0;
-
- $fmt = undef if !$fmt || $fmt eq '/';
- return $self->ResNotFound if $fmt && $fmt ne 'rss.xml';
-
- my $f = $self->FormCheck(
- { name => 'p', required => 0, default => 1, template => 'int' },
- { name => 'ip', required => 0, default => 0 }, # hidden option
- { name => 't', required => 0, default => 'a', enum => [ qw| v r p a | ] },
- { name => 'e', required => 0, default => 0, enum => [ 0..2 ] },
- { name => 'r', required => 0, default => $fmt ? 10 : 50, template => 'int' },
- { name => 'i', required => 0, default => 0, enum => [ 0..1 ] },
- { name => 'h', required => 0, default => 0, enum => [ 0..2 ] }, # hidden option
- );
- return $self->ResNotFound if $f->{_err};
-
- my $o =
- $type eq 'u' ? $self->DBGetUser(uid => $id)->[0] :
- $type eq 'v' ? $self->DBGetVN(id => $id)->[0] :
- $type eq 'r' ? $self->DBGetRelease(id => $id)->[0] :
- $type eq 'p' ? $self->DBGetProducer(id => $id)->[0] :
- undef;
- return $self->ResNotFound if $type && !$o;
- my $t =
- $type eq 'u' ? $o->{username} :
- $type eq 'v' ? $o->{title} :
- $type eq 'r' ? $o->{romaji} || $o->{title} :
- $type eq 'p' ? $o->{name} :
- undef;
-
- my($h, $np, $act);
-
- if($self->ReqMethod ne 'POST' || $fmt) {
- ($h, $np) = $self->DBGetHist(
- what => 'iid ititle user',
- type => $type,
- !$type && $f->{t} ne 'a' ? (
- type => $f->{t} ) : (),
- $f->{e} ? (
- edits => $f->{e} == 1 ? 0 : 1 ) : (),
- id => $id,
- page => $fmt ? 0 : $f->{p},
- results => $f->{r},
- releases => $type eq 'v' ? $f->{i} : 0,
- showhid => $f->{h},
- $f->{ip} ? (
- ip => $f->{ip} ) : (),
- );
- }
- else {
- my $frm = $self->FormCheck(
- { name => 'sel', required => 1, multi => 1 },
- { name => 'post', required => 1, default => 'Mass revert', enum => [ 'Mass revert', 'Mass delete' ] },
- );
- my @s = grep /^[0-9]+$/, @{$frm->{sel}};
- if(!$frm->{_err} && @s) {
- $np = 0;
- $h = $frm->{post} =~ /revert/ ? $self->HistRevert(\@s) : $self->HistDelete(\@s);
- $act = $frm->{post} =~ /revert/ ? 'r' : 'd';
- }
- }
-
- if(!$fmt) {
- $self->ResAddTpl(hist => {
- title => $t,
- selt => $f->{t},
- sele => $f->{e},
- seli => $f->{i},
- type => $type,
- id => $id,
- hist => $h,
- page => $f->{p},
- npage => $np,
- obj => $o,
- act => $act || '',
- });
- } else {
- my $x = $self->ResStartXML;
- $x->startTag('rss', version => '2.0');
- $x->startTag('channel');
- $x->dataElement('language', 'en');
- $x->dataElement('title', !$type ? 'Recent changes at VNDB.org' : $type eq 'u' ? 'Recent changes by '.$t : 'Edit history of '.$t);
- $x->dataElement('link', $self->{root_url}.(!$type ? '/hist' : '/'.$type.$id.'/hist'));
-
- for (@$h) {
- my $t = (qw| v r p |)[$_->{type}];
- my $url = $self->{root_url}.'/'.$t.$_->{iid}.'?rev='.$_->{id};
- $_->{comments} = VNDB::Util::Template::tpl::summary($_->{comments})||'[no summary]';
- $x->startTag('item');
- $x->dataElement(title => $_->{ititle});
- $x->dataElement(link => $url);
- $x->dataElement(pubDate => NTL::time2str($_->{requested}));
- $x->dataElement(guid => $url);
- $x->dataElement(description => $_->{comments});
- $x->endTag('item');
- }
-
- $x->endTag('channel');
- $x->endTag('rss');
- }
-}
-
-
-
-
-1;
-
-__END__
-
-
-#############################################################
-# E X P E R I M E N T A L S T U F F #
-# #
-
-# !WARNING!: this code has not been updated to reflect the recent database changes!
-
-
-# !WARNING!: this code uses rather many large SQL queries, use with care...
-sub HistRevert { # \@ids
- my($self, $l) = @_;
- my $comm = 'Mass revert to revision %d by %s';
-
- # first, get objects, remove newly created items and causedby edits and add original edits
- $l = $self->DBGetHist(cid => $l, results => 1000, what => 'iid');
- my @todo;
- for (@$l) {
- next if !$_->{prev}; # remove newly created items
- if($_->{causedby}) { # remove causedby edits
- push @todo, $self->DBGetHist(cid => [ $_->{causedby} ], what => 'iid')->[0]; # add original edit
- } else {
- push @todo, $_;
- }
- }
-
- # second, group all items and remove duplicate edits
- my %todo; # key=type.iid, value = [objects]
- for my $t (@todo) {
- my $k = $t->{type}.$t->{iid};
- $todo{$k} = [ $t ] and next
- if !$todo{$k};
- push @{$todo{$k}}, $t
- if !grep { $_->{id} == $t->{id} } @{$todo{$k}};
- }
-
- # third, make sure we don't revert edits we don't want to revert
- #TODO
-
- # fourth, get the lowest revision of each item to revert to (ignoring intermetiate edits)
- @todo = map { (sort { $a->{id} <=> $b->{id} } @{$todo{$_}})[0] } keys %todo;
-
- # fifth, actually revert the edits
- my @relupd;
- for (@todo) {
-
- if($_->{type} == 0) { # visual novel
- my $v = $self->DBGetVN(id => $_->{iid}, rev => $_->{prev}, what => 'extended changes relations')->[0];
- my $old = $self->DBGetVN(id => $_->{iid}, rev => $_->{id}, what => 'relations')->[0];
- my $cid = $self->DBEditVN($_->{iid},
- (map { $_ => $v->{$_} } qw| title desc alias categories comm length l_wp l_cisv l_vnn img_nsfw image|),
- relations => [ map { [ $_->{relation}, $_->{id} ] } @{$v->{relations}} ],
- comm => sprintf($comm, $v->{cid}, $v->{username}),
- );
- my %old = map { $_->{id} => $_->{relation} } @{$old->{relations}};
- my %new = map { $_->{id} => $_->{relation} } @{$v->{relations}};
- push @relupd, $self->VNUpdReverse(\%old, \%new, $_->{iid}, $cid);
- }
-
- if($_->{type} == 1) { # release
- my $r = $self->DBGetRelease(id => $_->{iid}, rev => $_->{prev}, what => 'producers platforms media vn changes')->[0];
- $self->DBEditRelease($_->{iid},
- (map { $_ => $r->{$_} } qw| title original language website notes minage type released platforms |),
- media => [ map { [ $_->{medium}, $_->{qty} ] } @{$r->{media}} ],
- producers => [ map { $_->{id} } @{$r->{producers}} ],
- comm => sprintf($comm, $r->{cid}, $r->{username}),
- vn => [ map { $_->{vid} } @{$r->{vn}} ],
- );
- }
-
- if($_->{type} == 2) { # producer
- my $p = $self->DBGetProducer(id => $_->{iid}, rev => $_->{prev}, what => 'changes')->[0];
- $self->DBEditProducer($_->{iid},
- (map { $_ => $p->{$_} } qw| name original website type lang desc |),
- comm => sprintf($comm, $p->{cid}, $p->{username}),
- );
- }
- }
- # update relation graphs
- $self->VNRecreateRel(@relupd) if @relupd;
-
- # sixth, create report of what happened
- my @done;
- for my $t (@todo, @$l) {
- next if $t->{_status};
- $t->{_status} =
- (scalar grep { $t->{id} == $_->{id} } @todo) ? 'reverted' :
- $t->{causedby} ? 'automated' :
- 'skipped';
- push @done, $t;
- }
- return \@done;
-}
-
-
-# ONLY DELETES NEWLY CREATED PAGES (for now...)
-sub HistDelete { # \@ids
- my ($self, $l) = @_;
-
- # get objects and add causedby edits
- $l = $self->DBGetHist(cid => $l, results => 1000, what => 'iid');
- my @todo = @$l;
-# for (@$l) {
-# if($_->{causedby}) { # remove causedby edits
-# my $n = $self->DBGetHist(cid => [ $_->{causedby} ])->[0]; # add original edit
-# push @todo, $n, $self->DBGetHist(causedby => $n->{id} ])->[0]; # add causedby edits
-# } else {
-# push @todo, $_;
-# }
-# }
-
- # remove duplicate edit
- # (not necessary now)
-
- # completely delete newly created items (sort on type to make sure we delete vn's before releases, which is faster)
- my @vns;
- for my $t (sort { $a->{type} <=> $b->{type} } @todo) {
- next if $t->{prev};
- $self->DBDelVN($t->{iid}) if $t->{type} == 0;
- $self->DBDelProducer($t->{iid}) if $t->{type} == 2;
- if($t->{type} == 1) { # we need to know the vn's to remove a release
- my $r = $self->DBGetRelease(id => $t->{iid}, what => 'vn')->[0];
- next if !$r; # we could have deleted this release by deleting the related vn
- $self->DBDelRelease([ map { $_->{vid} } @{$r->{vn}} ], $t->{iid});
- }
- }
-
- # delete individual edits
- #TODO
-
- return \@todo;
-}
-
-
diff --git a/lib/VNDB/Producers.pm b/lib/VNDB/Producers.pm
deleted file mode 100644
index 9756855d..00000000
--- a/lib/VNDB/Producers.pm
+++ /dev/null
@@ -1,176 +0,0 @@
-
-package VNDB::Producers;
-
-use strict;
-use warnings;
-use Exporter 'import';
-use Digest::MD5;
-
-use vars ('$VERSION', '@EXPORT');
-$VERSION = $VNDB::VERSION;
-@EXPORT = qw| PPage PBrowse PEdit PLock PHide PXML |;
-
-
-sub PPage {
- my $self = shift;
- my $id = shift;
- my $rev = shift||0;
-
- return $self->ResNotFound if $self->ReqParam('rev');
-
- my $p = $self->DBGetProducer(
- id => $id,
- $rev ? ( what => 'changes', rev => $rev ) : (),
- )->[0];
- return $self->ResNotFound if !$p->{id};
-
- my $c = $rev && $rev > 1 && $self->DBGetProducer(id => $id, rev => $rev-1, what => 'changes')->[0];
- $p->{next} = $rev && $p->{latest} > $p->{cid} ? $rev+1 : 0;
-
- return $self->ResAddTpl(ppage => {
- prod => $p,
- prev => $c,
- change => $rev,
- vn => $self->DBGetProducerVN($id),
- });
-}
-
-
-sub PBrowse {
- my $self = shift;
- my $chr = shift;
- $chr = 'all' if !defined $chr;
-
- my $p = $self->FormCheck(
- { name => 'p', required => 0, default => 1, template => 'int' },
- { name => 'q', required => 0, default => '' }
- );
- return $self->ResNotFound if $p->{_err};
-
- my($r, $np) = $self->DBGetProducer(
- $chr ne 'all' ? (
- char => $chr ) : (),
- $p->{q} ? (
- search => $p->{q} ) : (),
- page => $p->{p},
- results => 50,
- );
-
- $self->ResAddTpl(pbrowse => {
- prods => $r,
- page => $p->{p},
- npage => $np,
- query => $p->{q},
- chr => $chr,
- });
-}
-
-
-sub PEdit {
- my $self = shift;
- my $id = shift || 0; # 0 = new
-
- my $rev = $self->FormCheck({ name => 'rev', required => 0, default => 0, template => 'int' });
- return $self->ResNotFound if $rev->{_err};
- $rev = $rev->{rev};
-
- my $p = $self->DBGetProducer(id => $id, what => 'changes', $rev ? ( rev => $rev ) : ())->[0] if $id;
- return $self->ResNotFound() if $id && !$p;
-
- return $self->ResDenied if !$self->AuthCan('edit') || ($p->{locked} && !$self->AuthCan('lock'));
-
-
- my %b4 = $id ? (
- map { $_ => $p->{$_} } qw|name original website type lang desc|
- ) : ();
-
- my $frm = {};
- if($self->ReqMethod() eq 'POST') {
- $frm = $self->FormCheck(
- { name => 'type', required => 1, enum => [ keys %$VNDB::PROT ] },
- { name => 'name', required => 1, maxlength => 200 },
- { name => 'original', required => 0, maxlength => 200, default => '' },
- { name => 'lang', required => 1, enum => [ keys %$VNDB::LANG ] },
- { name => 'website', required => 0, maxlength => 200, template => 'url', default => '' },
- { name => 'desc', required => 0, maxlength => 10240, default => '' },
- { name => 'comm', required => 0, default => '' },
- );
-
- return $self->ResRedirect('/p'.$id, 'post')
- if $id && 6 == scalar grep { $_ ne 'comm' && $b4{$_} eq $frm->{$_} } keys %b4;
-
- if(!$frm->{_err}) {
- my $nrev = 1;
- ($nrev) = $self->DBEditProducer($id, %$frm) if $id; # edit
- ($id) = $self->DBAddProducer(%$frm) if !$id; # add
-
- $self->RunCmd('ircnotify p'.$id.'.'.$nrev);
- return $self->ResRedirect('/p'.$id.'.'.$nrev, 'post');
- }
- }
-
- if($id) {
- $frm->{$_} ||= $b4{$_} for (keys %b4);
- $frm->{comm} = sprintf 'Reverted to revision p%d.%d', $p->{id}, $p->{rev} if $p->{cid} != $p->{latest};
- } else {
- $frm->{lang} ||= 'ja';
- }
-
- $self->ResAddTpl(pedit => {
- form => $frm,
- id => $id,
- prod => $p,
- });
-}
-
-
-sub PLock {
- my $self = shift;
- my $id = shift;
-
- my $p = $self->DBGetProducer(id => $id)->[0];
- return $self->ResNotFound() if !$p;
- return $self->ResDenied if !$self->AuthCan('lock');
- $self->DBLockItem('producers', $id, $p->{locked}?0:1);
- return $self->ResRedirect('/p'.$id, 'perm');
-}
-
-
-sub PHide {
- my $self = shift;
- my $id = shift;
-
- my $p = $self->DBGetProducer(id => $id)->[0];
- return $self->ResNotFound() if !$p;
- return $self->ResDenied if !$self->AuthCan('del');
- $self->DBHideProducer($id, $p->{hidden}?0:1);
- return $self->ResRedirect('/p'.$id, 'perm');
-}
-
-sub PXML {
- my $self = shift;
-
- my $q = $self->FormCheck(
- { name => 'q', required => 0, maxlength => 100 }
- )->{q};
-
- my $r = [];
- if($q) {
- $r = $self->DBGetProducer(results => 10,
- $q =~ /^p([0-9]+)$/ ? (id => $1) : (search => $q));
- }
-
- my $x = $self->ResStartXML;
- $x->startTag('producers', results => $#$r+1, query => $q);
- for (@$r) {
- $x->startTag('item');
- $x->dataElement(id => $_->{id});
- $x->dataElement(name => $_->{name});
- $x->dataElement(original => $_->{original}) if $_->{original};
- $x->dataElement(website => $_->{website}) if $_->{website};
- $x->endTag('item');
- }
- $x->endTag('producers');
-}
-
-
diff --git a/lib/VNDB/Releases.pm b/lib/VNDB/Releases.pm
deleted file mode 100644
index 0f36c4fe..00000000
--- a/lib/VNDB/Releases.pm
+++ /dev/null
@@ -1,188 +0,0 @@
-
-package VNDB::Releases;
-
-use strict;
-use warnings;
-use Exporter 'import';
-use Digest::MD5;
-
-use vars ('$VERSION', '@EXPORT');
-$VERSION = $VNDB::VERSION;
-@EXPORT = qw| RPage REdit RLock RHide RVNCache |;
-
-
-sub RPage {
- my $self = shift;
- my $id = shift;
- my $rev = shift||0;
-
- return $self->ResNotFound if $self->ReqParam('rev');
-
- my $v = $self->DBGetRelease(
- id => $id,
- what => 'producers platforms media vn'.($rev ? ' changes':''),
- $rev ? ( rev => $rev ) : ()
- )->[0];
- return $self->ResNotFound if !$v->{id};
-
- my $c = $rev && $rev > 1 && $self->DBGetRelease(id => $id, rev => $rev-1, what => 'changes producers platforms media vn')->[0];
- $v->{next} = $rev && $v->{latest} > $v->{cid} ? $rev+1 : 0;
-
- $self->ResRedirect('/v'.$v->{vn}[0]{vid})
- if ($self->ReqHeader('Referer')||'') =~ m{^http://[^/]*(yahoo|google)} && @{$v->{vn}} == 1;
-
- $v->{rlist} = $self->DBGetRList(rids => [ $id ], uid => $self->AuthInfo->{id})->[0]
- if $self->AuthInfo->{id};
-
- return $self->ResAddTpl(rpage => {
- rel => $v,
- prev => $c,
- change => $rev,
- });
-}
-
-
-sub REdit {
- my $self = shift;
- my $act = shift||'v';
- my $id = shift || 0;
-
- my $rid = $act eq 'r' ? $id : 0;
-
- my $rev = $self->FormCheck({ name => 'rev', required => 0, default => 0, template => 'int' });
- return $self->ResNotFound if $rev->{_err};
- $rev = $rev->{rev};
-
- my $r = $self->DBGetRelease(id => $rid, what => 'changes producers platforms media vn', $rev ? ( rev => $rev ) : ())->[0] if $rid;
- my $ivn = $self->DBGetVN(id => $id)->[0] if !$rid;
- return $self->ResNotFound() if ($rid && !$r) || (!$rid && !$ivn);
-
- my $vn = $rid ? $r->{vn} : [ { vid => $id, title => $ivn->{title} } ];
-
- return $self->ResDenied if !$self->AuthCan('edit') || ($r->{locked} && !$self->AuthCan('lock'));
-
- my %b4 = $rid ? (
- (map { $_ => $r->{$_} } qw|title original gtin language website notes minage type platforms|),
- released => $r->{released} =~ /^([0-9]{4})([0-9]{2})([0-9]{2})$/ ? [ $1, $2, $3 ] : [ 0, 0, 0 ],
- media => join(',', map { $_->{medium} =~ /^(cd|dvd|gdr|blr)$/ ? ($_->{medium}.'_'.$_->{qty}) : $_->{medium} } @{$r->{media}}),
- producers => join('|||', map { $_->{id}.','.$_->{name} } @{$r->{producers}}),
- ) : ();
- $b4{vn} = join('|||', map { $_->{vid}.','.$_->{title} } @$vn);
-
- my $frm = {};
- if($self->ReqMethod() eq 'POST') {
- $frm = $self->FormCheck(
- { name => 'type', required => 1, enum => [ 0..$#{$VNDB::RTYP} ] },
- { name => 'title', required => 1, maxlength => 250 },
- { name => 'original', required => 0, maxlength => 250, default => '' },
- { name => 'gtin', required => 0, template => 'gtin', default => '0' },
- { name => 'language', required => 1, enum => [ keys %{$VNDB::LANG} ] },
- { name => 'website', required => 0, template => 'url', default => '' },
- { name => 'released', required => 0, multi => 1, template => 'int', default => 0 },
- { name => 'minage' , required => 0, enum => [ keys %{$VNDB::VRAGES} ], default => -1 },
- { name => 'notes', required => 0, maxlength => 10240, default => '' },
- { name => 'platforms', required => 0, multi => 1, enum => [ keys %$VNDB::PLAT ], default => '' },
- { name => 'media', required => 0, default => '' },
- { name => 'producers', required => 0, default => '' },
- { name => 'vn', required => 1, maxlength => 10240 },
- { name => 'comm', required => 0, default => '' },
- );
-
- my $released = !$frm->{released}[0] ? 0 :
- $frm->{released}[0] == 9999 ? 99999999 :
- sprintf '%04d%02d%02d', $frm->{released}[0], $frm->{released}[1]||99, $frm->{released}[2]||99;
- my $media = [ map { /_/ ? [ split /_/ ] : [ $_, 0 ] } split /,/, $frm->{media} ];
- my $producers = [ map { /^([0-9]+)/ ? $1 : () } split /\|\|\|/, $frm->{producers} ];
- my $new_vn = [ map { /^([0-9]+)/ ? $1 : () } split /\|\|\|/, $frm->{vn} ];
-
- $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'vn_1' ] : [ 'vn_1' ]
- if !@$new_vn;
-
- # weed out empty string
- $frm->{platforms} = [ map { $_ ? $_ : () } @{$frm->{platforms}} ];
-
- return $self->ResRedirect('/r'.$rid, 'post')
- if $rid && $released == $r->{released} &&
- (join(',', sort @{$b4{platforms}}) eq join(',', sort @{$frm->{platforms}})) &&
- 11 == scalar grep { $_ ne 'comm' && $_ ne 'released' && $_ ne 'platforms' && $frm->{$_} eq $b4{$_} } keys %b4;
-
- if(!$frm->{_err}) {
- my %opts = (
- vn => $new_vn,
- (map { $_ => $frm->{$_} } qw|title original gtin language website notes minage type comm platforms|),
- released => $released,
- media => $media,
- producers => $producers,
- );
- my $nrev = 1;
- ($nrev) = $self->DBEditRelease($rid, %opts) if $rid; # edit
- ($rid) = $self->DBAddRelease(%opts) if !$rid; # add
-
- $self->RVNCache(@$new_vn, (map { $_->{vid} } @$vn));
-
- $self->RunCmd('ircnotify r'.$rid.'.'.$nrev);
- return $self->ResRedirect('/r'.$rid.'.'.$nrev, 'post');
- }
- }
-
- if($rid) {
- $frm->{$_} ||= $b4{$_} for (keys %b4);
- $frm->{comm} = sprintf 'Reverted to revision r%d.%d', $r->{id}, $r->{rev} if $r->{cid} != $r->{latest};
- } else {
- $frm->{language} = 'ja';
- $frm->{vn} = $b4{vn};
- }
-
- $self->AddHid($frm);
- $frm->{_hid} = {map{$_=>1} qw| info pnm prod com |}
- if !$frm->{_hid} && !$rid;
- $self->ResAddTpl(redit => {
- form => $frm,
- id => $rid,
- rel => $r,
- vn => !$rid ? $ivn : $vn,
- });
-}
-
-
-sub RLock {
- my $self = shift;
- my $id = shift;
-
- my $r = $self->DBGetRelease(id => $id)->[0];
- return $self->ResNotFound() if !$r;
- return $self->ResDenied if !$self->AuthCan('lock');
- $self->DBLockItem('releases', $id, $r->{locked}?0:1);
- return $self->ResRedirect('/r'.$id, 'perm');
-}
-
-
-sub RHide {
- my $self = shift;
- my $id = shift;
-
- return $self->ResDenied if !$self->AuthCan('del');
- my $r = $self->DBGetRelease(id => $id, what => 'vn')->[0];
- return $self->ResNotFound if !$r;
- $self->DBHideRelease($id, $r->{hidden}?0:1);
- $self->RVNCache(map { $_->{vid} } @{$r->{vn}});
- return $self->ResRedirect('/r'.$id, 'perm');
-}
-
-
-sub RVNCache { # @vids - calls update_vncache and regenerates relation graphs if needed
- my($self, @vns) = @_;
- my $before = $self->DBGetVN(id => \@vns, order => 'v.id', what => 'relations');
- $self->DBVNCache(@vns);
- my $after = $self->DBGetVN(id => \@vns, order => 'v.id');
- my @upd = map {
- @{$before->[$_]{relations}} && (
- $before->[$_]{c_released} != $after->[$_]{c_released}
- || $before->[$_]{c_languages} ne $after->[$_]{c_languages}
- ) ? $before->[$_]{id} : ();
- } 0..$#$before;
- $self->RunCmd('relgraph '.join(' ', @upd)) if @upd;
-}
-
-1;
-
diff --git a/lib/VNDB/Users.pm b/lib/VNDB/Users.pm
deleted file mode 100644
index e2021627..00000000
--- a/lib/VNDB/Users.pm
+++ /dev/null
@@ -1,238 +0,0 @@
-
-package VNDB::Users;
-
-use strict;
-use warnings;
-use Exporter 'import';
-use Digest::MD5 'md5_hex';
-
-our $VERSION = $VNDB::VERSION;
-our @EXPORT = qw| UsrLogin UsrLogout UsrReg UsrPass UsrEdit UsrList UsrPage UsrDel |;
-
-
-sub UsrLogin {
- my $self = shift;
-
- (return $self->ResRedirect('/', 'temp')) if $self->AuthInfo()->{id};
-
- my $frm = {};
- if($self->ReqMethod() eq 'POST') {
- $frm = $self->FormCheck(
- { name => 'username', required => 1, minlength => 2, maxlength => 15, template => 'pname' },
- { name => 'userpass', required => 1, minlength => 4, maxlength => 15, template => 'asciiprint' },
- );
- if(!$frm->{_err}) {
- (my $ref = $self->ReqHeader('Referer')||'/') =~ s/^$self->{root_url}//;
- my $r = $self->AuthLogin($frm->{username}, $frm->{userpass}, 1, $ref);
- $r == 1 ? (return) : ($frm->{_err} = [ 'loginerr' ]);
- }
- }
-
- $self->ResAddTpl(userlogin => {
- log => $frm,
- } );
-}
-
-
-sub UsrLogout {
- shift->AuthLogout();
-}
-
-
-sub UsrReg {
- my $self = shift;
-
- (return $self->ResRedirect('/', 'temp')) if $self->AuthInfo()->{id};
-
- my $frm = {};
- if($self->ReqMethod() eq 'POST') {
- $frm = $self->FormCheck(
- { name => 'username', required => 1, minlength => 2, maxlength => 15, template => 'pname' },
- { name => 'mail', required => 1, template => 'mail' },
- { name => 'pass1', required => 1, minlength => 4, maxlength => 15, template => 'asciiprint' },
- { name => 'pass2', required => 1, minlength => 4, maxlength => 15, template => 'asciiprint' },
- );
- $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'badpass' ] : [ 'badpass' ]
- if $frm->{pass1} ne $frm->{pass2};
- $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'usrexists' ] : [ 'usrexists' ]
- if $frm->{username} eq 'anonymous' || $self->DBGetUser(username => $frm->{username})->[0];
- $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'mailexists' ] : [ 'mailexists' ]
- if $frm->{mail} && $self->DBGetUser(mail => $frm->{mail})->[0];
-
- if(!$frm->{_err}) {
- $self->DBAddUser($frm->{username}, md5_hex($frm->{pass1}), $frm->{mail}, 2);
- return $self->AuthLogin($frm->{username}, $frm->{pass1}, 1, '/');
- }
- }
- $self->ResAddTpl(userreg => {
- reg => $frm,
- });
-}
-
-
-sub UsrPass {
- my $self = shift;
-
- (return $self->ResRedirect('/', 'temp')) if $self->AuthInfo()->{id};
-
- my $d = $self->ReqParam('d');
-
- my $frm = {};
- if(!$d && $self->ReqMethod() eq 'POST') {
- $frm = $self->FormCheck({ name => 'mail', required => 1, template => 'mail' });
- my $unfo;
- if(!$frm->{_err}) {
- $frm->{mail} =~ s/%//g;
- $unfo = $self->DBGetUser(mail => $frm->{mail})->[0];
- $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'nomail' ] : [ 'nomail' ]
- if !$unfo;
- }
- if(!$frm->{_err}) {
- my @chars = ( 'A'..'Z', 'a'..'z', 0..9 );
- my $pass = join('', map $chars[int rand $#chars+1], 0..8);
- $self->DBUpdateUser($unfo->{id}, passwd => md5_hex($pass));
- $self->SendMail(sprintf(<<__, $unfo->{username}, $unfo->{username}, $pass),
-Hello %s,
-
-Your password has been reset, you can now login at http://vndb.org/ with the
-following information:
-
-Username: %s
-Password: %s
-
-Now don't forget your password again! :-)
-
-vndb.org
-__
- To => $frm->{mail},
- Subject => sprintf('Password request for %s', $unfo->{username}),
- );
- return $self->ResRedirect('/u/newpass?d=1', 'post');
- }
- }
-
- $self->ResAddTpl(userpass => {
- pas => $frm,
- done => $d,
- });
-}
-
-
-sub UsrEdit {
- my $self = shift;
- my $user = shift;
-
- my $u = $self->AuthInfo();
- return $self->ResDenied if !$u->{id};
- my $adm = $u->{id} != $user;
- return $self->ResDenied if $adm && !$self->AuthCan('usermod');
- $u = $self->DBGetUser(uid => $user)->[0] if $adm;
- return $self->ResNotFound if !$u->{id};
-
- my $d = $self->ReqParam('d');
-
- my $frm = {};
- if(!$d && $self->ReqMethod() eq 'POST') {
- $frm = $self->FormCheck(
- { name => 'mail', required => 1, template => 'mail' },
- { name => 'pass1', required => 0, template => 'asciiprint' },
- { name => 'pass2', required => 0, template => 'asciiprint' },
- $adm ? (
- { name => 'username',required => 1, template => 'pname', minlength => 2, maxlength => 15 },
- { name => 'rank', required => 1, enum => [ '1'..($#{$self->{ranks}}-1) ] },
- ) : (),
- { name => 'plist', required => 0 },
- { name => 'pign_nsfw', required => 0 },
- );
- if(($frm->{pass1} || $frm->{pass2}) && $frm->{pass1} ne $frm->{pass2}) {
- $frm->{_err} = [] if !$frm->{_err};
- push(@{$frm->{_err}}, 'badpass');
- }
- if(!$frm->{_err}) {
- my $pass = $frm->{pass1} ? md5_hex($frm->{pass1}) : '';
- my %opts = (
- username => $frm->{username}||undef,
- rank => $frm->{rank}||undef,
- mail => $frm->{mail},
- );
- $opts{passwd} = $pass if $pass;
- $opts{flags} = $frm->{plist} ? $VNDB::UFLAGS->{list} : 0;
- $opts{flags} += $VNDB::UFLAGS->{nsfw} if $frm->{pign_nsfw};
- $self->DBUpdateUser($u->{id}, %opts);
- return $adm ? $self->ResRedirect('/u'.$user.'/edit?d=1', 'post') :
- $pass ? $self->AuthLogin($u->{username}, $frm->{pass1}, 1, '/u'.$user.'/edit?d=1') :
- $self->ResRedirect('/u'.$user.'/edit?d=1', 'post');
- }
- }
-
- $frm->{$_} ||= $u->{$_}
- for (qw| username mail rank |);
- $frm->{plist} ||= $u->{flags} & $VNDB::UFLAGS->{list};
- $frm->{pign_nsfw} ||= $u->{flags} & $VNDB::UFLAGS->{nsfw};
- $self->ResAddTpl(useredit => {
- form => $frm,
- done => $d,
- adm => $adm,
- user => $user,
- u => $u
- });
-}
-
-
-sub UsrList {
- my $self = shift;
- my $chr = shift;
- $chr = 'all' if !defined $chr;
-
- my $f = $self->FormCheck(
- { name => 's', required => 0, default => 'username', enum => [ qw|username mail rank registered| ] },
- { name => 'o', required => 0, default => 'a', enum => [ 'a','d' ] },
- { name => 'p', required => 0, default => 1, template => 'int' },
- );
- return $self->ResNotFound if $f->{_err};
-
- my($unfo, $np) = $self->DBGetUser(
- order => $f->{s}.($f->{o} eq 'a' ? ' ASC' : ' DESC'),
- $chr ne 'all' ? (
- firstchar => $chr ) : (),
- results => 50,
- page => $f->{p},
- what => 'list',
- );
-
- $self->ResAddTpl(userlist => {
- users => $unfo,
- chr => $chr,
- page => $f->{p},
- npage => $np,
- order => [ $f->{s}, $f->{o} ],
- } );
-}
-
-
-sub UsrPage {
- my($self, $id) = @_;
-
- my $u = $self->DBGetUser(uid => $id, what => 'list')->[0];
- return $self->ResNotFound if !$u;
-
- $self->ResAddTpl(userpage => {
- user => $u,
- votes => {
- latest => scalar $self->DBGetVotes(uid => $id, results => 10),
- graph => $self->DBVoteStats(uid => $id),
- },
- });
-}
-
-
-sub UsrDel {
- my($s, $id) = @_;
- return $s->ResDenied if !$s->AuthCan('usermod');
- $s->DBDelUser($id);
- $s->ResRedirect('/u/list', 'temp');
-}
-
-
-1;
-
diff --git a/lib/VNDB/Util/Auth.pm b/lib/VNDB/Util/Auth.pm
index 6f265253..b520eeba 100644
--- a/lib/VNDB/Util/Auth.pm
+++ b/lib/VNDB/Util/Auth.pm
@@ -1,133 +1,96 @@
-
-
-
-
-# N E E D S M O A R S A L T !
-
-
package VNDB::Util::Auth;
+# This module is just a small improvement of the 1.x equivalent
+# and is designed to work with the cookies and database of VNDB 1.x
+# without modifications. A proper and more secure (incompatible)
+# implementation should be written at some point.
+
use strict;
use warnings;
use Exporter 'import';
use Digest::MD5 'md5_hex';
-use Crypt::Lite; # simple, small and easy encryption for cookies
+use Crypt::Lite;
+
-use vars ('$VERSION', '@EXPORT');
-$VERSION = $VNDB::VERSION;
-@EXPORT = qw| AuthCheckCookie AuthLogin AuthLogout AuthInfo AuthCan AuthAddTpl |;
+our @EXPORT = qw| authInit authLogin authLogout authInfo authCan |;
-{ # local data for these 2 methods only
- my $crl = Crypt::Lite->new(debug => 0);
- my $scrt = md5_hex($VNDB::COOKEY);
-
-sub AuthCheckCookie {
+# initializes authentication information and checks the vndb_auth cookie
+sub authInit {
my $self = shift;
- my $info = $self->{_Req} || $self;
- $info->{_auth} = {} if !exists $info->{_auth};
+ $self->{_auth} = undef;
- my $cookie = $self->ReqCookie('vndb_auth');
+ my $cookie = $self->reqCookie('vndb_auth');
return 0 if !$cookie;
- my $str = $crl->decrypt($cookie, $scrt);
+ my $str = Crypt::Lite->new()->decrypt($cookie, md5_hex($self->{cookie_key}));
return 0 if length($str) < 36;
my $pass = substr($str, 4, 32);
my $user = substr($str, 36);
- return _AuthCheck($self, $user, $pass);
+ _authCheck($self, $user, $pass);
}
-
-sub AuthLogin {
+
+
+# login, arguments: user, password, url-to-redirect-to-on-success
+# returns 1 on success (redirected), 0 otherwise (no reply sent)
+sub authLogin {
my $self = shift;
my $user = lc(scalar shift);
- my $psbk = shift;
- my $pass = md5_hex($psbk);
- my $keep = shift;
+ my $pass = md5_hex(shift);
my $to = shift;
- my $status = _AuthCheck($self, $user, $pass);
- if($status == 1) {
- (my $cookie = $crl->encrypt("VNDB$pass$user", $scrt)) =~ s/\r?\n//g;
- $self->ResRedirect($to, "post");
- $self->ResAddHeader('Set-Cookie', "vndb_auth=$cookie; " . ($keep ? 'expires=Sat, 01-Jan-2030 00:00:00 GMT; ' : ' ') . "path=/; domain=$self->{CookieDomain}");
+
+ if(_authCheck($self, $user, $pass)) {
+ (my $cookie = Crypt::Lite->new()->encrypt("VNDB$pass$user", md5_hex($self->{cookie_key}))) =~ s/\r?\n//g;
+ $self->resRedirect($to, 'post');
+ $self->resHeader('Set-Cookie', "vndb_auth=$cookie; expires=Sat, 01-Jan-2030 00:00:00 GMT; path=/; domain=$self->{cookie_domain}");
return 1;
}
- return $status;
+ return 0;
}
-} # end of local data
-sub AuthLogout {
- my $self = shift;
- $self->ResRedirect('/', 'temp');
- $self->ResAddHeader('Set-Cookie', "vndb_auth= ; expires=Sat, 01-Jan-2000 00:00:00 GMT; path=/; domain=$self->{CookieDomain}");
-}
-sub AuthInfo {
+# clears authentication cookie and redirects to /
+sub authLogout {
my $self = shift;
- my $info = $self->{_Req} || shift;
- return $info->{_auth} || {};
+ $self->resRedirect('/', 'temp');
+ $self->resHeader('Set-Cookie', "vndb_auth= ; expires=Sat, 01-Jan-2000 00:00:00 GMT; path=/; domain=$self->{cookie_domain}");
}
-sub AuthCan {
- my $self = shift;
- my $act = shift;
- my $info = $self->{_Req} || shift;
- return $self->{ranks}[($info->{_auth}{rank}||0)+1]{$act};
+
+# returns a hashref with information about the current loggedin user
+# the hash is identical to the hash returned by dbUserGet
+# returns empty hash if no user is logged in.
+sub authInfo {
+ return shift->{_auth} || {};
}
-sub _AuthCheck {
- my $self = shift;
- my $user = shift;
- my $pass = shift;
- my $info = $self->{_Req} || shift;
- $info->{_auth} = undef;
+# returns whether the currently loggedin or anonymous user can perform
+# a certain action. Argument is the action name as defined in global.pl
+sub authCan {
+ my($self, $act) = @_;
+ my $r = $self->{_auth}{rank}||0;
+ return scalar grep $_ eq $act, @{$self->{user_ranks}[$r]}[1..$#{$self->{user_ranks}[$r]}];
+}
- return 2 if !$user || length($user) > 15 || length($user) < 2;
- return 3 if !$pass || length($pass) != 32;
- my $d = $self->DBGetUser(username => $user, passwd => $pass)->[0];
- return 4 if !defined $d->{id};
- return 5 if !$d->{rank};
+# Checks for a valid login and writes information in _auth
+# Arguments: user, md5_hex(pass)
+# Returns: 1 if login is valid, 0 otherwise
+sub _authCheck {
+ my($self, $user, $pass) = @_;
- $d->{oldvnlist} = $self->DBGetVNList(uid => $d->{id}, results => 1)->[0] ? 1 : 0;
- $info->{_auth} = $d;
+ return 0 if
+ !$user || length($user) > 15 || length($user) < 2
+ || !$pass || length($pass) != 32;
+ my $d = $self->dbUserGet(username => $user, passwd => $pass)->[0];
+ return 0 if !defined $d->{id} || !$d->{rank};
+
+ $self->{_auth} = $d;
return 1;
}
-# adds the keys AuthLoggedin, AuthRank, AuthUsername, AuthMail, AuthId
-sub AuthAddTpl {
- my $self = shift;
- my $info = $self->{_Req} || shift;
- my %tpl;
-
- if($info->{_auth}{id}) {
- %tpl = (
- AuthLoggedin => 1,
- AuthRank => $info->{_auth}{rank},
- AuthRankname => $self->{ranks}[0][0][$info->{_auth}{rank}],
- AuthUsername => $info->{_auth}{username},
- AuthMail => $info->{_auth}{mail},
- AuthId => $info->{_auth}{id},
- AuthNsfw => $info->{_auth}{flags} & $VNDB::UFLAGS->{nsfw},
- AuthOldList => $info->{_auth}{oldvnlist},
- );
- } else {
- %tpl = (
- AuthLoggedin => 0,
- AuthRank => '',
- AuthRankname => '',
- AuthUsername => '',
- AuthMail => '',
- AuthId => 0,
- AuthNsfw => 0,
- );
- }
- $tpl{'Auth'.$_} = $self->{ranks}[($info->{_auth}{rank}||0)+1]{$_}
- for (keys %{$self->{ranks}[0][1]});
- $self->ResAddTpl(%tpl);
-}
-
1;
diff --git a/lib/VNDB/Util/CommonHTML.pm b/lib/VNDB/Util/CommonHTML.pm
new file mode 100644
index 00000000..70fdfde3
--- /dev/null
+++ b/lib/VNDB/Util/CommonHTML.pm
@@ -0,0 +1,497 @@
+
+package VNDB::Util::CommonHTML;
+
+use strict;
+use warnings;
+use YAWF ':html', 'xml_escape';
+use Exporter 'import';
+use Algorithm::Diff::XS 'compact_diff';
+use VNDB::Func;
+use Encode 'encode_utf8', 'decode_utf8';
+
+our @EXPORT = qw|
+ htmlMainTabs htmlDenied htmlHiddenMessage htmlBrowse htmlBrowseNavigate
+ htmlRevision htmlEditMessage htmlItemMessage htmlVoteStats htmlHistory
+|;
+
+
+# generates the "main tabs". These are the commonly used tabs for
+# 'objects', i.e. VN/producer/release entries and users
+# Arguments: u/v/r/p, object, currently selected item (empty=main)
+sub htmlMainTabs {
+ my($self, $type, $obj, $sel) = @_;
+ $sel ||= '';
+ my $id = $type.$obj->{id};
+
+ ul class => 'maintabs';
+ li $sel eq 'hist' ? (class => 'tabselected') : ();
+ a href => "/$id/hist", 'history';
+ end;
+
+ if($type ne 'r') {
+ my $cnt = $self->dbThreadCount($type, $obj->{id});
+ li $sel eq 'disc' ? (class => 'tabselected') : ();
+ a href => "/t/$id", "discussions ($cnt)";
+ end;
+ }
+
+ if($type eq 'u' && $obj->{show_list}) {
+ li $sel eq 'wish' ? (class => 'tabselected') : ();
+ a href => "/$id/wish", 'wishlist';
+ end;
+
+ li $sel eq 'list' ? (class => 'tabselected') : ();
+ a href => "/$id/list", 'list';
+ end;
+ }
+
+ if($type eq 'u' && ($self->authInfo->{id} && $obj->{id} == $self->authInfo->{id} || $self->authCan('usermod'))
+ || $type ne 'u' && $self->authCan('edit') && (!$obj->{locked} || $self->authCan('lock')) && (!$obj->{hidden} || $self->authCan('del'))) {
+ li $sel eq 'edit' ? (class => 'tabselected') : ();
+ a href => "/$id/edit", 'edit';
+ end;
+ }
+
+ if($type ne 'u' && $self->authCan('del')) {
+ li;
+ a href => "/$id/hide", $obj->{hidden} ? 'unhide' : 'hide';
+ end;
+ }
+
+ if($type ne 'u' && $self->authCan('lock')) {
+ li;
+ a href => "/$id/lock", $obj->{locked} ? 'unlock' : 'lock';
+ end;
+ }
+
+ if($type eq 'u' && $self->authCan('usermod')) {
+ li $sel eq 'del' ? (class => 'tabselected') : ();
+ a href => "/$id/del", 'del';
+ end;
+ }
+
+ if($type eq 'v' && $obj->{rgraph}) {
+ li $sel eq 'rg' ? (class => 'tabselected') : ();
+ a href => "/$id/rg", 'relations';
+ end;
+ }
+
+ li !$sel ? (class => 'tabselected') : ();
+ a href => "/$id", $id;
+ end;
+ end;
+}
+
+
+# generates a full error page, including header and footer
+sub htmlDenied {
+ my $self = shift;
+ $self->htmlHeader(title => 'Access Denied');
+ div class => 'mainbox';
+ h1 'Access Denied';
+ div class => 'warning';
+ if(!$self->authInfo->{id}) {
+ h2 'You need to be logged in to perform this action.';
+ p;
+ lit 'Please <a href="/u/login">login</a>, or <a href="/u/register">create an account</a> '
+ .'if you don\'t have one yet.';
+ end;
+ } else {
+ h2 "You are not allowed to perform this action.";
+ p 'It seems you don\'t have the proper rights to perform the action you wanted to perform...';
+ }
+ end;
+ end;
+ $self->htmlFooter;
+}
+
+
+# Generates message saying that the current item has been deleted,
+# Arguments: [pvr], obj
+# Returns 1 if the use doesn't have access to the page, 0 otherwise
+sub htmlHiddenMessage {
+ my($self, $type, $obj) = @_;
+ return 0 if !$obj->{hidden};
+ div class => 'mainbox';
+ h1 $obj->{title}||$obj->{name};
+ div class => 'warning';
+ h2 'Item deleted';
+ p;
+ lit qq|This item has been deleted from the database, File a request on the|
+ .qq| <a href="/t/$type$obj->{id}">discussion board</a> to undelete this page.|;
+ end;
+ end;
+ end;
+ return $self->htmlFooter() || 1 if !$self->authCan('del');
+ return 0;
+}
+
+
+# generates a browse box, arguments:
+# items => arrayref with the list items
+# options => hashref containing at least the keys s (sort key), o (order) and p (page)
+# nextpage => whether there's a next page or not
+# sorturl => base URL to append the sort options to (if there are any sortable columns)
+# pageurl => base URL to append the page option to
+# class => classname of the mainbox
+# header =>
+# can be either an arrayref or subroutine reference,
+# in the case of a subroutine, it will be called when the header should be written,
+# in the case of an arrayref, the array should contain the header items. Each item
+# can again be either an arrayref or subroutine ref. The arrayref would consist of
+# two elements: the name of the header, and the name of the sorting column if it can
+# be sorted
+# row => subroutine ref, which is called for each item in $list, arguments will be
+# $self, $item_number (starting from 0), $item_value
+# footer => subroutine ref, called after all rows have been processed
+sub htmlBrowse {
+ my($self, %opt) = @_;
+
+ $opt{sorturl} .= $opt{sorturl} =~ /\?/ ? ';' : '?' if $opt{sorturl};
+
+ # top navigation
+ $self->htmlBrowseNavigate($opt{pageurl}, $opt{options}{p}, $opt{nextpage}, 't');
+
+ div class => 'mainbox browse'.($opt{class} ? ' '.$opt{class} : '');
+ table;
+
+ # header
+ thead;
+ Tr;
+ if(ref $opt{header} eq 'CODE') {
+ $opt{header}->($self);
+ } else {
+ for(0..$#{$opt{header}}) {
+ if(ref $opt{header}[$_] eq 'CODE') {
+ $opt{header}[$_]->($self, $_+1);
+ } else {
+ td class => 'tc'.($_+1), $opt{header}[$_][2] ? (colspan => $opt{header}[$_][2]) : ();
+ lit $opt{header}[$_][0];
+ if($opt{header}[$_][1]) {
+ lit ' ';
+ lit $opt{options}{s} eq $opt{header}[$_][1] && $opt{options}{o} eq 'a' ? "\x{25B4}" : qq|<a href="$opt{sorturl}o=a;s=$opt{header}[$_][1]">\x{25B4}</a>|;
+ lit $opt{options}{s} eq $opt{header}[$_][1] && $opt{options}{o} eq 'd' ? "\x{25BE}" : qq|<a href="$opt{sorturl}o=d;s=$opt{header}[$_][1]">\x{25BE}</a>|;
+ }
+ end;
+ }
+ }
+ }
+ end;
+ end;
+
+ # footer
+ if($opt{footer}) {
+ tfoot;
+ $opt{footer}->($self);
+ end;
+ }
+
+ # rows
+ $opt{row}->($self, $_+1, $opt{items}[$_])
+ for 0..$#{$opt{items}};
+
+ end;
+ end;
+
+ # bottom navigation
+ $self->htmlBrowseNavigate($opt{pageurl}, $opt{options}{p}, $opt{nextpage}, 'b');
+}
+
+
+# creates next/previous buttons (tabs), if needed
+# Arguments: page url, current page (1..n), nextpage (0/1), alignment (t/b), noappend (0/1)
+sub htmlBrowseNavigate {
+ my($self, $url, $p, $np, $al, $na) = @_;
+ return if $p == 1 && !$np;
+
+ $url .= $url =~ /\?/ ? ';p=' : '?p=' unless $na;
+ ul class => 'maintabs ' . ($al eq 't' ? 'notfirst' : 'bottom');
+ if($p > 1) {
+ li class => 'left';
+ a href => $url.($p-1), '<- previous';
+ end;
+ }
+ if($np) {
+ li;
+ a href => $url.($p+1), 'next ->';
+ end;
+ }
+ end;
+}
+
+
+# Shows a revision, including diff if there is a previous revision.
+# Arguments: v|p|r, old revision, new revision, @fields
+# Where @fields is a list of fields as arrayrefs with:
+# [ shortname, displayname, %options ],
+# Where %options:
+# diff => 1/0, whether do show a diff on this field
+# serialize => coderef, should convert the field into a readable string, no HTML allowed
+# htmlize => same as serialize, but HTML is allowed and this can't be diff'ed
+# split => coderef, should return an array of HTML strings that can be diff'ed. (implies diff => 1)
+# join => used in combination with split, specifies the string used for joining the HTML strings
+sub htmlRevision {
+ my($self, $type, $old, $new, @fields) = @_;
+ div class => 'mainbox revision';
+ h1 'Revision '.$new->{rev};
+
+ # previous/next revision links
+ a class => 'prev', href => sprintf('/%s%d.%d', $type, $new->{id}, $new->{rev}-1), '<- earlier revision'
+ if $new->{rev} > 1;
+ a class => 'next', href => sprintf('/%s%d.%d', $type, $new->{id}, $new->{rev}+1), 'later revision ->'
+ if $new->{cid} != $new->{latest};
+ p class => 'center';
+ a href => "/$type$new->{id}", "$type$new->{id}";
+ end;
+
+ # no previous revision, just show info about the revision itself
+ if(!$old) {
+ div;
+ revheader($type, $new);
+ br;
+ b 'Edit summary:';
+ br; br;
+ lit bb2html($new->{comments})||'[no summary]';
+ end;
+ }
+
+ # otherwise, compare the two revisions
+ else {
+ table;
+ thead;
+ Tr;
+ td; lit '&nbsp;'; end;
+ td; revheader($type, $old); end;
+ td; revheader($type, $new); end;
+ end;
+ Tr;
+ td; lit '&nbsp;'; end;
+ td colspan => 2;
+ b 'Edit summary of revision '.$new->{rev}.':';
+ br; br;
+ lit bb2html($new->{comments})||'[no summary]';
+ end;
+ end;
+ end;
+ my $i = 1;
+ revdiff(\$i, $old, $new, @$_) for (@fields);
+ end;
+ }
+ end;
+}
+
+sub revheader { # type, obj
+ my($type, $obj) = @_;
+ b 'Revision '.$obj->{rev};
+ txt ' (';
+ a href => "/$type$obj->{id}.$obj->{rev}/edit", 'edit';
+ txt ')';
+ br;
+ txt 'By ';
+ lit userstr($obj);
+ txt ' on ';
+ lit date $obj->{added}, 'full';
+}
+
+sub revdiff {
+ my($i, $old, $new, $short, $name, %o) = @_;
+
+ $o{serialize} ||= $o{htmlize};
+ $o{diff}++ if $o{split};
+ $o{join} ||= '';
+
+ my $ser1 = $o{serialize} ? $o{serialize}->($old->{$short}) : $old->{$short};
+ my $ser2 = $o{serialize} ? $o{serialize}->($new->{$short}) : $new->{$short};
+ return if $ser1 eq $ser2;
+
+ if($o{diff} && $ser1 && $ser2) {
+ # compact_diff doesn't like utf8 encoded strings, so encode input, decode output
+ my @ser1 = map encode_utf8($_), $o{split} ? $o{split}->($ser1) : map xml_escape($_), split //, $ser1;
+ my @ser2 = map encode_utf8($_), $o{split} ? $o{split}->($ser2) : map xml_escape($_), split //, $ser2;
+ return if $o{split} && $#ser1 == $#ser2 && !grep $ser1[$_] ne $ser2[$_], 0..$#ser1;
+
+ $ser1 = $ser2 = '';
+ my @d = compact_diff(\@ser1, \@ser2);
+ for my $i (0..($#d-2)/2) {
+ # $i % 2 == 0 -> equal, otherwise it's different
+ my $a = join($o{join}, @ser1[ $d[$i*2] .. $d[$i*2+2]-1 ]);
+ my $b = join($o{join}, @ser2[ $d[$i*2+1] .. $d[$i*2+3]-1 ]);
+ $ser1 .= ($ser1?$o{join}:'').($i % 2 ? qq|<b class="diff_del">$a</b>| : $a) if $a;
+ $ser2 .= ($ser2?$o{join}:'').($i % 2 ? qq|<b class="diff_add">$b</b>| : $b) if $b;
+ }
+ $ser1 = decode_utf8($ser1);
+ $ser2 = decode_utf8($ser2);
+ } elsif(!$o{htmlize}) {
+ $ser1 = xml_escape $ser1;
+ $ser2 = xml_escape $ser2;
+ }
+
+ $ser1 = '[empty]' if !$ser1 && $ser1 ne '0';
+ $ser2 = '[empty]' if !$ser2 && $ser2 ne '0';
+
+ Tr $$i++ % 2 ? (class => 'odd') : ();
+ td $name;
+ td class => 'tcval'; lit $ser1; end;
+ td class => 'tcval'; lit $ser2; end;
+ end;
+}
+
+
+# Generates a generic message to show as the header of the edit forms
+# Arguments: v/r/p, obj
+sub htmlEditMessage {
+ my($self, $type, $obj) = @_;
+ my $full = {v => 'visual novel', r => 'release', p => 'producer'}->{$type};
+ my $guidelines = {v => 2, r => 3, p => 4}->{$type};
+
+ div class => 'mainbox';
+ h1 $obj ? 'Edit '.($obj->{name}||$obj->{title}) : "Add new $full";
+ div class => 'notice';
+ h2 'Before editing:';
+ ul;
+ li; lit qq|Read the <a href="/d$guidelines">guidelines</a>!|; end;
+ if($obj) {
+ li; lit qq|Check for any existing discussions on the <a href="/t/$type$obj->{id}">discussion board</a>|; end;
+ li; lit qq|Browse the <a href="/$type$obj->{id}/hist">edit history</a> for any recent changes related to what you want to change.|; end;
+ } elsif($type ne 'r') {
+ li; lit qq|<a href="/$type/all">Search the database</a> to see if we already have information about this $full|; end;
+ }
+ end;
+ end;
+ if($obj && $obj->{latest} != $obj->{cid}) {
+ div class => 'warning';
+ h2 'Reverting';
+ p qq|You are editing an old revision of this $full. If you save it, all changes made after this revision will be reverted!|;
+ end;
+ }
+ end;
+}
+
+
+# Generates a small message when the user can't edit the item,
+# or the item is locked.
+# Arguments: v/r/p, obj
+sub htmlItemMessage {
+ my($self, $type, $obj) = @_;
+
+ if($obj->{locked}) {
+ p class => 'locked', 'Locked for editing'
+ } elsif(!$self->authInfo->{id}) {
+ p class => 'locked';
+ lit 'You need to be <a href="/u/login">logged in</a> to edit this page';
+ end;
+ } elsif(!$self->authCan('edit')) {
+ p class => 'locked', "You're not allowed to edit this page";
+ }
+}
+
+
+# generates two tables, one with a vote graph, other with recent votes
+sub htmlVoteStats {
+ my($self, $type, $obj, $stats) = @_;
+
+ my($max, $count, $total) = (0, 0);
+ for (0..$#$stats) {
+ $max = $stats->[$_] if $stats->[$_] > $max;
+ $count += $stats->[$_];
+ $total += $stats->[$_]*($_+1);
+ }
+ div class => 'votestats';
+ table class => 'votegraph';
+ thead; Tr;
+ td colspan => 2, 'Vote graph';
+ end; end;
+ tfoot; Tr;
+ td colspan => 2, sprintf '%d votes total, average %.2f%s', $count, $total/$count,
+ $type eq 'v' ? ' ('.$self->{votes}[sprintf '%.0f', $total/$count-1].')' : '';
+ end; end;
+ for (reverse 0..$#$stats) {
+ Tr;
+ td class => 'number', $_+1;
+ td class => 'graph';
+ div style => 'width: '.($stats->[$_] ? $stats->[$_]/$max*250 : 0).'px', ' ';
+ txt $stats->[$_];
+ end;
+ end;
+ }
+ end;
+
+ my $recent = $self->dbVoteGet(
+ $type.'id' => $obj->{id},
+ results => 8,
+ order => 'date DESC',
+ what => $type eq 'v' ? 'user' : 'vn',
+ hide => $type eq 'v',
+ );
+ if(@$recent) {
+ table class => 'recentvotes';
+ thead; Tr;
+ td colspan => 3, 'Recent votes';
+ end; end;
+ for (0..$#$recent) {
+ Tr $_ % 2 == 0 ? (class => 'odd') : ();
+ td;
+ if($type eq 'u') {
+ a href => "/v$recent->[$_]{vid}", title => $recent->[$_]{original}||$recent->[$_]{title}, shorten $recent->[$_]{title}, 40;
+ } else {
+ a href => "/u$recent->[$_]{uid}", $recent->[$_]{username};
+ }
+ end;
+ td $recent->[$_]{vote};
+ td date $recent->[$_]{date};
+ end;
+ }
+ end;
+ }
+ clearfloat;
+ end;
+}
+
+
+sub htmlHistory {
+ my($self, $list, $f, $np, $url) = @_;
+ $self->htmlBrowse(
+ items => $list,
+ options => $f,
+ nextpage => $np,
+ pageurl => $url,
+ class => 'history',
+ header => [
+ sub { td colspan => 2, class => 'tc1', 'Rev.' },
+ [ 'Date' ],
+ [ 'User' ],
+ [ 'Page' ],
+ ],
+ row => sub {
+ my($s, $n, $i) = @_;
+ my $tc = [qw|v r p|]->[$i->{type}];
+ my $revurl = "/$tc$i->{iid}.$i->{rev}";
+
+ Tr $n % 2 ? ( class => 'odd' ) : ();
+ td class => 'tc1_1';
+ a href => $revurl, "$tc$i->{iid}";
+ end;
+ td class => 'tc1_2';
+ a href => $revurl, ".$i->{rev}";
+ end;
+ td class => 'tc2', date $i->{added};
+ td class => 'tc3';
+ lit userstr($i);
+ end;
+ td;
+ a href => $revurl, title => $i->{ioriginal}, shorten $i->{ititle}, 80;
+ end;
+ end;
+ if($i->{comments}) {
+ Tr $n % 2 ? ( class => 'odd' ) : ();
+ td colspan => 5, class => 'editsum';
+ lit bb2html $i->{comments}, 150;
+ end;
+ end;
+ }
+ },
+ );
+}
+
+
+1;
diff --git a/lib/VNDB/Util/DB.pm b/lib/VNDB/Util/DB.pm
deleted file mode 100644
index dac01a52..00000000
--- a/lib/VNDB/Util/DB.pm
+++ /dev/null
@@ -1,1582 +0,0 @@
-
-package VNDB::Util::DB;
-
-use strict;
-use warnings;
-use DBI;
-use Exporter 'import';
-
-use vars ('$VERSION', '@EXPORT');
-$VERSION = $VNDB::VERSION;
-
-@EXPORT = qw|
- DBInit DBCheck DBCommit DBRollBack DBExit
- DBLanguageCount DBCategoryCount DBTableCount DBGetHist DBLockItem DBIncId DBAddScreenshot DBGetScreenshot
- DBGetUser DBAddUser DBUpdateUser DBDelUser
- DBGetVotes DBVoteStats DBAddVote DBDelVote
- DBGetVNList DBDelVNList
- DBGetWishList DBEditWishList DBDelWishList
- DBGetRList DBGetRLists DBEditRList DBDelRList
- DBGetVN DBAddVN DBEditVN DBHideVN DBVNCache
- DBGetRelease DBAddRelease DBEditRelease DBHideRelease
- DBGetProducer DBGetProducerVN DBAddProducer DBEditProducer DBHideProducer
- DBGetThreads DBGetPosts DBAddPost DBEditPost DBEditThread DBAddThread
- DBExec DBRow DBAll
-|;
-
-
-
-
-
-#-----------------------------------------------------------------------------#
-# I M P O R T A N T S T U F F #
-#-----------------------------------------------------------------------------#
-
-
-sub new {
- my $me = shift;
-
- my $type = ref($me) || $me;
- $me = bless { o => \@_ }, $type;
-
- $me->DBInit();
-
- return $me;
-}
-
-
-sub DBInit {
- my $self = shift;
- my $info = $self->{_DB} || $self;
-
- $info->{sql} = DBI->connect(@{$self->{o}}, {
- PrintError => 0, RaiseError => 1,
- AutoCommit => 0, pg_enable_utf8 => 1,
- }
- );
-}
-
-
-sub DBCheck {
- my $self = shift;
- my $info = $self->{_DB} || $self;
-
- require Time::HiRes
- if $self->{debug} && !$Time::Hires::VERSION;
- $info->{Queries} = [] if $self->{debug};
- my $start = [Time::HiRes::gettimeofday()] if $self->{debug};
-
- if(!$info->{sql}->ping) {
- warn "Ping failed, reconnecting";
- $self->DBInit;
- }
- $info->{sql}->rollback();
- push(@{$info->{Queries}},
- [ 'ping/rollback', Time::HiRes::tv_interval($start) ])
- if $self->{debug};
-}
-
-
-sub DBCommit {
- my $self = shift;
- my $info = $self->{_DB} || $self;
- my $start = [Time::HiRes::gettimeofday()] if $self->{debug};
- $info->{sql}->commit();
- push(@{$info->{Queries}},
- [ 'commit', Time::HiRes::tv_interval($start) ])
- if $self->{debug};
-}
-
-
-sub DBRollBack {
- my $self = shift;
- my $info = $self->{_DB} || $self;
- $info->{sql}->rollback();
-}
-
-
-sub DBExit {
- my $self = shift;
- my $info = $self->{_DB} || $self;
- $info->{sql}->disconnect();
-}
-
-
-# XXX: this function should be disabled when performance is going to be a problem
-sub DBCategoryCount {
- return {
- (map { map { $_, 0 } keys %{$VNDB::CAT->{$_}[1]} } keys %{$VNDB::CAT}),
- map { $_->{cat}, $_->{cnt} } @{shift->DBAll(q|
- SELECT cat, COUNT(vid) AS cnt
- FROM vn_categories vc
- JOIN vn v ON v.latest = vc.vid
- WHERE v.hidden = FALSE
- GROUP BY cat
- ORDER BY cnt|
- )}
- };
-}
-
-
-# XXX: Above comment also applies to this function
-sub DBLanguageCount {
- return { (map { $_ => 0 } keys %$VNDB::LANG ),
- map { $_->{language} => $_->{count} } @{shift->DBAll(q|
- SELECT rr.language, COUNT(DISTINCT v.id) AS count
- FROM releases_rev rr
- JOIN releases r ON r.latest = rr.id
- JOIN releases_vn rv ON rv.rid = rr.id
- JOIN vn v ON v.id = rv.vid
- WHERE r.hidden = FALSE
- AND v.hidden = FALSE
- AND rr.type <> 2
- AND rr.released <= TO_CHAR('today'::timestamp, 'YYYYMMDD')::integer
- GROUP BY rr.language|)} };
-}
-
-
-sub DBTableCount { # table (users, producers, vn, releases, votes)
- return $_[0]->DBRow(q|
- SELECT COUNT(*) as cnt
- FROM !s
- !W|,
- $_[1],
- $_[1] =~ /producers|vn|releases/ ? { 'hidden = ?' => 0 } : {},
- )->{cnt} - ($_[1] eq 'users' ? 1 : 0);
-}
-
-
-
-# XXX: iid, ititle and hidden columns should be cached if performance will be a problem
-sub DBGetHist { # %options->{ type, id, cid, caused, next, page, results, ip, edits, showhid, what } (Item hist)
- my($s, %o) = @_;
-
- $o{results} ||= $o{next} ? 1 : 50;
- $o{page} ||= 1;
- $o{type} ||= '';
- $o{what} ||= ''; #flags: user iid ititle
- $o{showhid} ||= $o{type} && $o{type} ne 'u' && $o{id} || $o{cid} ? 1 : 0;
-
- my %where = (
- $o{cid} ? (
- 'c.id IN(!l)' => [$o{cid}] ) : (),
- $o{type} eq 'u' ? (
- 'c.requester = ?' => $o{id} ) : (),
-
- $o{type} eq 'v' && !$o{releases} ? ( 'c.type = ?' => 0,
- $o{id} ? ( 'vr.vid = ?' => $o{id} ) : () ) : (),
- $o{type} eq 'v' && $o{releases} ? (
- '((c.type = ? AND vr.vid = ?) OR (c.type = ? AND rv.vid = ?))' => [0,$o{id},1,$o{id}] ) : (),
-
- $o{type} eq 'r' ? ( 'c.type = ?' => 1,
- $o{id} ? ( 'rr.rid = ?' => $o{id} ) : () ) : (),
- $o{type} eq 'p' ? ( 'c.type = ?' => 2,
- $o{id} ? ( 'pr.pid = ?' => $o{id} ) : () ) : (),
-
- $o{caused} ? (
- 'c.causedby = ?' => $o{caused} ) : (),
- $o{ip} ? (
- 'c.ip = ?' => $o{ip} ) : (),
- defined $o{edits} && !$o{edits} ? (
- 'c.rev = ?' => 1 ) : (),
- $o{edits} ? (
- 'c.rev > ?' => 1 ) : (),
-
- # get rid of 'hidden' items
- !$o{showhid} ? (
- '(v.hidden IS NOT NULL AND v.hidden = FALSE OR r.hidden IS NOT NULL AND r.hidden = FALSE OR p.hidden IS NOT NULL AND p.hidden = FALSE)' => 1,
- ) : $o{showhid} == 2 ? (
- '(v.hidden IS NOT NULL AND v.hidden = TRUE OR r.hidden IS NOT NULL AND r.hidden = TRUE OR p.hidden IS NOT NULL AND p.hidden = TRUE)' => 1,
- ) : (),
- );
-
- my $select = 'c.id, c.type, c.added, c.requester, c.comments, c.rev, c.causedby';
- $select .= ', u.username' if $o{what} =~ /user/;
- $select .= ', COALESCE(vr.vid, rr.rid, pr.pid) AS iid' if $o{what} =~ /iid/;
- $select .= ', COALESCE(vr2.title, rr2.title, pr2.name) AS ititle, COALESCE(vr2.original, rr2.original, pr2.original) AS ioriginal' if $o{what} =~ /ititle/;
-
- my $join = '';
- $join .= ' JOIN users u ON u.id = c.requester' if $o{what} =~ /user/;
- $join .= ' LEFT JOIN vn_rev vr ON c.type = 0 AND c.id = vr.id'.
- ' LEFT JOIN releases_rev rr ON c.type = 1 AND c.id = rr.id'.
- ' LEFT JOIN producers_rev pr ON c.type = 2 AND c.id = pr.id' if $o{what} =~ /(iid|ititle)/ || $o{releases} || $o{id} || !$o{showhid};
- # these joins should be optimised away at some point (cache the required columns in changes as mentioned above)
- $join .= ' LEFT JOIN vn v ON v.id = vr.vid'.
- ' LEFT JOIN vn_rev vr2 ON vr2.id = v.latest'.
- ' LEFT JOIN releases r ON r.id = rr.rid'.
- ' LEFT JOIN releases_rev rr2 ON rr2.id = r.latest'.
- ' LEFT JOIN producers p ON p.id = pr.pid'.
- ' LEFT JOIN producers_rev pr2 ON pr2.id = p.latest' if $o{what} =~ /ititle/ || $o{releases} || !$o{showhid};
- $join .= ' LEFT JOIN releases_vn rv ON c.id = rv.rid' if $o{type} eq 'v' && $o{releases};
-
- my $r = $s->DBAll(qq|
- SELECT $select
- FROM changes c
- $join
- !W
- ORDER BY c.id !s
- LIMIT ? OFFSET ?|,
- \%where,
- $o{next} ? 'ASC' : 'DESC',
- $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
- );
- return $r if !wantarray;
- return ($r, 0) if $#$r != $o{results};
- pop @$r;
- return ($r, 1);
-}
-
-
-sub DBLockItem { # table, id, locked
- my($s, $tbl, $id, $l) = @_;
- $s->DBExec(q|
- UPDATE !s
- SET locked = ?
- WHERE id = ?|,
- $tbl, $l?1:0, $id);
-}
-
-
-sub DBIncId { # sequence (this is a rather low-level function... aww heck...)
- return $_[0]->DBRow(q|SELECT nextval(?) AS ni|, $_[1])->{ni};
-}
-
-
-sub DBAddScreenshot { # just returns an ID
- return $_[0]->DBRow(q|INSERT INTO screenshots (status) VALUES(0) RETURNING id|)->{id};
-}
-
-
-sub DBGetScreenshot { # ids
- return $_[0]->DBAll(q|SELECT * FROM screenshots WHERE id IN(!l)|, $_[1]);
-}
-
-
-
-#-----------------------------------------------------------------------------#
-# A U T H / U S E R S T U F F #
-#-----------------------------------------------------------------------------#
-
-
-sub DBGetUser { # %options->{ username mail passwd order firstchar uid results page what }
- my $s = shift;
- my %o = (
- order => 'username ASC',
- page => 1,
- results => 10,
- what => '',
- @_
- );
-
- my %where = (
- 'id > 0' => 1,
- $o{username} ? (
- 'username = ?' => $o{username} ) : (),
- $o{mail} ? (
- 'mail = ?' => $o{mail} ) : (),
- $o{passwd} ? (
- 'passwd = decode(?, \'hex\')' => $o{passwd} ) : (),
- $o{firstchar} ? (
- 'SUBSTRING(username from 1 for 1) = ?' => $o{firstchar} ) : (),
- !$o{firstchar} && defined $o{firstchar} ? (
- 'ASCII(username) < 97 OR ASCII(username) > 122' => 1 ) : (),
- $o{uid} ? (
- 'id = ?' => $o{uid} ) : (),
- !$o{uid} && !$o{username} ? (
- 'id > 0' => 1 ) : (),
- );
-
- my $r = $s->DBAll(q|
- SELECT *
- FROM users u
- !W
- ORDER BY !s
- LIMIT ? OFFSET ?|,
- \%where,
- $o{order},
- $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
- );
-
- # XXX: easy to cache, good performance win
- if($o{what} =~ /list/ && $#$r >= 0) {
- my %r = map {
- $r->[$_]{votes} = 0;
- $r->[$_]{changes} = 0;
- ($r->[$_]{id}, $_)
- } 0..$#$r;
-
- $r->[$r{$_->{uid}}]{votes} = $_->{cnt} for (@{$s->DBAll(q|
- SELECT uid, COUNT(vid) AS cnt
- FROM votes
- WHERE uid IN(!l)
- GROUP BY uid|,
- [ keys %r ]
- )});
-
- $r->[$r{$_->{requester}}]{changes} = $_->{cnt} for (@{$s->DBAll(q|
- SELECT requester, COUNT(id) AS cnt
- FROM changes
- WHERE requester IN(!l)
- GROUP BY requester|,
- [ keys %r ]
- )});
- }
-
- return $r if !wantarray;
- return ($r, 0) if $#$r != $o{results};
- pop @$r;
- return ($r, 1);
-}
-
-
-sub DBAddUser { # username, passwd, mail, rank
- return $_[0]->DBExec(q|
- INSERT INTO users
- (username, passwd, mail, rank, registered)
- VALUES (?, decode(?, 'hex'), ?, ?, ?)|,
- lc($_[1]), $_[2], $_[3], $_[4], time
- );
-}
-
-
-sub DBUpdateUser { # uid, %options->{ columns in users table }
- my $s = shift;
- my $user = shift;
- my %opt = @_;
- my %h;
-
- defined $opt{$_} && ($h{$_.' = ?'} = $opt{$_})
- for (qw| username mail rank flags |);
- $h{'passwd = decode(?, \'hex\')'} = $opt{passwd}
- if defined $opt{passwd};
-
- return 0 if scalar keys %h <= 0;
- return $s->DBExec(q|
- UPDATE users
- !H
- WHERE id = ?|,
- \%h, $user);
-}
-
-
-sub DBDelUser { # uid
- my($s, $id) = @_;
- $s->DBExec($_, $id) for (
- q|DELETE FROM vnlists WHERE uid = ?|,
- q|DELETE FROM rlists WHERE uid = ?|,
- q|DELETE FROM wlists WHERE uid = ?|,
- q|DELETE FROM votes WHERE uid = ?|,
- q|UPDATE changes SET requester = 0 WHERE requester = ?|,
- q|UPDATE threads_posts SET uid = 0 WHERE uid = ?|,
- q|DELETE FROM users WHERE id = ?|
- );
-}
-
-
-
-
-
-
-#-----------------------------------------------------------------------------#
-# V O T E S #
-#-----------------------------------------------------------------------------#
-
-
-sub DBGetVotes { # %options->{ uid vid hide order results page }
- my($s, %o) = @_;
- $o{order} ||= 'n.date DESC';
- $o{results} ||= 50;
- $o{page} ||= 1;
-
- my %where = (
- $o{uid} ? ( 'n.uid = ?' => $o{uid} ) : (),
- $o{vid} ? ( 'n.vid = ?' => $o{vid} ) : (),
- $o{hide} ? ( 'u.flags & ? = ?' => [ $VNDB::UFLAGS->{list}, $VNDB::UFLAGS->{list} ] ) : (),
- );
-
- my $r = $s->DBAll(q|
- SELECT n.vid, vr.title, vr.original, n.vote, n.date, n.uid, u.username
- FROM votes n
- JOIN vn v ON v.id = n.vid
- JOIN vn_rev vr ON vr.id = v.latest
- JOIN users u ON u.id = n.uid
- !W
- ORDER BY !s
- LIMIT ? OFFSET ?|,
- \%where,
- $o{order},
- $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
- );
- return $r if !wantarray;
- return ($r, 0) if $#$r < $o{results};
- pop @$r;
- return ($r, 1);
-}
-
-
-sub DBVoteStats { # uid|vid => id
- my($s, $col, $id) = @_;
- my $r = [ qw| 0 0 0 0 0 0 0 0 0 0 | ];
- $r->[$_->{vote}-1] = $_->{votes} for (@{$s->DBAll(q|
- SELECT vote, COUNT(vote) as votes
- FROM votes
- !W
- GROUP BY vote|,
- $col ? { '!s = ?' => [ $col, $id ] } : {},
- )});
- return $r;
-}
-
-
-sub DBAddVote { # vid, uid, vote
- $_[0]->DBExec(q|
- UPDATE votes
- SET vote = ?
- WHERE vid = ?
- AND uid = ?|,
- $_[3], $_[1], $_[2]
- ) || $_[0]->DBExec(q|
- INSERT INTO votes
- (vid, uid, vote, date)
- VALUES (!l)|,
- [ @_[1..3], time ]
- );
-}
-
-
-sub DBDelVote { # uid, vid # uid = 0 to delete all
- $_[0]->DBExec(q|
- DELETE FROM votes
- !W|,
- { 'vid = ?' => $_[2],
- $_[1] ? ('uid = ?' => $_[1]) : ()
- }
- );
-}
-
-
-
-
-
-#-----------------------------------------------------------------------------#
-# U S E R V I S U A L N O V E L L I S T S #
-#-----------------------------------------------------------------------------#
-
-
-sub DBGetVNList { # %options->{ uid vid hide order results page status }
- my($s, %o) = @_;
- $o{results} ||= 10;
- $o{page} ||= 1;
- $o{order} ||= 'l.date DESC';
-
- my %where = (
- $o{uid} ? (
- 'l.uid = ?' => $o{uid} ) : (),
- $o{vid} ? (
- 'l.vid = ?' => $o{vid} ) : (),
- defined $o{status} ? (
- 'l.status = ?' => $o{status} ) : (),
- $o{hide} ? ( 'u.flags & ? = ?' => [ $VNDB::UFLAGS->{list}, $VNDB::UFLAGS->{list} ] ) : (),
- );
-
- return wantarray ? ([], 0) : [] if !keys %where;
-
- my $r = $s->DBAll(q|
- SELECT l.vid, vr.title, l.status, l.comments, l.date, l.uid, u.username
- FROM vnlists l
- JOIN vn v ON l.vid = v.id
- JOIN vn_rev vr ON vr.id = v.latest
- JOIN users u ON l.uid = u.id
- !W
- ORDER BY !s
- LIMIT ? OFFSET ?|,
- \%where,
- $o{order},
- $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
- );
- return $r if !wantarray;
- return ($r, 0) if $#$r < $o{results};
- pop @$r;
- return ($r, 1);
-}
-
-
-sub DBDelVNList { # uid, @vid # uid = 0 to delete all
- my($s, $uid, @vid) = @_;
- $s->DBExec(q|
- DELETE FROM vnlists
- !W|,
- { 'vid IN (!l)' => [\@vid],
- $uid ? ('uid = ?' => $uid) : ()
- }
- );
-}
-
-
-
-
-
-#-----------------------------------------------------------------------------#
-# U S E R W I S H L I S T S #
-#-----------------------------------------------------------------------------#
-
-
-sub DBGetWishList { # %options->{ uid vid what order page results }
- my($s, %o) = @_;
-
- $o{order} ||= 'wl.wstat ASC';
- $o{page} ||= 1;
- $o{results} ||= 50;
- $o{what} ||= '';
-
- my %where = (
- 'wl.uid = ?' => $o{uid},
- $o{vid} ? ( 'wl.vid = ?' => $o{vid} ) : (),
- );
-
- my $select = 'wl.vid, wl.wstat, wl.added';
- my @join;
- if($o{what} =~ /vn/) {
- $select .= ', vr.title, vr.original';
- push @join, 'JOIN vn v ON v.id = wl.vid',
- 'JOIN vn_rev vr ON vr.id = v.latest';
- }
-
- my $r = $s->DBAll(qq|
- SELECT $select
- FROM wlists wl
- @join
- !W
- ORDER BY !s
- LIMIT ? OFFSET ?|,
- \%where,
- $o{order},
- $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
- );
- return $r if !wantarray;
- return ($r, 0) if $#$r < $o{results};
- pop @$r;
- return ($r, 1);
-}
-
-
-sub DBEditWishList { # %options->{ uid vid wstat }
- my($s, %o) = @_;
- $s->DBExec(q|UPDATE wlists SET wstat = ? WHERE uid = ? AND vid IN(!l)|,
- $o{wstat}, $o{uid}, ref($o{vid}) eq 'ARRAY' ? $o{vid} : [ $o{vid} ])
- ||
- $s->DBExec(q|INSERT INTO wlists (uid, vid, wstat)
- VALUES(!l)|,
- [@o{qw| uid vid wstat |}]);
-}
-
-
-sub DBDelWishList { # uid, vids
- my($s, $uid, $vid) = @_;
- $s->DBExec(q|DELETE FROM wlists WHERE uid = ? AND vid IN(!l)|, $uid, $vid);
-}
-
-
-
-
-
-
-#-----------------------------------------------------------------------------#
-# U S E R R E L E A S E L I S T S #
-#-----------------------------------------------------------------------------#
-
-
-sub DBGetRList { # %options->{ uid rids }
- my($s, %o) = @_;
-
- my %where = (
- 'uid = ?' => $o{uid},
- $o{rids} ? (
- 'rid IN(!l)' => [$o{rids}] ) : (),
- );
-
- return $s->DBAll(q|
- SELECT uid, rid, rstat, vstat
- FROM rlists
- !W|,
- \%where);
-}
-
-
-# separate function, which also fetches VN info and votes
-sub DBGetRLists { # %options->{ uid order char rstat vstat voted page results }
- my($s, %o) = @_;
-
- $o{results} ||= 50;
- $o{page} ||= 1;
-
- # bit ugly...
- my $where = !$o{rstat} && !$o{vstat} ? 'vo.vote IS NOT NULL' : '';
- $where .= ($where?' OR ':'').q|v.id IN(
- SELECT irv.vid
- FROM rlists irl
- JOIN releases ir ON ir.id = irl.rid
- JOIN releases_vn irv ON irv.rid = ir.latest
- !W
- )| if !$o{voted};
- $where = '('.$where.') AND LOWER(SUBSTR(vr.title, 1, 1)) = \''.$o{char}.'\'' if $o{char};
- $where = '('.$where.') AND (ASCII(vr.title) < 97 OR ASCII(vr.title) > 122) AND (ASCII(vr.title) < 65 OR ASCII(vr.title) > 90)' if defined $o{char} && !$o{char};
-
- # WHERE clause for the rlists subquery
- my %where = (
- 'uid = ?' => $o{uid},
- defined $o{rstat} ? ( 'rstat = ?' => $o{rstat} ) : (),
- defined $o{vstat} ? ( 'vstat = ?' => $o{vstat} ) : (),
- );
-
- my $r = $s->DBAll(qq|
- SELECT vr.vid, vr.title, vr.original, v.c_released, v.c_languages, v.c_platforms, COALESCE(vo.vote, 0) AS vote
- FROM vn v
- JOIN vn_rev vr ON vr.id = v.latest
- !s JOIN votes vo ON vo.vid = v.id AND vo.uid = ?
- WHERE $where
- ORDER BY !s
- LIMIT ? OFFSET ?|,
- $o{voted} ? '' : 'LEFT', $o{uid}, # JOIN if we only want votes, LEFT JOIN if we also want rlist items
- $o{voted} ? () : \%where,
- $o{order},
- $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
- );
-
- # now fetch the releases and link them to VNs
- if(@$r) {
- my %vns = map { $_->{rels}=[]; $_->{vid}, $_->{rels} } @$r;
- push @{$vns{$_->{vid}}}, $_ for (@{$s->DBAll(q|
- SELECT rv.vid, rr.rid, rr.title, rr.original, rr.released, rr.type, rr.language, rr.minage, rl.rstat, rl.vstat
- FROM rlists rl
- JOIN releases r ON rl.rid = r.id
- JOIN releases_rev rr ON rr.id = r.latest
- JOIN releases_vn rv ON rv.rid = r.latest
- WHERE rl.uid = ?
- AND rv.vid IN(!l)
- ORDER BY rr.released ASC|,
- $o{uid}, [ keys %vns ]
- )});
- }
-
- return $r if !wantarray;
- return ($r, 0) if $#$r < $o{results};
- pop @$r;
- return ($r, 1);
-}
-
-
-sub DBEditRList { # %options->{ uid rid rstat vstat }
- # rid can only be a arrayref with UPDATE
- my($s, %o) = @_;
- my %s = (
- defined $o{rstat} ? ( 'rstat = ?', $o{rstat} ) : (),
- defined $o{vstat} ? ( 'vstat = ?', $o{vstat} ) : (),
- );
- $o{rstat}||=0;
- $o{vstat}||=0;
-
- $s->DBExec(q|UPDATE rlists !H WHERE uid = ? AND rid IN(!l)|,
- \%s, $o{uid}, ref($o{rid}) eq 'ARRAY' ? $o{rid} : [ $o{rid} ])
- ||
- $s->DBExec(q|INSERT INTO rlists (uid, rid, rstat, vstat)
- VALUES(!l)|,
- [@o{qw| uid rid rstat vstat |}]);
-}
-
-
-sub DBDelRList { # uid, \@rids
- my($s, $uid, $rid) = @_;
- $s->DBExec(q|DELETE FROM rlists WHERE uid = ? AND rid IN(!l)|, $uid, ref($rid) eq 'ARRAY' ? $rid : [ $rid ]);
-}
-
-
-
-
-
-#-----------------------------------------------------------------------------#
-# V I S U A L N O V E L S #
-#-----------------------------------------------------------------------------#
-
-
-sub DBGetVN { # %options->{ id rev char search order results page what cati cate lang platform }
- my $s = shift;
- my %o = (
- page => 1,
- results => 50,
- order => 'vr.title ASC',
- what => '',
- @_ );
-
- my %where = (
- !$o{id} && !$o{rev} ? ( # don't fetch hidden items unless we ask for an ID
- 'v.hidden = ?' => 0 ) : (),
- $o{id} && !ref($o{id}) ? (
- 'v.id = ?' => $o{id} ) : (),
- $o{id} && ref($o{id}) ? (
- 'v.id IN(!l)' => [$o{id}] ) : (),
- $o{rev} ? (
- 'c.rev = ?' => $o{rev} ) : (),
- $o{char} ? (
- 'LOWER(SUBSTR(vr.title, 1, 1)) = ?' => $o{char} ) : (),
- defined $o{char} && !$o{char} ? (
- '(ASCII(vr.title) < 97 OR ASCII(vr.title) > 122) AND (ASCII(vr.title) < 65 OR ASCII(vr.title) > 90)' => 1 ) : (),
- $o{cati} && @{$o{cati}} ? ( q|
- v.id IN(SELECT iv.id
- FROM vn_categories ivc
- JOIN vn iv ON iv.latest = ivc.vid
- WHERE cat IN(!l)
- GROUP BY iv.id
- HAVING COUNT(cat) = ?)| => [ $o{cati}, $#{$o{cati}}+1 ] ) : (),
- $o{cate} && @{$o{cate}} ? ( q|
- v.id NOT IN(SELECT iv.id
- FROM vn_categories ivc
- JOIN vn iv ON iv.latest = ivc.vid
- WHERE cat IN(!l)
- GROUP BY iv.id)| => [ $o{cate} ] ) : (),
- # this needs some proper handling...
- $o{lang} && @{$o{lang}} ? (
- '('.join(' OR ', map "v.c_languages ILIKE '%%$_%%'", @{$o{lang}}).')' => 1 ) : (),
- $o{platform} && @{$o{platform}} ? (
- '('.join(' OR ', map "v.c_platforms ILIKE '%%$_%%'", @{$o{platform}}).')' => 1 ) : (),
- );
-
- if($o{search}) {
- my @w;
- for (split /[ -,]/, $o{search}) {
- s/%//g;
- next if length($_) < 2;
- if(VNDB::GTINType($_)) {
- push @w, 'irr.gtin = ?', $_;
- } else {
- $_ = "%$_%";
- push @w, '(ivr.title ILIKE ? OR ivr.alias ILIKE ? OR irr.title ILIKE ? OR irr.original ILIKE ?)',
- [ $_, $_, $_, $_ ];
- }
- }
- $where{ q|
- v.id IN(SELECT iv.id
- FROM vn iv
- JOIN vn_rev ivr ON iv.latest = ivr.id
- LEFT JOIN releases_vn irv ON irv.vid = iv.id
- LEFT JOIN releases_rev irr ON irr.id = irv.rid
- LEFT JOIN releases ir ON ir.latest = irr.id
- !W
- GROUP BY iv.id)| } = [ \@w ] if @w;
- }
-
- my @join = (
- $o{rev} ?
- 'JOIN vn v ON v.id = vr.vid' :
- 'JOIN vn v ON vr.id = v.latest',
- $o{what} =~ /changes/ || $o{rev} ? (
- 'JOIN changes c ON c.id = vr.id',
- 'JOIN users u ON u.id = c.requester' ) : (),
- $o{what} =~ /relgraph/ ? (
- 'LEFT JOIN relgraph rg ON rg.id = v.rgraph' ) : (),
- );
-
- my $sel = 'v.id, v.locked, v.hidden, v.c_released, v.c_languages, v.c_platforms, vr.title, vr.original, vr.id AS cid';
- $sel .= ', vr.alias, vr.image AS image, vr.img_nsfw, vr.length, vr.desc, vr.l_wp, vr.l_encubed, vr.l_renai, vr.l_vnn' if $o{what} =~ /extended/;
- $sel .= ', c.added, c.requester, c.comments, v.latest, u.username, c.rev, c.causedby' if $o{what} =~ /changes/;
- $sel .= ', v.rgraph, rg.cmap' if $o{what} =~ /relgraph/;
-
- my $r = $s->DBAll(qq|
- SELECT $sel
- FROM vn_rev vr
- @join
- !W
- ORDER BY !s
- LIMIT ? OFFSET ?|,
- \%where,
- $o{order},
- $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
- );
- $_->{c_released} = sprintf '%08d', $_->{c_released} for @$r;
-
- if($o{what} =~ /(?:relations|categories|anime|screenshots)/ && $#$r >= 0) {
- my %r = map {
- $r->[$_]{relations} = [];
- $r->[$_]{categories} = [];
- $r->[$_]{anime} = [];
- $r->[$_]{screenshots} = [];
- ($r->[$_]{cid}, $_)
- } 0..$#$r;
-
- if($o{what} =~ /categories/) {
- push(@{$r->[$r{$_->{vid}}]{categories}}, [ $_->{cat}, $_->{lvl} ]) for (@{$s->DBAll(q|
- SELECT vid, cat, lvl
- FROM vn_categories
- WHERE vid IN(!l)|,
- [ keys %r ]
- )});
- }
-
- if($o{what} =~ /anime/) {
- push(@{$r->[$r{$_->{vid}}]{anime}}, $_) && delete $_->{vid} for (@{$s->DBAll(q|
- SELECT va.vid, a.*
- FROM vn_anime va
- JOIN anime a ON va.aid = a.id
- WHERE va.vid IN(!l)|,
- [ keys %r ]
- )});
- }
-
- if($o{what} =~ /screenshots/) {
- push(@{$r->[$r{$_->{vid}}]{screenshots}}, $_) && delete $_->{vid} for (@{$s->DBAll(q|
- SELECT vs.vid, s.id, vs.nsfw, vs.rid, s.width, s.height
- FROM vn_screenshots vs
- JOIN screenshots s ON vs.scr = s.id
- WHERE vs.vid IN(!l)
- ORDER BY vs.scr|,
- [ keys %r ]
- )});
- }
-
- if($o{what} =~ /relations/) {
- my $rel = $s->DBAll(q|
- SELECT rel.vid1, rel.vid2, rel.relation, vr.title, vr.original
- FROM vn_relations rel
- JOIN vn v ON rel.vid2 = v.id
- JOIN vn_rev vr ON v.latest = vr.id
- WHERE rel.vid1 IN(!l)|,
- [ keys %r ]);
- push(@{$r->[$r{$_->{vid1}}]{relations}}, {
- relation => $_->{relation},
- id => $_->{vid2},
- title => $_->{title},
- original => $_->{original}
- }) for (@$rel);
- }
- }
-
- return $r if !wantarray;
- return ($r, 0) if $#$r != $o{results};
- pop @$r;
- return ($r, 1);
-}
-
-
-sub DBAddVN { # %options->{ comm + _insert_vn_rev }
- my($s, %o) = @_;
-
- my $id = $s->DBRow(q|
- INSERT INTO changes (type, requester, ip, comments)
- VALUES (!l)
- RETURNING id|,
- [ 0, $s->AuthInfo->{id}, $s->ReqIP, $o{comm} ]
- )->{id};
-
- my $vid = $s->DBRow(q|
- INSERT INTO vn (latest)
- VALUES (?)
- RETURNING id|, $id
- )->{id};
-
- _insert_vn_rev($s, $id, $vid, \%o);
-
- return ($vid, $id); # item id, global revision
-}
-
-
-sub DBEditVN { # id, %options->( comm + _insert_vn_rev + uid + causedby }
- my($s, $vid, %o) = @_;
-
- my $c = $s->DBRow(q|
- INSERT INTO changes (type, requester, ip, comments, rev, causedby)
- VALUES (?, ?, ?, ?, (
- SELECT c.rev+1
- FROM changes c
- JOIN vn_rev vr ON vr.id = c.id
- WHERE vr.vid = ?
- ORDER BY c.id DESC
- LIMIT 1
- ), ?)
- RETURNING id, rev|,
- 0, $o{uid}||$s->AuthInfo->{id}, $s->ReqIP, $o{comm}, $vid, $o{causedby}||undef);
-
- _insert_vn_rev($s, $c->{id}, $vid, \%o);
-
- $s->DBExec(q|UPDATE vn SET latest = ? WHERE id = ?|, $c->{id}, $vid);
- return ($c->{rev}, $c->{id}); # local revision, global revision
-}
-
-
-sub _insert_vn_rev { # columns in vn_rev + categories + screenshots + relations
- my($s, $cid, $vid, $o) = @_;
-
- $$o{img_nsfw} = $$o{img_nsfw}?1:0;
- $s->DBExec(q|
- INSERT INTO vn_rev (id, vid, title, original, "desc", alias, image, img_nsfw, length, l_wp, l_encubed, l_renai, l_vnn)
- VALUES (!l)|,
- [ $cid, $vid, @$o{qw|title original desc alias image img_nsfw length l_wp l_encubed l_renai l_vnn|} ]);
-
- $s->DBExec(q|
- INSERT INTO vn_categories (vid, cat, lvl)
- VALUES (?, ?, ?)|,
- $cid, $_->[0], $_->[1]
- ) for (@{$o->{categories}});
-
- $s->DBExec(q|
- INSERT INTO vn_screenshots (vid, scr, nsfw, rid)
- VALUES (?, ?, ?, ?)|,
- $cid, $_->[0], $_->[1]?1:0, $_->[2]
- ) for (@{$o->{screenshots}});
-
- $s->DBExec(q|
- INSERT INTO vn_relations (vid1, vid2, relation)
- VALUES (?, ?, ?)|,
- $cid, $_->[1], $_->[0]
- ) for (@{$o->{relations}});
-
- if(@{$o->{anime}}) {
- $s->DBExec(q|
- INSERT INTO vn_anime (vid, aid)
- VALUES (?, ?)|,
- $cid, $_
- ) for (@{$o->{anime}});
-
- # insert unknown anime
- my $a = $s->DBAll(q|
- SELECT id FROM anime WHERE id IN(!l)|,
- $o->{anime});
- $s->DBExec(q|
- INSERT INTO anime (id) VALUES (?)|, $_
- ) for (grep {
- my $ia = $_;
- !(scalar grep $ia == $_->{id}, @$a)
- } @{$o->{anime}});
- }
-}
-
-
-sub DBHideVN { # id, hidden
- my($s, $id, $h) = @_;
- $s->DBExec(q|
- UPDATE vn
- SET hidden = ?
- WHERE id = ?|,
- $h?1:0, $id);
-}
-
-
-sub DBVNCache { # @vids
- my($s,@vn) = @_;
- $s->DBExec('SELECT update_vncache(?)', $_) for (@vn);
-}
-
-
-
-
-
-#-----------------------------------------------------------------------------#
-# R E L E A S E S #
-#-----------------------------------------------------------------------------#
-
-
-sub DBGetRelease { # %options->{ id vid results page rev }
- my($s, %o) = @_;
-
- $o{results} ||= 50;
- $o{page} ||= 1;
- $o{what} ||= '';
- $o{order} ||= 'rr.released ASC';
- my %where = (
- !$o{id} && !$o{rev} ? (
- 'r.hidden = ?' => 0 ) : (),
- $o{id} ? (
- 'r.id = ?' => $o{id} ) : (),
- $o{rev} ? (
- 'c.rev = ?' => $o{rev} ) : (),
- $o{vid} ? (
- 'rv.vid = ?' => $o{vid} ) : (),
- defined $o{unreleased} ? (
- q|rr.released !s TO_CHAR('today'::timestamp, 'YYYYMMDD')::integer| => $o{unreleased} ? '>' : '<=' ) : (),
- );
-
- my @join;
- push @join, $o{rev} ? 'JOIN releases r ON r.id = rr.rid' : 'JOIN releases r ON rr.id = r.latest';
- push @join, 'JOIN changes c ON c.id = rr.id' if $o{what} =~ /changes/ || $o{rev};
- push @join, 'JOIN users u ON u.id = c.requester' if $o{what} =~ /changes/;
- push @join, 'JOIN releases_vn rv ON rv.rid = rr.id' if $o{vid};
-
- my $select = 'r.id, r.locked, r.hidden, rr.id AS cid, rr.title, rr.original, rr.gtin, rr.language, rr.website, rr.released, rr.notes, rr.minage, rr.type';
- $select .= ', c.added, c.requester, c.comments, r.latest, u.username, c.rev' if $o{what} =~ /changes/;
-
- my $r = $s->DBAll(qq|
- SELECT $select
- FROM releases_rev rr
- @join
- !W
- ORDER BY !s
- LIMIT ? OFFSET ?|,
- \%where,
- $o{order},
- $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
- );
- $_->{released} = sprintf '%08d', $_->{released} for @$r;
-
- if($#$r >= 0 && $o{what} =~ /(vn|producers|platforms|media)/) {
- my %r = map {
- $r->[$_]{producers} = [];
- $r->[$_]{platforms} = [];
- $r->[$_]{media} = [];
- $r->[$_]{vn} = [];
- ($r->[$_]{cid}, $_)
- } 0..$#$r;
-
- if($o{what} =~ /vn/) {
- push(@{$r->[$r{$_->{rid}}]{vn}}, $_) for (@{$s->DBAll(q|
- SELECT rv.rid, vr.vid, vr.title, vr.original
- FROM releases_vn rv
- JOIN vn v ON v.id = rv.vid
- JOIN vn_rev vr ON vr.id = v.latest
- WHERE rv.rid IN(!l)|,
- [ keys %r ]
- )});
- }
-
- if($o{what} =~ /producers/) {
- push(@{$r->[$r{$_->{rid}}]{producers}}, $_) for (@{$s->DBAll(q|
- SELECT rp.rid, p.id, pr.name, pr.original, pr.type
- FROM releases_producers rp
- JOIN producers p ON rp.pid = p.id
- JOIN producers_rev pr ON pr.id = p.latest
- WHERE rp.rid IN(!l)|,
- [ keys %r ]
- )});
- }
- if($o{what} =~ /platforms/) {
- push(@{$r->[$r{$_->{rid}}]{platforms}}, $_->{platform}) for (@{$s->DBAll(q|
- SELECT rid, platform
- FROM releases_platforms
- WHERE rid IN(!l)|,
- [ keys %r ]
- )});
- }
- if($o{what} =~ /media/) {
- ($_->{medium}=~s/\s+//||1)&&push(@{$r->[$r{$_->{rid}}]{media}}, $_) for (@{$s->DBAll(q|
- SELECT rid, medium, qty
- FROM releases_media
- WHERE rid IN(!l)|,
- [ keys %r ]
- )});
- }
- }
-
- return $r if !wantarray;
- return ($r, 0) if $#$r < $o{results};
- pop @$r;
- return ($r, 1);
-}
-
-
-sub DBAddRelease { # options -> { comm + _insert_release_rev }
- my($s, %o) = @_;
-
- my $id = $s->DBRow(q|
- INSERT INTO changes (type, requester, ip, comments)
- VALUES (!l)
- RETURNING id|,
- [ 1, $s->AuthInfo->{id}, $s->ReqIP, $o{comm} ]
- )->{id};
-
- my $rid = $s->DBRow(q|
- INSERT INTO releases (latest)
- VALUES (?)
- RETURNING id|, $id)->{id};
-
- _insert_release_rev($s, $id, $rid, \%o);
- return ($rid, $id); # item id, global revision
-}
-
-
-sub DBEditRelease { # id, %opts->{ comm + _insert_release_rev }
- my($s, $rid, %o) = @_;
-
- my $c = $s->DBRow(q|
- INSERT INTO changes (type, requester, ip, comments, rev)
- VALUES (?, ?, ?, ?, (
- SELECT c.rev+1
- FROM changes c
- JOIN releases_rev rr ON rr.id = c.id
- WHERE rr.rid = ?
- ORDER BY c.id DESC
- LIMIT 1
- ))
- RETURNING id, rev|,
- 1, $s->AuthInfo->{id}, $s->ReqIP, $o{comm}, $rid);
-
- _insert_release_rev($s, $c->{id}, $rid, \%o);
-
- $s->DBExec(q|UPDATE releases SET latest = ? WHERE id = ?|, $c->{id}, $rid);
- return ($c->{rev}, $c->{id}); # local revision, global revision
-}
-
-
-sub _insert_release_rev { # %option->{ columns in releases_rev + producers + platforms + vn + media }
- my($s, $cid, $rid, $o) = @_;
-
- $s->DBExec(q|
- INSERT INTO releases_rev (id, rid, title, original, gtin, language, website, released, notes, minage, type)
- VALUES (!l)|,
- [ $cid, $rid, @$o{qw| title original gtin language website released notes minage type|} ]);
-
- $s->DBExec(q|
- INSERT INTO releases_producers (rid, pid)
- VALUES (?, ?)|,
- $cid, $_
- ) for (@{$o->{producers}});
-
- $s->DBExec(q|
- INSERT INTO releases_platforms (rid, platform)
- VALUES (?, ?)|,
- $cid, $_
- ) for (@{$o->{platforms}});
-
- $s->DBExec(q|
- INSERT INTO releases_vn (rid, vid)
- VALUES (?, ?)|,
- $cid, $_
- ) for (@{$o->{vn}});
-
- $s->DBExec(q|
- INSERT INTO releases_media (rid, medium, qty)
- VALUES (?, ?, ?)|,
- $cid, $_->[0], $_->[1]
- ) for (@{$o->{media}});
-}
-
-
-sub DBHideRelease { # id, hidden
- my($s, $id, $h) = @_;
- $s->DBExec(q|
- UPDATE releases
- SET hidden = ?
- WHERE id = ?|,
- $h?1:0, $id);
-}
-
-
-
-#-----------------------------------------------------------------------------#
-# P R O D U C E R S #
-#-----------------------------------------------------------------------------#
-
-
-sub DBGetProducer { # %options->{ id search char results page rev }
- my($s, %o) = @_;
-
- $o{results} ||= 50;
- $o{page} ||= 1;
- $o{search} =~ s/%//g if $o{search};
- $o{what} ||= '';
- my %where = (
- !$o{id} && !$o{rev} ? (
- 'p.hidden = ?' => 0 ) : (),
- $o{id} ? (
- 'p.id = ?' => $o{id} ) : (),
- $o{search} ? (
- '(pr.name ILIKE ? OR pr.original ILIKE ?)', [ '%%'.$o{search}.'%%', '%%'.$o{search}.'%%' ] ) : (),
- $o{char} ? (
- 'LOWER(SUBSTR(pr.name, 1, 1)) = ?' => $o{char} ) : (),
- defined $o{char} && !$o{char} ? (
- '(ASCII(pr.name) < 97 OR ASCII(pr.name) > 122) AND (ASCII(pr.name) < 65 OR ASCII(pr.name) > 90)' => 1 ) : (),
- $o{rev} ? (
- 'c.rev = ?' => $o{rev} ) : (),
- );
-
- my @join;
- push @join, $o{rev} ? 'JOIN producers p ON p.id = pr.pid' : 'JOIN producers p ON pr.id = p.latest';
- push @join, 'JOIN changes c ON c.id = pr.id' if $o{what} =~ /changes/ || $o{rev};
- push @join, 'JOIN users u ON u.id = c.requester' if $o{what} =~ /changes/;
-
- my $select = 'p.id, p.locked, p.hidden, pr.type, pr.name, pr.original, pr.website, pr.lang, pr.desc';
- $select .= ', c.added, c.requester, c.comments, p.latest, pr.id AS cid, u.username, c.rev' if $o{what} =~ /changes/;
-
- my $r = $s->DBAll(qq|
- SELECT $select
- FROM producers_rev pr
- @join
- !W
- ORDER BY pr.name ASC
- LIMIT ? OFFSET ?|,
- \%where,
- $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
- );
-
- return $r if !wantarray;
- return ($r, 0) if $#$r < $o{results};
- pop @$r;
- return ($r, 1);
-}
-
-
-sub DBGetProducerVN { # pid
- return $_[0]->DBAll(q|
- SELECT v.id, MAX(vr.title) AS title, MAX(vr.original) AS original, MIN(rr.released) AS date
- FROM releases_producers vp
- JOIN releases_rev rr ON rr.id = vp.rid
- JOIN releases r ON r.latest = rr.id
- JOIN releases_vn rv ON rv.rid = rr.id
- JOIN vn v ON v.id = rv.vid
- JOIN vn_rev vr ON vr.id = v.latest
- WHERE vp.pid = ?
- AND v.hidden = ?
- GROUP BY v.id
- ORDER BY date|,
- $_[1], 0);
-}
-
-
-sub DBAddProducer { # %opts->{ comm + _insert_producer_rev }
- my($s, %o) = @_;
-
- my $id = $s->DBRow(q|
- INSERT INTO changes (type, requester, ip, comments)
- VALUES (!l)
- RETURNING id|,
- [ 2, $s->AuthInfo->{id}, $s->ReqIP, $o{comm} ]
- )->{id};
-
- my $pid = $s->DBRow(q|
- INSERT INTO producers (latest)
- VALUES (?)
- RETURNING id|, $id
- )->{id};
-
- _insert_producer_rev($s, $id, $pid, \%o);
-
- return ($pid, $id); # item id, global revision
-}
-
-
-sub DBEditProducer { # id, %opts->{ comm + _insert_producer_rev }
- my($s, $pid, %o) = @_;
-
- my $c = $s->DBRow(q|
- INSERT INTO changes (type, requester, ip, comments, rev)
- VALUES (?, ?, ?, ?, (
- SELECT c.rev+1
- FROM changes c
- JOIN producers_rev pr ON pr.id = c.id
- WHERE pr.pid = ?
- ORDER BY c.id DESC
- LIMIT 1
- ))
- RETURNING id, rev|,
- 2, $s->AuthInfo->{id}, $s->ReqIP, $o{comm}, $pid);
-
- _insert_producer_rev($s, $c->{id}, $pid, \%o);
-
- $s->DBExec(q|UPDATE producers SET latest = ? WHERE id = ?|, $c->{id}, $pid);
- return ($c->{rev}, $c->{id}); # local revision, global revision
-}
-
-
-sub _insert_producer_rev { # %opts->{ columns in produces_rev }
- my($s, $cid, $pid, $o) = @_;
- $s->DBExec(q|
- INSERT INTO producers_rev (id, pid, name, original, website, type, lang, "desc")
- VALUES (!l)|,
- [ $cid, $pid, @$o{qw| name original website type lang desc|} ]);
-}
-
-
-sub DBHideProducer { # id, hidden
- my($s, $id, $h) = @_;
- $s->DBExec(q|
- UPDATE producers
- SET hidden = ?
- WHERE id = ?|,
- $h?1:0, $id);
-}
-
-
-
-
-
-#-----------------------------------------------------------------------------#
-# D I S C U S S I O N S #
-#-----------------------------------------------------------------------------#
-
-
-sub DBGetThreads { # %options->{ id type iid results page what }
- my($s, %o) = @_;
-
- $o{results} ||= 50;
- $o{page} ||= 1;
- $o{what} ||= '';
- $o{order} ||= 't.id DESC';
-
- my %where = (
- $o{id} ? (
- 't.id = ?' => $o{id} ) : (),
- !$o{id} ? (
- 't.hidden = ?' => 0 ) : (),
- $o{type} && !$o{iid} ? (
- 't.id IN(SELECT tid FROM threads_tags WHERE type = ?)' => $o{type} ) : (),
- $o{type} && $o{iid} ? (
- 'tt.type = ?' => $o{type}, 'tt.iid = ?' => $o{iid} ) : (),
- );
-
- my $select = 't.id, t.title, t.count, t.locked, t.hidden';
- $select .= ', tp.uid, tp.date, u.username' if $o{what} =~ /firstpost/;
- $select .= ', tp2.uid AS luid, tp2.date AS ldate, u2.username AS lusername' if $o{what} =~ /lastpost/;
-
- my @join;
- push @join, 'JOIN threads_posts tp ON tp.tid = t.id AND tp.num = 1' if $o{what} =~ /firstpost/;
- push @join, 'JOIN users u ON u.id = tp.uid' if $o{what} =~ /firstpost/;
- push @join, 'JOIN threads_posts tp2 ON tp2.tid = t.id AND tp2.num = t.count' if $o{what} =~ /lastpost/;
- push @join, 'JOIN users u2 ON u2.id = tp2.uid' if $o{what} =~ /lastpost/;
- push @join, 'JOIN threads_tags tt ON tt.tid = t.id' if $o{type} && $o{iid};
-
- my $r = $s->DBAll(qq|
- SELECT $select
- FROM threads t
- @join
- !W
- ORDER BY !s
- LIMIT ? OFFSET ?|,
- \%where,
- $o{order},
- $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
- );
-
- if($o{what} =~ /(tags|tagtitles)/ && $#$r >= 0) {
- my %r = map {
- $r->[$_]{tags} = [];
- ($r->[$_]{id}, $_)
- } 0..$#$r;
-
- if($o{what} =~ /tags/) {
- ($_->{type}=~s/ +//||1) && push(@{$r->[$r{$_->{tid}}]{tags}}, [ $_->{type}, $_->{iid} ]) for (@{$s->DBAll(q|
- SELECT tid, type, iid
- FROM threads_tags
- WHERE tid IN(!l)|,
- [ keys %r ]
- )});
- }
- if($o{what} =~ /tagtitles/) {
- ($_->{type}=~s/ +//||1) && push(@{$r->[$r{$_->{tid}}]{tags}}, [ $_->{type}, $_->{iid}, $_->{title}, $_->{original} ]) for (@{$s->DBAll(q|
- SELECT tt.tid, tt.type, tt.iid, COALESCE(u.username, vr.title, pr.name) AS title, COALESCE(u.username, vr.original, pr.original) AS original
- FROM threads_tags tt
- LEFT JOIN vn v ON tt.type = 'v' AND v.id = tt.iid
- LEFT JOIN vn_rev vr ON vr.id = v.latest
- LEFT JOIN producers p ON tt.type = 'p' AND p.id = tt.iid
- LEFT JOIN producers_rev pr ON pr.id = p.latest
- LEFT JOIN users u ON tt.type = 'u' AND u.id = tt.iid
- WHERE tt.tid IN(!l)|,
- [ keys %r ]
- )});
- }
- }
-
- return $r if !wantarray;
- return ($r, 0) if $#$r < $o{results};
- pop @$r;
- return ($r, 1);
-}
-
-
-sub DBGetPosts { # %options->{ tid num page results }
- my($s, %o) = @_;
-
- $o{results} ||= 50;
- $o{page} ||= 1;
-
- my %where = (
- 'tp.tid = ?' => $o{tid},
- $o{num} ? (
- 'tp.num = ?' => $o{num} ) : (),
- );
-
- my $r = $s->DBAll(q|
- SELECT tp.num, tp.date, tp.edited, tp.msg, tp.hidden, tp.uid, u.username
- FROM threads_posts tp
- JOIN users u ON u.id = tp.uid
- !W
- ORDER BY tp.num ASC
- LIMIT ? OFFSET ?|,
- \%where,
- $o{results}, $o{results}*($o{page}-1)
- );
-
- return $r if !wantarray;
-}
-
-
-sub DBAddPost { # %options->{ tid uid msg num }
- my($s, %o) = @_;
-
- $o{num} ||= $s->DBRow('SELECT num FROM threads_posts WHERE tid = ? ORDER BY num DESC LIMIT 1', $o{tid})->{num}+1;
- $o{uid} ||= $s->AuthInfo->{id};
-
- $s->DBExec(q|
- INSERT INTO threads_posts (tid, num, uid, msg)
- VALUES(?, ?, ?, ?)|,
- @o{qw| tid num uid msg |}
- );
- $s->DBExec(q|
- UPDATE threads
- SET count = count+1
- WHERE id = ?|,
- $o{tid});
-
- return $o{num};
-}
-
-
-sub DBEditPost { # %options->{ tid num msg hidden }
- my($s, %o) = @_;
-
- my %set = (
- 'msg = ?' => $o{msg},
- 'edited = ?' => time,
- 'hidden = ?' => $o{hidden}?1:0,
- );
-
- $s->DBExec(q|
- UPDATE threads_posts
- !H
- WHERE tid = ?
- AND num = ?|,
- \%set, $o{tid}, $o{num}
- );
-}
-
-
-sub DBEditThread { # %options->{ id title locked hidden tags }
- my($s, %o) = @_;
-
- my %set = (
- 'title = ?' => $o{title},
- 'locked = ?' => $o{locked}?1:0,
- 'hidden = ?' => $o{hidden}?1:0,
- );
-
- $s->DBExec(q|
- UPDATE threads
- !H
- WHERE id = ?|,
- \%set, $o{id});
-
- if($o{tags}) {
- $s->DBExec('DELETE FROM threads_tags WHERE tid = ?', $o{id});
- $s->DBExec(q|
- INSERT INTO threads_tags (tid, type, iid)
- VALUES (?, ?, ?)|,
- $o{id}, $_->[0], $_->[1]||0
- ) for (@{$o{tags}});
- }
-}
-
-
-sub DBAddThread { # %options->{ title hidden locked tags }
- my($s, %o) = @_;
-
- my $id = $s->DBRow(q|
- INSERT INTO threads (title, hidden, locked)
- VALUES (?, ?, ?)
- RETURNING id|,
- $o{title}, $o{hidden}?1:0, $o{locked}?1:0
- )->{id};
-
- $s->DBExec(q|
- INSERT INTO threads_tags (tid, type, iid)
- VALUES (?, ?, ?)|,
- $id, $_->[0], $_->[1]
- ) for (@{$o{tags}});
-
- return $id;
-}
-
-
-
-
-
-#-----------------------------------------------------------------------------#
-# U T I L I T I E S #
-#-----------------------------------------------------------------------------#
-
-
-sub DBExec { return sqlhelper(shift, 0, @_); }
-sub DBRow { return sqlhelper(shift, 1, @_); }
-sub DBAll { return sqlhelper(shift, 2, @_); }
-
-sub sqlhelper { # type, query, @list
- my $self = shift;
- my $type = shift;
- my $sqlq = shift;
- my $s = $self->{_DB}->{sql};
-
- my $start = [Time::HiRes::gettimeofday()] if $self->{debug};
-
- $sqlq =~ s/\r?\n/ /g;
- $sqlq =~ s/ +/ /g;
- my(@q) = @_ ? sqlprint(0, $sqlq, @_) : ($sqlq);
- #warn join(', ', map "'$_'", @q)."\n";
-
- my $q = $s->prepare($q[0]);
- $q->execute($#q ? @q[1..$#q] : ());
- my $r = $type == 1 ? $q->fetchrow_hashref :
- $type == 2 ? $q->fetchall_arrayref({}) :
- $q->rows;
- $q->finish();
-
- push(@{$self->{_DB}->{Queries}}, [ $q[0], Time::HiRes::tv_interval($start), @q[1..$#q] ]) if $self->{debug};
-
- $r = 0 if $type == 0 && !$r;
- $r = {} if $type == 1 && (!$r || ref($r) ne 'HASH');
- $r = [] if $type == 2 && (!$r || ref($r) ne 'ARRAY');
-
- return $r;
-}
-
-
-# sqlprint:
-# ? normal placeholder
-# !l list of placeholders, expects arrayref
-# !H list of SET-items, expects hashref or arrayref: format => (bind_value || \@bind_values)
-# !W same as !H, but for WHERE clauses (AND'ed together)
-# !s the classic sprintf %s, use with care
-# This isn't sprintf, so all other things won't work,
-# Only the ? placeholder is supported, so no dollar sign numbers or named placeholders
-# Indeed, this also means you can't use PgSQL operators containing a question mark
-
-sub sqlprint { # start, query, bind values. Returns new query + bind values
- my @a;
- my $q='';
- my $s = shift;
- for my $p (split /(\?|![lHWs])/, shift) {
- next if !defined $p;
- if($p eq '?') {
- push @a, shift;
- $q .= '$'.(@a+$s);
- } elsif($p eq '!s') {
- $q .= shift;
- } elsif($p eq '!l') {
- my $l = shift;
- $q .= join ', ', map '$'.(@a+$s+$_+1), 0..$#$l;
- push @a, @$l;
- } elsif($p eq '!H' || $p eq '!W') {
- my $h=shift;
- my @h=ref $h eq 'HASH' ? %$h : @$h;
- my @r;
- while(my($k,$v) = (shift(@h), shift(@h))) {
- last if !defined $k;
- my($n,@l) = sqlprint($#a+1, $k, ref $v eq 'ARRAY' ? @$v : $v);
- push @r, $n;
- push @a, @l;
- }
- $q .= ($p eq '!W' ? 'WHERE ' : 'SET ').join $p eq '!W' ? ' AND ' : ', ', @r
- if @r;
- } else {
- $q .= $p;
- }
- }
- return($q, @a);
-}
-
-1;
-
diff --git a/lib/VNDB/Util/FormHTML.pm b/lib/VNDB/Util/FormHTML.pm
new file mode 100644
index 00000000..80b8c4f3
--- /dev/null
+++ b/lib/VNDB/Util/FormHTML.pm
@@ -0,0 +1,259 @@
+
+package VNDB::Util::FormHTML;
+
+use strict;
+use warnings;
+use YAWF ':html';
+use Exporter 'import';
+
+our @EXPORT = qw| htmlFormError htmlFormPart htmlForm |;
+
+
+# form error messages
+my %formerr_names = (
+ alias => 'Aliases',
+ anime => 'Anime',
+ desc => 'Description',
+ editsum => 'Edit summary',
+ gtin => 'JAN/EAN/UPC',
+ lang => 'Language',
+ language => 'Language',
+ length => 'Length',
+ l_wp => 'Wikipedia link',
+ l_encubed => 'Novelnews link',
+ l_renai => 'Renai.us link',
+ l_vnn => 'V-N.net link',
+ mail => 'Email',
+ media => 'Media',
+ minage => 'Age rating',
+ msg => 'Message',
+ name => 'Name',
+ notes => 'Notes',
+ original => 'Original',
+ platforms => 'Platforms',
+ producers => 'Producers',
+ released => 'Release date',
+ tags => 'Tags',
+ title => 'Title',
+ type => 'Type',
+ usrname => 'Username',
+ usrpass => 'Password',
+ usrpass2 => 'Password (confirm)',
+ vn => 'Visual novels',
+ website => 'Website',
+);
+my %formerr_exeptions = (
+ login_failed => 'Invalid username or password',
+ nomail => 'No user found with that email address',
+ passmatch => 'Passwords do not match',
+ usrexists => 'Someone already has this username, please choose something else',
+ mailexists => 'Someone already registered with that email address',
+ noimage => 'Image must be in JPEG or PNG format',
+ toolarge => 'Image is too large, only 50kB allowed',
+);
+
+
+# Displays friendly error message when form validation failed
+# Argument is the return value of formValidate, and an optional
+# argument indicating whether we should create a special mainbox
+# for the errors.
+sub htmlFormError {
+ my($self, $frm, $mainbox) = @_;
+ return if !$frm->{_err};
+ if($mainbox) {
+ div class => 'mainbox';
+ h1 'Error';
+ }
+ div class => 'warning';
+ h2 'Form could not be sent:';
+ ul;
+ for my $e (@{$frm->{_err}}) {
+ if(!ref $e) {
+ li $formerr_exeptions{$e};
+ next;
+ }
+ my($field, $type, $rule) = @$e;
+ $field = $formerr_names{$field}||$field;
+ li sprintf '%s is a required field!', $field if $type eq 'required';
+ li sprintf '%s should have at least %d characters', $field, $rule if $type eq 'minlength';
+ li sprintf '%s: only %d characters allowed', $field, $rule if $type eq 'maxlength';
+ li sprintf '%s must be one of the following: %s', $field, join ', ', @$rule if $type eq 'enum';
+ li sprintf 'Wrong tag: %s', $rule if $type eq 'wrongtag';
+ li $rule->[1] if $type eq 'func' || $type eq 'regex';
+ if($type eq 'template') {
+ li sprintf
+ $rule eq 'mail' ? 'Invalid email address' :
+ $rule eq 'url' ? '%s: Invalid URL' :
+ $rule eq 'asciiprint' ? '%s may only contain ASCII characters' :
+ $rule eq 'int' ? '%s: Not a valid number' :
+ $rule eq 'pname' ? '%s can only contain lowercase alphanumberic characters and a hyphen, and must start with a character' : '',
+ $field;
+ }
+ }
+ end;
+ end;
+ end if $mainbox;
+}
+
+
+# Generates a form part.
+# A form part is a arrayref, with the first element being the type of the part,
+# and all other elements forming a hash with options specific to that type.
+# Type Options
+# hidden short, (value)
+# input short, name, (width, pre, post)
+# passwd short, name
+# static content, (label, nolabel)
+# check name, short, (value)
+# select name, short, options, (width)
+# text name, short, (rows, cols)
+# part title
+# TODO: Find a way to write this function in a readable way...
+sub htmlFormPart {
+ my($self, $frm, $fp) = @_;
+ my($type, %o) = @$fp;
+ local $_ = $type;
+
+ if(/hidden/) {
+ Tr class => 'hidden';
+ td colspan => 2;
+ input type => 'hidden', id => $o{short}, name => $o{short}, value => $o{value}||$frm->{$o{short}}||'';
+ end;
+ end;
+ return
+ }
+
+ if(/part/) {
+ Tr class => 'newpart';
+ td colspan => 2, $o{title};
+ end;
+ return;
+ }
+
+ if(/check/) {
+ Tr class => 'newfield';
+ td class => 'label';
+ lit '&nbsp;';
+ end;
+ td class => 'field';
+ input type => 'checkbox', class => 'checkbox', name => $o{short}, id => $o{short},
+ value => $o{value}||'true', $frm->{$o{short}} ? ( checked => 'checked' ) : ();
+ label for => $o{short};
+ lit $o{name};
+ end;
+ end;
+ end;
+ return;
+ }
+
+ Tr $o{name}||$o{label} ? (class => 'newfield') : ();
+ if(!$o{nolabel}) {
+ td class => 'label';
+ if($o{short} && $o{name}) {
+ label for => $o{short}, $o{name} ;
+ } elsif($o{label}) {
+ txt $o{label};
+ } else {
+ lit '&nbsp;';
+ }
+ end;
+ }
+ td class => 'field', $o{nolabel} ? (colspan => 2) : ();
+ if(/input/) {
+ lit $o{pre} if $o{pre};
+ input type => 'text', class => 'text', name => $o{short}, id => $o{short},
+ value => $frm->{$o{short}}||'', $o{width} ? (style => "width: $o{width}px") : ();
+ lit $o{post} if $o{post};
+ }
+ if(/passwd/) {
+ input type => 'password', class => 'text', name => $o{short}, id => $o{short},
+ value => $frm->{$o{short}}||'';
+ }
+ if(/static/) {
+ lit ref $o{content} eq 'CODE' ? $o{content}->($self, \%o) : $o{content};
+ }
+ if(/select/) {
+ Select name => $o{short}, id => $o{short}, $o{width} ? (style => "width: $o{width}px") : ();
+ option value => $_->[0], defined $frm->{$o{short}} && $frm->{$o{short}} eq $_->[0] ? (selected => 'selected') : (), $_->[1]
+ for @{$o{options}};
+ end;
+ }
+ if(/text/) {
+ (my $txt = $frm->{$o{short}}||'') =~ s/&/&amp;/;
+ $txt =~ s/</&lt;/;
+ $txt =~ s/>/&gt;/;
+ textarea name => $o{short}, id => $o{short}, rows => $o{rows}||5, cols => $o{cols}||60;
+ lit $txt;
+ end;
+ }
+ end;
+ end;
+}
+
+
+# Generates a form, first argument is a hashref with global options, keys:
+# frm => the $frm as returned by formValidate,
+# action => The location the form should POST to
+# upload => 1/0, adds an enctype.
+# editsum => 1/0, adds an edit summary field before the submit button
+# The other arguments are a list of subforms in the form
+# of (subform-name => [form parts]). Each subform is shown as a
+# (JavaScript-powered) tab, and has it's own 'mainbox'. This function
+# automatically calls htmlFormError
+sub htmlForm {
+ my($self, $options, @subs) = @_;
+ form action => '/nospam?'.$options->{action}, method => 'post', 'accept-charset' => 'utf-8',
+ $options->{upload} ? (enctype => 'multipart/form-data') : ();
+
+ $self->htmlFormError($options->{frm}, 1);
+
+ # tabs
+ if(@subs > 2) {
+ ul class => 'maintabs notfirst', id => 'jt_select';
+ for (0..$#subs/2) {
+ (my $short = lc $subs[$_*2]) =~ s/[^\w\d]+/_/g;
+ li class => 'left';
+ a href => "#$short", id => "jt_sel_$short", $subs[$_*2];
+ end;
+ }
+ end;
+ }
+
+ # form subs
+ while(my($name, $parts) = (shift(@subs), shift(@subs))) {
+ last if !$name || !$parts;
+ (my $short = lc $name) =~ s/[^\w\d]+/_/g;
+ div class => 'mainbox', id => 'jt_box_'.$short;
+ h1 $name;
+ fieldset;
+ legend $name;
+ table class => 'formtable';
+ $self->htmlFormPart($options->{frm}, $_) for @$parts;
+ end;
+ end;
+ end;
+ }
+
+ # edit summary / submit button
+ div class => 'mainbox';
+ fieldset class => 'submit';
+ if($options->{editsum}) {
+ (my $txt = $options->{frm}{editsum}||'') =~ s/&/&amp;/;
+ $txt =~ s/</&lt;/;
+ $txt =~ s/>/&gt;/;
+ h2 'Edit summary';
+ textarea name => 'editsum', id => 'editsum', rows => 4, cols => 50;
+ lit $txt;
+ end;
+ br;
+ }
+ input type => 'submit', value => 'Submit', class => 'submit';
+ end;
+ end;
+
+ end;
+}
+
+
+1;
+
diff --git a/lib/VNDB/Util/LayoutHTML.pm b/lib/VNDB/Util/LayoutHTML.pm
new file mode 100644
index 00000000..ad0be091
--- /dev/null
+++ b/lib/VNDB/Util/LayoutHTML.pm
@@ -0,0 +1,162 @@
+
+package VNDB::Util::LayoutHTML;
+
+use strict;
+use warnings;
+use YAWF ':html';
+use Exporter 'import';
+use VNDB::Func;
+
+our @EXPORT = qw|htmlHeader htmlFooter|;
+
+
+sub htmlHeader { # %options->{ title, js, noindex, search }
+ my($self, %o) = @_;
+
+ # heading
+ html;
+ head;
+ title $o{title};
+ Link rel => 'shortcut icon', href => '/favicon.ico', type => 'image/x-icon';
+ Link rel => 'stylesheet', href => $self->{url_static}.'/f/style.css', type => 'text/css', media => 'all';
+ if($o{js}) {
+ script type => 'text/javascript', src => $self->{url_static}.'/f/forms.js'; end;
+ }
+ script type => 'text/javascript', src => $self->{url_static}.'/f/script.js';
+ # most browsers don't like a self-closing <script> tag...
+ end;
+ meta name => 'robots', content => 'noindex, follow', undef if $o{noindex};
+ end;
+ body;
+ div id => 'bgright', ' ';
+ div id => 'header';
+ h1 $self->debug ? (class => 'debug') : ();
+ a href => '/', lc $self->{site_title};
+ end;
+ end;
+
+ _menu($self, %o);
+
+ div id => 'maincontent';
+}
+
+
+sub _menu {
+ my($self, %o) = @_;
+
+ div id => 'menulist';
+
+ div class => 'menubox';
+ h2 'Menu';
+ div;
+ a href => '/', 'Home'; br;
+ a href => '/v/all', 'Visual novels'; br;
+ a href => '/p/all', 'Producers'; br;
+ a href => '/u/all', 'Users'; br;
+ a href => '/hist', 'Recent changes'; br;
+ a href => '/t', 'Discussion board'; br;
+ a href => '/d6', 'FAQ'; br;
+ a href => 'irc://irc.synirc.net/vndb', '#vndb';
+ lit ' (<a href="http://cgiirc.synirc.net/?chan=%23vndb">webchat</a>)';
+ end;
+ form action => '/v/all', method => 'get', id => 'search';
+ fieldset;
+ legend 'Search';
+ input type => 'text', class => 'text', id => 'sq', name => 'sq', value => $o{search}||'search';
+ input type => 'submit', class => 'submit', value => 'Seach';
+ end;
+ end;
+ end;
+
+ div class => 'menubox';
+ if($self->authInfo->{id}) {
+ my $uid = sprintf '/u%d', $self->authInfo->{id};
+ h2;
+ a href => $uid, ucfirst $self->authInfo->{username};
+ txt ' ('.$self->{user_ranks}[$self->authInfo->{rank}][0].')';
+ end;
+ div;
+ a href => "$uid/edit", 'My Profile'; br;
+ a href => "$uid/list", 'My Visual Novel List'; br;
+ a href => "$uid/wish", 'My Wishlist'; br;
+ a href => "/t$uid", 'My Messages'; br;
+ a href => "$uid/hist", 'My Recent Changes'; br;
+ br;
+ a href => '/v/new', 'Add Visual Novel'; br;
+ a href => '/p/new', 'Add Producer'; br;
+ br;
+ a href => '/u/logout', 'Logout';
+ end;
+ } else {
+ h2;
+ a href => '/u/login', 'Login';
+ end;
+ div;
+ form action => '/nospam?/u/login', id => 'loginform', method => 'post';
+ fieldset;
+ legend 'Login';
+ input type => 'text', class => 'text', id => 'username', name => 'usrname';
+ input type => 'password', class => 'text', id => 'userpass', name => 'usrpass';
+ input type => 'submit', class => 'submit', value => 'Login';
+ end;
+ end;
+ p;
+ lit 'Need to <a href="/u/register">register</a>,<br />';
+ lit 'or <a href="/u/newpass">forgot your password?</a>';
+ end;
+ end;
+ }
+ end;
+
+ my @stats = (
+ [ vn => 'Visual Novels' ],
+ [ releases => 'Releases' ],
+ [ producers => 'Producers' ],
+ [ users => 'Users' ],
+ [ threads => 'Threads' ],
+ [ posts => 'Posts' ],
+ );
+ my $stats = $self->dbStats;
+ div class => 'menubox';
+ h2 'Database Statistics';
+ div;
+ dl;
+ for (@stats) {
+ dt $$_[1];
+ dd $stats->{$$_[0]};
+ }
+ end;
+ clearfloat;
+ end;
+ end;
+ end;
+}
+
+
+sub htmlFooter {
+ my $self = shift;
+ div id => 'footer';
+ txt "vndb $self->{version} | ";
+ a href => '/d7', 'about us';
+ txt ' | ';
+ a href => 'mailto:contact@vndb.org', 'contact@vndb.org';
+ end;
+ end; # /div maincontent
+ end; # /body
+ end; # /html
+
+ # write the SQL queries as a HTML comment when debugging is enabled
+ if($self->debug) {
+ lit "\n<!--\n SQL Queries:\n";
+ for (@{$self->{_YAWF}{DB}{queries}}) {
+ my $q = !ref $_->[0] ? $_->[0] :
+ $_->[0][0].(exists $_->[0][1] ? ' | "'.join('", "', @{$_->[0]}[1..$#{$_->[0]}]).'"' : '');
+ $q =~ s/^\s//g;
+ lit sprintf " [%6.2fms] %s\n", $_->[1]*1000, $q;
+ }
+ lit "-->\n";
+ }
+}
+
+
+1;
diff --git a/lib/VNDB/Util/Misc.pm b/lib/VNDB/Util/Misc.pm
new file mode 100644
index 00000000..94cdcd1e
--- /dev/null
+++ b/lib/VNDB/Util/Misc.pm
@@ -0,0 +1,28 @@
+
+package VNDB::Util::Misc;
+
+use strict;
+use warnings;
+use Exporter 'import';
+use Tie::ShareLite ':lock';
+
+our @EXPORT = qw|multiCmd|;
+
+
+# Sends a command to Multi
+# Argument: the commands to add to the queue, or none to send the queue to Multi
+sub multiCmd {
+ my $self = shift;
+
+ $self->{_multiCmd} = [] if !$self->{_multiCmd};
+ return push @{$self->{_multiCmd}}, @_ if @_;
+
+ return if !@{$self->{_multiCmd}};
+
+ my $s = tie my %s, 'Tie::ShareLite', -key => $self->{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
+ $s->lock(LOCK_EX);
+ my @q = ( ($s{queue} ? @{$s{queue}} : ()), @{$self->{_multiCmd}} );
+ $s{queue} = \@q;
+ $s->unlock();
+ $self->{_multiCmd} = [];
+}
diff --git a/lib/VNDB/Util/Request.pm b/lib/VNDB/Util/Request.pm
deleted file mode 100644
index 5cffec99..00000000
--- a/lib/VNDB/Util/Request.pm
+++ /dev/null
@@ -1,46 +0,0 @@
-
-package VNDB::Util::Request;
-
-use strict;
-use warnings;
-use Encode;
-use Exporter 'import';
-
-our @EXPORT;
-@EXPORT = qw| ReqParam ReqSaveUpload ReqCookie
- ReqMethod ReqHeader ReqUri ReqIP |;
-
-sub new {
- return bless {}, ref($_[0]) || $_[0];
-}
-sub ReqParam {
- my($s,$n) = @_;
- return wantarray
- ? map { decode 'UTF-8', defined $_ ? $_ : '' } $FCGI::Handler::c->param($n)
- : decode 'UTF-8', defined $FCGI::Handler::c->param($n) ? $FCGI::Handler::c->param($n) : '';
-}
-sub ReqSaveUpload {
- my($s,$n,$f) = @_;
- open my $F, '>', $f or die "Unable to write to $f: $!";
- print $F $FCGI::Handler::c->param($n);
- close $F;
-}
-sub ReqCookie {
- my $c = CGI::Cookie::XS->fetch;
- return $c && ref($c) eq 'HASH' && $c->{$_[1]} ? $c->{$_[1]}[0] : '';
-}
-sub ReqMethod {
- return ($ENV{REQUEST_METHOD}||'') =~ /post/i ? 'POST' : 'GET';
-}
-sub ReqHeader {
- (my $v = uc $_[1]) =~ tr/-/_/;
- return $ENV{"HTTP_$v"}||'';
-}
-sub ReqUri {
- return $ENV{REQUEST_URI};
-}
-sub ReqIP {
- return $ENV{REMOTE_ADDR};
-}
-
-1;
diff --git a/lib/VNDB/Util/Response.pm b/lib/VNDB/Util/Response.pm
deleted file mode 100644
index c5e9a704..00000000
--- a/lib/VNDB/Util/Response.pm
+++ /dev/null
@@ -1,220 +0,0 @@
-
-package VNDB::Util::Response;
-
-
-use strict;
-use warnings;
-use POSIX ();
-use Encode;
-use XML::Writer;
-use Compress::Zlib;
-use Exporter 'import';
-require bytes;
-
-use vars ('$VERSION', '@EXPORT');
-$VERSION = $VNDB::VERSION;
-@EXPORT = qw| ResRedirect ResNotFound ResDenied ResFile
- ResForceBody ResSetContentType ResAddHeader ResAddTpl ResAddDefaultStuff
- ResStartXML ResGetXML ResGetBody ResGet ResGetCGI ResSetModPerl |;
-
-sub new {
- my $self = shift;
- my $tplo = shift;
- my $type = ref($self) || $self;
- my $me = bless {
- headers => [ ],
- contenttype => 'text/html; charset=UTF-8',
- code => 200,
- tplo => $tplo,
- tpl => { },
- body => undef,
- xmlobj => undef,
- xmldata => undef,
- whattouse => 1,
- }, $type;
-
- return $me;
-}
-
-
-## Some ready-to-use methods
-sub ResRedirect {
- my $self = shift;
- my $url = shift; # should start with '/', if no URL specified, use referer or '/'
- my $type = shift;
- my $info = $self->{_Res} || $self;
-
- if(!$url) {
- $url = "/";
- my $ref = $self->ReqHeader('Referer');
- ($url = $ref) =~ s/^$self->{root_url}// if $ref;
- }
-
- my $code = !$type ? 301 :
- $type eq 'temp' ? 307 :
- $type eq 'post' ? 303 : 301;
- $info->{body} = 'Redirecting...';
- $info->{code} = $code;
- $info->{headers} = [ 'Location', "$self->{root_url}$url" ];
- $info->{contenttype} = 'text/html; charset=UTF-8';
- $info->{whattouse} = 1;
-}
-
-sub ResNotFound {
- my $s = shift;
- my $i = $s->{_Res};
- $i->{code} = 404;
- $i->{whattouse} = 2;
- $i->{tpl} = {
- page => { error => {
- err => 'notfound'
- }},
- };
-}
-
-sub ResDenied {
- my $self = shift;
- $self->ResRedirect('/u/register?n=1', 'temp');
-}
-
-sub ResFile {
- my($s,$f,@h) = @_;
- my $i = $s->{_Res};
- $i->{whattouse} = 4;
- $i->{code} = 200;
- $i->{contenttype} = '';
- push @{$i->{headers}},
- 'X-Sendfile' => $f,
- 'Cache-Control' => sprintf('max-age=%d, public', 7*24*3600),
- @h;
-}
-
-## And some often-used methods
-sub ResForceBody {
- my $self = shift;
- my $body = shift;
- my $info = $self->{_Res} || $self;
- $info->{whattouse} = 1;
- $info->{body} = $body;
-}
-
-sub ResSetContentType {
- my $self = shift;
- my $ctype = shift;
- my $info = $self->{_Res} || $self;
- $info->{contenttype} = $ctype;
- return 1;
-}
-
-sub ResAddHeader {
- my $self = shift;
- die("Odd number in parameters, must be in key => value format!") unless ((@_ % 2) == 0);
- my $info = $self->{_Res} || $self;
- $info->{headers} = [ @{$info->{headers}}, @_ ];
- return 1;
-}
-
-sub ResAddTpl {
- my $self = shift;
- die("Odd number in parameters, must be in key=>value format") unless ((@_ % 2) == 0);
- my $info = $self->{_Res} || $self;
- $info->{tpl} = { page => { } } if !$info->{tpl}->{page};
- $info->{tpl}->{page} = { %{$info->{tpl}->{page}}, @_ };
- $info->{whattouse} = 2;
- return 1;
-}
-
-sub ResStartXML {
- my $self = shift;
- my $info = $self->{_Res} || $self;
- $info->{xmldata} = undef;
- $info->{xmlobj} = XML::Writer->new(
- OUTPUT => \$info->{xmldata},
- NEWLINES => 0,
- ENCODING => 'UTF-8',
- DATA_MODE => 1,
- DATA_INDENT => 2,
- );
- $info->{xmlobj}->xmlDecl();
- $info->{contenttype} = "text/xml; charset=UTF-8";
- # disable caching on XML content, IE < 7 has "some" bugs...
- $self->ResAddHeader('Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
- 'Pragma' => 'public');
- $info->{whattouse} = 3;
- return $info->{xmlobj};
-}
-
-## And of course some methods to get the information
-sub ResGetXML {
- my $self = shift;
- my $info = $self->{_Res} || $self;
- return undef if !$info->{xmlobj} || !$info->{xmldata};
- $info->{xmlobj}->end();
- my $tmpvar = $info->{xmldata};
- undef $info->{xmldata};
- return $tmpvar;
-}
-
-sub ResGetBody {
- my $self = shift;
- my $info = $self->{_Res} || $self;
- my $whattouse = shift || $info->{whattouse};
- if($whattouse == 1) { return $info->{body}; }
- if($whattouse == 2) {
- $self->AddDefaultStuff() if exists $info->{tpl}->{page};
- my $start = [Time::HiRes::gettimeofday()] if $self->{debug} && $Time::HiRes::VERSION;
- my $output = $info->{tplo}->compile($info->{tpl});
- $info->{_tpltime} = Time::HiRes::tv_interval($start) if $self->{debug} && $Time::HiRes::VERSION;
- return $output;
- }
- if($whattouse == 3) { return $self->ResGetXML; }
-}
-
-sub ResGet {
- my $self = shift;
- my $info = $self->{_Res} || $self;
- my $whattouse = shift || $info->{whattouse};
-
- return ($info->{code}, $info->{headers}, $info->{contenttype}, $self->ResGetBody($whattouse));
-}
-
-
-my %scodes = (
- # just a few useful codes
- 200 => 'OK',
- 301 => 'Moved Permanently',
- 302 => 'Found',
- 303 => 'See Other',
- 304 => 'Not Modified',
- 307 => 'Temporary Redirect',
- 403 => 'Forbidden',
- 404 => 'Not Found',
- 500 => 'Internal Server Error'
-);
-
-# don't rename!
-sub ResSetModPerl {
- my $s = shift;
- my $i = $s->{_Res};
- printf "Status: %d %s\r\n", $i->{code}, $scodes{$i->{code}};
- print "X-Powered-By: Perl\r\n";
- printf "Content-Type: %s\r\n", $i->{contenttype} if $i->{contenttype};
- my $c=0;
- printf "%s: %s\r\n", $i->{headers}[$c++], $i->{headers}[$c++]
- while ($c<$#{$i->{headers}});
-
- my $b = $s->ResGetBody||'';
- if($b && $s->ReqHeader('Accept-Encoding') =~ /gzip/ && $i->{contenttype} =~ /^text/) {
- my $ol = bytes::length($b) if $s->{debug};
- $b = Compress::Zlib::memGzip(Encode::encode_utf8($b));
- $i->{_gzip} = [ $ol, bytes::length($b) ];
- print "Content-Encoding: gzip\n";
- }
- my $l = bytes::length($b);
- printf "Content-Length: %d\r\n", $l if $l;
- print "\r\n";
- print $b;
- $FCGI::Handler::outputted = 1;
-}
-
-1;
diff --git a/lib/VNDB/Util/Template.pm b/lib/VNDB/Util/Template.pm
deleted file mode 100644
index f9c63998..00000000
--- a/lib/VNDB/Util/Template.pm
+++ /dev/null
@@ -1,235 +0,0 @@
-# VNDB::Util::Template - A direct copy of NTL::Util::Template
-
-# This file has not been edited for at least a year,
-# and there's probably no need to do so in the near future
-
-# template specific stuff:
-# [[ perl code to execute at the specified place ]]
-# [[= perl code, append return value to the template at the specified place ]]
-# [[: same as above, but escape special HTML chars (<, >, &, " and \n) ]]
-# [[% same as above, but also escape as an URL (expects UTF-8 strings) ]]
-# [[! perl code, append at the top of the script (useful for subroutine-declarations etc) ]]
-# [[+ path to a file to include, relative to $searchdir ]]
-
-package VNDB::Util::Template;
-
-use strict;
-use warnings;
-
-use vars ('$VERSION', '@EXPORT');
-$VERSION = $VNDB::VERSION;
-
-
-sub new {
- my $pack = shift;
- my %ops = @_;
- my $me = bless {
- namespace => __PACKAGE__ . '::tpl',
- pre_chomp => 0,
- post_chomp => 0,
- rm_newlines => 0,
- %ops,
- lastreload => 0
- }, ref($pack) || $pack;
-
- $me->{mainfile} = sprintf '%s/%s', $me->{searchdir}, $me->{filename};
-
- die "No filename specified!" if !$me->{filename};
- die "No searchdir specified!" if !$me->{searchdir};
- die "Filename does not exist!" if !-e $me->{mainfile};
- die "No place for the compiled script specified!" if !$me->{compiled};
-
- $me->includescript();
-
- return $me;
-}
-
-sub includescript {
- my $self = shift;
-
- my $dt = 0;
- my $dc = (stat($self->{compiled}))[9] || 0;
-
- if(-s $self->{compiled} && !exists $INC{$self->{compiled}}) {
- eval { require $self->{compiled}; };
- if(!$@) {
- $self->{lastreload} = $dc;
- } else {
- # make sure we can fix the problem and try again
- $INC{$self->{compiled}} = $self->{compiled};
- die $@;
- }
- }
-
- my $T_version = eval(sprintf '$%s::VERSION;', $self->{namespace});
-
- if($dc > $self->{lastreload} || !$T_version) {
- $dt = 1;
- }
- elsif($self->{deep_reload} && $T_version >= 0.1) {
- my @T_files = @{ eval(sprintf '\@%s::T_FILES;', $self->{namespace}) };
- if($#T_files >= 0) {
- foreach (@T_files) {
- if((stat(sprintf('%s/%s', $self->{searchdir}, $_)))[9] > $dc) {
- $dt = 2;
- last;
- }
- }
- }
- } elsif((stat($self->{mainfile}))[9] > $dc) {
- $dt = 2;
- }
- if($dt) {
- $self->compiletpl() if $dt == 2 || $dc <= $self->{lastreload};
- delete $INC{$self->{compiled}};
- eval { require $self->{compiled}; };
- if(!$@) {
- warn "Reloaded template\n";
- } else {
- $INC{$self->{compiled}} = $self->{compiled};
- warn "Template contains errors, not reloading\n";
- }
- $self->{lastreload} = (stat($self->{compiled}))[9];
- }
-}
-
-sub compile {
- my $self = shift;
- my $X = shift;
- $self->includescript();
-
- return $self->{namespace}->compile($X);
-}
-
-sub compiletpl {
- my $self = shift;
- open(my $T, '>', $self->{compiled}) || die sprintf '%s: %s', $self->{compiled}, $!;
- printf $T <<__, __PACKAGE__, $self->{namespace}, ($self->compilefile());
-# Compiled from a template by %s
-package %s;
-
-use strict;
-use warnings;
-no warnings qw(redefine);
-use URI::Escape \'uri_escape_utf8\';
-
-our \$VERSION = 0.1;
-our \@T_FILES = qw| %s |;
-
-sub _hchar { local\$_=shift||return\'\';s/&/&amp;/g;s/</&lt;/g;s/>/&gt;/g;s/"/&quot;/g;s/\\r?\\n/<br \\/>/g;return\$_; }
-sub _huri { _hchar(uri_escape_utf8((scalar shift)||return \'\')) }
-%s
-%s
-%s
-1;
-__
- close($T);
- warn "Recompiled template\n";
-}
-
-sub compilefile {
- my $self = shift;
- my $file = shift||$self->{filename};
- my $func = shift||'compile';
-
- my $files = $file;
- $file = sprintf('%s/%s', $self->{searchdir}, $file);
- open(my $F, '<', $file) || die "$file: $!";
- my $tpl = '';
- $tpl .= $_ while(<$F>);
- close($F);
- my @t = split(//, $tpl);
- $tpl = undef;
-
- my $inperl = 0;
- my $top = '';
- my $R = '';
- my $bottom = '';
- my $dat = '';
- my $perl = '';
-
- for(my $i=0; $i<=$#t; $i++) {
- # [[= (2), [[: (3) and [[% (4)
- if(!$inperl && $t[$i] eq '[' && $t[$i+1] eq '[' && $t[$i+2] =~ /[=:%]/) {
- $i+=2;
- if($t[$i] eq '=') {
- $inperl=2;
- $perl = '\' . ( scalar ';
- } elsif($t[$i] eq ':') {
- $inperl=3;
- $perl = '\' . _hchar( scalar ';
- } else {
- $inperl=4;
- $perl = '\' . _huri( scalar ';
- }
- $R .= $self->_pd($dat);
- } elsif($inperl >= 2 && $inperl <= 4 && $t[$i] eq ']' && $t[$i+1] eq ']') {
- $inperl=0; $i++;
- $R .= $perl . "\n) . '";
- $dat = '';
- # [[! (5)
- } elsif(!$inperl && $t[$i] eq '[' && $t[$i+1] eq '[' && $t[$i+2] eq '!') {
- $inperl=5; $i+=2;
- $perl = '';
- $R .= $self->_pd($dat);
- } elsif($inperl == 5 && $t[$i] eq ']' && $t[$i+1] eq ']') {
- $inperl=0; $i++;
- $top .= $perl . "\n";
- $dat = '';
- # [[+ (6)
- } elsif(!$inperl && $t[$i] eq '[' && $t[$i+1] eq '[' && $t[$i+2] eq '+') {
- $inperl=6; $i+=2;
- $R .= $self->_pd($dat);
- $perl = '';
- } elsif($inperl == 6 && $t[$i] eq ']' && $t[$i+1] eq ']') {
- $inperl=0;$i++;
- $perl =~ s/[\r\n\s]//g;
- die "Invalid file specified: $perl\n" if $perl !~ /^[a-zA-Z0-9-_\.\/]+$/;
- (my $func = $perl) =~ s/[^a-zA-Z0-9_]/_/g;
- my($ifiles, $itop, $imid, $ibot) = $self->compilefile($perl, "T_$func");
- $files .= ' ' . $ifiles;
- $top .= $itop;
- $bottom .= "\n\n$imid\n$ibot\n";
- $R .= "' . T_$func(\$X) . '";
- $dat = '';
- # [[ (1)
- } elsif(!$inperl && $t[$i] eq '[' && $t[$i+1] eq '[') {
- $inperl = 1; $i++;
- $R .= $self->_pd($dat);
- $perl = "';\n";
- } elsif($inperl == 1 && $t[$i] eq ']' && $t[$i+1] eq ']') {
- $inperl=0; $i++;
- $R .= $perl . "\n \$R .= '";
- $dat = '';
- # data
- } elsif(!$inperl) {
- (my $l = $t[$i]) =~ s/'/\\'/;
- $dat .= $l;
- } else {
- $perl .= $t[$i];
- }
- }
- if(!$inperl) {
- $R .= $self->_pd($dat) . "';\n";
- } else {
- die "Error, no ']]' found at $file!\n";
- }
- $R = "sub $func {
- my \$X = \$_[". ($func eq 'compile' ? 1 : 0) . "];
- my \$R = '".$R."
- return \$R;
- }";
- return($files, $top, $R, $bottom);
-}
-
-sub _pd { # Parse Dat
- my $self = shift;
- local $_ = shift;
-
- s/[\r\n\s]+$//g if $_ !~ s/-$// && $self->{pre_chomp};
- s/^[\r\n\s]+//g if $_ !~ s/^-// && $self->{post_chomp};
- s/([\s\t]*)[\r\n]+([\s\t]*)/{ $1||$2?' ':'' }/eg if $self->{rm_newlines};
- return $_;
-}
-
-1;
diff --git a/lib/VNDB/Util/Tools.pm b/lib/VNDB/Util/Tools.pm
deleted file mode 100644
index ee295a67..00000000
--- a/lib/VNDB/Util/Tools.pm
+++ /dev/null
@@ -1,172 +0,0 @@
-
-package VNDB::Util::Tools;
-
-use strict;
-use warnings;
-use Encode;
-use Tie::ShareLite ':lock';
-use Exporter 'import';
-
-our $VERSION = $VNDB::VERSION;
-our @EXPORT = qw| FormCheck AddHid GTINType SendMail AddDefaultStuff RunCmd |;
-
-
-# ...this function could use some serious rewriting
-sub FormCheck {
- my $self = shift;
- my @ps = @_;
- my %hash; my @err;
-
- foreach my $i (0..$#ps) {
- next if !$ps[$i] || ref($ps[$i]) ne 'HASH';
- my $k = $ps[$i]{name};
- $hash{$k} = [ ( $self->ReqParam($k) ) ];
- $hash{$k}[0] = '' if !defined $hash{$k}[0];
- foreach my $j (0..$#{$hash{$k}}) {
- my $val = \$hash{$k}[$j]; my $e = 0;
- $e = 1 if !$e && $ps[$i]{required} && !$$val && length($$val) < 1 && $$val ne '0';
- $e = 2 if !$e && $ps[$i]{minlength} && length($$val) < $ps[$i]{minlength};
- $e = 3 if !$e && $ps[$i]{maxlength} && length($$val) > $ps[$i]{maxlength};
- if(!$e && $ps[$i]{template}) {
- my $t = $ps[$i]{template};
- $hash{$k}[$j] = lc $hash{$k}[$j] if $t eq 'pname';
- $e = 4 if ($t eq 'mail' && $$val !~ # From regexlib.com, author: Gavin Sharp
- /^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+\@((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6}$/)
- || ($t eq 'url' && $$val !~ # From regexlib.com, author: M H
- /^(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:\/~\+#]*[\w\-\@?^=%&\/~\+#])?$/)
- || ($t eq 'pname' && $$val !~ /^[a-z0-9][a-z0-9\-]*$/)
- || ($t eq 'asciiprint' && $$val !~ /^[\x20-\x7E]*$/)
- || ($t eq 'int' && $$val !~ /^\-?[0-9]+$/)
- || ($t eq 'date' && $$val !~ /^[0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?$/)
- || ($t eq 'gtin' && !GTINType($$val));
- }
- $e = 5 if !$e && $ps[$i]{enum} && ref($ps[$i]{enum}) eq "ARRAY" && !_inarray($$val, $ps[$i]{enum});
- if($e) {
- if(!$ps[$i]{required} && !$$val && length($$val) < 1 && $$val ne '0') {
- $hash{$k}[$j] = exists $ps[$i]{default} ? $ps[$i]{default} : undef;
- } else {
- my $errc = $ps[$i]{name}.'-'.$e;
- $errc .= '-'.$ps[$i]{minlength} if $e == 2;
- $errc .= '-'.$ps[$i]{maxlength} if $e == 3;
- $errc .= '-'.$ps[$i]{template} if $e == 4;
- push(@err, $errc);
- last;
- }
- }
- last if !$ps[$i]{multi};
- }
- $hash{$k} = $hash{$k}[0] if !$ps[$i]{multi};
- }
- $hash{_err} = $#err >= 0 ? \@err : 0;
-
- return \%hash;
-}
-
-
-sub AddHid {
- my $fh = $_[0]->FormCheck({ name => 'fh', required => 0, maxlength => 30 })->{fh};
- $_[1]->{_hid} = { map { $_ => 1 } 'com', 'mod', split /,/, $fh }
- if $fh;
-}
-
-
-sub GTINType { # returns 'JAN', 'EAN', 'UPC' or undef
- (my $c = $_[0]) =~ s/^0+//;
- return undef if $c !~ /^[0-9]{12,13}$/; # only gtin-12 and 13
- $c = ('0'x(13-length $c)) . $c; # pad with zeros
-
- # calculate check digit according to
- # http://www.gs1.org/productssolutions/barcodes/support/check_digit_calculator.html#how
- my @n = reverse split //, $c;
- my $n = shift @n;
- $n += $n[$_] * ($_ % 2 != 0 ? 1 : 3) for (0..$#n);
- return undef if $n % 10 != 0;
-
- # Do some rough guesses based on:
- # http://www.gs1.org/productssolutions/barcodes/support/prefix_list.html
- # and http://en.wikipedia.org/wiki/List_of_GS1_country_codes
- local $_ = $c;
- return 'JAN' if /^4[59]/; # prefix code 450-459 & 490-499
- return 'UPC' if /^(?:0[01]|0[6-9]|13|75[45])/; # prefix code 000-019 & 060-139 & 754-755
- return undef if /(?:0[2-5]|2|97[789]|9[6-9])/; # some codes we don't want: 020–059 & 200-299 & 977-999
- return 'EAN'; # let's just call everything else EAN :)
-}
-
-
-sub _inarray { # errr... this is from when I didn't know about grep
- foreach (@{$_[1]}) {
- (return 1) if $_[0] eq $_;
- }
- return 0;
-}
-
-
-sub SendMail {
- my $self = shift;
- my $body = shift;
- my %hs = @_;
-
- die "No To: specified!\n" if !$hs{To};
- die "No Subject specified!\n" if !$hs{Subject};
- $hs{'Content-Type'} ||= 'text/plain; charset=\'UTF-8\'';
- $hs{From} ||= 'vndb <noreply@vndb.org>';
- $hs{'X-mailer'} ||= "VNDB $VERSION";
- $body =~ s/\r?\n/\n/g; # force a '\n'-linebreak
-
- my $mail = '';
- foreach (keys %hs) {
- $hs{$_} =~ s/[\r\n]//g;
- $mail .= sprintf "%s: %s\n", $_, $hs{$_};
- }
- $mail .= sprintf "\n%s", $body;
-
- if(open(my $mailer, "|/usr/sbin/sendmail -t -f '$hs{From}'")) {
- print $mailer encode('UTF-8', $mail);
- die "Error running sendmail ($!)"
- if !close($mailer);
- } else {
- die "Error opening sendail: $!";
- }
-}
-
-
-sub AddDefaultStuff {
- my $self = shift;
-
- $self->AuthAddTpl;
- $self->ResAddTpl(st => $self->{static_url});
-
- $self->ResAddTpl('Stat'.$_, $self->DBTableCount($_))
- for (qw|users producers vn releases|);
-
- # development shit
- if($self->{debug}) {
- my $sqls;
- for (@{$self->{_DB}->{Queries}}) {
- $_->[0] =~ s/^\s//g;
- $sqls .= sprintf("[%6.2fms] %s\n", $_->[1]*1000, $_->[0] || '[undef]');
- $sqls .= " ".join(', ', map "'$_'", @{$_}[2..$#$_])."\n" if exists $$_[2];
- }
- $self->ResAddTpl(devshit => $sqls);
- }
-}
-
-
-# commands aren't actually sent until the function is called without cmd parameter
-sub RunCmd { # cmd
- my($self, $c) = @_;
- if($c) {
- push @{$self->{cmds}}, $c;
- } elsif(@{$self->{cmds}}) {
- my $s = tie my %s, 'Tie::ShareLite', @VNDB::SHMOPTS;
- $s->lock(LOCK_EX);
- my @q = ( ($s{queue} ? @{$s{queue}} : ()), @{$self->{cmds}} );
- $s{queue} = \@q;
- $s->unlock();
- $self->{cmds} = [];
- }
-}
-
-
-1;
-
diff --git a/lib/VNDB/VN.pm b/lib/VNDB/VN.pm
deleted file mode 100644
index c25f97dd..00000000
--- a/lib/VNDB/VN.pm
+++ /dev/null
@@ -1,419 +0,0 @@
-
-package VNDB::VN;
-
-use strict;
-use warnings;
-use Exporter 'import';
-use Digest::MD5;
-require bytes;
-
-use vars ('$VERSION', '@EXPORT');
-$VERSION = $VNDB::VERSION;
-@EXPORT = qw| VNPage VNEdit VNLock VNHide VNBrowse VNXML VNScrXML VNUpdReverse |;
-
-
-sub VNPage {
- my $self = shift;
- my $id = shift;
- my $page = shift || '';
- my $rev = shift || 0;
-
- return $self->ResNotFound if $self->ReqParam('rev');
-
- my $what = 'extended relations categories anime screenshots';
- $what .= ' changes' if $rev;
- $what .= ' relgraph' if $page eq 'rg';
-
- my $v = $self->DBGetVN(
- id => $id,
- what => $what,
- $rev ? ( rev => $rev ) : ()
- )->[0];
- return $self->ResNotFound if !$v->{id};
-
- my $c = $rev && $rev > 1 && $self->DBGetVN(id => $id, rev => $rev-1, what => $what)->[0];
- $v->{next} = $rev && $v->{latest} > $v->{cid} ? $rev+1 : 0;
-
- my $rel = $self->DBGetRelease(vid => $id, what => 'producers platforms');
-
- if(!$page && @$rel && $self->AuthInfo->{id}) {
- my $rl = $self->DBGetRList(
- rids => [ map $_->{id}, @$rel ],
- uid => $self->AuthInfo->{id}
- );
- for my $i (@$rl) {
- my $r = (grep $i->{rid} == $_->{id}, @$rel)[0];
- $r->{rlist} = $i;
- }
- }
-
- $self->ResAddTpl(vnpage => {
- vote => $self->AuthInfo->{id} ? $self->DBGetVotes(uid => $self->AuthInfo->{id}, vid => $id)->[0] : {},
- wlist => $self->AuthInfo->{id} ? $self->DBGetWishList(uid => $self->AuthInfo->{id}, vid => $id)->[0] : {},
- vn => $v,
- rel => $rel,
- prev => $c,
- page => $page,
- change => $rev,
- $page eq 'stats' ? (
- votes => {
- latest => scalar $self->DBGetVotes(vid => $id, results => 10, hide => 1),
- graph => $self->DBVoteStats(vid => $id),
- },
- ) : (),
- });
-}
-
-
-sub VNEdit {
- my $self = shift;
- my $id = shift; # 0 = new
-
- my $rev = $self->FormCheck({ name => 'rev', required => 0, default => 0, template => 'int' });
- return $self->ResNotFound if $rev->{_err};
- $rev = $rev->{rev};
-
- my $v = $self->DBGetVN(id => $id, what => 'extended changes relations categories anime screenshots', $rev ? ( rev => $rev ) : ())->[0] if $id;
- return $self->ResNotFound() if $id && !$v;
-
- return $self->ResDenied if !$self->AuthCan('edit') || ($v->{locked} && !$self->AuthCan('lock'));
-
- my %b4 = $id ? (
- ( map { $_ => $v->{$_} } qw| title original desc alias img_nsfw length l_wp l_encubed l_renai l_vnn | ),
- relations => join('|||', map { $_->{relation}.','.$_->{id}.','.$_->{title} } @{$v->{relations}}),
- categories => join(',', map { $_->[0].$_->[1] } sort { $a->[0] cmp $b->[0] } @{$v->{categories}}),
- anime => join(' ', sort { $a <=> $b } map $_->{id}, @{$v->{anime}}),
- screenshots => join(' ', map sprintf('%d,%d,%d', $$_{id}, $$_{nsfw}?1:0, $$_{rid}||0), @{$v->{screenshots}}),
- ) : ();
-
- my $frm = {};
- if($self->ReqMethod() eq 'POST') {
- $frm = $self->FormCheck(
- { name => 'title', required => 1, maxlength => 250 },
- { name => 'original', required => 0, maxlength => 250 },
- { name => 'alias', required => 0, maxlength => 500, default => '' },
- { name => 'desc', required => 1, maxlength => 10240 },
- { name => 'length', required => 0, enum => [ 0..($#$VNDB::VNLEN+1) ], default => 0 },
- { name => 'l_wp', required => 0, default => '', maxlength => 150 },
- { name => 'l_encubed', required => 0, default => '', maxlength => 100 },
- { name => 'l_renai', required => 0, default => '', maxlength => 100 },
- { name => 'l_vnn', required => 0, default => 0, template => 'int' },
- { name => 'anime', required => 0, default => '' },
- { name => 'img_nsfw', required => 0 },
- { name => 'categories', required => 0, default => '' },
- { name => 'relations', required => 0, default => '' },
- { name => 'screenshots', required => 0, default => '' },
- { name => 'comm', required => 0, default => '' },
- );
- my $relations = [ map { /^([0-9]+),([0-9]+)/ && $2 != $id ? ( [ $1, $2 ] ) : () } split /\|\|\|/, $frm->{relations} ];
- my $cat = [ map { [ substr($_,0,3), substr($_,3,1) ] } split /,/, $frm->{categories} ];
- my $anime = [ grep /^[0-9]+$/, split / +/, $frm->{anime} ];
- my $screenshots = [ map { local $_=[split /,/];$$_[2]||=undef; $_ } grep /^[0-9]+,[01],[0-9]+$/, split / +/, $frm->{screenshots} ];
-
- $frm->{img_nsfw} = $frm->{img_nsfw} ? 1 : 0;
- $frm->{anime} = join ' ', sort { $a <=> $b } @$anime; # re-sort
- $frm->{screenshots} = join ' ', map sprintf('%d,%d,%d', $$_[0], $$_[1]?1:0, $$_[2]||0), sort { $$a[0] <=> $$b[0] } @$screenshots;
-
- return $self->ResRedirect('/v'.$id, 'post')
- if $id && !$self->ReqParam('img') && 14 == scalar grep { $b4{$_} eq $frm->{$_} } keys %b4;
-
- # upload image
- my $imgid = 0;
- if($self->ReqParam('img')) {
- my $tmp = sprintf '%s/00/tmp.%d.jpg', $self->{imgpath}, $$*int(rand(1000)+1);
- $self->ReqSaveUpload('img', $tmp);
-
- my $l;
- open(my $T, '<:raw:bytes', $tmp) || die $1;
- read $T, $l, 2;
- close($T);
-
- $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'nojpeg' ] : [ 'nojpeg' ]
- if $l ne pack('H*', 'ffd8') && $l ne pack('H*', '8950');
- $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'toolarge' ] : [ 'toolarge' ]
- if !$frm->{_err} && -s $tmp > 512*1024; # 500 KB max.
-
- if($frm->{_err}) {
- unlink $tmp;
- } else {
- $imgid = $self->DBIncId('covers_seq');
- my $new = sprintf '%s/%02d/%d.jpg', $self->{imgpath}, $imgid%100, $imgid;
- rename $tmp, $new or die $!;
- chmod 0666, $new;
- $self->RunCmd(sprintf 'coverimage %d', $imgid);
- $imgid = -1*$imgid;
- }
- } elsif($id) {
- $imgid = $v->{image};
- }
-
- my %args = (
- ( map { $_ => $frm->{$_} } qw| title original desc alias comm length l_wp l_encubed l_renai l_vnn img_nsfw| ),
- image => $imgid,
- anime => $anime,
- relations => $relations,
- categories => $cat,
- screenshots => $screenshots,
- );
-
- if(!$frm->{_err}) {
- my($oid, $nrev, $cid) = ($id, 1, 0);
- ($nrev, $cid) = $self->DBEditVN($id, %args) if $id; # edit
- ($id, $cid) = $self->DBAddVN(%args) if !$id; # add
-
- # update reverse relations and relation graph
- if((!$oid && $#$relations >= 0) || ($oid && $frm->{relations} ne $b4{relations})) {
- my %old = $oid ? (map { $_->{id} => $_->{relation} } @{$v->{relations}}) : ();
- my %new = map { $_->[1] => $_->[0] } @$relations;
- $self->VNUpdReverse(\%old, \%new, $id, $cid, $nrev);
- }
- # also regenerate relation graph if the title changes
- elsif(@$relations && $frm->{title} ne $b4{title}) {
- $self->RunCmd('relgraph '.$id);
- }
-
- # check for new anime data
- $self->RunCmd('anime') if $oid && $frm->{anime} ne $b4{anime} || !$oid && $frm->{anime};
-
- $self->RunCmd('ircnotify v'.$id.'.'.$nrev);
- return $self->ResRedirect('/v'.$id.'.'.$nrev, 'post');
- }
- }
-
- if($id) {
- $frm->{$_} ||= $b4{$_} for (keys %b4);
- $frm->{comm} = sprintf 'Reverted to revision v%d.%d', $v->{id}, $v->{rev} if $v->{cid} != $v->{latest};
- }
-
- $self->AddHid($frm);
- $frm->{_hid} = {map{$_=>1} qw| info cat img com |}
- if !$frm->{_hid} && !$id;
- $self->ResAddTpl(vnedit => {
- form => $frm,
- id => $id,
- vn => $v,
- rel => scalar $self->DBGetRelease(vid => $id),
- });
-}
-
-
-sub VNLock {
- my $self = shift;
- my $id = shift;
-
- my $v = $self->DBGetVN(id => $id)->[0];
- return $self->ResNotFound() if !$v;
- return $self->ResDenied if !$self->AuthCan('lock');
- $self->DBLockItem('vn', $id, $v->{locked}?0:1);
- $self->DBLockItem('releases', $_->{id}, $v->{locked}?0:1)
- for (@{$self->DBGetRelease(vid => $id)});
- return $self->ResRedirect('/v'.$id, 'perm');
-}
-
-
-sub VNHide {
- my $self = shift;
- my $id = shift;
-
- my $v = $self->DBGetVN(id => $id, what => 'relations')->[0];
- return $self->ResNotFound() if !$v;
- return $self->ResDenied if !$self->AuthCan('del');
- $self->DBHideVN($id, $v->{hidden}?0:1);
- return $self->ResRedirect('/v'.$id, 'perm');
-}
-
-
-sub VNBrowse {
- my $self = shift;
- my $chr = shift;
- $chr = 'all' if !defined $chr;
-
- my $f = $self->FormCheck(
- { name => 's', required => 0, default => 'title', enum => [ qw|title released votes| ] },
- { name => 'o', required => 0, default => 'a', enum => [ 'a','d' ] },
- { name => 'q', required => 0, default => '' },
- { name => 'sq', required => 0, default => '' },
- { name => 'p', required => 0, template => 'int', default => 1},
- );
- return $self->ResNotFound if $f->{_err};
- $f->{s} = 'title' if $f->{s} eq 'votes';
-
- $f->{q} ||= $f->{sq};
-
- my(@cati, @cate, @plat, @lang);
- my $q = $f->{q};
- if($chr eq 'search') {
- # VNDBID
- return $self->ResRedirect('/'.$1.$2.(!$3 ? '' : $1 eq 'd' ? '#'.$3 : '.'.$3), 'temp')
- if $q =~ /^([vrptud])([0-9]+)(?:\.([0-9]+))?$/;
-
- if(!($q =~ s/^title://)) {
- # categories
- my %catl = map {
- my $ic = $_;
- map { $ic.$_ => $VNDB::CAT->{$ic}[1]{$_} } keys %{$VNDB::CAT->{$ic}[1]}
- } keys %$VNDB::CAT;
-
- $q =~ s/-(?:$catl{$_}|c:$_)//ig && push @cate, $_ for keys %catl;
- $q =~ s/(?:$catl{$_}|c:$_)//ig && push @cati, $_ for keys %catl;
-
- # platforms
- $_ ne 'oth' && $q =~ s/(?:$VNDB::PLAT->{$_}|p:$_)//ig && push @plat, $_ for keys %$VNDB::PLAT;
-
- # languages
- $q =~ s/($VNDB::LANG->{$_}|l:$_)//ig && push @lang, $_ for keys %$VNDB::LANG;
- }
- }
- $q =~ s/ +$//;
- $q =~ s/^ +//;
-
- my($r, $np) = $chr ne 'search' || $q || @lang || @plat || @cati || @cate ? ($self->DBGetVN(
- $chr =~ /^[a-z0]$/ ? ( char => $chr ) : (),
- $q ? ( search => $q ) : (),
- @cati ? ( cati => \@cati ) : (),
- @cate ? ( cate => \@cate ) : (),
- @lang ? ( lang => \@lang ) : (),
- @plat ? ( platform => \@plat ) : (),
- results => 50,
- page => $f->{p},
- order => {title => 'vr.title', released => 'v.c_released',
- }->{$f->{s}}.{a=>' ASC',d=>' DESC'}->{$f->{o}},
- )) : ([], 0);
-
- $self->ResRedirect('/v'.$r->[0]{id}, 'temp')
- if $chr eq 'search' && $#$r == 0;
-
- $self->ResAddTpl(vnbrowse => {
- vn => $r,
- npage => $np,
- page => $f->{p},
- chr => $chr,
- $chr eq 'search' ? (
- cat => $self->DBCategoryCount,
- langc => $self->DBLanguageCount,
- ) : (),
- order => [ $f->{s}, $f->{o} ],
- },
- searchquery => $f->{q});
-}
-
-
-sub VNXML {
- my $self = shift;
-
- my $q = $self->FormCheck(
- { name => 'q', required => 0, maxlength => 100 }
- )->{q};
-
- my $r = [];
- if($q) {
- ($r,undef) = $self->DBGetVN(results => 10,
- $q =~ /^v([0-9]+)$/ ? (id => $1) : (search => $q));
- }
-
- my $x = $self->ResStartXML;
- $x->startTag('vn', results => $#$r+1, query => $q);
- for (@$r) {
- $x->startTag('item');
- $x->dataElement(id => $_->{id});
- $x->dataElement(title => $_->{title});
- $x->endTag('item');
- }
- $x->endTag('vn');
-}
-
-
-sub VNScrXML {
- my $self = shift;
- return $self->ResDenied if !$self->AuthCan('edit');
-
- # check the status of recently uploaded screenshots
- if($self->ReqMethod ne 'POST') {
- my $ids = $self->FormCheck(
- { name => 'id', required => 1, template => 'int', multi => 1 }
- );
- return $self->ResNotFound if $ids->{_err};
- my $r = $self->DBGetScreenshot($ids->{id});
- return $self->ResNotFound if !@$r;
- my $x = $self->ResStartXML;
- $x->startTag('images');
- $x->emptyTag('image', id => $_->{id}, status => $_->{status}, width => $_->{width}, height => $_->{height})
- for (@$r);
- $x->endTag('images');
- return;
- }
-
- # upload new screenshot
- my $i = $self->FormCheck(
- { name => 'itemnumber', required => 1, template => 'int' }
- );
- return $self->ResNotFound if $i->{_err};
- $i = $i->{itemnumber};
-
- my $tmp = sprintf '%s/00/tmp.%d.jpg', $self->{sfpath}, $$*int(rand(1000)+1);
- $self->ReqSaveUpload('scrAddFile'.$i, $tmp);
-
- my $id = 0;
- $id = -2 if !-s $tmp;
- if(!$id) {
- my $l;
- open(my $T, '<:raw:bytes', $tmp) || die $1;
- read $T, $l, 2;
- close($T);
- $id = -1 if $l ne pack('H*', 'ffd8') && $l ne pack('H*', '8950');
- }
-
- if($id) {
- unlink $tmp;
- } else {
- $id = $self->DBAddScreenshot;
- my $new = sprintf '%s/%02d/%d.jpg', $self->{sfpath}, $id%100, $id;
- rename $tmp, $new or die $!;
- chmod 0666, $new;
- $self->RunCmd('screenshot');
- }
-
- my $x = $self->ResStartXML;
- $x->pi('xml-stylesheet', 'href="'.$self->{static_url}.'/files/blank.css" type="text/css"');
- $x->emptyTag('image', id => $id);
-}
-
-
-# Update reverse relations
-sub VNUpdReverse { # old, new, id, cid, rev
- my($self, $old, $new, $id, $cid, $rev) = @_;
- my %upd;
- for (keys %$old, keys %$new) {
- if(exists $$old{$_} and !exists $$new{$_}) {
- $upd{$_} = -1;
- } elsif((!exists $$old{$_} and exists $$new{$_}) || ($$old{$_} != $$new{$_})) {
- $upd{$_} = $$new{$_};
- if($VNDB::VRELW->{$upd{$_}}) { $upd{$_}-- }
- elsif($VNDB::VRELW->{$upd{$_}+1}) { $upd{$_}++ }
- }
- }
-
- for my $i (keys %upd) {
- my $r = $self->DBGetVN(id => $i, what => 'extended relations categories anime screenshots')->[0];
- my @newrel;
- $_->{id} != $id && push @newrel, [ $_->{relation}, $_->{id} ]
- for (@{$r->{relations}});
- push @newrel, [ $upd{$i}, $id ] if $upd{$i} != -1;
- $self->DBEditVN($i,
- relations => \@newrel,
- comm => 'Reverse relation update caused by revision v'.$id.'.'.$rev,
- causedby => $cid,
- uid => 1, # Multi - hardcoded
- anime => [ map $_->{id}, @{$r->{anime}} ],
- screenshots => [ map [ $_->{id}, $_->{nsfw}, $_->{rid} ], @{$r->{screenshots}} ],
- ( map { $_ => $r->{$_} } qw| title original desc alias categories img_nsfw length l_wp l_encubed l_renai l_vnn image | )
- );
- }
-
- $self->RunCmd('relgraph '.join(' ', $id, keys %upd));
-}
-
-
-
-1;
-
diff --git a/lib/VNDB/VNLists.pm b/lib/VNDB/VNLists.pm
deleted file mode 100644
index 0b7a5207..00000000
--- a/lib/VNDB/VNLists.pm
+++ /dev/null
@@ -1,230 +0,0 @@
-
-package VNDB::VNLists;
-
-use strict;
-use warnings;
-use Exporter 'import';
-
-use vars ('$VERSION', '@EXPORT');
-$VERSION = $VNDB::VERSION;
-@EXPORT = qw| VNMyList VNVote RListMod RList WListMod WList |;
-
-
-sub VNMyList {
- my $self = shift;
- my $user = shift;
-
- my $u = $self->DBGetUser(uid => $user)->[0];
- return $self->ResNotFound if !$user || !$u || !$self->AuthInfo->{id} || $self->AuthInfo->{id} != $user;
-
- my $f = $self->FormCheck(
- { name => 's', required => 0, default => 'title', enum => [ qw|title date| ] },
- { name => 'o', required => 0, default => 'a', enum => [ 'a','d' ] },
- { name => 'p', required => 0, template => 'int', default => 1 },
- { name => 't', required => 0, enum => [ -1..$#$VNDB::LSTAT ], default => -1 },
- );
- return $self->ResNotFound if $f->{_err};
-
- if($self->ReqMethod eq 'POST') {
- my $f = $self->FormCheck({ name => 'sel', required => 1, multi => 1, template => 'int' });
- $self->DBDelVNList($user, @{$f->{sel}}) if !$f->{_err};
- }
-
- my $order = $f->{s} . ($f->{o} eq 'a' ? ' ASC' : ' DESC');
- my($list, $np) = $self->DBGetVNList(
- uid => $u->{id},
- order => $order,
- results => 50,
- page => $f->{p},
- $f->{t} >= 0 ? (
- status => $f->{t} ) : ()
- );
-
- $self->ResAddTpl(vnlist => {
- npage => $np,
- page => $f->{p},
- list => $list,
- order => [ $f->{s}, $f->{o} ],
- user => $u,
- status => $f->{t},
- });
-}
-
-
-sub VNVote {
- my $self = shift;
- my $id = shift;
-
- my $uid = $self->AuthInfo()->{id};
- return $self->ResDenied() if !$uid;
-
- my $f = $self->FormCheck(
- { name => 'v', required => 0, default => 0, enum => [ '-1','1'..'10'] }
- );
- return $self->ResNotFound() if !$f->{v};
-
-
- $self->DBDelVote($uid, $id) if $f->{v} == -1 || $self->DBGetVotes(uid => $uid, vid => $id)->[0]{vid};
- $self->DBAddVote($id, $uid, $f->{v}) if $f->{v} > 0;
-
- $self->ResRedirect('/v'.$id, 'temp');
-}
-
-
-sub RListMod {
- my $self = shift;
- my $rid = shift;
-
- my $f = $self->FormCheck(
- { name => 'd', required => 0 },
- { name => 'r', required => 0, enum => [ 0..$#$VNDB::RSTAT ] },
- { name => 'v', required => 0, enum => [ 0..$#$VNDB::VSTAT ] },
- );
-
- return $self->ResNotFound if $f->{_err};
- return $self->ResDenied if !$self->AuthInfo->{id};
-
- if($f->{d}) {
- $self->DBDelRList($self->AuthInfo->{id}, $rid);
- } else {
- $self->DBEditRList(
- uid => $self->AuthInfo->{id},
- rid => $rid,
- rstat => $f->{r},
- vstat => $f->{v},
- );
- }
-
- my $r = $self->ReqHeader('Referer');
- $r = $r && $r =~ /([vr][0-9]+)$/ ? $1 : 'r'.$rid;
- return $self->ResRedirect('/'.$r, 'temp');
-}
-
-
-sub RList {
- my $self = shift;
- my $uid = shift;
-
- my $u = $self->DBGetUser(uid => $uid)->[0];
- return $self->ResNotFound if !$uid || !$u || (($self->AuthInfo->{id}||0) != $uid && !($u->{flags} & $VNDB::UFLAGS->{list}));
-
- my $f = $self->FormCheck(
- { name => 's', required => 0, default => 'title', enum => [ qw|title vote| ] },
- { name => 'o', required => 0, default => 'a', enum => [ 'a','d' ] },
- { name => 'p', required => 0, template => 'int', default => 1 },
- { name => 'c', required => 0, default => 'all', enum => [ 'a'..'z', '0', 'all' ] },
- );
- return $self->ResNotFound if $f->{_err};
-
- if($self->ReqMethod eq 'POST') {
- return $self->ResDenied if $uid != $self->AuthInfo->{id};
- my $frm = $self->FormCheck(
- { name => 'vnlistchange', required => 1, enum => [ 'd', 'r0'..('r'.$#$VNDB::RSTAT), 'v0'..('v'.$#$VNDB::VSTAT) ] },
- { name => 'rsel', required => 1, multi => 1, template => 'int' },
- );
- if(!$frm->{_err} && @{$frm->{rsel}}) {
- $self->DBDelRList($uid, $frm->{rsel}) if $frm->{vnlistchange} eq 'd';
- $self->DBEditRList(
- uid => $uid,
- rid => $frm->{rsel},
- substr($frm->{vnlistchange},0,1).'stat', substr($frm->{vnlistchange},1)
- ) if $frm->{vnlistchange} ne 'd';
- }
- }
-
- my $order = $f->{s} . ($f->{o} eq 'a' ? ' ASC' : ' DESC');
- my($list, $np) = $self->DBGetRLists(
- uid => $uid,
- results => 50,
- page => $f->{p},
- order => $order,
- char => $f->{c} eq 'all' ? undef : $f->{c},
- );
-
- $self->ResAddTpl(rlist => {
- user => $u,
- list => $list,
- char => $f->{c},
- order => [ $f->{s}, $f->{o} ],
- page => $f->{p},
- npage => $np,
- });
-}
-
-
-sub WListMod {
- my $self = shift;
- my $vid = shift;
-
- my $f = $self->FormCheck(
- { name => 'w', required => 1, enum => [ -1..$#$VNDB::RSTAT ] },
- );
-
- return $self->ResNotFound if $f->{_err};
- return $self->ResDenied if !$self->AuthInfo->{id};
-
- if($f->{w} == -1) {
- $self->DBDelWishList($self->AuthInfo->{id}, [ $vid ]);
- } else {
- $self->DBEditWishList(
- uid => $self->AuthInfo->{id},
- vid => $vid,
- wstat => $f->{w}
- );
- }
-
- return $self->ResRedirect('/v'.$vid, 'temp');
-}
-
-
-sub WList {
- my $self = shift;
- my $uid = shift;
-
- my $u = $self->DBGetUser(uid => $uid)->[0];
- return $self->ResNotFound if !$uid || !$u || (($self->AuthInfo->{id}||0) != $uid && !($u->{flags} & $VNDB::UFLAGS->{list}));
-
- my $f = $self->FormCheck(
- { name => 's', required => 0, default => 'title', enum => [ qw|title wstat added| ] },
- { name => 'o', required => 0, default => 'a', enum => [ 'a','d' ] },
- { name => 'p', required => 0, template => 'int', default => 1 },
- );
- return $self->ResNotFound if $f->{_err};
-
- if($self->ReqMethod eq 'POST') {
- return $self->ResDenied if $uid != $self->AuthInfo->{id};
- my $frm = $self->FormCheck(
- { name => 'sel', required => 1, multi => 1, template => 'int' },
- { name => 'vnlistchange', required => 1, enum => [ 'd', '0'.."$#$VNDB::WLIST" ] },
- );
- if(!$frm->{_err} && @{$frm->{sel}}) {
- $self->DBDelWishList($uid, $frm->{sel}) if $frm->{vnlistchange} eq 'd';
- $self->DBEditWishList(
- uid => $uid,
- vid => $frm->{sel},
- wstat => $frm->{vnlistchange}
- ) if $frm->{vnlistchange} ne 'd';
- }
- }
-
- my $order = $f->{s} . ($f->{o} eq 'a' ? ' ASC' : ' DESC');
- $order .= ', title' . ($f->{o} eq 'a' ? ' ASC' : ' DESC') if $f->{s} eq 'wstat';
- my($list, $np) = $self->DBGetWishList(
- uid => $u->{id},
- order => $order,
- results => 50,
- what => 'vn',
- page => $f->{p},
- );
-
- $self->ResAddTpl(wlist => {
- npage => $np,
- page => $f->{p},
- list => $list,
- order => [ $f->{s}, $f->{o} ],
- user => $u,
- });
-}
-
-
-1;
diff --git a/lib/global.pl b/lib/global.pl
deleted file mode 100644
index 355ed5bd..00000000
--- a/lib/global.pl
+++ /dev/null
@@ -1,687 +0,0 @@
-package VNDB;
-
-our @DBLOGIN = ( 'dbi:Pg:dbname=vndb', 'vndb', 'passwd' );
-our @SHMOPTS = ( -key => 'VNDB', -create => 'yes', -destroy => 'no', -mode => 0666);
-our $DEBUG = 1;
-our $VERSION = 'svn';
-our $COOKEY = '73jkS39Sal2)'; # encryption key for cookies (not to worry, this one is fake)
-
-our $MULTI = [
- RG => {},
- Image => {},
- Sitemap => {},
- #Anime => { user => '', pass => '' },
- Maintenance => {},
- #IRC => { user => 'Multi'},
-];
-
-our %VNDBopts = (
- CookieDomain => '.vndb.org',
- root_url => 'http://vndb.org',
- static_url => 'http://static.vndb.org',
- tplopts => {
- filename => 'main',
- searchdir => '/www/vndb/data/tpl',
- compiled => '/www/vndb/data/tplcompiled.pm',
- namespace => 'VNDB::Util::Template::tpl',
- pre_chomp => 1,
- post_chomp => 1,
- rm_newlines => 0,
- deep_reload => 1,
- },
- ranks => [
- [ [ qw| visitor loser user mod admin | ], [] ],
- {map{$_,1}qw| hist |}, # 0 - visitor (not logged in)
- {map{$_,1}qw| hist board |}, # 1 - loser
- {map{$_,1}qw| hist board edit |}, # 2 - user
- {map{$_,1}qw| hist board boardmod edit mod lock del |}, # 3 - mod
- {map{$_,1}qw| hist board boardmod edit mod lock del usermod |}, # 4 - admin
- ],
- postsperpage => 25,
- imgpath => '/www/vndb/static/cv', # cover images
- sfpath => '/www/vndb/static/sf', # full-size screenshots
- stpath => '/www/vndb/static/st', # screenshot thumbnails
- docpath => '/www/vndb/data/docs',
-);
-$VNDBopts{ranks}[0][1] = { (map{$_,1} map { keys %{$VNDBopts{ranks}[$_]} } 1..5) };
-
-
-# I wonder why I even made this hash, almost everything is still hardcoded anyway...
-our $DTAGS = {
- an => 'Announcements', # 0 - usage restricted to boardmods
- db => 'VNDB Discussions', # 0
- v => 'Visual novels', # vid
- p => 'Producers', # pid
- u => 'Users', # uid
-};
-
-
-our $PLAT = {
- win => 'Windows',
- lin => 'Linux',
- mac => 'Mac OS',
- dvd => 'DVD Player',
- gba => 'Game Boy Advance',
- msx => 'MSX',
- nds => 'Nintendo DS',
- nes => 'Famicom',
- psp => 'Playstation Portable',
- ps1 => 'Playstation 1',
- ps2 => 'Playstation 2',
- ps3 => 'Playstation 3',
- drc => 'Dreamcast',
- sfc => 'Super Nintendo',
- wii => 'Nintendo Wii',
- xb3 => 'Xbox 360',
- oth => 'Other'
-};
-
-
-# NOTE: don't forget to update dyna.js
-our $MED = {
- cd => 'CD',
- dvd => 'DVD',
- gdr => 'GD-ROM',
- blr => 'Blu-Ray disk',
- in => 'Internet download',
- pa => 'Patch',
- otc => 'Other (console)',
-};
-
-
-our $PROT = {
- co => 'Company',
- in => 'Individual',
- ng => 'Amateur group',
-};
-
-
-our $RTYP = [
- 'Complete',
- 'Partial',
- 'Trial'
-];
-
-
-# Yes, this is the category list. No, changing something here may
-# not change it on the entire site - many things are still hardcoded
-our $CAT = {
- g => [ 'Gameplay', {
- aa => 'NVL', # 0..1
- ab => 'ADV', # 0..1
- ac => "Act\x{200B}ion", # Ugliest. Hack. Ever.
- rp => 'RPG',
- st => 'Strategy',
- si => 'Simulation',
- }, 2 ],
- p => [ 'Plot', { # 0..1
- li => 'Linear',
- br => 'Branching',
- }, 3 ],
- e => [ 'Elements', {
- ac => 'Action',
- co => 'Comedy',
- dr => 'Drama',
- fa => 'Fantasy',
- ho => 'Horror',
- my => 'Mystery',
- ro => 'Romance',
- sc => 'School Life',
- sf => 'SciFi',
- sj => 'Shoujo Ai',
- sn => 'Shounen Ai',
- }, 1 ],
- t => [ 'Time', { # 0..1
- fu => 'Future',
- pa => 'Past',
- pr => 'Present',
- }, 4 ],
- l => [ 'Place', { # 0..1
- ea => 'Earth',
- fa => "Fant\x{200B}asy world",
- sp => 'Space',
- }, 5 ],
- h => [ 'Protagonist', { # 0..1
- fa => 'Male',
- fe => "Fem\x{200B}ale",
- }, 6 ],
- s => [ 'Sexual content', {
- aa => 'Sexual content',
- be => 'Bestiality',
- in => 'Incest',
- lo => 'Lolicon',
- sh => 'Shotacon',
- ya => 'Yaoi',
- yu => 'Yuri',
- ra => 'Rape',
- }, 7 ],
-};
-
-
-our $RSTAT = [
- 'Unknown',
- 'Pending',
- 'Obtained', # hardcoded
- 'On loan',
- 'Deleted',
-];
-our $VSTAT = [
- 'Unknown',
- 'Playing',
- 'Finished', # hardcoded
- 'Stalled',
- 'Dropped',
-];
-
-our $WSTAT = [
- 'High',
- 'Medium',
- 'Low',
- 'Blacklist',
-];
-
-
-# OLD
-our $LSTAT = [
- 'Wishlist',
- 'Blacklist',
- 'Playing',
- 'Finished',
- 'Stalled',
- 'Dropped',
- 'Other', # XXX: hardcoded at 6
-];
-
-
-our $VREL = [
- 'Sequel',
- 'Prequel', # 1
- 'Same setting',
- 'Alternative setting',
- 'Alternative version',
- 'Same characters',
- 'Side story',
- 'Parent story',# 7
- 'Summary',
- 'Full story', # 9
- 'Other',
-];
-# these reverse relations need a [relation]-1
-our $VRELW = {map{$_=>1}qw| 1 7 9 |};
-
-
-# users.flags
-our $UFLAGS = {
- list => 4,
- nsfw => 8,
-};
-
-
-our $VNLEN = [
- [ 'Unkown', '', '' ],
- [ 'Very short', '< 2 hours', 'OMGWTFOTL, A Dream of Summer' ],
- [ 'Short', '2 - 10 hours', 'Narcissu, Planetarian' ],
- [ 'Medium', '10 - 30 hours', 'Kana: Little Sister' ],
- [ 'Long', '30 - 50 hours', 'Tsukihime' ],
- [ 'Very long', '> 50 hours', 'Clannad' ],
-];
-
-
-our $VRAGES = {
- -1 => 'Unknown',
- 0 => 'All ages',
- map { $_ => $_.'+' } 6..18
-};
-
-
-our $ANITYPE = [
- # VNDB AniDB
- [ 'unknown', 'unknown', ],
- [ 'TV', 'TV Series' ],
- [ 'OVA', 'OVA' ],
- [ 'Movie', 'Movie' ],
- [ 'unknown', 'Other' ],
- [ 'unknown', 'Web' ],
- [ 'TV Special', 'TV Special' ],
- [ 'unknown', 'Music Video' ],
-];
-# AniDB defines:
-# id="1", name="unknown
-# id="2", name="TV Series
-# id="3", name="OVA
-# id="4", name="Movie
-# id="5", name="Other
-# id="6", name="Web
-# id="7", name="TV Special
-# id="8", name="Music Video
-
-
-
-
-
-our $LANG = {
-# 'aa' => q|Afar|,
-# 'ab' => q|Abkhazian|,
-# 'ace' => q|Achinese|,
-# 'ach' => q|Acoli|,
-# 'ada' => q|Adangme|,
-# 'ady' => q|Adyghe|,
-# 'ae' => q|Avestan|,
-# 'af' => q|Afrikaans|,
-# 'afh' => q|Afrihili|,
-# 'ak' => q|Akan|,
-# 'akk' => q|Akkadian|,
-# 'ale' => q|Aleut|,
-# 'alg' => q|Algonquian languages|,
-# 'am' => q|Amharic|,
-# 'an' => q|Aragonese|,
-# 'apa' => q|Apache languages|,
-# 'ar' => q|Arabic|,
-# 'arc' => q|Aramaic|,
-# 'arn' => q|Araucanian|,
-# 'arp' => q|Arapaho|,
-# 'arw' => q|Arawak|,
-# 'as' => q|Assamese|,
-# 'ast' => q|Asturian|,
-# 'ath' => q|Athapascan languages|,
-# 'aus' => q|Australian languages|,
-# 'av' => q|Avaric|,
-# 'awa' => q|Awadhi|,
-# 'ay' => q|Aymara|,
-# 'az' => q|Azerbaijani|,
-# 'ba' => q|Bashkir|,
-# 'bad' => q|Banda|,
-# 'bai' => q|Bamileke languages|,
-# 'bal' => q|Baluchi|,
-# 'ban' => q|Balinese|,
-# 'bas' => q|Basa|,
-# 'be' => q|Belarusian|,
-# 'bej' => q|Beja|,
-# 'bem' => q|Bemba|,
-# 'bg' => q|Bulgarian|,
-# 'bh' => q|Bihari|,
-# 'bho' => q|Bhojpuri|,
-# 'bi' => q|Bislama|,
-# 'bik' => q|Bikol|,
-# 'bin' => q|Bini|,
-# 'bla' => q|Siksika|,
-# 'bm' => q|Bambara|,
-# 'bn' => q|Bengali|,
-# 'bo' => q|Tibetan|,
-# 'br' => q|Breton|,
-# 'bra' => q|Braj|,
-# 'bs' => q|Bosnian|,
-# 'btk' => q|Batak (Indonesia)|,
-# 'bua' => q|Buriat|,
-# 'bug' => q|Buginese|,
-# 'ca' => q|Catalan|,
-# 'cad' => q|Caddo|,
-# 'car' => q|Carib|,
-# 'ce' => q|Chechen|,
-# 'ceb' => q|Cebuano|,
-# 'ch' => q|Chamorro|,
-# 'chb' => q|Chibcha|,
-# 'chg' => q|Chagatai|,
-# 'chk' => q|Chuukese|,
-# 'chm' => q|Mari|,
-# 'chn' => q|Chinook Jargon|,
-# 'cho' => q|Choctaw|,
-# 'chp' => q|Chipewyan|,
-# 'chr' => q|Cherokee|,
-# 'chy' => q|Cheyenne|,
-# 'cmc' => q|Chamic languages|,
-# 'co' => q|Corsican|,
-# 'cop' => q|Coptic|,
-# 'cr' => q|Cree|,
-# 'crh' => q|Crimean Turkish|,
- 'cs' => q|Czech|,
-# 'csb' => q|Kashubian|,
-# 'cu' => q|Church Slavic|,
-# 'cv' => q|Chuvash|,
-# 'cy' => q|Welsh|,
- 'da' => q|Danish|,
-# 'dak' => q|Dakota|,
-# 'dar' => q|Dargwa|,
-# 'day' => q|Dayak|,
- 'de' => q|German|,
-# 'del' => q|Delaware|,
-# 'dgr' => q|Dogrib|,
-# 'din' => q|Dinka|,
-# 'doi' => q|Dogri|,
-# 'dua' => q|Duala|,
-# 'dv' => q|Divehi|,
-# 'dyu' => q|Dyula|,
-# 'dz' => q|Dzongkha|,
-# 'ee' => q|Ewe|,
-# 'efi' => q|Efik|,
-# 'eka' => q|Ekajuk|,
-# 'el' => q|Modern Greek|,
-# 'elx' => q|Elamite|,
- 'en' => q|English|,
-# 'eo' => q|Esperanto|,
- 'es' => q|Spanish|,
-# 'et' => q|Estonian|,
-# 'eu' => q|Basque|,
-# 'ewo' => q|Ewondo|,
-# 'fa' => q|Persian|,
-# 'fan' => q|Fang|,
-# 'fat' => q|Fanti|,
-# 'ff' => q|Fulah|,
- 'fi' => q|Finnish|,
-# 'fj' => q|Fijian|,
-# 'fo' => q|Faroese|,
-# 'fon' => q|Fon|,
- 'fr' => q|French|,
-# 'fur' => q|Friulian|,
-# 'fy' => q|Frisian|,
-# 'ga' => q|Irish|,
-# 'gaa' => q|Ga|,
-# 'gay' => q|Gayo|,
-# 'gba' => q|Gbaya|,
-# 'gd' => q|Scots Gaelic|,
-# 'gez' => q|Geez|,
-# 'gil' => q|Gilbertese|,
-# 'gl' => q|Gallegan|,
-# 'gn' => q|Guarani|,
-# 'gon' => q|Gondi|,
-# 'gor' => q|Gorontalo|,
-# 'got' => q|Gothic|,
-# 'grb' => q|Grebo|,
-# 'grc' => q|Ancient Greek|,
-# 'gu' => q|Gujarati|,
-# 'gv' => q|Manx|,
-# 'gwi' => q|Gwich'in|,
-# 'ha' => q|Hausa|,
-# 'hai' => q|Haida|,
-# 'haw' => q|Hawaiian|,
-# 'he' => q|Hebrew|,
-# 'hi' => q|Hindi|,
-# 'hil' => q|Hiligaynon|,
-# 'him' => q|Himachali|,
-# 'hit' => q|Hittite|,
-# 'hmn' => q|Hmong|,
-# 'ho' => q|Hiri Motu|,
-# 'hr' => q|Croatian|,
-# 'ht' => q|Haitian|,
-# 'hu' => q|Hungarian|,
-# 'hup' => q|Hupa|,
-# 'hy' => q|Armenian|,
-# 'hz' => q|Herero|,
-# 'i-ami' => q|Ami|,
-# 'i-bnn' => q|Bunun|,
-# 'i-klingon' => q|Klingon|,
-# 'i-mingo' => q|Mingo|,
-# 'i-pwn' => q|Paiwan|,
-# 'i-tao' => q|Tao|,
-# 'i-tay' => q|Tayal|,
-# 'i-tsu' => q|Tsou|,
-# 'iba' => q|Iban|,
-# 'id' => q|Indonesian|,
-# 'ie' => q|Interlingue|,
-# 'ig' => q|Igbo|,
-# 'ii' => q|Sichuan Yi|,
-# 'ijo' => q|Ijo|,
-# 'ik' => q|Inupiaq|,
-# 'ilo' => q|Iloko|,
-# 'inh' => q|Ingush|,
-# 'io' => q|Ido|,
-# 'iro' => q|Iroquoian languages|,
-# 'is' => q|Icelandic|,
- 'it' => q|Italian|,
-# 'iu' => q|Inuktitut|,
- 'ja' => q|Japanese|,
-# 'jpr' => q|Judeo-Persian|,
-# 'jrb' => q|Judeo-Arabic|,
-# 'jv' => q|Javanese|,
-# 'ka' => q|Georgian|,
-# 'kaa' => q|Kara-Kalpak|,
-# 'kab' => q|Kabyle|,
-# 'kac' => q|Kachin|,
-# 'kam' => q|Kamba|,
-# 'kar' => q|Karen|,
-# 'kaw' => q|Kawi|,
-# 'kbd' => q|Kabardian|,
-# 'kg' => q|Kongo|,
-# 'kha' => q|Khasi|,
-# 'kho' => q|Khotanese|,
-# 'ki' => q|Kikuyu|,
-# 'kj' => q|Kuanyama|,
-# 'kk' => q|Kazakh|,
-# 'kl' => q|Kalaallisut|,
-# 'km' => q|Khmer|,
-# 'kmb' => q|Kimbundu|,
-# 'kn' => q|Kannada|,
- 'ko' => q|Korean|,
-# 'kok' => q|Konkani|,
-# 'kos' => q|Kosraean|,
-# 'kpe' => q|Kpelle|,
-# 'kr' => q|Kanuri|,
-# 'krc' => q|Karachay-Balkar|,
-# 'kro' => q|Kru|,
-# 'kru' => q|Kurukh|,
-# 'ks' => q|Kashmiri|,
-# 'ku' => q|Kurdish|,
-# 'kum' => q|Kumyk|,
-# 'kut' => q|Kutenai|,
-# 'kv' => q|Komi|,
-# 'kw' => q|Cornish|,
-# 'ky' => q|Kirghiz|,
-# 'la' => q|Latin|,
-# 'lad' => q|Ladino|,
-# 'lah' => q|Lahnda|,
-# 'lam' => q|Lamba|,
-# '#lb' => q|Letzeburgesch|,
-# 'lez' => q|Lezghian|,
-# 'lg' => q|Ganda|,
-# 'li' => q|Limburgish|,
-# 'ln' => q|Lingala|,
-# 'lo' => q|Lao|,
-# 'lol' => q|Mongo|,
-# 'loz' => q|Lozi|,
-# 'lt' => q|Lithuanian|,
-# 'lu' => q|Luba-Katanga|,
-# 'lua' => q|Luba-Lulua|,
-# 'lui' => q|Luiseno|,
-# 'lun' => q|Lunda|,
-# 'luo' => q|Luo (Kenya and Tanzania)|,
-# 'lus' => q|Lushai|,
-# 'lv' => q|Latvian|,
-# 'mad' => q|Madurese|,
-# 'mag' => q|Magahi|,
-# 'mai' => q|Maithili|,
-# 'mak' => q|Makasar|,
-# 'man' => q|Mandingo|,
-# 'mas' => q|Masai|,
-# 'mdf' => q|Moksha|,
-# 'mdr' => q|Mandar|,
-# 'men' => q|Mende|,
-# 'mg' => q|Malagasy|,
-# 'mh' => q|Marshall|,
-# 'mi' => q|Maori|,
-# 'mic' => q|Micmac|,
-# 'min' => q|Minangkabau|,
-# 'mk' => q|Macedonian|,
-# 'ml' => q|Malayalam|,
-# 'mn' => q|Mongolian|,
-# 'mnc' => q|Manchu|,
-# 'mni' => q|Manipuri|,
-# 'mno' => q|Manobo languages|,
-# 'mo' => q|Moldavian|,
-# 'moh' => q|Mohawk|,
-# 'mos' => q|Mossi|,
-# 'mr' => q|Marathi|,
-# 'ms' => q|Malay|,
-# 'mt' => q|Maltese|,
-# 'mul' => q|Multiple languages|,
-# 'mun' => q|Munda languages|,
-# 'mus' => q|Creek|,
-# 'mwr' => q|Marwari|,
-# 'my' => q|Burmese|,
-# 'myn' => q|Mayan languages|,
-# 'myv' => q|Erzya|,
-# 'na' => q|Nauru|,
-# 'nah' => q|Nahuatl|,
-# 'nap' => q|Neapolitan|,
-# 'nb' => q|Norwegian Bokmal|,
-# 'nd' => q|North Ndebele|,
-# 'ne' => q|Nepali|,
-# 'new' => q|Newari|,
-# 'ng' => q|Ndonga|,
-# 'nia' => q|Nias|,
-# 'niu' => q|Niuean|,
- 'nl' => q|Dutch|,
- 'no' => q|Norwegian|,
-# 'nog' => q|Nogai|,
-# 'non' => q|Old Norse|,
-# 'nr' => q|South Ndebele|,
-# 'nso' => q|Northern Sotho|,
-# 'nub' => q|Nubian languages|,
-# 'nv' => q|Navajo|,
-# 'ny' => q|Chichewa|,
-# 'nym' => q|Nyamwezi|,
-# 'nyn' => q|Nyankole|,
-# 'nyo' => q|Nyoro|,
-# 'nzi' => q|Nzima|,
-# 'oj' => q|Ojibwa|,
-# 'om' => q|Oromo|,
-# 'or' => q|Oriya|,
-# 'os' => q|Ossetian; Ossetic|,
-# 'osa' => q|Osage|,
-# 'oto' => q|Otomian languages|,
-# 'pa' => q|Panjabi|,
-# 'pag' => q|Pangasinan|,
-# 'pal' => q|Pahlavi|,
-# 'pam' => q|Pampanga|,
-# 'pap' => q|Papiamento|,
-# 'pau' => q|Palauan|,
-# 'phn' => q|Phoenician|,
-# 'pi' => q|Pali|,
- 'pl' => q|Polish|,
-# 'pon' => q|Pohnpeian|,
-# 'pra' => q|Prakrit languages|,
-# 'ps' => q|Pushto|,
- 'pt' => q|Portuguese|,
-# 'pt-br' => q|Brazilian Portuguese|,
-# 'pt-pt' => q|Portugal Portuguese|,
-# 'qu' => q|Quechua|,
-# 'raj' => q|Rajasthani|,
-# 'rap' => q|Rapanui|,
-# 'rar' => q|Rarotongan|,
-# 'rm' => q|Raeto-Romance|,
-# 'rn' => q|Rundi|,
-# 'ro' => q|Romanian|,
-# 'rom' => q|Romany|,
- 'ru' => q|Russian|,
-# 'rw' => q|Kinyarwanda|,
-# 'sa' => q|Sanskrit|,
-# 'sad' => q|Sandawe|,
-# 'sah' => q|Yakut|,
-# 'sal' => q|Salishan languages|,
-# 'sam' => q|Samaritan Aramaic|,
-# 'sas' => q|Sasak|,
-# 'sat' => q|Santali|,
-# 'sc' => q|Sardinian|,
-# 'sco' => q|Scots|,
-# 'sd' => q|Sindhi|,
-# 'se' => q|Northern Sami|,
-# 'sel' => q|Selkup|,
-# 'sg' => q|Sango|,
-# 'shn' => q|Shan|,
-# 'si' => q|Sinhalese|,
-# 'sid' => q|Sidamo|,
-# 'sio' => q|Siouan languages|,
-# 'sk' => q|Slovak|,
-# 'sl' => q|Slovenian|,
-# 'sm' => q|Samoan|,
-# 'sma' => q|Southern Sami|,
-# 'smj' => q|Lule Sami|,
-# 'smn' => q|Inari Sami|,
-# 'sms' => q|Skolt Sami|,
-# 'sn' => q|Shona|,
-# 'snk' => q|Soninke|,
-# 'so' => q|Somali|,
-# 'sog' => q|Sogdian|,
-# 'son' => q|Songhai|,
-# 'sq' => q|Albanian|,
-# 'sr' => q|Serbian|,
-# 'srr' => q|Serer|,
-# 'ss' => q|Swati|,
-# 'st' => q|Southern Sotho|,
-# 'su' => q|Sundanese|,
-# 'suk' => q|Sukuma|,
-# 'sus' => q|Susu|,
-# 'sux' => q|Sumerian|,
- 'sv' => q|Swedish|,
-# 'sw' => q|Swahili|,
-# 'syr' => q|Syriac|,
-# 'ta' => q|Tamil|,
-# 'te' => q|Telugu|,
-# 'tem' => q|Timne|,
-# 'ter' => q|Tereno|,
-# 'tet' => q|Tetum|,
-# 'tg' => q|Tajik|,
-# 'th' => q|Thai|,
-# 'ti' => q|Tigrinya|,
-# 'tig' => q|Tigre|,
-# 'tiv' => q|Tiv|,
-# 'tk' => q|Turkmen|,
-# 'tkl' => q|Tokelau|,
-# 'tl' => q|Tagalog|,
-# 'tli' => q|Tlingit|,
-# 'tmh' => q|Tamashek|,
-# 'tn' => q|Tswana|,
-# 'to' => q|Tonga (Tonga Islands)|,
-# 'tog' => q|Tonga (Nyasa)|,
-# 'tpi' => q|Tok Pisin|,
- 'tr' => q|Turkish|,
-# 'ts' => q|Tsonga|,
-# 'tsi' => q|Tsimshian|,
-# 'tt' => q|Tatar|,
-# 'tum' => q|Tumbuka|,
-# 'tup' => q|Tupi languages|,
-# 'tvl' => q|Tuvalu|,
-# 'tw' => q|Twi|,
-# 'ty' => q|Tahitian|,
-# 'tyv' => q|Tuvinian|,
-# 'udm' => q|Udmurt|,
-# 'ug' => q|Uighur|,
-# 'uga' => q|Ugaritic|,
-# 'uk' => q|Ukrainian|,
-# 'umb' => q|Umbundu|,
-# 'ur' => q|Urdu|,
-# 'uz' => q|Uzbek|,
-# 'vai' => q|Vai|,
-# 've' => q|Venda|,
-# 'vi' => q|Vietnamese|,
-# 'vo' => q|Volapuk|,
-# 'vot' => q|Votic|,
-# 'wa' => q|Walloon|,
-# 'wak' => q|Wakashan languages|,
-# 'wal' => q|Walamo|,
-# 'war' => q|Waray|,
-# 'was' => q|Washo|,
-# 'wen' => q|Sorbian languages|,
-# 'wo' => q|Wolof|,
-# 'xal' => q|Kalmyk|,
-# 'xh' => q|Xhosa|,
-# 'yao' => q|Yao|,
-# 'yap' => q|Yapese|,
-# 'yi' => q|Yiddish|,
-# 'yo' => q|Yoruba|,
-# 'ypk' => q|Yupik languages|,
-# 'za' => q|Zhuang|,
-# 'zap' => q|Zapotec|,
-# 'zen' => q|Zenaga|,
- 'zh' => q|Chinese|,
-# 'znd' => q|Zande|,
-# 'zu' => q|Zulu|,
-# 'zun' => q|Zuni|,
-};
-
-
-# override config vars
-require '/www/vndb/data/config.pl' if -e '/www/vndb/data/config.pl';
-
-
-1;
-
diff --git a/static/f/bg.jpg b/static/f/bg.jpg
new file mode 100644
index 00000000..f91e414e
--- /dev/null
+++ b/static/f/bg.jpg
Binary files differ
diff --git a/static/f/bgright.jpg b/static/f/bgright.jpg
new file mode 100644
index 00000000..e1dff0cd
--- /dev/null
+++ b/static/f/bgright.jpg
Binary files differ
diff --git a/static/files/blank.css b/static/f/blank.css
index e69de29b..e69de29b 100644
--- a/static/files/blank.css
+++ b/static/f/blank.css
diff --git a/static/f/boxbg.png b/static/f/boxbg.png
new file mode 100644
index 00000000..43b546b5
--- /dev/null
+++ b/static/f/boxbg.png
Binary files differ
diff --git a/static/f/forms.js b/static/f/forms.js
new file mode 100644
index 00000000..190ddf66
--- /dev/null
+++ b/static/f/forms.js
@@ -0,0 +1,871 @@
+// various form functions
+// called by script.js
+
+function qq(v) {
+ return v.replace(/&/g,"&amp;").replace(/</,"&lt;").replace(/>/,"&gt;").replace(/"/g,'&quot;');
+}
+function shorten(v, l) {
+ return qq(v.length > l ? v.substr(0, l-3)+'...' : v);
+}
+var http_request = false;
+function ajax(url, func) {
+ if(http_request)
+ http_request.abort();
+ http_request = (window.ActiveXObject) ? new ActiveXObject('Microsoft.XMLHTTP') : new XMLHttpRequest();
+ if(http_request == null) {
+ alert("Your browse does not support the functionality this website requires.");
+ return;
+ }
+ http_request.onreadystatechange = function() {
+ if(!http_request || http_request.readyState != 4 || !http_request.responseText)
+ return;
+ if(http_request.status != 200)
+ return alert('Whoops, error! :(');
+ func(http_request);
+ };
+ url += (url.indexOf('?')>=0 ? ';' : '?')+(Math.floor(Math.random()*999)+1);
+ http_request.open('GET', url, true);
+ http_request.send(null);
+}
+
+
+
+
+ /************************\
+ * C A T E G O R I E S *
+ \************************/
+
+
+function catLoad() {
+ var i;
+ var cats=[];
+ var ct = x('categories');
+ var l = ct.value.split(',');
+ for(i=0;i<l.length;i++)
+ cats[l[i].substr(0,3)] = Math.floor(l[i].substr(3,1));
+
+ l = x('jt_box_categories').getElementsByTagName('a');
+ for(i=0;i<l.length;i++) {
+ if(l[i].id.substr(0, 4) != 'cat_')
+ continue;
+ catSet(l[i].id.substr(4), cats[l[i].id.substr(4)]||0);
+ l[i].onclick = function() {
+ var c = this.id.substr(4);
+ if(!cats[c]) cats[c] = 0;
+ if(c.substr(0,1) == 'p' || c == 'gaa' || c == 'gab' || c.substr(0,1) == 'h' || c.substr(0,1) == 'l' || c.substr(0,1) == 't') {
+ if(cats[c]++)
+ cats[c] = 0;
+ } else if(++cats[c] == 4)
+ cats[c] = 0;
+ catSet(c, cats[c]);
+
+ // has to be ordered before serializing!
+ var r;l=[];i=0;
+ for(r in cats)
+ l[i++] = r;
+ l = l.sort();
+ r='';
+ for(i=0;i<l.length;i++)
+ if(cats[l[i]] > 0)
+ r+=(r?',':'')+l[i]+cats[l[i]];
+ ct.value = r;
+ return false;
+ };
+ }
+}
+
+function catSet(id, rnk) {
+ // doesn't work very nice with skins...
+ var c = rnk == 0 ? '' :
+ rnk == 1 ? '#0c0' :
+ rnk == 2 ? '#cc0' : '#c00';
+ x('b_'+id).style.color = c;
+ x('cat_'+id).style.color = c;
+ x('b_'+id).innerHTML = rnk;
+}
+
+
+
+
+
+
+ /***********************************\
+ * D R O P D O W N S E A R C H *
+ \***********************************/
+
+
+function dsInit(obj, url, trfunc, serfunc, retfunc) {
+ obj.onkeydown = dsKeyDown;
+ obj.onblur = function() {
+ if(x('ds_box'))
+ x('ds_box').style.top = '-500px';
+ };
+ // all local data is stored in the DOM input object
+ obj.returnFunc = retfunc;
+ obj.trFunc = trfunc;
+ obj.serFunc = serfunc;
+ obj.searchURL = url;
+ obj.selectedId = 0;
+}
+
+function dsKeyDown(ev) {
+ var c = document.layers ? ev.which : document.all ? event.keyCode : ev.keyCode;
+ var obj = this;
+
+ if(c == 9) // tab
+ return true;
+
+ // do some processing when the enter key has been pressed
+ if(c == 13) {
+ if(obj.selectedId != 0)
+ obj.value = obj.serFunc(x('ds_box_'+obj.selectedId).itemData);
+ else if(obj.returnFunc)
+ obj.returnFunc();
+ else
+ return true;
+ x('ds_box').style.top = '-500px';
+ obj.selectedId = 0;
+
+ // opera hack: make sure to not send the form on the return key
+ while(obj && obj.nodeName.toLowerCase() != 'form')
+ obj = obj.parentNode;
+ if(obj) {
+ var oldsubmit = obj.onsubmit;
+ obj.onsubmit = function() { return false };
+ setTimeout(function() { obj.onsubmit = oldsubmit }, 100);
+ }
+
+ return false;
+ }
+
+ // process up/down keys
+ if(x('ds_box') && (c == 38 || c == 40)) {
+ var l = x('ds_box').getElementsByTagName('tr');
+ if(l.length < 1)
+ return true;
+
+ if(obj.selectedId == 0) {
+ if(c == 38) // up
+ obj.selectedId = l[l.length-1].id.substr(7);
+ else
+ obj.selectedId = l[0].id.substr(7);
+ } else {
+ var sel = null;
+ for(var i=0;i<l.length;i++)
+ if(l[i].id == 'ds_box_'+obj.selectedId) {
+ if(c == 38) // up
+ sel = i>0 ? l[i-1] : l[l.length-1];
+ else
+ sel = l[i+1] ? l[i+1] : l[0];
+ }
+ obj.selectedId = sel.id.substr(7);
+ }
+
+ for(var i=0;i<l.length;i++)
+ l[i].className = l[i].id == 'ds_box_'+obj.selectedId ? 'selected' : '';
+ return true;
+ }
+
+ // this.value isn't available in a keydown event
+ setTimeout(function() {
+ dsSearch(obj);
+ }, 10);
+
+ return true;
+}
+
+function dsSearch(obj) {
+ var b = x('ds_box');
+
+ // show/hide the ds_box div
+ if(obj.value.length < 2) {
+ if(b)
+ b.style.top = '-500px';
+ obj.selectedId = 0;
+ return;
+ }
+ if(!b) {
+ b = document.createElement('div');
+ b.setAttribute('id', 'ds_box');
+ b.innerHTML = '<b>Loading...</b>';
+ document.body.appendChild(b);
+ }
+
+ // position the div
+ var ddx=0;
+ var ddy=obj.offsetHeight;
+ var o = obj;
+ do {
+ ddx += o.offsetLeft;
+ ddy += o.offsetTop;
+ } while(o = o.offsetParent);
+
+ b.style.position = 'absolute';
+ b.style.left = ddx+'px';
+ b.style.top = ddy+'px';
+ b.style.width = obj.offsetWidth+'px';
+
+ // perform search
+ ajax(obj.searchURL + encodeURIComponent(obj.value), function(hr) {
+ dsResults(hr, obj);
+ });
+}
+
+function dsResults(hr, obj) {
+ var l = hr.responseXML.getElementsByTagName('item');
+ var b = x('ds_box');
+ if(l.length < 1) {
+ b.innerHTML = '<b>No results...</b>';
+ obj.selectedId = 0;
+ return;
+ }
+
+ b.innerHTML = '<table><tbody></tbody></table>';
+ tb = b.getElementsByTagName('tbody')[0];
+ for(var i=0;i<l.length;i++) {
+ var id = l[i].getAttribute('id');
+ var tr = document.createElement('tr');
+ tr.setAttribute('id', 'ds_box_'+id);
+ tr.itemData = l[i];
+ if(obj.selectedId == id)
+ tr.setAttribute('class', 'selected');
+ obj.trFunc(l[i], tr);
+ tb.appendChild(tr);
+ }
+
+ if(obj.selectedId != 0 && !x('ds_box_'+obj.selectedId))
+ obj.selectedId = 0;
+}
+
+
+
+
+
+
+ /*****************************\
+ * V N R E L A T I O N S *
+ \*****************************/
+
+
+var relTypes = [];
+function relLoad() {
+ var i;var l;var o;
+
+ // fetch the relation types from the add new relation selectbox
+ l = x('relation_new').getElementsByTagName('select')[0].options;
+ for(i=0;i<l.length;i++)
+ relTypes[Math.floor(l[i].value)] = l[i].text;
+
+ // read the current relations
+ l = x('relations').value.split('|||');
+ if(l[0]) {
+ for(i=0;i<l.length;i++) {
+ var rel = l[i].split(',', 3);
+ relAdd(rel[0], rel[1], rel[2]);
+ }
+ }
+ relEmpty();
+
+ // make sure the title is up-to-date
+ x('title').onchange = function() {
+ l = x('jt_box_relations').getElementsByTagName('td');
+ for(i=0;i<l.length;i++)
+ if(l[i].className == 'tc3')
+ l[i].innerHTML = shorten(this.value, 40);
+ };
+
+ // bind the add-link
+ x('relation_new').getElementsByTagName('a')[0].onclick = relFormAdd;
+
+ // dropdown
+ dsInit(x('relation_new').getElementsByTagName('input')[0], '/xml/vn.xml?q=', function(item, tr) {
+ var td = document.createElement('td');
+ td.innerHTML = 'v'+item.getAttribute('id');
+ td.style.textAlign = 'right';
+ td.style.paddingRight = '5px';
+ tr.appendChild(td);
+ td = document.createElement('td');
+ td.innerHTML = shorten(item.firstChild.nodeValue, 40);
+ tr.appendChild(td);
+ }, function(item) {
+ return 'v'+item.getAttribute('id')+':'+item.firstChild.nodeValue;
+ }, relFormAdd);
+}
+
+function relAdd(rel, vid, title) {
+ var o = document.createElement('tr');
+ o.setAttribute('id', 'relation_tr_'+vid);
+
+ var t = document.createElement('td');
+ t.className = 'tc1';
+ t.innerHTML = 'v'+vid+':<a href="/v'+vid+'">'+shorten(title, 40)+'</a>';
+ o.appendChild(t);
+
+ var options = '';
+ for(var i=0;i<relTypes.length;i++)
+ options += '<option value="'+i+'"'+(i == rel ? ' selected="selected"' : '')+'>'+qq(relTypes[i])+'</option>';
+ t = document.createElement('td');
+ t.className = 'tc2';
+ t.innerHTML = 'is a <select onchange="relSerialize()">'+options+'</select> of';
+ o.appendChild(t);
+
+ t = document.createElement('td');
+ t.className = 'tc3';
+ t.innerHTML = shorten(x('title').value, 40);
+ o.appendChild(t);
+
+ t = document.createElement('td');
+ t.className = 'tc4';
+ t.innerHTML = '<a href="#" onclick="return relDel('+vid+')">del</a>';
+ o.appendChild(t);
+
+ x('relation_tbl').appendChild(o);
+ relEmpty();
+}
+
+function relEmpty() {
+ if(x('relation_tbl').getElementsByTagName('tr').length > 0) {
+ if(x('relation_tr_none'))
+ x('relation_tbl').removeChild(x('relation_tr_none'));
+ return;
+ }
+ var o = document.createElement('tr');
+ o.setAttribute('id', 'relation_tr_none');
+ var t = document.createElement('td');
+ t.colspan = 4;
+ t.innerHTML = 'No relations selected.';
+ o.appendChild(t);
+ x('relation_tbl').appendChild(o);
+}
+
+function relSerialize() {
+ var r='';
+ var i;
+ var l = x('relation_tbl').getElementsByTagName('tr');
+ for(i=0;i<l.length;i++) {
+ var title = l[i].getElementsByTagName('td')[0];
+ title = title.innerText || title.textContent;
+ title = title.substr(title.indexOf(':')+1);
+ r += (r ? '|||' : '')
+ +l[i].getElementsByTagName('select')[0].selectedIndex
+ +','+l[i].id.substr(12)+','+title;
+ }
+ x('relations').value = r;
+}
+
+function relDel(vid) {
+ x('relation_tbl').removeChild(x('relation_tr_'+vid));
+ relSerialize();
+ relEmpty();
+ return false;
+}
+
+function relFormAdd() {
+ var txt = x('relation_new').getElementsByTagName('input')[0];
+ var sel = x('relation_new').getElementsByTagName('select')[0];
+ var lnk = x('relation_new').getElementsByTagName('a')[0];
+ var input = txt.value;
+
+ if(!input.match(/^v[0-9]+/)) {
+ alert('Visual novel textbox must start with an ID (e.g. v17)');
+ return false;
+ }
+
+ txt.disabled = true;
+ txt.value = 'loading...';
+ sel.disabled = true;
+ lnk.innerHTML = 'loading...';
+
+ ajax('/xml/vn.xml?q='+encodeURIComponent(input), function(hr) {
+ txt.disabled = false;
+ txt.value = '';
+ sel.disabled = false;
+ lnk.innerHTML = 'add';
+
+ var items = hr.responseXML.getElementsByTagName('item');
+ if(items.length < 1)
+ return alert('Visual novel not found!');
+
+ var id = items[0].getAttribute('id');
+ if(x('relation_tr_'+id))
+ return alert('This visual novel has already been selected!');
+
+ relAdd(sel.selectedIndex, id, items[0].firstChild.nodeValue);
+ sel.selectedIndex = 0;
+ relSerialize();
+ });
+ return false;
+}
+
+
+
+
+
+
+ /*********************************\
+ * V N S C R E E N S H O T S *
+ \*********************************/
+
+
+var scrRel = [ [ 0, '-- select release --' ] ];
+var scrStaticURL;
+function scrLoad() {
+ // load the releases
+ scrStaticURL = x('scr_rel').className;
+ var l = x('scr_rel').options;
+ for(var i=0;i<l.length;i++)
+ scrRel[i+1] = [ l[i].value, l[i].text ];
+ x('scr_rel').parentNode.removeChild(x('scr_rel'));
+
+ // load the current screenshots
+ l = x('screenshots').value.split(' ');
+ for(i=0;i<l.length;i++)
+ if(l[i].length > 2) {
+ var r = l[i].split(',');
+ scrAdd(r[0], r[1], r[2]);
+ }
+ scrLast();
+ scrCheckStatus();
+
+ scrSetSubmit();
+}
+
+// give an error when submitting the form while still uploading an image
+function scrSetSubmit() {
+ var o=x('screenshots');
+ while(o.nodeName.toLowerCase() != 'form')
+ o = o.parentNode;
+ oldfunc = o.onsubmit;
+ o.onsubmit = function() {
+ var c=0;var r=0;
+ var l = x('scr_table').getElementsByTagName('tr');
+ for(var i=0;i<l.length-1;i++) {
+ if(l[i].scrStatus > 0)
+ c=1;
+ else if(l[i].getElementsByTagName('select')[0].selectedIndex == 0)
+ r=1;
+ }
+ if(c) {
+ alert('Please wait for the screenshots to be uploaded before submitting the form.');
+ return false;
+ } else if(r) {
+ alert('Please select the appropriate release for every screenshot');
+ return false;
+ } else if(oldfunc)
+ return oldfunc();
+ };
+}
+
+
+function scrURL(id, t) {
+ return scrStaticURL+'/s'+t+'/'+(id%100<10?'0':'')+(id%100)+'/'+id+'.jpg';
+}
+
+function scrAdd(id, nsfw, rel) {
+ var tr = document.createElement('tr');
+ tr.scrId = id;
+ tr.scrStatus = id ? 2 : 1; // 0: done, 1: uploading, 2: waiting for thumbnail
+ tr.scrRel = rel;
+ tr.scrNSFW = nsfw;
+
+ var td = document.createElement('td');
+ td.className = 'thumb';
+ td.innerHTML = 'loading...';
+ tr.appendChild(td);
+
+ td = document.createElement('td');
+ if(id)
+ td.innerHTML = '<b>Generating thumbnail...</b><br />'
+ +'Note: if this takes longer than 30 seconds, there\'s probably something wrong on our side.'
+ +'Please try again later or report a bug if that is the case.';
+ else
+ td.innerHTML = '<b>Uploading screenshot...</b><br />'
+ +'This can take a while, depending on the file size and your upload speed.<br />'
+ +'<a href="#" onclick="return scrDel(this)">cancel</a>';
+ tr.appendChild(td);
+
+ x('scr_table').appendChild(tr);
+ scrStripe();
+ return tr;
+}
+
+function scrLast() {
+ if(x('scr_last'))
+ x('scr_table').removeChild(x('scr_last'));
+ var full = x('scr_table').getElementsByTagName('tr').length >= 10;
+
+ var tr = document.createElement('tr');
+ tr.setAttribute('id', 'scr_last');
+
+ var td = document.createElement('td');
+ td.className = 'thumb';
+ tr.appendChild(td);
+
+ var td = document.createElement('td');
+ if(full)
+ td.innerHTML = '<b>Enough screenshots</b><br />'
+ +'The limit of 10 screenshots per visual novel has been reached. '
+ +'If you want to add a new screenshot, please remove an existing one first.';
+ else
+ td.innerHTML = '<b>Add screenshot</b><br />'
+ +'Image must be smaller than 5MB and in PNG or JPEG format.<br />'
+ +'<input name="scr_upload" id="scr_upload" type="file" class="text" /><br />'
+ +'<input type="button" value="Upload!" class="submit" onclick="scrUpload()" />';
+
+ tr.appendChild(td);
+ x('scr_table').appendChild(tr);
+ scrStripe();
+}
+
+function scrStripe() {
+ var l = x('scr_table').getElementsByTagName('tr');
+ for(var i=0;i<l.length;i++)
+ l[i].className = i%2==0 ? 'odd' : '';
+}
+
+function scrCheckStatus() {
+ var ids = '';
+ var l = x('scr_table').getElementsByTagName('tr');
+ for(var i=0;i<l.length-1;i++)
+ if(l[i].scrStatus == 2)
+ ids += (ids ? ';' : '?')+'id='+l[i].scrId;
+ if(!ids)
+ return setTimeout(scrCheckStatus, 1000);
+
+ var ti = setTimeout(scrCheckStatus, 10000);
+ ajax('/xml/screenshots.xml'+ids, function(hr) {
+ var ls = hr.responseXML.getElementsByTagName('item');
+ var l = x('scr_table').getElementsByTagName('tr');
+ var tr;
+ for(var s=0;s<ls.length;s++) {
+ for(i=0;i<l.length-1;i++)
+ if(l[i].scrId == ls[s].getAttribute('id') && ls[s].getAttribute('status') > 0)
+ tr = l[i];
+ if(!tr)
+ continue;
+
+ tr.scrStatus = 0;
+ tr.getElementsByTagName('td')[0].innerHTML =
+ '<a href="'+scrURL(tr.scrId, 'f')+'" rel="iv:'+ls[s].getAttribute('width')+'x'+ls[s].getAttribute('height')+':edit">'
+ +'<img src="'+scrURL(tr.scrId, 't')+'" style="margin: 0; padding: 0; border: 0" /></a>';
+
+ var opt='';
+ for(var o=0;o<scrRel.length;o++)
+ opt += '<option value="'+scrRel[o][0]+'"'+(tr.scrRel && tr.scrRel == scrRel[o][0] ? ' selected="selected"' : '')+'>'+scrRel[o][1]+'</option>';
+
+ tr.getElementsByTagName('td')[1].innerHTML = '<b>Screenshot #'+tr.scrId+'</b>'
+ +' (<a href="#" onclick="return scrDel(this)">remove</a>)<br />'
+ +'Full size: '+ls[s].getAttribute('width')+'x'+ls[s].getAttribute('height')+'<br /><br />'
+ +'<input type="checkbox" onclick="scrSerialize()" id="scr_ser_'+tr.scrId+'" name="scr_ser_'+tr.scrId+'"'
+ +' '+(tr.scrNSFW > 0 ? 'checked = "checked"' : '')+' />'
+ +'<label for="scr_ser_'+tr.scrId+'">This screenshot is NSFW</label><br />'
+ +'<select onchange="scrSerialize()">'+opt+'</select>';
+ }
+ scrSerialize();
+ ivInit();
+ clearTimeout(ti);
+ setTimeout(scrCheckStatus, 1000);
+ });
+}
+
+function scrDel(what) {
+ while(what.nodeName.toLowerCase() != 'tr')
+ what = what.parentNode;
+ what.scrStatus = 3;
+ x('scr_table').removeChild(what);
+ scrSerialize();
+ scrLast();
+ return false;
+}
+
+var scrUplNr=0;
+function scrUpload() {
+ scrUplNr++;
+
+ // create temporary form
+ var d = document.createElement('div');
+ d.style.cssText = 'visibility: hidden; overflow: hidden; width: 1px; height: 1px; position: absolute; left: -500px; top: -500px';
+ d.innerHTML = '<iframe id="scr_upl_'+scrUplNr+'" name="scr_upl_'+scrUplNr+'" style="height: 0px; width: 0px; visibility: hidden"'
+ +' src="about:blank" onload="scrUploadComplete(this)"></iframe>'
+ +'<form method="post" action="/xml/screenshots.xml" target="scr_upl_'+scrUplNr+'" enctype="multipart/form-data" id="scr_frm_'+scrUplNr+'"></form>';
+ document.body.appendChild(d);
+
+ // submit form and delete it
+ d = x('scr_frm_'+scrUplNr);
+ d.appendChild(x('scr_upload'));
+ d.submit();
+ d.parentNode.removeChild(d);
+
+ d = scrAdd(0, 0, 0);
+ x('scr_upl_'+scrUplNr).theTR = d;
+ scrLast();
+
+ return false;
+}
+
+function scrUploadComplete(what) {
+ var f = window.frames[what.id];
+ if(f.location.href.indexOf('screenshots') < 0)
+ return;
+
+ var tr = what.theTR;
+ if(!tr || tr.scrStatus == 3)
+ return;
+
+ try {
+ tr.scrId = f.window.document.getElementsByTagName('image')[0].getAttribute('id');
+ } catch(e) {
+ tr.scrId = -10;
+ }
+ if(tr.scrId < 0) {
+ alert(
+ tr.scrId == -10 ?
+ 'Oops! Seems like something went wrong...\n'
+ +'Make sure the file you\'re uploading doesn\'t exceed 5MB in size.\n'
+ +'If that isn\'t the problem, then please report a bug.' :
+ tr.scrId == -1 ?
+ 'Upload failed!\nOnly JPEG or PNG images are accepted.' :
+ 'Upload failed!\nNo file selected, or an empty file?'
+ );
+ return scrDel(tr);
+ }
+
+ tr.scrStatus = 2;
+ tr.getElementsByTagName('td')[1].innerHTML =
+ '<b>Generating thumbnail...</b><br />'
+ +'Note: if this takes longer than 30 seconds, there\'s probably something wrong on our side.'
+ +'Please try again later or report a bug if that is the case.';
+
+ // remove the <div> in a timeout, otherwise some browsers think the page is still loading
+ setTimeout(function() { document.body.removeChild(what.parentNode) }, 100);
+}
+
+function scrSerialize() {
+ var r = '';
+ var l = x('scr_table').getElementsByTagName('tr');
+ for(var i=0;i<l.length-1;i++)
+ if(l[i].scrStatus == 0)
+ r += (r ? ' ' : '') + l[i].scrId + ','
+ + (l[i].getElementsByTagName('input')[0].checked ? 1 : 0) + ','
+ + scrRel[l[i].getElementsByTagName('select')[0].selectedIndex][0];
+ x('screenshots').value = r;
+}
+
+
+
+
+
+
+ /***************\
+ * M E D I A *
+ \***************/
+
+
+var medTypes = [ [ '', '- medium -', false ] ];
+function medLoad() {
+ // load the medTypes and clear the div
+ var l = x('media_div').getElementsByTagName('select')[0].options;
+ for(var i=0;i<l.length;i++)
+ medTypes[medTypes.length] = [ l[i].value, l[i].text, l[i].className.indexOf('noqty') ? false : true ];
+ x('media_div').innerHTML = '';
+
+ // load the selected media
+ l = x('media').value.split(',');
+ for(var i=0;i<l.length;i++)
+ if(l[i].length > 2)
+ medAddNew(l[i].split(' ')[0], Math.floor(l[i].split(' ')[1]));
+
+ medAddNew('', 0);
+ medSetSubmit();
+}
+
+function medSetSubmit() {
+ var o=x('media');
+ while(o.nodeName.toLowerCase() != 'form')
+ o = o.parentNode;
+ oldfunc = o.onsubmit;
+ o.onsubmit = function() {
+ var l = x('media_div').getElementsByTagName('span');
+ for(var i=0;i<l.length-1;i++) {
+ var s = l[i].getElementsByTagName('select');
+ if(!medTypes[s[1].selectedIndex][2] && s[0].selectedIndex == 0) {
+ alert('Media '+medTypes[s[1].selectedIndex][1]+' requires a quantity to be specified!');
+ return false;
+ }
+ }
+ return oldfunc ? oldfunc() : true;
+ };
+}
+
+function medAddNew(med, qty) {
+ var o = document.createElement('span');
+ var r = '<select class="qty" onchange="medSerialize()"><option value="0">- quantity -</option>';
+ for(var i=1;i<=10;i++)
+ r += '<option value="'+i+'"'+(qty == i ? ' selected="selected"' : '')+'>'+i+'</option>';
+ r += '</select><select class="medium" onchange="return medCheckNew(this)">';
+ for(i=0;i<medTypes.length;i++)
+ r += '<option value="'+medTypes[i][0]+'"'+(med == medTypes[i][0] ? ' selected="selected"' : '')+'>'+medTypes[i][1]+'</option>';
+ r += '</select>';
+ if(med != '')
+ r += '<input type="button" class="submit" onclick="return medDel(this)" value="remove" />';
+ o.innerHTML = r;
+ x('media_div').appendChild(o);
+}
+
+function medDel(what) {
+ what = what.nodeName ? what : this;
+ while(what.nodeName.toLowerCase() != 'span')
+ what = what.parentNode;
+ x('media_div').removeChild(what);
+ medSerialize();
+ return false;
+}
+
+function medCheckNew() {
+ // check for non-new items and add remove buttons
+ var l = x('media_div').getElementsByTagName('span');
+ var createnew=1;
+ for(var i=0;i<l.length;i++) {
+ var sel = l[i].getElementsByTagName('select')[1].selectedIndex;
+ if(sel == 0)
+ createnew = 0;
+ else if(l[i].getElementsByTagName('input').length < 1) {
+ var a = document.createElement('input');
+ a.type = 'button';
+ a.className = 'submit';
+ a.onclick = medDel;
+ a.value = 'remove';
+ l[i].appendChild(a);
+ }
+ }
+ if(createnew)
+ medAddNew('', 0);
+ medSerialize();
+
+ return true;
+}
+
+function medSerialize() {
+ var r = '';
+ var l = x('media_div').getElementsByTagName('span');
+ for(var i=0;i<l.length;i++) {
+ var sel = l[i].getElementsByTagName('select');
+ if(sel[1].selectedIndex != 0)
+ r += (r ? ',' : '') + medTypes[sel[1].selectedIndex][0] + ' ' + (medTypes[sel[1].selectedIndex][2] ? 0 : sel[0].selectedIndex);
+ }
+ x('media').value = r;
+}
+
+
+
+
+
+
+ /****************************************************\
+ * V I S U A L N O V E L S / P R O D U C E R S *
+ \****************************************************/
+
+
+function vnpLoad(type) {
+ // load currently selected VNs
+ var l = x(type).value.split('|||');
+ for(var i=0;i<l.length;i++)
+ if(l[i].length > 2)
+ vnpAdd(type, l[i].split(',',2)[0], l[i].split(',',2)[1]);
+ vnpCheckEmpty(type);
+
+ // dropdown
+ var n = x('jt_box_'+(type == 'vn' ? 'visual_novels' : type)).getElementsByTagName('div')[1];
+ dsInit(n.getElementsByTagName('input')[0], '/xml/'+type+'.xml?q=', function(item, tr) {
+ var td = document.createElement('td');
+ td.innerHTML = type.substr(0,1)+item.getAttribute('id');
+ td.style.textAlign = 'right';
+ td.style.paddingRight = '5px';
+ tr.appendChild(td);
+ td = document.createElement('td');
+ td.innerHTML = shorten(item.firstChild.nodeValue, 40);
+ tr.appendChild(td);
+ }, function(item) {
+ return type.substr(0,1)+item.getAttribute('id')+':'+item.firstChild.nodeValue;
+ }, function() { vnpFormAdd(type) });
+ n.getElementsByTagName('a')[0].onclick = function() { vnpFormAdd(type); return false };
+}
+
+function vnpAdd(type, id, title) {
+ var o = document.createElement('span');
+ o.innerHTML = '<i>'+type.substr(0,1)+id+':<a href="/'+type.substr(0,1)+id+'">'+shorten(title, 40)+'</a></i>'
+ +'<a href="#" onclick="return vnpDel(this, \''+type+'\')">remove</a>';
+ x(type+'sel').appendChild(o);
+ vnpStripe(type);
+ vnpCheckEmpty(type);
+}
+
+function vnpDel(what, type) {
+ what = what.nodeName ? what : this;
+ while(what.nodeName.toLowerCase() != 'span')
+ what = what.parentNode;
+ x(type+'sel').removeChild(what);
+ vnpCheckEmpty(type);
+ vnpSerialize(type);
+ return false;
+}
+
+function vnpCheckEmpty(type) {
+ var o = x(type+'sel');
+ if(o.getElementsByTagName('span').length < 1) {
+ if(o.getElementsByTagName('b').length < 1)
+ o.innerHTML = '<b>Nothing selected...</b>';
+ } else if(o.getElementsByTagName('b').length == 1)
+ o.removeChild(o.getElementsByTagName('b')[0]);
+}
+
+function vnpStripe(type) {
+ var l = x(type+'sel').getElementsByTagName('span');
+ for(var i=0;i<l.length;i++)
+ l[i].className = i%2 ? 'odd' : '';
+}
+
+function vnpFormAdd(type) {
+ var n = x('jt_box_'+(type == 'vn' ? 'visual_novels' : type)).getElementsByTagName('div')[1];
+ var txt = n.getElementsByTagName('input')[0];
+ var lnk = n.getElementsByTagName('a')[0];
+ var input = txt.value;
+
+ if(type == 'vn' && !input.match(/^v[0-9]+/)) {
+ alert('Visual novel textbox must start with an ID (e.g. v17)');
+ return false;
+ }
+ if(type == 'producers' && !input.match(/^p[0-9]+/)) {
+ alert('Producer textbox must start with an ID (e.g. p5)');
+ return false;
+ }
+
+ txt.disabled = true;
+ txt.value = 'loading...';
+ lnk.innerHTML = 'loading...';
+
+ ajax('/xml/'+type+'.xml?q='+encodeURIComponent(input), function(hr) {
+ txt.disabled = false;
+ txt.value = '';
+ lnk.innerHTML = 'add';
+
+ var items = hr.responseXML.getElementsByTagName('item');
+ if(items.length < 1)
+ return alert('Item not found!');
+
+ vnpAdd(type, items[0].getAttribute('id'), items[0].firstChild.nodeValue);
+ vnpSerialize(type);
+ });
+ return false;
+}
+
+function vnpSerialize(type) {
+ var r = '';
+ var l = x(type+'sel').getElementsByTagName('span');
+ for(var i=0;i<l.length;i++)
+ r += (r ? '|||' : '') + l[i].getElementsByTagName('i')[0].innerHTML.substr(1, l[i].getElementsByTagName('i')[0].innerHTML.indexOf(':')-1)
+ + ',' + l[i].getElementsByTagName('a')[0].innerHTML;
+ x(type).value = r;
+}
+
+
diff --git a/static/f/icons.png b/static/f/icons.png
new file mode 100644
index 00000000..9430e7c6
--- /dev/null
+++ b/static/f/icons.png
Binary files differ
diff --git a/static/f/script.js b/static/f/script.js
new file mode 100644
index 00000000..dcc901e0
--- /dev/null
+++ b/static/f/script.js
@@ -0,0 +1,396 @@
+
+/* G L O B A L S T U F F */
+
+function x(y){return document.getElementById(y)}
+function cl(o,f){if(x(o))x(o).onclick=f}
+function DOMLoad(y){var d=0;var f=function(){if(d++)return;y()};
+if(document.addEventListener)document.addEventListener("DOMCont"
++"entLoaded",f,false);document.write("<script id=_ie defer src="
++"javascript:void(0)><\/script>");document.getElementById('_ie')
+.onreadystatechange=function(){if(this.readyState=="complete")f()
+};if(/WebKit/i.test(navigator.userAgent))var t=setInterval(
+function(){if(/loaded|complete/.test(document.readyState)){
+clearInterval(t);f()}},10);window.onload=f;}
+
+
+
+
+function searchInit() {
+ cl('advselect', function() {
+ var e = x('advoptions');
+ e.className = e.className.indexOf('hidden')>=0 ? '' : 'hidden';
+ this.getElementsByTagName('i')[0].innerHTML = e.className.indexOf('hidden')>=0 ? '&#9656;' : '&#9662;';
+ return false;
+ });
+
+ var l = x('catselect').getElementsByTagName('li');
+ for(i=0;i<l.length;i++)
+ if(l[i].id.substr(0,4) == 'cat_')
+ l[i].onclick = function() {
+ searchParse(1, this.innerHTML);
+ };
+
+ l = x('advoptions').getElementsByTagName('input');
+ for(i=0;i<l.length;i++)
+ if(l[i].id.substr(0,5) == 'lang_' || l[i].id.substr(0,5) == 'plat_')
+ l[i].onclick = function() {
+ searchParse(0, this.parentNode.getElementsByTagName('acronym')[0].title);
+ };
+
+ x('q').onkeyup = searchParse;
+ searchParse();
+}
+
+function searchParse(add, term) {
+ var q = x('q').value;
+ var i;
+
+ if(add == 0 || add === 1) {
+ var qn = q;
+ if(!add)
+ eval('qn = qn.replace(/'+term+'/gi, "")');
+ else {
+ eval('qn = qn.replace(/(^|[^-])'+term+'/gi, "$1-'+term+'")');
+ if(qn == q)
+ eval('qn = qn.replace(/-'+term+'/gi, "")');
+ }
+ if(qn == q)
+ q += ' '+term;
+ else
+ q = qn;
+
+ q = q.replace(/^ +/, "");
+ q = q.replace(/ +$/, "");
+ q = q.replace(/ +/g, " ");
+
+ x('q').value = q;
+ }
+
+ q = q.toLowerCase();
+ var l = x('catselect').getElementsByTagName('li');
+ for(i=0;i<l.length;i++)
+ if(l[i].id.substr(0,4) == 'cat_') {
+ var cat = l[i].innerHTML.toLowerCase();
+ l[i].className = q.indexOf('-'+cat) >= 0 ? 'exc' : q.indexOf(cat) >= 0 ? 'inc' : '';
+ }
+
+ l = x('advoptions').getElementsByTagName('input');
+ for(i=0;i<l.length;i++)
+ if(l[i].id.substr(0,5) == 'lang_' || l[i].id.substr(0,5) == 'plat_')
+ l[i].checked = q.indexOf(l[i].parentNode.getElementsByTagName('acronym')[0].title.toLowerCase()) >= 0 ? true : false;
+
+ return false;
+}
+
+
+
+
+
+/* I M A G E V I E W E R */
+
+function ivInit() {
+ var init = 0;
+ var l = document.getElementsByTagName('a');
+ for(var i=0;i<l.length;i++)
+ if(l[i].rel.substr(0,3) == 'iv:') {
+ init++;
+ l[i].onclick = ivView;
+ }
+ if(init && !x('iv_view')) {
+ var d = document.createElement('div');
+ d.id = 'iv_view';
+ d.innerHTML = '<b id="ivimg"></b><br />'
+ +'<a href="#" id="ivfull">&nbsp;</a>'
+ +'<a href="#" onclick="return ivClose()" id="ivclose">close</a>'
+ +'<a href="#" onclick="return ivView(this)" id="ivprev">&lt;- previous</a>'
+ +'<a href="#" onclick="return ivView(this)" id="ivnext">next -&gt;</a>';
+ document.body.appendChild(d);
+ d = document.createElement('b');
+ d.id = 'ivimgload';
+ d.innerHTML = 'Loading...';
+ document.body.appendChild(d);
+ }
+}
+
+function ivView(what) {
+ what = what && what.rel ? what : this;
+ var u = what.href;
+ var opt = what.rel.split(':');
+ d = x('iv_view');
+
+ // fix prev/next links (if any)
+ if(opt[2]) {
+ var ol = document.getElementsByTagName('a');
+ var l=[];
+ for(i=0;i<ol.length;i++)
+ if(ol[i].rel.substr(0,3) == 'iv:' && ol[i].rel.indexOf(':'+opt[2]) > 4 && ol[i].className.indexOf('hidden') < 0 && ol[i].id != 'ivprev' && ol[i].id != 'ivnext')
+ l[l.length] = ol[i];
+ for(i=0;i<l.length;i++)
+ if(l[i].href == u) {
+ x('ivnext').style.visibility = l[i+1] ? 'visible' : 'hidden';
+ x('ivnext').href = l[i+1] ? l[i+1].href : '#';
+ x('ivnext').rel = l[i+1] ? l[i+1].rel : '';
+ x('ivprev').style.visibility = l[i-1] ? 'visible' : 'hidden';
+ x('ivprev').href = l[i-1] ? l[i-1].href : '#';
+ x('ivprev').rel = l[i-1] ? l[i-1].rel : '';
+ }
+ } else
+ x('ivnext').style.visibility = x('ivprev').style.visibility = 'hidden';
+
+ // calculate dimensions
+ var w = Math.floor(opt[1].split('x')[0]);
+ var h = Math.floor(opt[1].split('x')[1]);
+ var ww = typeof(window.innerWidth) == 'number' ? window.innerWidth : document.documentElement.clientWidth;
+ var wh = typeof(window.innerHeight) == 'number' ? window.innerHeight : document.documentElement.clientHeight;
+ var st = typeof(window.pageYOffset) == 'number' ? window.pageYOffset : document.body && document.body.scrollTop ? document.body.scrollTop : document.documentElement.scrollTop;
+ if(w+100 > ww || h+70 > wh) {
+ x('ivfull').href = u;
+ x('ivfull').innerHTML = w+'x'+h;
+ x('ivfull').style.visibility = 'visible';
+ if(w/h > ww/wh) { // width++
+ h *= (ww-100)/w;
+ w = ww-100;
+ } else { // height++
+ w *= (wh-70)/h;
+ h = wh-70;
+ }
+ } else
+ x('ivfull').style.visibility = 'hidden';
+ var dw = w;
+ var dh = h+20;
+ dw = dw < 200 ? 200 : dw;
+
+ // update document
+ d.style.display = 'block';
+ x('ivimg').innerHTML = '<img src="'+u+'" onclick="ivClose()" onload="document.getElementById(\'ivimgload\').style.top=\'-400px\'" style="width: '+w+'px; height: '+h+'px" />';
+ d.style.width = dw+'px';
+ d.style.height = dh+'px';
+ d.style.left = ((ww - dw) / 2 - 10)+'px';
+ d.style.top = ((wh - dh) / 2 + st - 20)+'px';
+ x('ivimgload').style.left = ((ww - 100) / 2 - 10)+'px';
+ x('ivimgload').style.top = ((wh - 20) / 2 + st)+'px';
+ return false;
+}
+
+function ivClose() {
+ x('iv_view').style.display = 'none';
+ x('iv_view').style.top = '-5000px';
+ x('ivimgload').style.top = '-400px';
+ x('ivimg').innerHTML = '';
+ return false;
+}
+
+
+
+
+
+/* J A V A S C R I P T T A B S */
+
+function jtInit() {
+ var sel = '';
+ var first = '';
+ var l = x('jt_select').getElementsByTagName('a');
+ for(var i=0;i<l.length;i++)
+ if(l[i].id.substr(0,7) == 'jt_sel_') {
+ l[i].onclick = jtSel;
+ if(!first)
+ first = l[i].id;
+ if(location.hash && l[i].id == 'jt_sel_'+location.hash.substr(1))
+ sel = l[i].id;
+ }
+ if(!first)
+ return;
+ if(!sel)
+ sel = first;
+ jtSel(sel, 1);
+}
+
+function jtSel(which, nolink) {
+ which = typeof(which) == 'string' ? which : which && which.id ? which.id : this.id;
+ which = which.substr(7);
+
+ var l = x('jt_select').getElementsByTagName('a');
+ for(var i=0;i<l.length;i++)
+ if(l[i].id.substr(0,7) == 'jt_sel_') {
+ var name = l[i].id.substr(7);
+ x('jt_box_'+name).style.display = name == which ? 'block' : 'none';
+ var o = x('jt_sel_'+name).parentNode;
+ if(o.className.indexOf('tabselected') >= 0) {
+ if(name != which)
+ o.className = o.className.replace(/tabselected/, '');
+ } else
+ if(name == which)
+ o.className += ' tabselected';
+ }
+
+ if(!nolink)
+ location.href = '#'+which;
+ return false;
+}
+
+
+
+
+/* O N L O A D E V E N T */
+
+DOMLoad(function() {
+
+ // search box
+ var i = x('sq');
+ i.onfocus = function () {
+ if(this.value == 'search') {
+ this.value = '';
+ this.style.fontStyle = 'normal'
+ }
+ };
+ i.onblur = function () {
+ if(this.value.length < 1) {
+ this.value = 'search';
+ this.style.fontStyle = 'italic'
+ }
+ };
+
+
+ // VN Voting
+ i = x('votesel');
+ if(i)
+ i.onchange = function() {
+ var s = this.options[this.selectedIndex].value;
+ if(s == 1 && !confirm(
+ "You are about to give this visual novel a 1 out of 10. This is a rather extreme rating, "
+ +"meaning this game has absolutely nothing to offer, and that it's the worst game you have ever played.\n"
+ +"Are you really sure this visual novel matches that description?"))
+ return;
+ if(s == 10 && !confirm(
+ "You are about to give this visual novel a 10 out of 10. This is a rather extreme rating, "
+ +"meaning this is one of the best visual novels you've ever played and it's unlikely "
+ +"that any other game could ever be better than this one.\n"
+ +"It is generally a bad idea to have more than three games in your vote list with this rating, choose carefully!"))
+ return;
+ if(s)
+ location.href = location.href.replace(/\.[0-9]+/, '')+'/vote?v='+s;
+ };
+
+ // VN Wishlist editing
+ i = x('wishsel');
+ if(i)
+ i.onchange = function() {
+ if(this.selectedIndex != 0)
+ location.href = location.href.replace(/\.[0-9]+/, '')+'/wish?s='+this.options[this.selectedIndex].value;
+ };
+ // Batch Wishlist editing
+ i = x('batchedit');
+ if(i)
+ i.onchange = function() {
+ var frm = this;
+ while(frm.nodeName.toLowerCase() != 'form')
+ frm = frm.parentNode;
+ if(this.selectedIndex != 0)
+ frm.submit();
+ };
+
+
+ // Release list editing
+ i = x('listsel');
+ if(i)
+ i.onchange = function() {
+ if(this.selectedIndex != 0)
+ location.href = location.href.replace(/\.[0-9]+/, '')+'/list?e='+this.options[this.selectedIndex].value;
+ };
+ // User VN list
+ i = x('relhidall');
+ if(i) {
+ var l = document.getElementsByTagName('tr');
+ for(var i=0;i<l.length;i++)
+ if(l[i].className.indexOf('relhid') >= 0)
+ l[i].style.display = 'none';
+ var l = document.getElementsByTagName('td');
+ for(var i=0;i<l.length;i++)
+ if(l[i].className.indexOf('relhid_but') >= 0)
+ l[i].onclick = function() {
+ var l = document.getElementsByTagName('tr');
+ for(var i=0;i<l.length;i++)
+ if(l[i].className.substr(7) == this.id) {
+ l[i].style.display = l[i].style.display == 'none' ? '' : 'none';
+ this.getElementsByTagName('i')[0].innerHTML = l[i].style.display == 'none' ? '&#9656;' : '&#9662;';
+ }
+ };
+ var allhid = 1;
+ x('relhidall').onclick = function() {
+ allhid = !allhid;
+ var l = document.getElementsByTagName('tr');
+ for(var i=0;i<l.length;i++)
+ if(l[i].className.indexOf('relhid') >= 0) {
+ l[i].style.display = allhid ? 'none' : '';
+ x(l[i].className.substr(7)).getElementsByTagName('i')[0].innerHTML = allhid ? '&#9656;' : '&#9662;';
+ }
+ this.getElementsByTagName('i')[0].innerHTML = allhid ? '&#9656;' : '&#9662;';
+ };
+ }
+
+
+ // Advanced VN search
+ if(x('advselect'))
+ searchInit();
+
+
+ // show/hide NSFW VN image
+ if(x('nsfw_show'))
+ x('nsfw_show').getElementsByTagName('a')[0].onclick = function() {
+ x('nsfw_show').style.display = 'none';
+ x('nsfw_hid').style.display = 'block';
+ x('nsfw_hid').onclick = function() {
+ x('nsfw_show').style.display = 'block';
+ x('nsfw_hid').style.display = 'none';
+ };
+ return false
+ };
+
+ // NSFW toggle for screenshots
+ cl('nsfwhide', function() {
+ var s=0;
+ var l = x('screenshots').getElementsByTagName('div');
+ for(var i=0;i<l.length;i++) {
+ if(l[i].className.indexOf('nsfw') >= 0) {
+ if(l[i].className.indexOf('hidden') >= 0) {
+ s++;
+ l[i].className = 'nsfw';
+ l[i].getElementsByTagName('a')[0].className = '';
+ } else {
+ l[i].className += ' hidden';
+ l[i].getElementsByTagName('a')[0].className = 'hidden';
+ }
+ } else
+ s++;
+ }
+ x('nsfwshown').innerHTML = s;
+ return false;
+ });
+
+ // initialize image viewer
+ ivInit();
+
+ // Javascript tabs
+ if(x('jt_select'))
+ jtInit();
+
+ // forms.js
+ if(x('categories'))
+ catLoad();
+ if(x('relations'))
+ relLoad();
+ if(x('jt_box_screenshots'))
+ scrLoad();
+ if(x('media'))
+ medLoad();
+ if(x('jt_box_visual_novels'))
+ vnpLoad('vn');
+ if(x('jt_box_producers'))
+ vnpLoad('producers');
+
+ // spam protection on all forms
+ if(document.forms.length >= 1)
+ for(i=0; i<document.forms.length; i++)
+ document.forms[i].action = document.forms[i].action.replace(/\/nospam\?/,'');
+
+});
diff --git a/static/f/select.png b/static/f/select.png
new file mode 100644
index 00000000..9473c8a9
--- /dev/null
+++ b/static/f/select.png
Binary files differ
diff --git a/static/f/style.css b/static/f/style.css
new file mode 100644
index 00000000..e1ca5036
--- /dev/null
+++ b/static/f/style.css
@@ -0,0 +1,1108 @@
+
+
+* {
+ margin: 0;
+ padding: 0;
+}
+
+body, td {
+ font: 8pt "Tahoma";
+}
+body {
+ background: #000 url(/f/bg.jpg) no-repeat;
+ color: #ddd;
+}
+a {
+ color: #fff;
+ text-decoration: none;
+}
+a:hover {
+ border-bottom: 1px dotted #fff;
+}
+table {
+ border-collapse: collapse;
+}
+table td {
+ vertical-align: top;
+ padding: 3px;
+}
+table tr.odd {
+ background: url(/f/boxbg.png) repeat;
+}
+img {
+ border: none;
+}
+
+#bgright {
+ position: absolute;
+ top: 0px;
+ right: 0px;
+ width: 433px;
+ height: 140px;
+ background: url(/f/bgright.jpg) no-repeat;
+}
+
+#header {
+ position: absolute;
+ top: 80px;
+ left: 400px;
+}
+#header h1, #header h1 a {
+ font-family: "Futura", "Century New Gothic", "Arial", Serif;
+ font-size: 19pt;
+ font-style: italic;
+ color: #135;
+ border: none!important;
+}
+#header h1.debug a {
+ color: #c00;
+}
+
+#footer {
+ margin: 15px auto 0 auto;
+ text-align: center;
+ color: #135;
+}
+#footer a {
+ color: #135;
+ text-decoration: underline;
+}
+
+ul {
+ margin-left: 35px;
+}
+p.locked {
+ float: right;
+ color: #e44;
+ font-style: italic;
+ margin: 0!important;
+}
+#maincontent h2 b {
+ font: 8pt "Tahoma";
+ font-weight: normal;
+}
+p.description {
+ margin: 10px 100px!important;
+}
+b.done { font-weight: normal; color: #0c0 }
+b.todo { font-weight: normal; color: #c00 }
+
+.clearfloat {
+ clear: both;
+ height: 0;
+}
+
+
+
+
+/***** general form markup *****/
+
+form, fieldset {
+ border: 0;
+ display: block;
+}
+legend {
+ display: none;
+}
+input.text, input.submit, select, textarea {
+ background-color: #135;
+ color: #ccc;
+ border: 1px solid #36A;
+ font: 9pt "Tahoma";
+ margin: 1px;
+}
+optgroup option {
+ padding-left: 10px;
+ font-style: normal;
+}
+input.submit {
+ background-color: #123;
+ padding: 1px;
+}
+input.text, select {
+ width: 200px;
+}
+fieldset.submit {
+ width: 100%;
+ text-align: center;
+ margin: 5px;
+}
+fieldset.submit input {
+ width: 200px;
+}
+fieldset.submit h2 {
+ font-size: 9pt!important;
+}
+fieldset.submit textarea {
+ margin: 0 20px 5px 20px;
+}
+table.formtable {
+ margin: 0 20px 20px 20px;
+}
+table.formtable td {
+ padding: 0;
+}
+table.formtable tr.newfield td {
+ padding-top: 5px;
+}
+table.formtable tr.newpart td {
+ padding-top: 20px;
+ font-weight: bold;
+}
+td.label, td.label label {
+ width: 90px;
+}
+td.label label {
+ display: block;
+}
+td.field label {
+ margin: 0 0 0 5px;
+}
+
+
+
+
+/***** main content *****/
+
+#maincontent {
+ position: absolute;
+ top: 169px;
+ left: 190px;
+ right: 30px;
+ margin: 0;
+ padding-bottom: 50px!important;
+}
+#maincontent h1, #maincontent h2 {
+ font-family: "Futura", "Century New Gothic", "Arial", Serif;
+ font-weight: normal;
+ font-size: 11pt;
+}
+#maincontent h1 {
+ color: #258;
+ font-size: 14pt;
+ margin: -5px 0 15px 0;
+}
+#maincontent h2.alttitle {
+ color: #fff;
+ margin: -17px 0 15px 15px;
+ font-weight: normal;
+}
+#maincontent div.mainbox {
+ border: 1px solid #258;
+ margin: 21px 0 -10px 0;
+ padding: 5px;
+ background: url(/f/boxbg.png) repeat;
+}
+#maincontent div.mainbox a {
+ color: #7bd;
+}
+#maincontent p {
+ margin: 3px 20px;
+}
+#maincontent div div p, #maincontent div table p {
+ margin: 0;
+}
+#maincontent h2 {
+ font-weight: bold;
+ font-size: 11pt;
+ margin: 10px 0 0 5px;
+}
+p.center {
+ text-align: center;
+}
+b.future {
+ font-weight: normal;
+ color: #e44;
+}
+
+
+
+
+/***** menu *****/
+
+#menulist {
+ position: absolute;
+ left: 30px;
+ top: 190px;
+ width: 150px;
+}
+#menulist div.menubox {
+ margin: 0 0 10px 0;
+ border: 1px solid #258;
+ background: url(boxbg.png) repeat;
+}
+#menulist div.menubox div {
+ padding: 2px 7px;
+}
+#menulist h2 {
+ border-bottom: 1px solid #258;
+ background-color: #234;
+ padding: 1px 3px;
+}
+#menulist h2, #menulist h2 a {
+ font-size: 8pt;
+ color: #fff;
+}
+#menulist dt {
+ display: block;
+ float: left;
+ width: 85px;
+ font-style: italic;
+}
+#menulist dd {
+ width: 40px;
+ float: left;
+ text-align: right;
+}
+#menulist p {
+ text-align: center;
+}
+#menulist input.text {
+ width: 100px;
+ margin-left: 15px;
+}
+#menulist input.submit {
+ width: 40px;
+ margin-left: 45px;
+}
+#menulist #search input.text {
+ width: 133px;
+ margin: 0 0 3px 7px;
+ font-style: italic;
+}
+#menulist #search input.submit {
+ display: none;
+}
+
+
+
+
+/***** main tabs *****/
+
+#maincontent ul.maintabs {
+ display: inline;
+ margin: 0;
+}
+#maincontent ul.maintabs.notfirst {
+ display: block;
+ height: 20px;
+}
+#maincontent ul.maintabs li {
+ display: inline;
+ list-style-type: none;
+}
+#maincontent ul.maintabs li a {
+ float: right;
+ display: block;
+ height: 14px;
+ border: 1px solid #258;
+ border-bottom: none;
+ padding: 1px 7px 5px 7px;
+ margin: 0 0 0 10px;
+ background-color: #012;
+}
+#maincontent ul.maintabs.notfirst li a {
+ margin-top: 20px;
+}
+#maincontent ul.maintabs.bottom li a {
+ margin-top: 10px;
+ border-bottom: 1px solid #258;
+ border-top: none;
+ padding: 4px 7px 2px 7px;
+}
+#maincontent ul.maintabs li.left a {
+ float: left;
+ margin-left: 0;
+ margin-right: 10px;
+}
+#maincontent ul.maintabs li.tabselected a,
+#maincontent ul.maintabs li a:hover {
+ background-color: #0c1925;
+ padding-bottom: 6px;
+}
+#maincontent ul.maintabs.bottom li.tabselected a,
+#maincontent ul.maintabs.bottom li a:hover {
+ padding-bottom: 2px;
+ padding-top: 5px;
+ margin-top: 9px;
+}
+
+
+
+
+
+/***** Homepage ******/
+
+#maincontent .mainbox.threelayout {
+ float: left;
+ width: 31%;
+ height: 170px;
+ margin: 21px 10px -10px 0;
+ padding: 2px;
+ overflow: hidden;
+}
+#maincontent .mainbox.threelayout.last {
+ margin-right: 0;
+}
+#maincontent .mainbox.threelayout h1 {
+ margin: -3px 0 1px 0;
+ font-size: 12pt;
+ font-weight: bold;
+}
+#maincontent .mainbox.threelayout h2 {
+ font-size: 10pt;
+ margin-top: 3px;
+}
+#maincontent .mainbox.threelayout a.right {
+ float: right;
+}
+#maincontent .mainbox.threelayout ul {
+ list-style-type: none;
+ margin-left: 10px;
+}
+p.screenshots {
+ text-align: center;
+ margin-top: 10px;
+ padding: 0;
+ height: 105px;
+}
+p.screenshots img {
+ margin: 2px;
+}
+
+
+
+
+
+/***** Browsing ******/
+
+p.browseopts {
+ text-align: center;
+ padding: 2px;
+}
+p.browseopts a {
+ padding: 1px 3px;
+ color: #fff!important;
+ border: 1px solid #258;
+ margin: 0 2px;
+}
+p.browseopts a.optselected, p.browseopts a:hover {
+ border: 0;
+ padding: 2px 4px;
+}
+#maincontent div.mainbox.browse {
+ padding: 0;
+}
+div.mainbox.browse table {
+ width: 100%;
+}
+div.mainbox.browse table td.tc1 {
+ padding-left: 25px;
+}
+table thead td {
+ font-weight: bold;
+ background-color: #135;
+}
+form.search {
+ display: block;
+ width: 100%;
+ text-align: center;
+ margin: 0 0 10px 0;
+}
+form.search .submit {
+ padding: 0 1px;
+}
+
+/* history browser */
+div.mainbox.history td.tc1_1 {
+ padding-left: 0;
+ padding-right: 0;
+ text-align: right;
+ width: 60px;
+ *width: auto; /* IE7 sucks */
+}
+div.mainbox.history td.tc1_2 {
+ padding-left: 0;
+ width: 25px;
+}
+div.mainbox.history td.tc2 { width: 65px; }
+div.mainbox.history td.tc3 { width: 90px }
+div.mainbox.history td.editsum {
+ color: #258;
+ padding-top: 0;
+ text-align: right;
+}
+
+
+
+
+
+
+/***** Discussions ******/
+
+/* threads page */
+#maincontent div.mainbox.thread {
+ padding: 0;
+}
+div.thread table {
+ width: 100%;
+}
+div.thread td {
+ border-bottom: 1px solid #258;
+}
+div.thread td.tc1 {
+ width: 150px;
+ padding: 5px 10px;
+ border-right: 1px solid #258;
+}
+div.thread td.tc2 {
+ padding: 10px 20px 10px 10px;
+}
+div.thread tr.deleted td {
+ padding: 1px 10px;
+}
+div.thread i.deleted {
+ font-style: normal;
+ color: #258;
+}
+div.thread i.lastmod {
+ float: right;
+ font-size: 7pt;
+ color: #258;
+ margin: -10px -10px -5px;
+}
+div.thread i.edit {
+ float: right;
+ color: #258;
+ font-style: normal;
+ margin: -10px -10px -5px;
+}
+
+/* threads browser */
+div.mainbox.discussions td.tc4 {
+ text-align: right;
+}
+div.mainbox.discussions a.locked {
+ text-decoration: line-through;
+}
+div.mainbox.discussions td.tags {
+ padding-top: 0;
+ padding-left: 60px;
+}
+div.mainbox.discussions td.tags a {
+ color: #258!important
+}
+div.discussions td.tc2 { width: 50px; }
+div.discussions td.tc3 { width: 90px; }
+div.discussions td.tc4 { width: 170px; }
+#maincontent h1.boxtitle, #maincontent h1.boxtitle a {
+ font-family: "Futura", "Century New Gothic", "Arial", Serif;
+ font-weight: bold;
+ font-style: italic;
+ color: #258;
+ font-size: 13pt;
+ margin: 20px 0 -20px 0;
+}
+
+
+
+
+
+/***** VN page *******/
+
+div.vndetails {
+ margin: 0 auto;
+ width: 700px;
+}
+div.vnimg {
+ float: left;
+ width: 250px;
+ margin: 0 10px;
+}
+div.vnimg i {
+ display: block;
+ width: 100%;
+ text-align: center;
+ font-size: 7pt;
+}
+div.vnimg p {
+ text-align: center;
+ padding: 0px;
+ margin: 0;
+}
+img#nsfw_hid {
+ display: none;
+ cursor: pointer;
+}
+div.vndetails table {
+ float: left;
+ width: 430px;
+}
+div.vndetails table td.key {
+ width: 80px;
+}
+div.vndetails table dt {
+ float: left;
+ font-style: italic;
+}
+div.vndetails table dd {
+ margin-left: 90px;
+}
+.catlvl_0, .catlvl_1, .catlvl_2, .catlvl_3 { font-style: normal; }
+.catlvl_1 { color: #444!important }
+.catlvl_2 { color: #777 }
+.catlvl_3 { color: #fff }
+div.vndetails td.relations dt {
+ float: none;
+ font-style: normal;
+}
+div.vndetails td.relations dd {
+ margin-left: 15px;
+}
+div.vndetails td.anime b {
+ font-size: 6pt;
+ font-weight: normal;
+ padding-right: 4px;
+}
+div.vndescription {
+ clear: left;
+ padding: 5px 0 0 0;
+}
+div.vndescription h2 {
+ margin: 0 30px!important;
+ border-top: 1px solid #258;
+ padding: 3px 70px;
+ font-weight: bold!important;
+}
+div.vndescription p {
+ margin: 0 100px!important;
+}
+
+.releases table, #screenshots table {
+ width: 100%;
+}
+.releases tr.lang td, #screenshots tr.rel td {
+ background: url(/f/boxbg.png) repeat;
+ font-weight: bold;
+}
+.releases td.tc1 {
+ padding-left: 30px;
+ width: 80px;
+}
+.releases td.tc2 {
+ text-align: center;
+ width: 50px;
+}
+.releases td.tc3 {
+ text-align: right;
+ padding: 0;
+ width: 90px;
+}
+.releases td.tc5 {
+ text-align: right;
+}
+.releases td.tc5 a {
+ color: #ddd!important;
+ border: 0;
+}
+.releases td.tc6 {
+ text-align: right;
+ width: 25px;
+ padding: 0;
+}
+a.addnew {
+ float: right;
+}
+#screenshots td.scr {
+ padding-left: 30px;
+}
+#screenshots td.scr div {
+ float: left;
+ width: 142px;
+ height: 108px;
+ margin: 2px;
+ text-align: center;
+}
+#screenshots td.scr div i { font-size: 7pt; }
+#screenshots td.scr img { border: 3px solid transparent; }
+#screenshots td.scr div.nsfw img { border: 3px solid #C00; }
+#screenshots td.scr a:hover img { border: 3px solid #258; }
+#screenshots td.scr a { border: none; }
+#screenshots div.hidden { display: none }
+#screenshots #nsfwshown { font-style: normal }
+#screenshots p.nsfwtoggle {
+ float: right;
+ margin: 0;
+}
+
+
+
+
+
+/***** Vote stats ****/
+
+.votestats { width: 610px; margin: 0 auto; }
+.votegraph { float: left; margin-right: 20px }
+.votegraph td { padding: 0 2px; }
+.votegraph td.number { text-align: right }
+.votegraph td div { float: left; height: 14px; background-color: #258; margin-right: 2px; }
+.votestats thead td { background: transparent; text-align: center; padding: 2px; }
+.votestats tfoot td { text-align: right }
+
+.recentvotes { width: 300px }
+
+
+
+
+/***** VN edit *****/
+
+#jt_box_categories ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+#jt_box_categories li {
+ display: block;
+ width: 170px;
+ float: left;
+ clear: none;
+ font-weight: bold;
+}
+#jt_box_categories li li {
+ display: list-item;
+ width: auto;
+ float: none;
+ clear: left;
+ font-weight: normal;
+ padding: 0 0 0 20px;
+ margin: 0;
+ cursor: pointer;
+ list-style-type: none;
+ padding: 0;
+}
+a.help {
+ vertical-align: super;
+ font-size: 6pt;
+ text-decoration: none;
+ font-weight: bold;
+ padding-left: 1px;
+}
+#maincontent #jt_box_categories li li a {
+ color: #ccc;
+ text-decoration: none;
+ display: block;
+ width: 160px;
+ border: none
+}
+
+#jt_box_relations table { margin-bottom: 10px; }
+#jt_box_relations h2 { margin: 0 0 3px 0px; }
+#jt_box_relations td { padding: 1px 2px; vertical-align: middle; }
+#jt_box_relations td.tc1 { width: 300px; text-align: right }
+#jt_box_relations td.tc2 { width: 170px; white-space: nowrap }
+#jt_box_relations td.tc3 { width: 200px; }
+#jt_box_relations td.tc4 { width: 40px; text-align: right }
+#jt_box_relations td.tc1 input { width: 280px; }
+#jt_box_relations td.tc2 select { width: 130px; }
+
+#ds_box {
+ border: 1px solid #258;
+ border-top: none;
+ background-color: #13273a;
+}
+#ds_box b {
+ padding: 2px 0 0 10px;
+}
+#ds_box tr.selected {
+ background: url(/f/boxbg.png) repeat;
+}
+#ds_box table {
+ width: 100%;
+}
+
+#jt_box_image div.img {
+ float: left;
+ height: 400px;
+ padding-right: 20px;
+}
+#jt_box_image h2 {
+ margin: 0;
+}
+
+#jt_box_screenshots table { width: 95% }
+#scr_table td { height: 108px; border-top: 1px solid #258; padding: 0; padding-right: 5px }
+#scr_table td.thumb { width: 136px; vertical-align: middle }
+#scr_table select { width: 400px; }
+
+
+
+
+
+/****** VN browse ********/
+
+.vnbrowse .tc2 { text-align: right; padding: 0; }
+.vnbrowse .tc3 { padding: 0; }
+#q { width: 300px }
+#advselect {
+ text-align: center;
+ display: block;
+ margin: 10px auto 3px auto;
+ border: none;
+ outline: none;
+}
+#advoptions {
+ width: 90%;
+ padding: 0 30px 5px 30px;
+ margin: 0 auto;
+ border-top: 1px solid #258;
+}
+#advoptions.hidden { display: none }
+#advoptions h2 {
+ clear: left;
+ padding-top: 10px;
+ margin-left: -20px;
+ margin-top: 0;
+ margin-bottom: 3px;
+}
+#advoptions span {
+ display: block;
+ float: left;
+ width: 170px;
+}
+#advoptions span input {
+ margin-right: 3px;
+}
+
+ul#catselect, ul#catselect ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+ul#catselect li {
+ display: block;
+ width: 170px;
+ float: left;
+ clear: none;
+ font-weight: bold;
+}
+ul#catselect li li {
+ display: list-item;
+ width: auto;
+ float: none;
+ clear: left;
+ font-weight: normal;
+ padding: 0 0 0 18px;
+ margin: 0;
+ cursor: pointer;
+ list-style-type: none;
+ background: url(/f/select.png) no-repeat;
+}
+ul#catselect li li.inc { background-position: 0px -16px; color: #0c0; }
+ul#catselect li li.exc { background-position: 0px -33px; color: #c00; }
+
+
+
+
+
+/***** Producer page/list *******/
+
+.producerpage ul {
+ margin: 0 50px;
+}
+.producerpage li {
+ padding-top: 3px;
+ list-style-type: none;
+}
+.producerpage li i {
+ display: block;
+ font-style: normal;
+ float: left;
+ width: 80px;
+}
+
+.producerbrowse ul {
+ float: left;
+ margin-top: -5px;
+ margin-left: 3%;
+ width: 28%;
+}
+.producerbrowse ul li {
+ list-style-type: none;
+}
+.producerbrowse ul li acronym {
+ margin-right: 5px;
+ margin-top: 1px;
+}
+.producerbrowse {
+ padding-bottom: 10px!important
+}
+
+
+
+
+/***** Release page *****/
+
+.release table {
+ width: 400px;
+ margin: 0 auto;
+}
+.release .key {
+ width: 70px;
+}
+
+/* edit */
+#released, #released_d { width: 70px }
+#released_m { width: 100px }
+.platforms { padding-left: 20px; }
+.platforms span { display: block; float: left; width: 150px; }
+
+#jt_box_platforms_media h2 { clear: left; padding-top: 10px; }
+#media_div select.qty { width: 90px; }
+#media_div select.medium { width: 150px }
+#media_div { padding-left: 20px; }
+#media_div span { display: block }
+
+#jt_box_visual_novels h2, #jt_box_producers h2 { clear: left; padding-top: 10px; }
+#jt_box_visual_novels div, #jt_box_producers div { padding-left: 20px }
+#jt_box_visual_novels input, #jt_box_producers input { margin-right: 10px; width: 300px }
+#jt_box_visual_novels span, #jt_box_producers span { clear: left; display: block; padding: 2px; }
+#jt_box_visual_novels span.odd, #jt_box_producers span.odd { background: url(/f/boxbg.png) repeat; }
+#jt_box_visual_novels i, #jt_box_producers i { font-style: normal; display: block; float: left; width: 310px }
+#jt_box_visual_novels b, #jt_box_producers b { font-weight: normal; }
+
+
+
+
+/***** Documentation pages *****/
+
+.docs { padding: 0 15% 20px 15%; }
+.docs h3 { margin-top: 25px; }
+.docs dd { padding-bottom: 5px; margin-left: 120px; }
+.docs dt { float: left }
+.docs ul.index { display: block; float: right; width: 150px; padding: 2px; margin: 0 0 10px 5px; background: url(/f/boxbg.png) repeat; border: 1px solid #258; }
+.docs ul.index li { list-style-type: none; }
+.docs ul.index li a { margin: 0 0 0 10px; }
+.docs .retired { text-decoration: line-through; }
+.docs dt b { color: #258; font-weight: normal; font-style: normal; font-size: 12px; }
+
+
+
+
+/***** Wishlist browser ******/
+
+.wishlist .tc1 { padding-top: 0; padding-bottom: 0; }
+.wishlist tfoot td { padding: 0 0 0 25px }
+
+
+
+/***** User VN list browser ******/
+
+.rlist .relhid { }
+.rlist .relhid_but, #relhidall { cursor: pointer }
+.browse.rlist .tc2 { width: 100px; }
+.browse.rlist .tc3 { width: 70px; }
+.browse.rlist .relhid .tc1 { padding-left: 40px; width: 70px; }
+.browse.rlist .relhid .tc1.own { padding: 0 0 0 25px; width: 95px }
+.browse.rlist .relhid input { margin-right: 5px }
+.browse.rlist .relhid .tc2 { padding: 0; width: 30px; }
+.browse.rlist .relhid .tc3 { width: auto }
+.browse.rlist .relhid .tc4 { text-align: right }
+
+
+
+
+/***** Userpage *****/
+
+.userpage table {
+ width: 400px;
+ margin: 0 auto;
+}
+.userpage .key {
+ width: 70px;
+}
+
+
+
+
+
+/***** Warning/Notice Box *****/
+
+div.warning, div.notice {
+ margin: 5px 10%;
+ padding: 15px;
+ background-color: #534;
+ border: 1px solid #C00;
+}
+div.notice {
+ background-color: #354;
+ border: 1px solid #0C0;
+}
+div.warning ul, div.notice ul {
+ margin-left: 0;
+}
+div.warning li, div.notice li {
+ margin-left: 20px;
+}
+#maincontent div.warning h2, #maincontent div.notice h2 {
+ font-size: 9pt;
+ font-weight: bold;
+ margin: 0;
+}
+
+
+
+
+/****** Revision information ******/
+
+div.revision {
+ padding-bottom: 10px!important;
+}
+div.revision div, div.revision table {
+ border: 1px solid #258;
+ margin: 0 auto;
+ width: 90%;
+ /*background: url(/f/boxbg.png) repeat;*/
+ background-color: #13273a;
+ clear: both;
+}
+div.revision table thead tr td {
+ background-color: transparent!important;
+ text-align: center;
+ font-weight: normal;
+}
+div.revision table td {
+ border-right: 1px solid #258;
+ padding: 5px;
+}
+div.revision td.tcval {
+ width: 44%;
+}
+div.revision div {
+ padding: 5px;
+ text-align: center;
+}
+.diff_add {
+ font-weight: normal;
+ background-color: #354;
+}
+.diff_del {
+ font-weight: normal;
+ background-color: #534;
+}
+div.revision .next {
+ float: right;
+ margin-right: 5%;
+}
+div.revision .prev {
+ float: left;
+ margin-left: 5%;
+}
+div.revision .item {
+ text-align: center;
+}
+
+
+
+/****** Image Viewer *****/
+
+div#iv_view {
+ position: absolute;
+ display: none;
+ top: -5000px;
+ left: -5000px;
+ background: url(/f/boxbg.png) repeat;
+ border: 1px solid #258;
+ padding: 5px;
+ text-align: center;
+}
+#iv_view a { border: 0; font-weight: bold; font-size: 10pt; }
+#iv_view img { cursor: pointer }
+#ivclose { float: right; padding-left: 10px }
+#ivnext { padding-left: 5px; }
+#ivprev { padding-right: 5px; }
+#ivfull { float: left; padding-right: 10px; }
+#ivimgload {
+ display: block;
+ position: absolute;
+ left: -500px;
+ top: -50px;
+ width: 100px;
+ padding: 3px;
+ background-color: #f5f5f5;
+ text-align: center;
+ border: 1px solid #ccc;
+ color: #000;
+}
+
+
+
+
+/****** Icons *******/
+
+.icons {
+ background: url(/f/icons.png) no-repeat;
+ width: 16px;
+ height: 14px;
+ margin: 0 2px 0 0;
+ overflow: hidden;
+ display:-moz-inline-stack;
+ display: inline-block;
+ padding: 0;
+ border: 0;
+ text-decoration: none;
+}
+.icons {
+ margin-top: 0px!important;
+}
+.icons.lang {
+ width: 13px;
+ height: 11px;
+ opacity: 0.5;
+}
+.icons.par, .icons.tri, .icons.com { width: 11px; }
+acronym.icons, acronym.uicons { cursor: default; }
+a .icons { cursor: pointer }
+.icons.oth { background: none; }
+.icons.drc { background-position: 0px 0px; }
+.icons.lin { background-position: 0px -14px; }
+.icons.nds { background-position: 0px -28px; }
+.icons.ps2 { background-position: 0px -42px; }
+.icons.sfc { background-position: 0px -56px; }
+.icons.gba { background-position: 0px -70px; }
+.icons.ps3 { background-position: 0px -84px; }
+
+.icons.dvd { background-position: -16px 0px; }
+.icons.mac { background-position: -16px -14px; }
+.icons.ps1 { background-position: -16px -28px; }
+.icons.psp { background-position: -16px -42px; }
+.icons.win { background-position: -16px -56px; }
+.icons.wii { background-position: -16px -70px; }
+.icons.xb3 { background-position: -16px -84px; }
+
+.icons.com { background-position: -32px 0px; }
+.icons.par { background-position: -32px -14px; }
+.icons.tri { background-position: -32px -28px; }
+.icons.ext { background-position: -32px -42px; }
+.icons.msx { background-position: -32px -56px; }
+.icons.nes { background-position: -32px -70px; }
+
+.icons.cs { background-position: -48px 0px; }
+.icons.da { background-position: -48px -11px; }
+.icons.de { background-position: -48px -22px; }
+.icons.en { background-position: -48px -33px; }
+.icons.es { background-position: -48px -44px; }
+.icons.fi { background-position: -48px -55px; }
+.icons.fr { background-position: -48px -66px; }
+.icons.it { background-position: -48px -77px; }
+.icons.ja { background-position: -48px -88px; }
+
+.icons.nl { background-position: -61px 0px; }
+.icons.no { background-position: -61px -11px; }
+.icons.pl { background-position: -61px -22px; }
+.icons.pt { background-position: -61px -33px; }
+.icons.ru { background-position: -61px -44px; }
+.icons.sv { background-position: -61px -55px; }
+.icons.tr { background-position: -61px -66px; }
+.icons.zh { background-position: -61px -77px; }
+.icons.ko { background-position: -61px -88px; }
+
diff --git a/static/files/def.js b/static/files/def.js
deleted file mode 100644
index 4928cb84..00000000
--- a/static/files/def.js
+++ /dev/null
@@ -1,479 +0,0 @@
-
-
-/* G L O B A L S T U F F */
-
-function x(y){return document.getElementById(y)}
-function cl(o,f){if(x(o))x(o).onclick=f}
-function DOMLoad(y){var d=0;var f=function(){if(d++)return;y()};
-if(document.addEventListener)document.addEventListener("DOMCont"
-+"entLoaded",f,false);document.write("<script id=_ie defer src="
-+"javascript:void(0)><\/script>");document.getElementById('_ie')
-.onreadystatechange=function(){if(this.readyState=="complete")f()
-};if(/WebKit/i.test(navigator.userAgent))var t=setInterval(
-function(){if(/loaded|complete/.test(document.readyState)){
-clearInterval(t);f()}},10);window.onload=f;}
-
-
-
-
-/* F O R M S U B S */
-
-var formsubs = [];
-function formhid() {
- var i;
- var j;
- var l = document.forms[1].getElementsByTagName('a');
- for(i=0; i<l.length; i++)
- if(l[i].className.indexOf('s_') != -1) {
- formsubs[ l[i].className.substr(l[i].className.indexOf('s_')+2) ] = 0;
- l[i].onclick = function() {
- formtoggle(this.className.substr(this.className.indexOf('s_')+2));
- return false;
- };
- }
-
- if(x('_hid') && x('_hid').value.length > 1) {
- l = x('_hid').value.split(/,/);
- for(i in formsubs) {
- var inz=0;
- for(j=0; j<l.length; j++)
- if(l[j] == i)
- inz = 1;
- if(!inz)
- formsubs[i] = !formsubs[i];
- }
- }
- if(x('screenshots') && !formsubs['scr'])
- scrLoad();
-}
-function formtoggle(n) {
- formsubs[n] = !formsubs[n];
- if(x('screenshots') && !formsubs['scr'] && !x('scrTbl'))
- scrLoad();
-
- var i;
- var l = document.forms[1].getElementsByTagName('a');
- for(i=0; i<l.length; i++)
- if(l[i].className.indexOf('s_'+n) != -1)
- l[i].innerHTML = (formsubs[n] ? '&#9656;' : '&#9662;') + l[i].innerHTML.substr(1);
-
- l = document.forms[1].getElementsByTagName('li');
- for(i=0; i<l.length; i++)
- if(l[i].className.indexOf('sf_'+n) != -1) {
- if(formsubs[n])
- l[i].className += ' formhid';
- else
- l[i].className = l[i].className.replace(/formhid/g, '');
- }
-
- if(x('_hid')) {
- l = [];
- for(i in formsubs)
- if(!formsubs[i])
- l[l.length] = i;
- x('_hid').value = l.toString();
- }
-}
-
-
-
-
-/* D R O P D O W N M E N U S */
-
-var ddx;var ddy;var dds=null;
-function dropDown(e) {
- e = e || window.event;
- var tg = e.target || e.srcElement;
- while(tg && (tg.nodeType == 3 || tg.nodeName.toLowerCase() != 'a'))
- tg = tg.parentNode;
-
- if(tg && tg.rel)
- tg.rel = tg.rel.replace(/ *nofollow */,"");
- if(!dds && (!tg || !tg.rel || tg.className.indexOf('dropdown') < 0))
- return;
-
- if(!dds) {
- var obj=tg;
- ddx = ddy = 0;
- do {
- ddx += obj.offsetLeft;
- ddy += obj.offsetTop;
- } while(obj = obj.offsetParent);
- if(tg.className.indexOf('above') >= 0) {
- ddx += 30;
- ddy -= x(tg.rel).offsetHeight - 20;
- }
- else
- ddy += 16;
- obj = x(tg.rel);
- obj.style.left = ddx+'px';
- obj.style.top = ddy+'px';
- dds = tg;
- }
-
- if(dds) {
- var mouseX = e.pageX || (e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft);
- var mouseY = e.pageY || (e.clientY + document.body.scrollTop + document.documentElement.scrollTop);
- var obj = x(dds.rel);
- if((mouseX < ddx-25 || mouseX > ddx+obj.offsetWidth+5 || mouseY < ddy-20 || mouseY > ddy + obj.offsetHeight)
- || (tg && tg.className.indexOf('dropdown') >= 0 && tg != dds)) {
- obj.style.left = '-500px';
- dds = null;
- }
- }
-}
-
-
-
-
-/* A D V A N C E D S E A R C H */
-
-var ad_cats = {};
-var ad_lang = {};
-var ad_plat = {};
-
-function adsearch() {
- x('vsearch').onsubmit = ad_dosearch;
- x('vsearch_sub').onclick = ad_dosearch;
- x('q').onkeyup = ad_update;
-
- x('adsearchclick').onclick = function() {
- if(x('adsearch').style.display == 'none') {
- x('adsearch').style.display = 'block';
- x('adsearchclick').innerHTML = '&#9662; advanced options';
- } else {
- x('adsearch').style.display = 'none';
- x('adsearchclick').innerHTML = '&#9656; advanced options';
- }
- };
-
- var l = x('cat').getElementsByTagName('li');
- for(i=0;i<l.length;i++)
- if(l[i].id.indexOf('cat_') != -1) {
- ad_cats[ l[i].id.substr(l[i].id.indexOf('cat_')+4, 3) ] = l[i].innerHTML.substring(0, l[i].innerHTML.indexOf('(')-1).toLowerCase();
- l[i].onclick = function () {
- try { document.selection.empty() } catch(e) { try { window.getSelection().collapse(this, 0) } catch(e) {} };
- ad_update(1, this.innerHTML.substring(0, this.innerHTML.indexOf('(')-1));
- };
- }
-
- l = x('lfilter').getElementsByTagName('input');
- for(i=0;i<l.length;i++) {
- ad_lang[ l[i].name.substring(5) ] = l[i].value.toLowerCase();
- l[i].onclick = function() { ad_update(0, this.value) };
- }
-
- l = x('pfilter').getElementsByTagName('input');
- for(i=0;i<l.length;i++) {
- ad_plat[ l[i].name.substring(5) ] = l[i].value.toLowerCase();
- l[i].onclick = function() { ad_update(0, this.value) };
- }
-
- ad_update();
-}
-
-function ad_update(add, term) {
- var q = x('q').value;
- var i;
-
- if(add == 0 || add === 1) {
- var qn = q;
- if(!add)
- eval('qn = qn.replace(/'+term+'/gi, "")');
- else {
- eval('qn = qn.replace(/(^|[^-])'+term+'/gi, "$1-'+term+'")');
- if(qn == q)
- eval('qn = qn.replace(/-'+term+'/gi, "")');
- }
- if(qn == q)
- q += ' '+term;
- else
- q = qn;
-
- q = q.replace(/^ +/, "");
- q = q.replace(/ +$/, "");
- q = q.replace(/ +/g, " ");
-
- x('q').value = q;
- }
-
- q = q.toLowerCase();
- for (i in ad_lang)
- x('lang_'+i).checked = q.indexOf(ad_lang[i]) >= 0 || q.indexOf('l:'+i) >= 0 ? true : false;
- for (i in ad_plat)
- x('plat_'+i).checked = q.indexOf(ad_plat[i]) >= 0 || q.indexOf('p:'+i) >= 0 ? true : false;
- for (i in ad_cats)
- x('cat_'+i).className = q.indexOf('-'+ad_cats[i]) >= 0 || q.indexOf('-c:'+i) >= 0 ? 'exc' : q.indexOf(ad_cats[i]) >= 0 || q.indexOf('c:'+i) >= 0 ? 'inc' : '';
-}
-
-function ad_dosearch() {
- location.href = '?q='+x('q').value;
- return false;
-}
-
-
-
-/* S C R E E N S H O T S */
-
-var scrNsfwEnabled = null;
-function scrNsfwHid() {
- var l=x('screenshots').getElementsByTagName('a');
- var i;
- if(scrNsfwEnabled == null)
- scrNsfwEnabled = 0;
- else {
- for(i=0;i<l.length;i++)
- if(l[i].className.indexOf('scr_nsfw')>=0)
- l[i].style.display = scrNsfwEnabled ? 'block' : 'none';
- scrNsfwEnabled = scrNsfwEnabled ? 0 : 1;
- }
-
- var t=0;var n=0;
- for(i=0;i<l.length;i++) {
- if(l[i].className.indexOf('shot')<0)
- continue;
- t++;
- if(l[i].className.indexOf('scr_nsfw')>=0)
- n++;
- if(l[i].style.display == 'none')
- scrNsfwEnabled = 1;
- }
- x('scrNsfwHid').innerHTML = 'Showing '+(t-(scrNsfwEnabled?n:0))+' out of '+t+' items. '
- +(scrNsfwEnabled ? '<a href="javascript:scrNsfwHid()">Show</a> / hide' : 'Show / <a href="javascript:scrNsfwHid()">hide</a>')
- +' nsfw.';
-}
-function scrView(what) {
- what = what && what.rel ? what : this;
- var u=what.href;
- var r=what.rel;
- d = x('scrView');
-
- // fix prev/next links (if any)
- var ol=x('screenshots').getElementsByTagName('a');
- if(ol.length > 0) {
- var l=[];
- for(i=0;i<ol.length;i++)
- if(ol[i].className.indexOf('shot')>=0 && (!scrNsfwEnabled || ol[i].className.indexOf('scr_nsfw')<0))
- l[l.length] = ol[i];
-
- ol=0;
- for(i=0;i<l.length;i++)
- if(l[i].href == u) {
- ol=1;
- x('scrnext').style.visibility = l[i+1] ? 'visible' : 'hidden';
- x('scrnext').href = l[i+1] ? l[i+1].href : '#';
- x('scrnext').rel = l[i+1] ? l[i+1].rel : '';
- x('scrprev').style.visibility = l[i-1] ? 'visible' : 'hidden';
- x('scrprev').href = l[i-1] ? l[i-1].href : '#';
- x('scrprev').rel = l[i-1] ? l[i-1].rel : '';
- }
- } else
- ol=0;
- if(!ol)
- x('scrnext').style.visibility = x('scrprev').style.visibility = 'hidden';
-
- // calculate dimensions
- var w = Math.floor(r.split('x')[0]);
- var h = Math.floor(r.split('x')[1]);
- var ww = typeof(window.innerWidth) == 'number' ? window.innerWidth : document.documentElement.clientWidth;
- var wh = typeof(window.innerHeight) == 'number' ? window.innerHeight : document.documentElement.clientHeight;
- var st = typeof(window.pageYOffset) == 'number' ? window.pageYOffset : document.body && document.body.scrollTop ? document.body.scrollTop : document.documentElement.scrollTop;
- if(w+100 > ww || h+70 > wh) {
- x('scrfull').href = u;
- x('scrfull').innerHTML = w+'x'+h;
- x('scrfull').style.visibility = 'visible';
- if(w/h > ww/wh) { // width++
- h *= (ww-100)/w;
- w = ww-100;
- } else { // height++
- w *= (wh-70)/h;
- h = wh-70;
- }
- } else
- x('scrfull').style.visibility = 'hidden';
- var dw = w;
- var dh = h+20;
- dw = dw < 200 ? 200 : dw;
-
- // update document
- d.style.display = 'block';
- x('scrimg').innerHTML = '<img src="'+u+'" onclick="scrClose()" onload="document.getElementById(\'scrimgload\').style.top=\'-400px\'" style="width: '+w+'px; height: '+h+'px" />';
- d.style.width = dw+'px';
- d.style.height = dh+'px';
- d.style.left = ((ww - dw) / 2 - 10)+'px';
- d.style.top = ((wh - dh) / 2 + st - 20)+'px';
- x('scrimgload').style.left = ((ww - 100) / 2 - 10)+'px';
- x('scrimgload').style.top = ((wh - 20) / 2 + st)+'px';
- return false;
-}
-function scrClose() {
- x('scrView').style.display = 'none';
- x('scrView').style.top = '-5000px';
- x('scrimg').innerHTML = '';
- return false;
-}
-
-
-
-
-/* O N L O A D */
-
-DOMLoad(function() {
- var i;
-
- // search box
- i = x('searchfield');
- i.onfocus = function () {
- if(this.value == 'search') {
- this.value = '';
- this.style.color = '#000'; } };
- i.onblur = function () {
- if(this.value.length < 1) {
- this.value = 'search';
- this.style.color = '#999';} };
-
- // advanced search
- if(x('adsearch'))
- adsearch();
-
- // userdel
- cl('userdel', function() { return confirm("Completely remove this account from the site?") });
-
- // vote warnings
- cl('dovote_10', function() { return confirm(
- "You are about to give this visual novel a 10 out of 10. This is a rather extreme rating, "
- +"meaning this is one of the best visual novels you've ever played and it's unlikely "
- +"that any other game could ever be better than this one.\n"
- +"It is generally a bad idea to have more than three games in your vote list with this rating, choose carefully!") });
- cl('dovote_1', function() { return confirm(
- "You are about to give this visual novel a 1 out of 10. This is a rather extreme rating, "
- +"meaning this game has absolutely nothing to offer, and that it's the worst game you have ever played.\n"
- +"Are you really sure this visual novel matches that description?") });
-
- // NSFW
- cl('nsfw', function () {
- this.src = this.className;
- this.id = '';
- });
-
- // rlists
- i = x('rli');
- if(i) {
- var l=i.getElementsByTagName('td');
- for(i=0;i<l.length;i++)
- if(l[i].className.indexOf('relhid')>=0)
- l[i].onclick = function() {
- var id=this.id.substr(2);
- var j=0;var o;
- var op=x('rr'+id+'-1').style.display == 'none' ? 1 : 0;
- while((o=x('rr'+id+'-'+(++j))) != null)
- o.style.display = op ? '' : 'none';
- x('rhd'+id).innerHTML = op ? '&#9662;' : '&#9656;';
- };
- var l=x('rli').getElementsByTagName('tr');
- for(i=0;i<l.length;i++)
- if(l[i].className.indexOf('relhid')>=0)
- l[i].style.display = 'none';
- var allhid=1;
- cl('relhidpar', function() {
- allhid=!allhid;
- l=x('rli').getElementsByTagName('tr');
- for(i=0;i<l.length;i++)
- if(l[i].className.indexOf('relhid')>=0)
- l[i].style.display = allhid ? 'none' : '';
- l=x('rli').getElementsByTagName('b');
- for(i=0;i<l.length;i++)
- if(l[i].id.substr(0,3) == 'rhd')
- l[i].innerHTML = !allhid ? '&#9662;' : '&#9656;';
- x('relhidparb').innerHTML = !allhid ? '&#9662;' : '&#9656;';
- });
- }
-
- // mass-change rlist or wlist status
- if(x('vnlistchange')) {
- x('vnlistchange').onchange = function() {
- var val = this.options[this.selectedIndex].value;
- if(val == 'n')
- return;
- var l = (x('rli')||x('twl')).getElementsByTagName('input');
- var y; var ch=0;
- for(y=0;y<l.length;y++)
- if(l[y].type == 'checkbox' && l[y].checked)
- ch++;
- if(!ch)
- return alert('Nothing selected...');
- if(val == 'd' && !confirm('Are you sure you want to remove the selected items from your list?'))
- return;
- document.forms[1].submit();
- }
- }
-
- // screenshots
- if(x('scrNsfwHid'))
- scrNsfwHid();
- if(x('screenshots')) {
- l=x('screenshots').getElementsByTagName('a');
- for(i=0;i<l.length;i++)
- l[i].onclick=scrView;
- var d = document.createElement('div');
- d.id = 'scrView';
- d.innerHTML = '<b id="scrimg"></b><br />'
- +'<a href="#" id="scrfull">&nbsp;</a>'
- +'<a href="#" onclick="return scrClose()" id="scrclose">close</a>'
- +'<a href="#" onclick="return scrView(this)" id="scrprev">&lt;- previous</a>'
- +'<a href="#" onclick="return scrView(this)" id="scrnext">next -&gt;</a>';
- document.body.appendChild(d);
- d = document.createElement('b');
- d.id = 'scrimgload';
- d.innerHTML = 'Loading...';
- document.body.appendChild(d);
- }
-
- // spam protection on all forms
- if(document.forms.length > 1)
- for(i=1; i<document.forms.length; i++)
- document.forms[i].action = document.forms[i].action.replace(/\/nospam\?/,'');
-
- // dropdown menus
- var z = document.getElementsByTagName('a');
- for(i=0;i<z.length;i++)
- if(z[i].rel && z[i].className.indexOf('dropdown') >= 0) {
- document.onmousemove = dropDown;
- break;
- }
-
- // form-stuff
- if(document.forms.length > 1) {
- formhid();
- // edit summary warning
- if(x('comm'))
- document.forms[1].onsubmit = function () {
- var z = x('comm');
- if(z.value.length > 5) return true;
- var y = prompt("Edit summary field is empty,\nPlease explain your edits and cite all sources!", z.value);
- if(y == null) return false;
- z.value = y;
- return true;
- };
- }
-
- // init dyna
- if(window.dInit)
- dInit();
-
- // zebra-striped tables (client side!? yes... client side :3)
- var sub = document.getElementsByTagName('tr');
- for(i=1; i<sub.length; i+=2)
- if(!sub[i].style.backgroundColor)
- sub[i].style.backgroundColor = '#f5f5f5';
-});
-
-
-
-
-// small hack because the mozilla -moz-inline-stack display hack sucks
-// (so we're counter-hacking a CSS hack using JS... right)
-if(navigator.userAgent.indexOf('Gecko') >= 0 && navigator.userAgent.indexOf('like Gecko') < 0 && navigator.userAgent.indexOf('3.0') < 0)
- document.write('<style type="text/css">.icons.lang { width: 15px; height: 13px; }</style>');
-
-
diff --git a/static/files/dyna.js b/static/files/dyna.js
deleted file mode 100644
index fd0411d2..00000000
--- a/static/files/dyna.js
+++ /dev/null
@@ -1,815 +0,0 @@
-var med = {
- cd: 'CD',
- dvd: 'DVD',
- gdr: 'GD-ROM',
- blr: 'Blu-Ray disk',
- 'in':'Internet download',
- pa: 'Patch',
- otc: 'Other (console)'
-};
-var vrel = [
- 'Sequel',
- 'Prequel',
- 'Same setting',
- 'Alternative setting',
- 'Alternative version',
- 'Same characters',
- 'Side story',
- 'Parent story',
- 'Summary',
- 'Full story',
- 'Other'
-];
-
-var md;var pd;var rl;var vn;var ct;
-
-function dInit() {
- md = x('md_select');
- if(md) {
- md.onclick = mdChangeSel;
- mdLoad();
- md.selectedIndex = 0;
- mdChangeSel();
- }
-
- pd = x('pd_select');
- if(pd) {
- pd.onclick = pdChangeSel;
- pdLoad();
- pd.selectedIndex = 0;
- pdChangeSel();
- }
-
- rl = x('rl_select');
- if(rl) {
- rl.onclick = rlChangeSel;
- rlLoad();
- rl.selectedIndex = 0;
- rlChangeSel();
- }
-
- vn = x('vn_select');
- if(vn) {
- vn.onclick = vnChangeSel;
- vnLoad();
- vn.selectedIndex = 0;
- vnChangeSel();
- }
-
- ct = x('categories');
- if(ct)
- catLoad();
-
-/* scrLoad() is called by the form sub functions in def.js
- if(x('scrfrm'))
- scrLoad();*/
-}
-
-function qq(v) {
- return v.replace(/&/g,"&amp;").replace(/</,"&lt;").replace(/>/,"&gt;").replace(/'/g,/*'*/ "\\'").replace(/"/g,/*"*/'&quot;');
-}
-
-// small AJAX wapper
-var hr = false;
-function ajax(url, func) {
- if(hr)
- hr.abort();
- hr = (window.ActiveXObject) ? new ActiveXObject('Microsoft.XMLHTTP') : new XMLHttpRequest();
- if(hr == null) {
- alert("Your browse does not support the functionality this website requires.");
- return;
- }
- hr.onreadystatechange = func;
- hr.open('GET', url, true);
- hr.send(null);
-}
-
-
-
-
- /************************\
- * M E D I A *
- \************************/
-
-
-function mdChangeSel() {
- var sel = md.options[md.selectedIndex || 0];
- var o = x('md_conts');
- var i;
- if(sel.value == '0_new') {
- var l = ''; var q = '<option value="0">Qty</option>';
- for(i in med)
- l += '<option value="'+i+'">'+med[i]+'</option>';
- for(i=1;i<10;i++)
- q += '<option value="'+i+'">'+i+'</option>';
- o.innerHTML = '<select id="md_Q" name="md_Q" style="width: 50px;">'+q+'</select>'
- + '<select id="md_S" name="md_S" style="width: 150px;">'+l+'</select>'
- + '<br style="clear: both" />'
- + '<button type="button" onclick="mdAddRem()">add/remove</button>'
- + '<br />Qty is only required for CD & DVD';
- } else {
- o.innerHTML = 'Selected "' + sel.text + '"<br />'
- + '<button type="button" onclick="mdAddRem(\'' + sel.value + '\')">remove</button>';
- }
-}
-
-function mdAddRem(id) {
- var i;
- var d = 0;
- var o = id ? null : x('md_S').options[x('md_S').selectedIndex];
- var qty = id ? null : x('md_Q').options[x('md_Q').selectedIndex].value;
- var v = id ? id : (o.value != 'cd' && o.value != 'dvd' && o.value != 'gdr' && o.value != 'blr' ? o.value : (o.value + '_' + qty));
- for(i=0;i<md.options.length;i++)
- if(md.options[i].value == v) {
- md.options[i] = null;
- d = 1;
- }
- if(!d && !id) {
- if(v.indexOf('_') >= 0 && qty == 0) {
- alert('Please specify the quantity');
- return;
- }
- md.options[md.options.length] = new Option(mdString(qty, o.value), v);
- }
- else if(id) {
- md.options[0].selected = true;
- mdChangeSel();
- }
- mdSerialize();
-}
-
-function mdSerialize() {
- var dest = x('media');
- var str = '';
- var i;
- for(i=0;i<md.options.length;i++)
- md.options[i].value != '0_new' && (str += (str.length>0 ? ',' : '') + md.options[i].value);
- dest.value = str;
-}
-
-function mdLoad() {
- var me = x('media').value.split(',');
- var i, j;
- for(i=0;i<me.length;i++) {
- var m = me[i].split('_');
- if(med[m[0]])
- md.options[md.options.length] = new Option(mdString(m[1], m[0]), me[i]);
- }
-}
-
-function mdString(qty, medium) {
- if(medium != 'cd' && medium != 'dvd' && medium != 'gdr' && medium != 'blr')
- return med[medium];
- else
- return qty + ' ' + med[medium] + (qty > 1 ? 's' : '');
-}
-
-
-
-
-
-
- /************************\
- * P R O D U C E R S *
- \************************/
-
-
-function pdChangeSel() {
- var sel = pd.options[pd.selectedIndex || 0];
- var o = x('pd_conts');
- var i;
- if(sel.value == '0_new') {
- o.innerHTML = '<input type="text" name="pd_S" id="pd_S" onkeyup="pdDoSearch(0)" onkeydown="return pdEnter(event)" style="width: 150px;" />'
- + '<button type="button" onclick="pdDoSearch(1)" style="width: 55px;">Search!</button><br style="clear: both" />'
- + '<span id="pd_R" style="display: block; width: 220px; height: 70px; overflow: auto"></span>'
- + '<a href="/p/add" target="_blank">Add new producer</a>';
- pdDoSearch('');
- } else {
- o.innerHTML = 'Selected "' + sel.text + '"<br />'
- + '<button type="button" onclick="pdAddRem(\'' + qq(sel.value) + '\')">remove</button>';
- }
-}
-
-function pdEnter(ev) {
- var c = document.layers ? ev.which : document.all ? event.keyCode : ev.keyCode;
- if(c == 13) {
- pdDoSearch(0);
- return false;
- }
- return true;
-}
-
-function pdDoSearch(f) {
- var v = x('pd_S').value;
- var d = x('pd_R');
- if(v.length < 1)
- d.innerHTML = 'Hint: type pX if you know the producer id.';
- else {
- if(f)
- d.innerHTML = '...searching...';
- ajax('/xml/producers.xml?q='+encodeURIComponent(v)+'&r='+(Math.floor(Math.random()*999)+1), function () {
- if(!hr || hr.readyState != 4 || !hr.responseText)
- return;
- if(hr.status != 200)
- return alert('Whoops, error! :(');
- var items = hr.responseXML.getElementsByTagName('item');
- if(!items || items.length < 1) {
- d.innerHTML = 'No results';
- return false;
- }
- var res = '';
- var i,j;
- for(i=0; i<items.length; i++) {
- var id = items[i].getElementsByTagName('id')[0].firstChild.nodeValue;
- var name = items[i].getElementsByTagName('name')[0].firstChild.nodeValue;
- var cid = id + ',' + name;
- var s = '';
- for(j=0; j<pd.options.length; j++)
- if(pd.options[j].value == cid)
- s = ' checked="checked"';
- res += '<input type="checkbox" id="pd_I'+id+'"'+s+' onclick="pdAddRem(\''+qq(cid)+'\', \''+qq(name)+'\')" />'
- + '<label style="width: auto" for="pd_I'+id+'">'+name+'</label><br style="clear: left" />';
- }
- d.innerHTML = res;
- });
- }
-}
-
-function pdAddRem(id, name) {
- var i;
- var d = 0;
- for(i=0;i<pd.options.length;i++)
- if(pd.options[i].value == id) {
- pd.options[i] = null;
- d = 1;
- }
- if(!d && name)
- pd.options[pd.options.length] = new Option(name, id);
- else if(!name) {
- pd.options[0].selected = true;
- pdChangeSel();
- }
- pdSerialize();
-}
-
-// id,name|||id,name
-function pdSerialize() {
- var dest = x('producers');
- var str = '';
- var i;
- for(i=0;i<pd.options.length;i++)
- pd.options[i].value != '0_new' && (str += (str.length>0 ? '|||' : '') + pd.options[i].value);
- dest.value = str;
-}
-
-function pdLoad() {
- var pds = x('producers').value.split('|||');
- if(!pds[0])
- return;
- var i;
- for(i=0;i<pds.length;i++)
- pd.options[pd.options.length] = new Option(pds[i].split(',',2)[1], pds[i]);
-}
-
-
-
-
-
-
-
-
- /************************\
- * R E L A T I O N S *
- \************************/
-
-
-var rlsel = ''; var rlname = '';
-function rlChangeSel() {
- var sel = rl.options[rl.selectedIndex || 0];
- var o = x('rl_conts');
- var i;
- rlsel = '';
- var ops='';
- for(i=0;i<vrel.length;i++)
- ops += '<option value="'+i+'">'+vrel[i]+'</option>';
- if(sel.value == '0_new') {
- o.innerHTML = '<input type="text" name="rl_S" id="rl_S" onkeyup="rlDoSearch(0)" onkeydown="return rlEnter(event)" style="width: 150px;" />'
- + '<button type="button" onclick="rlDoSearch(1)" style="width: 60px;">Search!</button><br style="clear: both" />'
- + '<span id="rl_R" style="display: block; width: 250px; height: 70px; overflow: auto"></span>'
- + '<select id="rl_L" name="rl_L" onchange="rlAddRem(0)"><option value="-1">...is a [..] of this visual novel</option>'+ops+'</select>';
- rlDoSearch('');
- } else {
- o.innerHTML = sel.value.split(',', 3)[2] + '<br />'
- + '<select id="rl_L" name="rl_L" onchange="rlAddRem(\''+qq(sel.value)+'\')">'
- + '<option value="-1"> - change - </option>'+ops+'<option value="-2"> - remove relation - </option></select>';
- }
-}
-
-function rlEnter(ev) {
- var c = document.layers ? ev.which : document.all ? event.keyCode : ev.keyCode;
- if(c == 13) {
- rlDoSearch(0);
- return false;
- }
- return true;
-}
-
-function rlDoSearch(f) {
- var v = x('rl_S').value;
- var d = x('rl_R');
- if(v.length < 1)
- d.innerHTML = 'Search for a visual novel to add a relation.<br /><br />'
- + 'Hint: type vX if you know the VN id.';
- else {
- if(f)
- d.innerHTML = '...searching...';
- ajax('/xml/vn.xml?q='+encodeURIComponent(v)+'&r='+(Math.floor(Math.random()*999)+1), function () {
- if(!hr || hr.readyState != 4 || !hr.responseText)
- return;
- if(hr.status != 200)
- return alert('Whoops, error! :(');
- rlsel = '';
- var items = hr.responseXML.getElementsByTagName('item');
- if(!items || items.length < 1) {
- d.innerHTML = 'No results';
- return false;
- }
- var res = '';
- var i,j;
- for(i=0; i<items.length; i++) {
- var id = items[i].getElementsByTagName('id')[0].firstChild.nodeValue;
- var title = items[i].getElementsByTagName('title')[0].firstChild.nodeValue;
- var cid = id + ',' + title;
- res += '<input type="radio" name="rl_rad" id="pd_I'+id+'" value="rl_I'+id+'" onclick="rlAddRem(\''+qq(cid)+'\', \''+qq(title)+'\')" />'
- + '<label style="width: auto" for="rl_I'+id+'">'+title+'</label><br style="clear: left" />';
- }
- d.innerHTML = res;
- });
- }
-}
-
-function rlAddRem(id, name) {
- var i;
- var rs = x('rl_L').selectedIndex;
- if(id && name) {
- rlsel = id;
- rlname = name;
- } else if(id) {
- if(!rs)
- return;
- if(rs == x('rl_L').options.length-1) { // remove
- for(i=0;i<rl.options.length;i++)
- if(rl.options[i].value == id)
- rl.options[i] = null;
- rl.options[0].selected = true;
- } else {
- var cur = id.split(',', 3);
- i = rl.selectedIndex;
- rs--;
- rl.options[i] = new Option(vrel[rs]+': '+cur[2], (rs)+','+cur[1]+','+cur[2]);
- rl.options[i].selected = true;
- }
- rlChangeSel();
- rlSerialize();
- return;
- } else if(!rlsel) {
- alert('No visual novel selected');
- return;
- }
-
- if(!id && rlsel && !rs) { // remove
- for(i=0;i<rl.options.length;i++)
- if(rl.options[i].value.indexOf(rlsel) != -1)
- rl.options[i] = null;
- rlSerialize();
- return;
- }
- if(!rs)
- return;
-
- // add/edit
- var mod = rl.options.length;
- rs--;
- for(i=0;i<rl.options.length;i++)
- if(rl.options[i].value.indexOf(rlsel) != -1)
- mod = i;
- rl.options[mod] = new Option(vrel[rs]+': '+rlname, rs+','+rlsel);
-
- rlSerialize();
-}
-
-// rel,id,name|||rel,id,name
-function rlSerialize() {
- var dest = x('relations');
- var str = '';
- var i;
- for(i=0;i<rl.options.length;i++)
- rl.options[i].value != '0_new' && (str += (str.length>0 ? '|||' : '') + rl.options[i].value);
- dest.value = str;
-}
-
-function rlLoad() {
- var rls = x('relations').value.split('|||');
- if(!rls[0])
- return;
- var i;
- for(i=0;i<rls.length;i++)
- rl.options[rl.options.length] = new Option(vrel[rls[i].split(',',3)[0]]+': '+rls[i].split(',',3)[2], rls[i]);
-}
-
-
-
-
-
-
-
-
- /************************\
- * VISUAL NOVELS *
- \************************/
-
-
-function vnChangeSel() {
- var sel = vn.options[vn.selectedIndex || 0];
- var o = x('vn_conts');
- var i;
- var ops='';
- for(i=0;i<vrel.length;i++)
- ops += '<option value="'+i+'">'+vrel[i]+'</option>';
- if(sel.value == '0_new') {
- o.innerHTML = '<input type="text" name="vn_S" id="vn_S" onkeyup="vnDoSearch(0)" onkeydown="return vnEnter(event)" style="width: 150px;" />'
- + '<button type="button" onclick="vnDoSearch(1)" style="width: 60px;">Search!</button><br style="clear: both" />'
- + '<span id="vn_R" style="display: block; width: 250px; height: 90px; overflow: auto"></span>';
- vnDoSearch('');
- } else {
- o.innerHTML = 'Selected "' + sel.text + '"<br />'
- + '<button type="button" onclick="vnAddRem(\'' + sel.value + '\')">remove</button>';
- }
-}
-
-function vnEnter(ev) {
- var c = document.layers ? ev.which : document.all ? event.keyCode : ev.keyCode;
- if(c == 13) {
- vnDoSearch(0);
- return false;
- }
- return true;
-}
-
-function vnDoSearch(f) {
- var v = x('vn_S').value;
- var d = x('vn_R');
- if(v.length < 1)
- d.innerHTML = 'Hint: type vX if you know the visual novel id.';
- else {
- if(f)
- d.innerHTML = '...searching...';
- ajax('/xml/vn.xml?q='+encodeURIComponent(v)+'&r='+(Math.floor(Math.random()*999)+1), function () {
- if(!hr || hr.readyState != 4 || !hr.responseText)
- return;
- if(hr.status != 200)
- return alert('Whoops, error! :(');
- var items = hr.responseXML.getElementsByTagName('item');
- if(!items || items.length < 1) {
- d.innerHTML = 'No results';
- return false;
- }
- var res = '';
- var i,j;
- for(i=0; i<items.length; i++) {
- var id = items[i].getElementsByTagName('id')[0].firstChild.nodeValue;
- var title = items[i].getElementsByTagName('title')[0].firstChild.nodeValue;
- var s = '';
- for(j=0; j<vn.options.length; j++)
- if(vn.options[j].value == id)
- s = ' checked="checked"';
- res += '<input type="checkbox" id="vn_I'+id+'"'+s+' onclick="vnAddRem(\''+qq(id)+'\', \''+qq(title)+'\')" />'
- + '<label style="width: auto" for="vn_I'+id+'">'+title+'</label><br style="clear: left" />';
- }
- d.innerHTML = res;
- });
- }
-}
-
-function vnAddRem(id, title) {
- var i;
- var d = 0;
- for(i=0;i<vn.options.length;i++)
- if(vn.options[i].value == id) {
- vn.options[i] = null;
- d = 1;
- }
- if(!d && title)
- vn.options[vn.options.length] = new Option(title, id);
- else if(!title) {
- vn.options[0].selected = true;
- vnChangeSel();
- }
- vnSerialize();
-}
-
-// id,title|||id,title
-function vnSerialize() {
- var dest = x('vn');
- var str = '';
- var i;
- for(i=0;i<vn.options.length;i++)
- vn.options[i].value != '0_new' && (str += (str.length>0 ? '|||' : '') + vn.options[i].value + ',' + vn.options[i].text);
- dest.value = str;
-}
-
-function vnLoad() {
- var vns = x('vn').value.split('|||');
- if(!vns[0])
- return;
- var i;
- for(i=0;i<vns.length;i++)
- vn.options[vn.options.length] = new Option(vns[i].split(',',2)[1], vns[i].split(',',2)[0]);
-}
-
-
-
-
-
-
- /************************\
- * C A T E G O R I E S *
- \************************/
-
-
-function catLoad() {
- var i;var cats=[];
- var l = ct.value.split(',');
- for(i=0;i<l.length;i++)
- cats[l[i].substr(0,3)] = Math.floor(l[i].substr(3,1));
-
- var l=x('cat').getElementsByTagName('a');
- for(i=0;i<l.length;i++) {
- if(l[i].id.substr(0, 4) != 'cat_')
- continue;
- catSet(l[i].id.substr(4), cats[l[i].id.substr(4)]||0);
- l[i].onclick = function() {
- var c = this.id.substr(4);
- if(!cats[c]) cats[c] = 0;
- if(c.substr(0,1) == 'p' || c == 'gaa' || c == 'gab' || c.substr(0,1) == 'h' || c.substr(0,1) == 'l' || c.substr(0,1) == 't') {
- if(cats[c]++)
- cats[c] = 0;
- } else if(++cats[c] == 4)
- cats[c] = 0;
- catSet(c, cats[c]);
-
- // has to be ordered before serializing!
- var r;l=[];i=0;
- for(r in cats)
- l[i++] = r;
- l = l.sort();
- r='';
- for(i=0;i<l.length;i++)
- if(cats[l[i]] > 0)
- r+=(r?',':'')+l[i]+cats[l[i]];
- ct.value = r;
- return false;
- };
- }
-}
-
-function catSet(id, rnk) {
- var c = rnk == 0 ? '#000' :
- rnk == 1 ? '#090' :
- rnk == 2 ? '#990' : '#900';
- x('b_'+id).style.color = c;
- x('cat_'+id).style.color = c;
- x('b_'+id).innerHTML = rnk;
-}
-
-
-
-
-
-
-
- /***************************\
- * S C R E E N S H O T S *
- \***************************/
-
-
-var scrL = []; // id, load, nsfw, obj, rid
-function scrLoad() {
- // 'screenshots' format: id,nsfw id,nsfw ..
- var l=x('screenshots').value.split(' ');
- for(var i=0;i<l.length;i++)
- if(l[i].length > 2)
- scrL[i] = { load: 2, id: l[i].split(',')[0], nsfw: l[i].split(',')[1]>0?1:0, rid: l[i].split(',')[2] };
-
- // <tbody> because IE can't operate on <table>
- x('scrfrm').innerHTML = '<table><tbody id="scrTbl"></tbody></table>';
- for(i=0;i<scrL.length;i++)
- scrGenerateTR(i);
- scrGenerateTR(i);
-
- setTimeout(scrSetSubmit, 1000);
- scrCheckStatus();
-}
-
-// give an error when submitting the form while still uploading an image
-function scrSetSubmit() {
- var o=document.forms[1].onsubmit;
- document.forms[1].onsubmit = function() {
- var c=0;var r=0;
- for(var i=0;i<scrL.length;i++) {
- if(scrL[i] && scrL[i].load)
- c=1;
- if(scrL[i] && scrL[i].rid == 0)
- r=1;
- }
- if(c) {
- alert('Please wait for the screenshots to be uploaded before submitting the form.');
- return false;
- } else if(r) {
- alert('Please select the appropriate release for every screenshot');
- return false;
- } else
- return o();
- };
-}
-
-function scrURL(id, t) {
- return x('scrfrm').className+'/s'+t+'/'+(id%100<10?'0':'')+(id%100)+'/'+id+'.jpg';
-}
-
-function scrGenerateTR(i) {
- if(!scrL[i])
- scrL[i] = { id: 0, load: 0 };
- var r = '<b style="width: auto; float: none;margin: 0; padding: 0; font-weight: bold">';
- if(!scrL[i].id && !scrL[i].load) {
- var c=0;
- for(var j=0,c=0; j<scrL.length; j++)
- if(scrL[j] && (scrL[j].load || scrL[j].id))
- c++;
- if(c >= 10)
- r += 'Enough screenshots</b>'
- +'The limit of 10 screenshots per visual novel has been reached. '
- +'If you want to add a new screenshot, please remove an existing one first.';
- else
- r += 'Add screenshot</b>'
- +'<input type="file" name="scrAddFile'+i+'" id="scrAddFile'+i+'" style="float: none; height: auto; width: auto;" />'
- +'<input type="button" value="Upload!" style="float: none; height: auto; width: auto; display: inline;" onclick="scrUpload('+i+')" /><br />'
- +'Image must be smaller than 5MB and in PNG or JPEG format.';
- }
- if(scrL[i].load && scrL[i].load == 1)
- r += 'Uploading...</b>This could take a while, depending on the file size and your upload speed.<br />'
- +'<a href="javascript:scrDel('+i+')">cancel</a>';
- if(scrL[i].load && scrL[i].load == 2)
- r += 'Generating thumbnail...</b>Note: if this takes longer than 30 seconds, there\'s probably something wrong on our side.'
- +'Please try again later or report a bug if that is the case.';
- if(scrL[i].id && !scrL[i].load)
- r += 'Screenshot #'+scrL[i].id+'</b>'
- +'<input type="checkbox" name="scrNSFW'+i+'" id="scrNSFW'+i+'"'+(scrL[i].nsfw?' checked="checked"':'')+' style="float: left" onclick="scrSer()" /> '
- +'<label for="scrNSFW'+i+'" class="checkbox">&nbsp;This screenshot is NSFW.</label>'
- +'<input type="button" value="remove" onclick="scrDel('+i+')" style="float: right; width: auto; height: auto" />'
- +'<br /><b style="float: left; width: auto; margin-right: 5px">Release:</b>'
- +'<select style="width: 350px; float: none; height: auto;" onchange="scrSer()" id="scrRel'+i+'">'+scrRelList(scrL[i].rid)+'</select>'
- +'<br />Full size: '+scrL[i].width+'x'+scrL[i].height+'px';
-
- if(scrL[i].obj) {
- x('scrTr'+i).getElementsByTagName('td')[1].innerHTML = r;
- return;
- }
-
- // the slow and tedious way, because we need to use DOM functions to manipulate the table contents...
- var o = document.createElement('tr');
- o.setAttribute('id', 'scrTr'+i);
- o.style.cssText = 'border-top: 1px solid #ccc';
- var d = document.createElement('td');
- d.style.cssText = 'width: 141px; height: 102px; padding: 0;';
- d.innerHTML = scrL[i].id && !scrL[i].load ? '<img src="'+scrURL(scrL[i].id, 't')+'" style="margin: 0; padding: 0; border: 0" />' : '&nbsp;';
- var e = document.createElement('td');
- e.innerHTML = r;
- o.appendChild(d);
- o.appendChild(e);
- x('scrTbl').appendChild(o);
- scrL[i].obj = o;
- scrStripe();
-}
-
-function scrUpload(i) {
- scrL[i].load = 1;
- // move the file selection box into a temporary form and post it into a temporary iframe
- var d = document.createElement('div');
- d.id = 'scrUpl'+i;
- d.style.cssText = 'visibility: hidden; overflow: hidden; width: 1px; height: 1px; position: absolute; left: -500px; top: -500px';
- d.innerHTML = '<iframe name="scrIframe'+i+'" id="scrIframe'+i+'" style="height: 0px; width: 0px; visibility: hidden"'
- +' src="about:blank" onload="scrUploadComplete('+i+')"></iframe>'
- +'<form method="post" action="/xml/screenshots.xml" target="scrIframe'+i+'" enctype="multipart/form-data" id="scrUplFrm'+i+'" name="scrUplFrm'+i+'">'
- +'<input type="hidden" name="itemnumber" value="'+i+'" />'
- +'</form>';
- document.body.appendChild(d);
- x('scrUplFrm'+i).appendChild(x('scrAddFile'+i));
- x('scrUplFrm'+i).submit();
- scrGenerateTR(i);
- scrGenerateTR(i+1);
- return false;
-}
-
-function scrStripe() {
- var l = x('scrTbl').getElementsByTagName('tr');
- for(var j=0; j<l.length; j++)
- l[j].style.backgroundColor = j%2==0 ? '#fff' : '#f5f5f5';
-}
-
-function scrRelLine(rid, sel) {
- var r;
- for(var i=0;i<scrRel.length;i++)
- if(scrRel[i][0] == rid)
- r = scrRel[i];
- return '<option value="'+r[0]+'"'+(sel?' selected="selected"':'')+'>['+r[1]+'] '+r[2]+'</option>';
-}
-
-function scrRelList(rid) {
- var r='<option value="0">-- select release --</option>';
- for(var i=0;i<scrRel.length;i++)
- r += scrRelLine(scrRel[i][0], rid == scrRel[i][0] ? 1 : 0);
- return r;
-}
-
-function scrUploadComplete(i) {
- if(window.frames['scrIframe'+i].location.href.indexOf('screenshots') > 0) {
- try {
- scrL[i].id = window.frames['scrIframe'+i].window.document.getElementsByTagName('image')[0].getAttribute('id');
- } catch(e) {
- scrL[i].id = -10;
- }
- if(scrL[i].id < 0) {
- alert(
- scrL[i].id == -10 ?
- 'Oops! Seems like something went wrong...\n'
- +'Make sure the file you\'re uploading doesn\'t exceed 5MB in size.\n'
- +'If that isn\'t the problem, then please report a bug.' :
- scrL[i].id == -1 ?
- 'Upload failed!\nOnly JPEG or PNG images are accepted.' :
- 'Upload failed!\nNo file selected, or an empty file?');
- return scrDel(i);
- }
- scrL[i].load = 2;
- scrGenerateTR(i);
- scrImageFail(i);
- }
-}
-
-function scrCheckStatus() {
- var ids='';
- for(var i=0;i<scrL.length;i++)
- if(scrL[i] && scrL[i].load == 2)
- ids+=(ids?';':'')+'id='+scrL[i].id;
- if(!ids)
- return setTimeout(scrCheckStatus, 1000);
- var ti = setTimeout(scrCheckStatus, 10000);
- ajax('/xml/screenshots.xml?'+ids+';r='+(Math.floor(Math.random()*999)+1), function () {
- if(!hr || hr.readyState != 4 || !hr.responseText)
- return;
- if(hr.status != 200)
- return alert('Whoops, error! :(');
- var l = hr.responseXML.getElementsByTagName('image');
- for(var s=0;s<l.length;s++) {
- for(i=0;i<scrL.length;i++)
- if(scrL[i] && scrL[i].id == l[s].getAttribute('id') && l[s].getAttribute('status') > 0) {
- scrL[i].load = 0;
- scrL[i].width = l[s].getAttribute('width');
- scrL[i].height = l[s].getAttribute('height');
- x('scrTr'+i).getElementsByTagName('td')[0].innerHTML =
- '<a href="'+scrURL(scrL[i].id, 'f')+'" rel="'+scrL[i].width+'x'+scrL[i].height+'" onclick="return scrView(this)">'
- +'<img src="'+scrURL(scrL[i].id, 't')+'" style="margin: 0; padding: 0; border: 0" /></a>';
- scrGenerateTR(i);
- scrSer();
- }
- }
- clearTimeout(ti);
- setTimeout(scrCheckStatus, 1000);
- });
-}
-
-function scrDel(i) {
- x('scrTbl').removeChild(x('scrTr'+i));
- if(scrL[i].load)
- document.body.removeChild(x('scrUpl'+i));
- scrL[i]=null;
- scrGenerateTR(scrL.length-1);
- scrSer();
- scrStripe();
-}
-
-function scrSer() {
- var r='';
- for(var i=0;i<scrL.length;i++) {
- if(scrL[i] && scrL[i].id && !scrL[i].load) {
- scrL[i].nsfw = x('scrNSFW'+i).checked ? '1' : '0';
- scrL[i].rid = x('scrRel'+i).options[x('scrRel'+i).selectedIndex].value;
- r += ' '+scrL[i].id+','+scrL[i].nsfw+','+scrL[i].rid;
- }
- }
- x('screenshots').value = r;
-}
-
-
-
-
diff --git a/static/files/footer.gif b/static/files/footer.gif
deleted file mode 100644
index c87fb121..00000000
--- a/static/files/footer.gif
+++ /dev/null
Binary files differ
diff --git a/static/files/graph.png b/static/files/graph.png
deleted file mode 100644
index bb56f758..00000000
--- a/static/files/graph.png
+++ /dev/null
Binary files differ
diff --git a/static/files/headerbg.jpg b/static/files/headerbg.jpg
deleted file mode 100644
index 81f4dd75..00000000
--- a/static/files/headerbg.jpg
+++ /dev/null
Binary files differ
diff --git a/static/files/headerbot.png b/static/files/headerbot.png
deleted file mode 100644
index 6e04ab05..00000000
--- a/static/files/headerbot.png
+++ /dev/null
Binary files differ
diff --git a/static/files/icons.png b/static/files/icons.png
deleted file mode 100644
index 4cb1a180..00000000
--- a/static/files/icons.png
+++ /dev/null
Binary files differ
diff --git a/static/files/rss.png b/static/files/rss.png
deleted file mode 100644
index 923c3822..00000000
--- a/static/files/rss.png
+++ /dev/null
Binary files differ
diff --git a/static/files/select.png b/static/files/select.png
deleted file mode 100644
index ac219e05..00000000
--- a/static/files/select.png
+++ /dev/null
Binary files differ
diff --git a/static/files/sidebarbg.jpg b/static/files/sidebarbg.jpg
deleted file mode 100644
index 00eb5697..00000000
--- a/static/files/sidebarbg.jpg
+++ /dev/null
Binary files differ
diff --git a/static/files/sidebarbot.jpg b/static/files/sidebarbot.jpg
deleted file mode 100644
index 49884ded..00000000
--- a/static/files/sidebarbot.jpg
+++ /dev/null
Binary files differ
diff --git a/static/files/sidebg.jpg b/static/files/sidebg.jpg
deleted file mode 100644
index 65fd3306..00000000
--- a/static/files/sidebg.jpg
+++ /dev/null
Binary files differ
diff --git a/static/files/style.css b/static/files/style.css
deleted file mode 100644
index 6c51bacf..00000000
--- a/static/files/style.css
+++ /dev/null
@@ -1,923 +0,0 @@
-
-body {
- margin: 15px 0 0 0;
- padding: 0 0 60px 0;
- background: #fff;
- font-family: Arial, Helvetica, sans-serif;
- font-size: 13px;
- color: #203C36;
-}
-
-form {
- margin: 0;
- padding: 0;
-}
-
-fieldset {
- margin: 0;
- padding: 0;
- border: none;
-}
-
-legend {
- display: none;
-}
-
-input, textarea {
- padding: 2px 5px;
- border: 1px solid #B8E0D7;
- font: normal 1em Arial, Helvetica, sans-serif;
- color: #203C36;
-}
-
-h1, h2 {
- color: #203C36;
- margin: 0;
-}
-
-h1 {
- text-transform: lowercase;
- letter-spacing: -1px;
- font-size: 3em;
-}
-
-h2 {
- font-size: 1.7em;
- clear: right;
-}
-
-h3 {
- font-size: 1.1em;
- margin: 0;
-}
-h3.alttitle {
- margin: 0 0 0 20px;
-}
-
-p, ul, ol {
- margin: 0;
-}
-a {
- color: #69A89A;
-}
-a:hover {
- text-decoration: none;
-}
-
-img {
- border: none;
-}
-
-/* Header */
-
-#header {
- width: 960px;
- height: 80px;
- margin: 0 auto;
- background: url(/files/headerbg.jpg);
-}
-
-#header h1 {
- margin: 0;
- padding: 15px 0 0 20px;
- letter-spacing: normal;
- font-size: 1em;
- color: #FFFFFF;
-}
-
-#header h1 a {
- text-decoration: none;
- color: #FFFFFF;
-}
-
-#header h1 a:hover {
- text-decoration: underline;
-}
-
-#header b {
- display: block;
- letter-spacing: -2px;
- font-size: 2.4em;
-}
-
-/* Search */
-
-#search {
- float: right;
- width: 180px;
- padding-top: 30px;
-}
-
-#searchfield {
- width: 150px;
-}
-
-#searchsubmit {
- display: none;
-}
-
-/* Login */
-
-#loginform {
- padding: 0;
- margin: 0 0 0 20px;
-}
-#loginform input {
- width: 40px;
- margin: 0;
- padding: 1px;
- position: relative;
-}
-#loginform #usrname, #loginform #usrpass {
- width: 70px;
- border: 0;
-}
-
-/* Page */
-
-#page {
- width: 960px;
- margin: 0 auto;
- padding: 40px 0;
- background: url(/files/headerbot.png) no-repeat;
-}
-
-#content {
- float: left;
- width: 700px;
-}
-
-/* Side */
-
-#side {
- float: right;
- padding: 0;
- background: url(/files/sidebg.jpg);
- color: #fff;
-}
-#side div {
- background: url(/files/sidebarbg.jpg) no-repeat;
-}
-#side div div {
- width: 240px;
- padding: 0 0 60px 0;
- background: url(/files/sidebarbot.jpg) no-repeat left bottom;
-}
-
-#side a {
- color: #fff;
-}
-
-#side h2 {
- padding: 20px 20px 0 15px;
- text-transform: uppercase;
- font-family: "Arial Black", Arial, Helvetica, sans-serif;
- font-size: .8em;
- color: #fff;
-}
-
-#side ul {
- margin: 0;
- padding: 0 20px 0 20px;
- list-style: none;
-}
-
-#side li {
- padding: 0;
-}
-
-#side p {
- margin: 0;
- padding: 0 0 0 20px;
-}
-#side li.more {
- font-style: italic;
-}
-
-/* Footer */
-
-#footer {
- clear: both;
- padding: 10px 0;
-}
-
-#footer p {
- margin: 0;
- text-align: center;
- color: #999999;
- font-size: 0.8em;
-}
-
-#footer a {
- color: #999999;
-}
-
-/* Forms */
-#content form {
- display: block;
- margin: 20px 0 10px 0;
- padding: 0;
-}
-#content form.tblf {
- margin: 0;
-}
-form ul {
- margin: 0;
- padding: 0;
-}
-form li {
- display: block;
- clear: left;
- list-style-type: none;
- margin: 0;
- padding: 0;
-}
-form ul ul {
- margin: 0; padding: 0;
- clear: left;
-}
-form li.nolabel, form ul ul {
- padding: 0 0 0 110px;
-}
-form li.nextpart {
- padding: 10px 0 0 0;
-}
-#content label, #content input, #content textarea, #content select, form li b, form li p {
- display: block;
- float: left;
-}
-form li label i, form p.formnotice i {
- display: inline;
- color: #f00;
-}
-input.text, select {
- width: 200px;
-}
-select, input.text, textarea {
- border: 1px solid #B8E0D7;
- font: normal 1em Arial, Helvetica, sans-serif;
- color: #203C36;
-}
-form label.checkbox {
- width: auto;
-}
-select.multiple {
- height: 120px;
- width: 230px;
-}
-#md_conts, #pd_conts, #rl_conts, #vn_conts {
- display: block;
- float: left;
- background-color: #e3ecff;
- border: 1px dashed #b4b4ff;
- padding: 2px;
-}
-#rl_select {
- width: 280px;
-}
-form li.longopts select {
- width: 350px;
-}
-form li.shortopts input {
- width: 50px;
-}
-form li i {
- float: left;
-}
-input.hidden {
- display: none;
- position: absolute;
- top: -30px;
-}
-label, form li b {
- width: 110px;
- font-weight: normal;
-}
-form li.nolabel b {
- display: inline;
- width: auto;
- font-weight: bold;
- float: none;
-}
-form li.subform {
- padding: 10px 0 0 0;
- clear: both;
-}
-form li.subform a {
- display: block;
- margin: 0;
- padding: 0;
- width: 100%;
- background-color: #f0f0f0;
- text-decoration: none;
- font-weight: bold;
- color: #7AB9AB;
-}
-li.formhid {
- float: none;
- position: absolute;
- top: -2000px;
- left: -2000px;
- width: 10px;
-}
-form li.date select {
- width: 100px;
-}
-
-/* Platform selecter */
-form ul.platforms {
- clear: none;
- padding: 0;
- margin: 0 0 0 110px;
-}
-form ul.platforms li {
- float: left;
- clear: none;
-}
-form ul.platforms li label {
- width: 150px;
-}
-form ul.platforms input {
- padding: 0;
- margin: 0 2px 0 2px;
- border: 0;
- height: 14px;
-}
-
-/* Warning & msg box */
-span.warning {
- display: block;
- margin: 5px 10% 5px 10%;
- padding: 3px 5px 3px 60px;
- background: #ffece3 url('/files/warning.png') no-repeat;
- border: 1px dashed #ffb4b4;
- min-height: 57px;
-}
-* html span.warning { height: 57px; }
-span.msg {
- display: block;
- margin: 5px 10% 5px 10%;
- padding: 3px 5px 3px 5px;
- border: 1px dashed #3cb700;
- background: #bfffb5;
-}
-span.warning ul, span.msg ul {
- padding: 0 0 0 15px;
-}
-a.help {
- vertical-align: super;
- font-size: 10px;
- text-decoration: none;
- font-weight: bold;
- padding-left: 1px;
- /*background: #eee;*/
-}
-
-
-/* VN page header, general dl and list markup */
-p.nsfw { color: #999; font-size: 10px; font-style: italic; }
-dt {
- float: left;
- font-style: italic;
-}
-dd {
- padding: 0 0 0 50px;
-}
-dl.vnrel dd, dl.vncat dd {
- padding: 0 0 0 75px;
-}
-ul.vnani i { color: #999 }
-ul.vnani b { color: #999; font-weight: normal; font-size: 10px; }
-ul.vnani acronym { border: 0 }
-ul {
- padding: 0 0 0 17px;
-}
-p.actions {
- display: inline;
- margin: 0 0 0 5px;
- font-weight: normal;
- font-size: 12px;
- color: #999;
-}
-#vnheader div {
- width: 256px;
- float: left;
- text-align: center;
-}
-#vnheader div img {
- margin: 0 auto 0 auto;
-}
-#vnheader #nsfw {
- cursor: pointer;
-}
-#vnheader dl, #vnheader ul {
- margin: 0 0 0 270px;
- padding: 0;
-}
-#vnheader li { list-style-type: none; margin: 0; padding: 0; }
-#vnheader h3 {
- margin: 10px 0 0 260px;
- font-size: 13px;
-}
-#vnheader p.mod {
- margin: 0;
-}
-
-
-/* producer search */
-
-form#psearch fieldset {
- display: block;
- width: 700px;
- text-align: center;
-}
-form#psearch input { margin: 0 auto; float: none; position: relative; display: inline }
-
-
-ul.home {
- float: left;
- list-style-type: none;
- width: 210px;
- margin-top: 30px;
-}
-ul.home li { padding: 0; }
-ul.home.break { clear: left; }
-h3.home {
- clear: left;
- padding-top: 30px;
-}
-
-
-/* edit links and stuff */
-
-p.mod {
- float: right;
- color: #999;
-}
-div.dropdown {
- width: 100px;
- position: absolute;
- left: -500px;
- top: 0;
- border: 1px solid #d0d0d0;
- border-bottom: none;
-}
-div.dropdown ul, div.dropdown ul li {
- list-style-type: none;
- margin: 0;
- padding: 0;
- width: 100px;
- background-color: #f0f0f0;
- border-bottom: 1px solid #d0d0d0;
-}
-div.dropdown a, div.dropdown b {
- display: block;
- width: 90px;
- margin: 0;
- padding: 2px 5px;
- text-decoration: none;
-}
-div.dropdown b {
- font-weight: normal;
- font-style: italic;
- color: #666;
-}
-div.dropdown li.center a, div.dropdown li.center b {
- text-align: center;
-}
-div.dropdown a:hover {
- background-color: #e0e0e0;
-}
-div.rlistdd {
- width: 200px;
-}
-div.rlistdd ul, div.rlistdd ul li {
- float: left;
-}
-div.rlistdd a, div.rlistdd b {
- padding: 0px 5px;
-}
-div.dropdown ul.full a, div.dropdown ul.full b {
- width: 190px;
- padding: 1px 5px;
-}
-div.dropdown ul.full, div.dropdown ul.full li {
- clear: left;
- width: 200px;
-}
-
-
-/* paragraph markups... */
-
-p#relations {
- margin: 20px 0 0 0;
- width: 100%;
- text-align: center;
-}
-p.desc {
- padding: 0 10px 0 10px;
-}
-p.chr {
- width: 100%;
- text-align: center;
- font-size: 1.1em;
-}
-p.browse {
- width: 100%;
- text-align: center;
-}
-b.mod, a.mod {
- color: #f00;
-}
-
-
-/* vn search */
-
-form#vsearch fieldset {
- display: block;
- width: 700px;
- text-align: center;
-}
-form#vsearch input { margin: 0 auto; float: none; position: relative; display: inline }
-form#vsearch input.text { width: 400px; }
-b#adsearchclick { cursor: pointer }
-
-ul.filter { margin: 0 0 0 15px; padding: 0; list-style-type: none }
-ul.filter li { float: left; padding: 4px 10px 0 0; }
-ul.filter label { width: auto; color: #666; font-size: 11px; margin: 0; padding: 0 0 0 1px;}
-ul.filter input { border: 0; margin: 0; }
-
-
-
-/* categories */
-
-ul#cat {
- margin: 0 0 0 15px;
- padding: 0;
-}
-ul#cat ul {
- list-style-type: none;
- padding: 0;
-}
-ul#cat input {
- padding: 0;
- margin: 0 2px 0 2px;
- border: 0;
- height: 14px;
-}
-ul#cat li, form ul#cat li {
- display: block;
- width: 170px;
- float: left;
- clear: none;
- font-weight: bold;
-}
-ul#cat li li {
- display: list-item;
- width: auto;
- float: none;
- clear: left;
- font-weight: normal;
- padding: 0 0 0 20px;
- margin: 0;
- cursor: pointer;
- list-style-type: none;
- background: url(/files/select.png) no-repeat;
-}
-form ul#cat li li {
- padding: 0;
- background: none;
-}
-form ul#cat li li b { width: 13px; font-weight: bold; }
-form ul#cat li li a { color: #000; text-decoration: none; display: block; width: 160px; }
-ul#cat li li.inc { background-position: 0px -16px; color: #090; }
-ul#cat li li.exc { background-position: 0px -33px; color: #900; }
-i.crgn0 { font-style: normal; }
-i.crgn1 { font-style: normal; color: #bbb; }
-i.crgn2 { font-style: normal; }
-i.crgn3 { font-style: normal; font-weight: bold; }
-
-
-
-/* DOCUMENTATION PAGES */
-#dpage h3 { margin-top: 25px; }
-#dpage h3 a { color: #203C36; text-decoration: none; }
-#dpage dd { padding-bottom: 5px; margin-left: 70px; }
-#dpage ul.index { display: block; float: right; width: 150px; padding: 2px; margin: 0 0 10px 5px; background-color: #f1f1f1; border: 1px solid #ccc; }
-#dpage ul.index li { list-style-type: none; }
-#dpage ul.index li a { margin: 0 0 0 10px; }
-#dpage .retired { text-decoration: line-through; }
-#dpage dt b { color: #999; font-weight: normal; font-style: normal; font-size: 12px; }
-
-/* Screenshots */
-div#screenshots {
- display: block;
- width: 700px;
- margin: 0;
- padding: 0;
-}
-div#screenshots b {
- display: block;
- margin: 0;
- padding: 0;
- width: 100%;
- background-color: #f5f5f5;
- font-weight: normal;
-}
-div#screenshots a.shot {
- display: block;
- float: left;
- width: 136px;
- height: 102px;
- margin: 0;
- padding: 2px;
- text-decoration: none;
- text-align: center;
-}
-div#screenshots a.shot:hover {
- background-color: #ccc;
-}
-div#screenshots a.shot b {
- display: block;
- margin: -16px 0 0 0;
- padding: 0;
- height: 14px;
- width: 136px;
- text-align: right;
- font-size: 12px;
- color: #f00;
-}
-div#screenshots img { border: 0; padding: 0; }
-div#scrView {
- position: absolute;
- display: none;
- top: -5000px;
- left: -5000px;
- background-color: #fff;
- border: 2px solid #ccc;
- padding: 10px;
- text-align: center;
-}
-#scrView img { cursor: pointer }
-#scrclose { float: right; padding-left: 10px; }
-#scrnext { padding-left: 5px; }
-#scrprev { padding-right: 5px; }
-#scrfull { float: left; padding-right: 10px; }
-#scrimgload {
- display: block;
- position: absolute;
- left: -500px;
- top: -50px;
- width: 100px;
- padding: 3px;
- background-color: #f5f5f5;
- text-align: center;
- border: 1px solid #ccc;
- color: #000;
-}
-
-
-
-#content input.right, #content select.right {
- float: right;
-}
-#vnlistchange { width: 130px; }
-p.opts {
- clear: left;
- text-align: center;
- width: 100%;
- background-color: #f0f0f0;
- margin-bottom: 10px;
- margin-top: 10px;
- padding: 1px 0;
-}
-ul#stats {
- clear: left;
- list-style-type: none;
- padding: 0px 0 0 5px;
-}
-ul#stats li {
- padding: 0;
- display: block;
- width: 345px;
- float: left;
- margin: 0 0 20px 0;
-}
-ul#stats li.break {
- clear: left;
-}
-
-acronym {
- border-bottom: 1px dotted #999;
-}
-acronym.date {
- border: 0;
-}
-b.future { font-weight: normal; color: #900; }
-
-
-/* icon image sprites */
-
-a.rss {
- display: block;
- height: 16px;
- width: 16px;
- background: url(/files/rss.png) no-repeat;
- text-indent: -9999px;
- overflow: hidden;
- float: right;
-}
-.icons, .uicons {
- background: url(/files/icons.png) no-repeat;
- width: 16px;
- height: 14px;
- margin: 0 2px 0 0;
- overflow: hidden;
- display:-moz-inline-stack;
- display: inline-block;
- padding: 0;
- border: 0;
- text-decoration: none;
-}
-.uicons {
- width: 14px;
- margin: 0;
- background: url(/files/uicons.png) no-repeat;
-}
-.icons.lang {
- width: 13px;
- height: 11px;
- border: 1px solid #999;
- /* see def.js for an ugly FF hack */
-}
-.icons.par, .icons.tri, .icons.com { width: 11px; }
-acronym.icons, acronym.uicons { cursor: default; }
-.icons.oth { background: none; }
-.icons.drc { background-position: 0px 0px; }
-.icons.lin { background-position: 0px -14px; }
-.icons.nds { background-position: 0px -28px; }
-.icons.ps2 { background-position: 0px -42px; }
-.icons.sfc { background-position: 0px -56px; }
-.icons.gba { background-position: 0px -70px; }
-.icons.ps3 { background-position: 0px -84px; }
-
-.icons.dvd { background-position: -16px 0px; }
-.icons.mac { background-position: -16px -14px; }
-.icons.ps1 { background-position: -16px -28px; }
-.icons.psp { background-position: -16px -42px; }
-.icons.win { background-position: -16px -56px; }
-.icons.wii { background-position: -16px -70px; }
-.icons.xb3 { background-position: -16px -84px; }
-
-.icons.com { background-position: -32px 0px; }
-.icons.par { background-position: -32px -14px; }
-.icons.tri { background-position: -32px -28px; }
-.icons.ext { background-position: -32px -42px; }
-.icons.msx { background-position: -32px -56px; }
-.icons.nes { background-position: -32px -70px; }
-
-.icons.cs { background-position: -48px 0px; }
-.icons.da { background-position: -48px -11px; }
-.icons.de { background-position: -48px -22px; }
-.icons.en { background-position: -48px -33px; }
-.icons.es { background-position: -48px -44px; }
-.icons.fi { background-position: -48px -55px; }
-.icons.fr { background-position: -48px -66px; }
-.icons.it { background-position: -48px -77px; }
-.icons.ja { background-position: -48px -88px; }
-
-.icons.nl { background-position: -61px 0px; }
-.icons.no { background-position: -61px -11px; }
-.icons.pl { background-position: -61px -22px; }
-.icons.pt { background-position: -61px -33px; }
-.icons.ru { background-position: -61px -44px; }
-.icons.sv { background-position: -61px -55px; }
-.icons.tr { background-position: -61px -66px; }
-.icons.zh { background-position: -61px -77px; }
-.icons.ko { background-position: -61px -88px; }
-
-.uicons.no { background-position: 0px 0px; }
-.uicons.r0 { background-position: 0px -14px; }
-.uicons.v0 { background-position: 0px -14px; }
-.uicons.r1 { background-position: 0px -28px; }
-.uicons.r2 { background-position: 0px -42px; }
-.uicons.r3 { background-position: 0px -56px; }
-
-.uicons.r4 { background-position: -14px 0px; }
-.uicons.v1 { background-position: -14px -14px; }
-.uicons.v2 { background-position: -14px -28px; }
-.uicons.v3 { background-position: -14px -42px; }
-.uicons.v4 { background-position: -14px -56px; }
-
-
-/* tables */
-
-table td { vertical-align: top; }
-table { width: 100%; border-collapse: collapse; }
-thead tr td { font-weight: bold; }
-thead tr td a { text-decoration: none; }
-#content table input {
- width: 13px;
- height: 13px;
- margin: 0;
- float: right;
-}
-
-/* revisions */
-#tmc { width: 650px; margin: 0 0 30px 30px; border: 1px solid #ddd; background-color: #fbfbfb; clear: both; }
-#tmc thead tr td { font-weight: normal; text-align: center; }
-#tmc .tc1, #tmc .tc2 { border-right: 1px solid #ddd; }
-#tmc .tc1 { font-weight: bold; padding-right: 10px; }
-#tmc .tc1 { width: 100px; }
-#tmc .tc2, #tmc .tc3 { width: 275px; }
-div#tmc { text-align: center; }
-div#revbrowse { margin: 20px 0 0 30px; width: 650px; text-align: center; } /* position: relative; top: 17px; left: 30px; */
-a#revnext { float: right }
-a#revprev { float: left; }
-b.diff_add { font-weight: normal; background-color: #cfc; }
-b.diff_del { font-weight: normal; background-color: #fcc; }
-
-#tvg tr, #tus tr { background-color: #fff!important }
-#tvg .tc1, #tus .tc1 { width: 25px; text-align: right; padding-right: 3px; }
-#tvg .tc2 div, #tus .tc2 div { margin: 0 5px 0 0; padding: 0; float: left; background: url(/files/graph.png); height: 13px; }
-#tus .tc1 { width: 60px }
-
-#tvr .tc3, #tvl .tc5, #tur .tc3, #tul .tc8, #thi .tc6 { text-align: right }
-
-#tvl .tc1, #tvl .tc2, #tvl .tc3 { white-space: nowrap; padding-right: 10px; }
-
-#thi { clear: both }
-#thi .tc1_1 { text-align: right; padding-right: 0; width: 10px; }
-#thi .tc1_2 { padding-left: 0; padding-right: 10px; }
-#thi .tc1_1 a, #thi .tc1_2 a { text-decoration: none }
-#thi .tc1 { width: 10px }
-#thi .tc2 { width: 110px; }
-
-#rli tr.relhid { background-color: #fff!important; }
-#rli td.relhid, #relhidpar { cursor: pointer; }
-#rli td.relnone { color: #aaa }
-#rli .tc1 { width: 560px; }
-#rli .tc2 { width: 80px; }
-#rli .tc3 { width: 50px; }
-#rli .tc1_1 { width: 70px; padding-left: 20px }
-#rli .tc1_2 { width: 40px; padding:0; text-align: right; }
-#rli .tc1_3 { width: 430px; }
-#rli .tc1_3 a { color: #203c36; }
-#rli .tc1_5 { text-align: right }
-
-#tre tr { background-color: #fff!important; }
-#tre tr.lang { background-color: #f5f5f5!important; font-style: italic; }
-
-#tre .tc1 { width: 75px; padding-left: 10px; }
-#tre .tc2 { width: 50px; text-align: center; white-space: nowrap; }
-#tre .tc3, #tre .tc6 { width: 16px; margin: 0; padding: 0; white-space: nowrap; text-align: right }
-#tre .tc3 { width: 70px; }
-#tre .tc5 { width: 30px; text-align: right; padding: 0 10px 0 0; }
-#tre .tc5 a { display: block; width: 30px; text-align: right;text-decoration: none; }
-
-#tbv .tc2 acronym, #tbv .tc3 acronym {
- zoom: 1;
- opacity: 0.7;
- filter:alpha(opacity=70);
- -moz-opacity: 0.7;
-}
-#tbv .tc2, #tbv .tc3 { margin: 0; padding: 0; white-space: nowrap }
-#tbv .tc2 { text-align: right; padding-right: 3px; }
-
-#tpd .tc1 { margin: 0; padding: 0; }
-
-
-/* message board */
-
-#tth tr { border-top: 1px solid #999 }
-#tth .tc1 { width: 150px; border-right: 1px solid #999; padding: 5px; }
-#tth .tc2 { padding: 5px }
-#tth p.mod { margin: -5px; font-size: 10px; }
-#tth .tc2 i { font-size: 10px; float: right; }
-#tth .tc2 b.hidden { font-size: 10px; color: #888; font-weight: normal; }
-#tin .tc1 { width: 400px; }
-#tin .tc2 { width: 25px; }
-#tin .tc4 { text-align: right }
-#tin .tc1 b { font-weight: normal; color: #888; }
-#qreply { display: block; width: 100%; text-align: center }
-#qreply textarea, #qreply input { float: none; margin: auto; }
-a.right { float: right }
-b.spoiler { font-weight: normal; color: #ccc; background-color: #ccc }
-b.spoiler:hover { color: #000; background-color: #eee }
-
-
-
-#debug {
- border-top: 1px solid #ffb4b4;
- background-color: #ffece3;
- height: 70px;
- overflow: auto;
- position: fixed;
- left: 0;
- bottom: 0;
- width: 100%;
- font-size: 10px;
- margin: 0;
- padding: 0;
-}
-
diff --git a/static/files/uicons.png b/static/files/uicons.png
deleted file mode 100644
index 09f8da17..00000000
--- a/static/files/uicons.png
+++ /dev/null
Binary files differ
diff --git a/static/files/warning.png b/static/files/warning.png
deleted file mode 100644
index b8af1a53..00000000
--- a/static/files/warning.png
+++ /dev/null
Binary files differ
diff --git a/util/dbgraph.pl b/util/dbgraph.pl
new file mode 100755
index 00000000..be76bca9
--- /dev/null
+++ b/util/dbgraph.pl
@@ -0,0 +1,92 @@
+#!/usr/bin/perl
+
+
+# Generates a graphviz relation graph of the complete SQL database,
+# information is parsed from dump.sql (has to be in the 'current directory').
+# outputs the graph in dot format, usable as input to graphviz.
+#
+# Usage:
+# ./dbgraph.pl | dot -Tpng >dbgraph.png
+#
+# (this is a rather fast-written Perl hack, don't expect too much)
+
+
+use strict;
+use warnings;
+
+
+my %subgraphs = (
+ 'Producers' => [qw| FFFFCC producers producers_rev |],
+ 'Releases' => [qw| C8FFC8 releases releases_rev releases_media releases_platforms releases_producers releases_vn |],
+ 'Visual Novels' => [qw| FFE6BE vn vn_rev vn_relations vn_categories vn_anime vn_screenshots |],
+ 'Users' => [qw| CCFFFF users votes vnlists rlists wlists |],
+ 'Discussion board' => [qw| FFDCDC threads threads_tags threads_posts |],
+ 'Misc' => [qw| F5F5F5 changes anime screenshots relgraph stats_cache |],
+);
+
+my %tables; # table_name => [ [ col1, pri ], ... ]
+my @rel; # 'table:col -- table:col', ...
+
+sub parse_dump {
+ open my $R, '<', 'dump.sql' or die $!;
+ my $in='';
+ while (<$R>) {
+ chomp;
+ if(/^ALTER TABLE ([a-z_]+) +ADD FOREIGN KEY \(([a-z0-9_]+)\) +REFERENCES ([a-z_]+) +\(([a-z0-9_]+)\)/) {
+ push @rel, sprintf '%s:%s -- %s:%s', $1, $2, $3, $4;
+ }
+ if(!$in) {
+ next if !/^CREATE TABLE ([a-z_]+) /;
+ $in = $1;
+ $tables{$in} = [];
+ next;
+ }
+ if(/^\);/) {
+ $in = '';
+ next;
+ }
+ if(/^\s+"?([a-z0-9_]+)"?\s/) {
+ push @{$tables{$in}}, [ $1, 0 ];
+ $tables{$in}[$#{$tables{$in}}][1] = /PRIMARY KEY/ ? 1 : 0;
+ next;
+ }
+ if(/^\s+PRIMARY KEY\((.+)\)/) {
+ for my $c (split /,\s*/, $1) {
+ $_->[1]=1 for (grep $_->[0] eq $c, @{$tables{$in}});
+ }
+ }
+ }
+ close $R;
+}
+
+sub table_node { # table_name
+ return $_[0].' [ label=<<TABLE CELLSPACING="0" CELLPADDING="1" BORDER="0">'
+ .'<TR><TD BGCOLOR="#99CCFF" BORDER="1">'.$_[0].'</TD></TR>'
+ .join('', map {
+ '<TR><TD BGCOLOR="#FFFFFF" PORT="'.$_->[0].'" BORDER="1">'.$_->[0].'</TD></TR>'
+ } @{$tables{$_[0]}})
+ .'</TABLE>> ]';
+}
+
+
+parse_dump;
+my $clus=0;
+print
+ qq|graph G {\n|.
+ #qq| ratio = "compress"\n|.
+ #qq| overlap = "false"\n|.
+ qq| rankdir = "LR"\n|.
+ qq| node [ shape="plaintext" ]\n|.
+ #qq| edge [ color="#cccccc" ]\n|.
+ qq| labelloc="t"\n|.
+ sprintf(qq| label="VNDB Database Structure (%04d-%02d-%02d)"\n|, (localtime)[5]+1900, (localtime)[4]+1, (localtime)[3]).
+ join('', map {
+ qq| subgraph cluster_|.(++$clus).qq| {\n|.
+ qq| label="$_"\n|.
+ qq| bgcolor="#|.shift(@{$subgraphs{$_}}).qq|"\n |.
+ join("\n ", map table_node($_), @{$subgraphs{$_}}).qq|\n|.
+ qq| }\n|
+ } keys %subgraphs).
+ qq| |.join("\n ", @rel).qq|\n|.
+ qq|}|;
+
diff --git a/util/dump.sql b/util/dump.sql
index 4addb15d..848ddd34 100644
--- a/util/dump.sql
+++ b/util/dump.sql
@@ -137,6 +137,12 @@ CREATE TABLE screenshots (
height smallint NOT NULL DEFAULT 0
);
+-- stats_cache
+CREATE TABLE stats_cache (
+ section varchar(25) NOT NULL PRIMARY KEY,
+ count integer NOT NULL DEFAULT 0
+);
+
-- threads
CREATE TABLE threads (
id SERIAL NOT NULL PRIMARY KEY,
@@ -171,10 +177,14 @@ CREATE TABLE users (
id SERIAL NOT NULL PRIMARY KEY,
username varchar(20) NOT NULL UNIQUE,
mail varchar(100) NOT NULL,
- rank smallint NOT NULL DEFAULT 2,
+ rank smallint NOT NULL DEFAULT 3,
passwd bytea NOT NULL DEFAULT '',
registered bigint NOT NULL DEFAULT 0,
- flags integer NOT NULL DEFAULT 7
+ flags integer NOT NULL DEFAULT 7,
+ show_nsfw boolean NOT NULL DEFAULT FALSE,
+ show_list boolean NOT NULL DEFAULT TRUE,
+ c_votes integer NOT NULL DEFAULT 0,
+ c_changes integer NOT NULL DEFAULT 0
);
-- vn
@@ -404,6 +414,84 @@ $$ LANGUAGE plpgsql;
+
+-----------------------
+-- T R I G G E R S --
+-----------------------
+
+
+-- keep the c_* columns in the users table up to date
+CREATE OR REPLACE FUNCTION update_users_cache() RETURNS TRIGGER AS $$
+BEGIN
+ IF TG_TABLE_NAME = 'votes' THEN
+ IF TG_OP = 'INSERT' THEN
+ UPDATE users SET c_votes = c_votes + 1 WHERE id = NEW.uid;
+ ELSE
+ UPDATE users SET c_votes = c_votes - 1 WHERE id = OLD.uid;
+ END IF;
+ ELSE
+ IF TG_OP = 'INSERT' THEN
+ UPDATE users SET c_changes = c_changes + 1 WHERE id = NEW.requester;
+ ELSE
+ UPDATE users SET c_changes = c_changes - 1 WHERE id = OLD.requester;
+ END IF;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE 'plpgsql';
+
+CREATE TRIGGER users_changes_update AFTER INSERT OR DELETE ON changes FOR EACH ROW EXECUTE PROCEDURE update_users_cache();
+CREATE TRIGGER users_votes_update AFTER INSERT OR DELETE ON votes FOR EACH ROW EXECUTE PROCEDURE update_users_cache();
+
+
+-- the stats_cache table
+CREATE OR REPLACE FUNCTION update_stats_cache() RETURNS TRIGGER AS $$
+BEGIN
+ IF TG_OP = 'INSERT' THEN
+ IF TG_TABLE_NAME = 'users' THEN
+ UPDATE stats_cache SET count = count+1 WHERE section = TG_TABLE_NAME;
+ ELSIF NEW.hidden = FALSE THEN
+ IF TG_TABLE_NAME = 'threads_posts' THEN
+ IF EXISTS(SELECT 1 FROM threads WHERE id = NEW.tid AND hidden = FALSE) THEN
+ UPDATE stats_cache SET count = count+1 WHERE section = TG_TABLE_NAME;
+ END IF;
+ ELSE
+ UPDATE stats_cache SET count = count+1 WHERE section = TG_TABLE_NAME;
+ END IF;
+ END IF;
+
+ ELSIF TG_OP = 'UPDATE' AND TG_TABLE_NAME <> 'users' THEN
+ IF OLD.hidden = TRUE AND NEW.hidden = FALSE THEN
+ IF TG_TABLE_NAME = 'threads' THEN
+ UPDATE stats_cache SET count = count+NEW.count WHERE section = 'threads_posts';
+ END IF;
+ UPDATE stats_cache SET count = count+1 WHERE section = TG_TABLE_NAME;
+ ELSIF OLD.hidden = FALSE AND NEW.hidden = TRUE THEN
+ IF TG_TABLE_NAME = 'threads' THEN
+ UPDATE stats_cache SET count = count-NEW.count WHERE section = 'threads_posts';
+ END IF;
+ UPDATE stats_cache SET count = count-1 WHERE section = TG_TABLE_NAME;
+ END IF;
+
+ ELSIF TG_OP = 'DELETE' AND TG_TABLE_NAME = 'users' THEN
+ UPDATE stats_cache SET count = count-1 WHERE section = TG_TABLE_NAME;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE 'plpgsql';
+
+CREATE TRIGGER vn_stats_update AFTER INSERT OR UPDATE ON vn FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+CREATE TRIGGER producers_stats_update AFTER INSERT OR UPDATE ON producers FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+CREATE TRIGGER releases_stats_update AFTER INSERT OR UPDATE ON releases FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+CREATE TRIGGER threads_stats_update AFTER INSERT OR UPDATE ON threads FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+CREATE TRIGGER threads_posts_stats_update AFTER INSERT OR UPDATE ON threads_posts FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+CREATE TRIGGER users_stats_update AFTER INSERT OR DELETE ON users FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+
+
+
+
+
+
---------------------------------
-- M I S C E L L A N E O U S --
---------------------------------
@@ -412,8 +500,18 @@ $$ LANGUAGE plpgsql;
-- Sequences used for ID generation of items not in the DB
CREATE SEQUENCE covers_seq;
+
+-- Rows that are assumed to be available
INSERT INTO users (id, username, mail, rank)
VALUES (0, 'deleted', 'del@vndb.org', 0);
INSERT INTO users (username, mail, rank, registered)
VALUES ('multi', 'multi@vndb.org', 0, EXTRACT(EPOCH FROM NOW()));
+INSERT INTO stats_cache (section, count) VALUES
+ ('users', 1),
+ ('vn', 0),
+ ('producers', 0),
+ ('releases', 0),
+ ('threads', 0),
+ ('threads_posts', 0);
+
diff --git a/util/fcgi.pl b/util/fcgi.pl
deleted file mode 100644
index 6a391386..00000000
--- a/util/fcgi.pl
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/usr/bin/perl
-
-package FCGI::Handler;
-
-use strict;
-use warnings;
-use lib '/www/vndb/lib';
-
-use strict;
-use warnings;
-use FCGI;
-use CGI::Minimal ();
-use CGI::Cookie::XS;
-use Time::HiRes;
-use VNDB;
-
-my $elog = "/www/err.log";
-
-our $req = FCGI::Request();
-our $c;
-our $outputted = 0;
-
-my $VNDB = VNDB->new(%VNDB::VNDBopts);
-
-our @WRN;
-$SIG{__WARN__} = sub { push @FCGI::Handler::WRN, @_; };
-
-while($req->Accept() >= 0) {
- # lighty doesn't always split the query string from REQUEST_URI
- ($ENV{REQUEST_URI}, $ENV{QUERY_STRING}) = split /\?/, $ENV{REQUEST_URI}
- if ($ENV{REQUEST_URI}||'') =~ /\?/;
-
- # re-init CGI::Minimal (can die())
- eval {
- CGI::Minimal::reset_globals;
- CGI::Minimal::allow_hybrid_post_get(1);
- CGI::Minimal::max_read_size(5*1024*1024); # allow 5MB of POST data
- $c = CGI::Minimal->new();
- };
- if($@) {
- send500();
- $req->Finish();
- next;
- }
-
- # figure out some required variables
- my $o = $VNDB;
- my $start = [ Time::HiRes::gettimeofday ] if $o->{debug};
-
- # call appropriate functions in VNDB.pm
- my $e = eval {
- if($c->truncated) {
- send500();
- warn "Truncated post request!\n";
- } else {
- $o->DBCheck;
- $o->get_page; # automatically calls DBCommit on success
- }
- 1;
- };
-
- # Error handling
- if(@WRN && $e && !$@ && open(my $F, '>>', $elog)) {
- for (@WRN) {
- chomp;
- printf $F "[%s] %s: %s\n", scalar localtime(), $ENV{HTTP_HOST}.$ENV{REQUEST_URI}.'?'.$ENV{QUERY_STRING}, $_;
- }
- close $F;
- }
- if(!defined $e && $@ && open(my $F, '>>', $elog)) {
- printf $F "[%s] %s: FATAL ERROR!\n", scalar localtime(), $ENV{HTTP_HOST}.$ENV{REQUEST_URI}.'?'.$ENV{QUERY_STRING};
- print $F " ENV-dump:\n";
- printf $F " %s: %s\n", $_, $ENV{$_} for (sort keys %ENV);
- print $F " PARAM-dump:\n";
- printf $F " %s: %s\n", $_, $c->param($_) for (sort $c->param());
- my $err = $@; chomp($err);
- printf $F " ERROR:\n %s\n", $err;
- if(@WRN) {
- print $F " WARNINGS:\n";
- for (@WRN) {
- chomp;
- printf $F " %s\n", $_;
- }
- }
- print $F "\n";
- close $F;
- eval { $o->DBRollBack; };
- send500() if !$outputted;
- }
-
- # Debug info
- if($o->{debug} && open(my $F, '>>', $elog)) {
- my($sqlt, $sqlc) = (0, 0);
- for (@{$o->{_DB}->{Queries}}) {
- if($_->[0]) {
- $sqlc++;
- $sqlt += $_->[1];
- }
- }
- my $time = Time::HiRes::tv_interval($start);
- my $tpl = $o->{_Res}->{_tpltime} ? $o->{_Res}->{_tpltime}/$time*100 : 0;
- my $gzip = 0;
- $gzip = 100 - $o->{_Res}->{_gzip}->[1]/$o->{_Res}->{_gzip}->[0]*100
- if($o->{_Res}->{_gzip} && ref($o->{_Res}->{_gzip}) eq 'ARRAY' && $o->{_Res}->{_gzip}->[0] > 0);
- printf $F "Took %3dms (SQL/TPL/perl: %4.1f%% %4.1f%% %4.1f%%) (GZIP: %4.1f%%) to parse %s\n",
- $time*1000, $sqlt/$time*100, $tpl, 100-($sqlt/$time*100)-$tpl, $gzip, $ENV{REQUEST_URI};
- close $F;
- }
-
- # reset vars
- @WRN = ();
- $outputted = 0;
- undef $o->{_Res};
- undef $o->{_Req};
-
- $req->Finish();
-}
-
-sub send500 {
- print "Status: 500 Internal Server Error\n";
- print "Content-Type: text/html\n";
- print "X-Sendfile: /www/vndb/www/files/err.html\n\n";
-}
-
diff --git a/util/init.pl b/util/init.pl
new file mode 100755
index 00000000..5c171705
--- /dev/null
+++ b/util/init.pl
@@ -0,0 +1,70 @@
+#!/usr/bin/perl
+
+
+# This script should be run after you've somehow managed to fetch
+# all the versioned files from the git repo.
+
+
+print "Initializing the files and directories needed to run VNDB...\n";
+
+
+# determine our root directory
+use Cwd 'abs_path';
+our $ROOT;
+BEGIN {
+ ($ROOT = abs_path $0) =~ s{/util/init\.pl$}{};
+}
+
+
+print " Using project root: $ROOT\n";
+print "\n";
+
+
+
+print "Creating directory structures...\n";
+for my $d (qw| cv rg st sf |) {
+ print " /static/$d\n";
+ mkdir "$ROOT/static/$d" or die "mkdir '$ROOT/static/$d': $!\n";
+ for my $i (0..99) {
+ my $n = sprintf '%s/static/%s/%02d', $ROOT, $d, $i;
+ mkdir $n or die "mkdir '$n': $!\n";
+ chmod 0777, $n or die "chmod 777 '$n': $!\n";
+ }
+}
+print "\n";
+
+
+print "Creating /www\n";
+print " You can use this directory to store all files you want to\n";
+print " be available from the main domain. A favicon.ico for example.\n";
+mkdir "$ROOT/www" or die $!;
+print "\n";
+
+
+print "Writing robots.txt in /static and /www\n";
+print " You probably don't want your personal copy of VNDB to end up\n";
+print " in the google results, so I'll install a default robots.txt\n";
+print " for you. You're free to modify them as you wish.\n";
+for ('static/robots.txt', 'www/robots.txt') {
+ print " $_ exists, skipping...\n", next if -f "$ROOT/$_";
+ open my $F, '>', "$ROOT/$_" or die "$_: $!\n";
+ print $F "User-agent: *\nDisallow: /\n";
+ close $F;
+}
+print "\n";
+
+
+if(!-f "$ROOT/data/config.pl") {
+ # TODO: create a template config file
+ print "No custom config file found, please write one!\n";
+}
+print "\n";
+
+
+print "Everything is initialized! Now make sure to configure your\n";
+print "webserver and to initialize a postgresql database (using\n";
+print "dump.sql)\n";
+
+
+
+
diff --git a/util/multi.pl b/util/multi.pl
index 6ec3dd02..e234534d 100755
--- a/util/multi.pl
+++ b/util/multi.pl
@@ -21,25 +21,32 @@ package Multi;
use strict;
use warnings;
+no warnings 'once';
use Tie::ShareLite ':lock';
use Time::HiRes;
use POE;
use DBI;
+use Cwd 'abs_path';
-use lib '/www/vndb/lib';
-use Multi::Core;
-BEGIN { require 'global.pl' }
+# loading & initialization
+
+our $ROOT;
+BEGIN { ($ROOT = abs_path $0) =~ s{/util/multi\.pl$}{}; *VNDB::ROOT = \$ROOT }
+use lib $VNDB::ROOT.'/lib';
+use Multi::Core;
+require $VNDB::ROOT.'/data/global.pl';
-our $LOGDIR = '/www/vndb/data/log';
-our $LOGLVL = 3; # 3:DEBUG, 2:ACTIONS, 1:WARN
our $STOP = 0;
our $DAEMONIZE = (grep /^-c$/, @ARGV) ? 1 : (grep /^-s$/, @ARGV) ? 2 : 0;
+
+# only add commands with the -a argument
+
if(grep /^-a$/, @ARGV) {
- my $s = tie my %s, 'Tie::ShareLite', @VNDB::SHMOPTS;
+ my $s = tie my %s, 'Tie::ShareLite', -key => $VNDB::S{sharedmem_key}, -create => 'yes', -destroy => 'no', -mode => 0666;
$s->lock(LOCK_EX);
my @q = ( ($s{queue} ? @{$s{queue}} : ()), (grep !/^-/, @ARGV) );
$s{queue} = \@q;
@@ -48,15 +55,15 @@ if(grep /^-a$/, @ARGV) {
}
# one shared pgsql connection for all sessions
-our $SQL = DBI->connect(@VNDB::DBLOGIN,
+our $SQL = DBI->connect(@{$VNDB::O{db_login}},
{ PrintError => 1, RaiseError => 0, AutoCommit => 1, pg_enable_utf8 => 1 });
Multi::Core->spawn();
# dynamically load and spawn modules
-for (0..(@$VNDB::MULTI/2+1)) {
- my($mod, $args) = @{$VNDB::MULTI}[$_*2, $_*2+1];
+for (keys %{$VNDB::M{modules}}) {
+ my($mod, $args) = ($_, $VNDB::M{modules}{$_});
next if !$args || ref($args) ne 'HASH';
require "Multi/$mod.pm";
# I'm surprised the strict pagma isn't complaining about this
diff --git a/util/updates/update_2.0.sql b/util/updates/update_2.0.sql
new file mode 100644
index 00000000..f371c4de
--- /dev/null
+++ b/util/updates/update_2.0.sql
@@ -0,0 +1,140 @@
+
+
+-- cache users vote and edit count
+ALTER TABLE users ADD COLUMN c_votes integer NOT NULL DEFAULT 0;
+ALTER TABLE users ADD COLUMN c_changes integer NOT NULL DEFAULT 0;
+
+
+-- may be an idea to run this query as a monthly cron or something
+UPDATE users SET
+ c_votes = COALESCE(
+ (SELECT COUNT(vid)
+ FROM votes
+ WHERE uid = users.id
+ GROUP BY uid
+ ), 0),
+ c_changes = COALESCE(
+ (SELECT COUNT(id)
+ FROM changes
+ WHERE requester = users.id
+ GROUP BY requester
+ ), 0);
+
+
+-- one function to rule them all
+CREATE OR REPLACE FUNCTION update_users_cache() RETURNS TRIGGER AS $$
+BEGIN
+ IF TG_TABLE_NAME = 'votes' THEN
+ IF TG_OP = 'INSERT' THEN
+ UPDATE users SET c_votes = c_votes + 1 WHERE id = NEW.uid;
+ ELSE
+ UPDATE users SET c_votes = c_votes - 1 WHERE id = OLD.uid;
+ END IF;
+ ELSE
+ IF TG_OP = 'INSERT' THEN
+ UPDATE users SET c_changes = c_changes + 1 WHERE id = NEW.requester;
+ ELSE
+ UPDATE users SET c_changes = c_changes - 1 WHERE id = OLD.requester;
+ END IF;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE 'plpgsql';
+
+CREATE TRIGGER users_changes_update AFTER INSERT OR DELETE ON changes FOR EACH ROW EXECUTE PROCEDURE update_users_cache();
+CREATE TRIGGER users_votes_update AFTER INSERT OR DELETE ON votes FOR EACH ROW EXECUTE PROCEDURE update_users_cache();
+
+
+
+
+-- users.flags -> users.(show_nsfw|show_list)
+ALTER TABLE users ADD COLUMN show_nsfw boolean NOT NULL DEFAULT FALSE;
+ALTER TABLE users ADD COLUMN show_list boolean NOT NULL DEFAULT TRUE;
+
+UPDATE users SET
+ show_nsfw = (flags & 8 = 8),
+ show_list = (flags & 4 = 4);
+
+ALTER TABLE users DROP COLUMN flags;
+
+
+
+
+-- get rid of \r and leading and trailing whitespace
+UPDATE vn_rev
+ SET "desc" = trim(both E'\n ' from translate("desc", E'\r', '')),
+ alias = trim(both E'\n ' from translate(alias, E'\r', ''));
+UPDATE releases_rev
+ SET notes = trim(both E'\n ' from translate(notes, E'\r', ''));
+UPDATE producers_rev
+ SET "desc" = trim(both E'\n ' from translate("desc", E'\r', ''));
+UPDATE changes
+ SET comments = trim(both E'\n ' from translate(comments, E'\r', ''));
+UPDATE threads_posts
+ SET msg = trim(both E'\n ' from translate(msg, E'\r', ''));
+
+
+
+
+-- cache some database statistics
+CREATE TABLE stats_cache (
+ section varchar(25) NOT NULL PRIMARY KEY,
+ count integer NOT NULL DEFAULT 0
+);
+INSERT INTO stats_cache (section, count) VALUES
+ ('users', (SELECT COUNT(*) FROM users)-1),
+ ('vn', (SELECT COUNT(*) FROM vn WHERE hidden = FALSE)),
+ ('producers', (SELECT COUNT(*) FROM producers WHERE hidden = FALSE)),
+ ('releases', (SELECT COUNT(*) FROM releases WHERE hidden = FALSE)),
+ ('threads', (SELECT COUNT(*) FROM threads WHERE hidden = FALSE)),
+ ('threads_posts', (SELECT COUNT(*) FROM threads_posts WHERE hidden = FALSE AND EXISTS(SELECT 1 FROM threads WHERE threads.id = tid AND threads.hidden = FALSE)));
+
+CREATE OR REPLACE FUNCTION update_stats_cache() RETURNS TRIGGER AS $$
+BEGIN
+ IF TG_OP = 'INSERT' THEN
+ IF TG_TABLE_NAME = 'users' THEN
+ UPDATE stats_cache SET count = count+1 WHERE section = TG_TABLE_NAME;
+ ELSIF NEW.hidden = FALSE THEN
+ IF TG_TABLE_NAME = 'threads_posts' THEN
+ IF EXISTS(SELECT 1 FROM threads WHERE id = NEW.tid AND hidden = FALSE) THEN
+ UPDATE stats_cache SET count = count+1 WHERE section = TG_TABLE_NAME;
+ END IF;
+ ELSE
+ UPDATE stats_cache SET count = count+1 WHERE section = TG_TABLE_NAME;
+ END IF;
+ END IF;
+
+ ELSIF TG_OP = 'UPDATE' AND TG_TABLE_NAME <> 'users' THEN
+ IF OLD.hidden = TRUE AND NEW.hidden = FALSE THEN
+ IF TG_TABLE_NAME = 'threads' THEN
+ UPDATE stats_cache SET count = count+NEW.count WHERE section = 'threads_posts';
+ END IF;
+ UPDATE stats_cache SET count = count+1 WHERE section = TG_TABLE_NAME;
+ ELSIF OLD.hidden = FALSE AND NEW.hidden = TRUE THEN
+ IF TG_TABLE_NAME = 'threads' THEN
+ UPDATE stats_cache SET count = count-NEW.count WHERE section = 'threads_posts';
+ END IF;
+ UPDATE stats_cache SET count = count-1 WHERE section = TG_TABLE_NAME;
+ END IF;
+
+ ELSIF TG_OP = 'DELETE' AND TG_TABLE_NAME = 'users' THEN
+ UPDATE stats_cache SET count = count-1 WHERE section = TG_TABLE_NAME;
+ END IF;
+ RETURN NULL;
+END;
+$$ LANGUAGE 'plpgsql';
+
+CREATE TRIGGER vn_stats_update AFTER INSERT OR UPDATE ON vn FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+CREATE TRIGGER producers_stats_update AFTER INSERT OR UPDATE ON producers FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+CREATE TRIGGER releases_stats_update AFTER INSERT OR UPDATE ON releases FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+CREATE TRIGGER threads_stats_update AFTER INSERT OR UPDATE ON threads FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+CREATE TRIGGER threads_posts_stats_update AFTER INSERT OR UPDATE ON threads_posts FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+CREATE TRIGGER users_stats_update AFTER INSERT OR DELETE ON users FOR EACH ROW EXECUTE PROCEDURE update_stats_cache();
+
+
+
+-- extra user rank
+UPDATE users SET rank = rank+1;
+ALTER TABLE users ALTER COLUMN rank SET DEFAULT 3;
+
+
diff --git a/util/vndb.pl b/util/vndb.pl
new file mode 100755
index 00000000..5ef0bf40
--- /dev/null
+++ b/util/vndb.pl
@@ -0,0 +1,75 @@
+#!/usr/bin/perl
+
+
+package VNDB;
+
+use strict;
+use warnings;
+
+
+use Cwd 'abs_path';
+our $ROOT;
+BEGIN { ($ROOT = abs_path $0) =~ s{/util/vndb\.pl$}{}; }
+
+
+use lib $ROOT.'/yawf/lib';
+use lib $ROOT.'/lib';
+
+
+use YAWF ':html';
+
+
+our(%O, %S);
+
+
+# load settings from global.pl
+require $ROOT.'/data/global.pl';
+
+
+YAWF::init(
+ %O,
+ namespace => 'VNDB',
+ object_data => \%S,
+ pre_request_handler => \&reqinit,
+ post_request_handler => \&reqdone,
+ error_404_handler => \&handle404,
+);
+
+
+sub reqinit {
+ my $self = shift;
+ $self->authInit;
+
+ # check for IE6
+ if($self->reqHeader('User-Agent') && $self->reqHeader('User-Agent') =~ /MSIE 6/
+ && !$self->reqCookie('ie-sucks') && $self->reqPath ne 'we-dont-like-ie6') {
+ # act as if we're opening /we-dont-like-ie6 (ugly hack, until YAWF supports preventing URL handlers from firing)
+ $ENV{HTTP_REFERER} = $ENV{REQUEST_URI};
+ $ENV{REQUEST_URI} = '/we-dont-like-ie6';
+ }
+}
+
+
+sub reqdone {
+ my $self = shift;
+ $self->dbCommit;
+ $self->multiCmd;
+}
+
+
+sub handle404 {
+ my $self = shift;
+ $self->resStatus(404);
+ $self->htmlHeader(title => 'Page Not Found');
+ div class => 'mainbox';
+ h1 'Page not found';
+ div class => 'warning';
+ h2 'Oops!';
+ p "It seems the page you were looking for does not exists,\n".
+ "you may want to try using the menu on your left to find what you are looking for.";
+ end;
+ end;
+ $self->htmlFooter;
+}
+
+
diff --git a/yawf b/yawf
new file mode 160000
+Subproject aed1e6fb5fca211479b60564251e928f31f079f