diff options
author | Yorhel <git@yorhel.nl> | 2020-04-24 16:39:20 +0200 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2020-04-24 16:39:28 +0200 |
commit | f603d5182351beb9cdd63668757afd05028fcd63 (patch) | |
tree | dd531abd1dce4aeeb4e27db3852a6d29923a4abe /lib | |
parent | b6f98d51910e0a9252b18f3b160e32d97ba64b2c (diff) | |
parent | 00381a0a88bbc7eaac1d5515fb3ffb30da9b7a66 (diff) |
Merge branch 'v2rw/vnpage'
Diffstat (limited to 'lib')
-rw-r--r-- | lib/VNDB/DB/ULists.pm | 77 | ||||
-rw-r--r-- | lib/VNDB/ExtLinks.pm | 8 | ||||
-rw-r--r-- | lib/VNDB/Handler/Chars.pm | 28 | ||||
-rw-r--r-- | lib/VNDB/Handler/ULists.pm | 51 | ||||
-rw-r--r-- | lib/VNDB/Handler/VNPage.pm | 717 | ||||
-rw-r--r-- | lib/VNDB/Types.pm | 20 | ||||
-rw-r--r-- | lib/VNDB/Util/CommonHTML.pm | 84 | ||||
-rw-r--r-- | lib/VNWeb/HTML.pm | 16 | ||||
-rw-r--r-- | lib/VNWeb/Prelude.pm | 9 | ||||
-rw-r--r-- | lib/VNWeb/Releases/Lib.pm | 24 | ||||
-rw-r--r-- | lib/VNWeb/Releases/Page.pm | 2 | ||||
-rw-r--r-- | lib/VNWeb/User/Lists.pm | 2 | ||||
-rw-r--r-- | lib/VNWeb/VN/Page.pm | 402 |
13 files changed, 457 insertions, 983 deletions
diff --git a/lib/VNDB/DB/ULists.pm b/lib/VNDB/DB/ULists.pm deleted file mode 100644 index 4c1d10ae..00000000 --- a/lib/VNDB/DB/ULists.pm +++ /dev/null @@ -1,77 +0,0 @@ - -package VNDB::DB::ULists; - -use strict; -use warnings; -use Exporter 'import'; - - -our @EXPORT = qw| - dbRListGet dbRListAdd dbRListDel - dbVoteStats -|; - - -# Options: uid rid -sub dbRListGet { - my($self, %o) = @_; - - my %where = ( - 'uid = ?' => $o{uid}, - $o{rid} ? ('rid IN(!l)' => [ ref $o{rid} ? $o{rid} : [$o{rid}] ]) : (), - ); - - return $self->dbAll(q| - SELECT uid, rid, status - FROM rlists - !W|, - \%where - ); -} - - -# Arguments: uid rid status -# rid can be an arrayref only when the rows are already present, in which case an update is done -sub dbRListAdd { - my($self, $uid, $rid, $stat) = @_; - $self->dbExec( - 'UPDATE rlists SET status = ? WHERE uid = ? AND rid IN(!l)', - $stat, $uid, ref($rid) ? $rid : [ $rid ] - ) - || - $self->dbExec( - 'INSERT INTO rlists (uid, rid, status) VALUES(?, ?, ?)', - $uid, $rid, $stat - ); -} - - -# Arguments: uid, rid -sub dbRListDel { - my($self, $uid, $rid) = @_; - $self->dbExec( - 'DELETE FROM rlists WHERE uid = ? AND rid IN(!l)', - $uid, ref($rid) ? $rid : [ $rid ] - ); -} - - -# Arguments: 'vid', id -# Returns an arrayref with 10 elements containing the [ count(vote), sum(vote) ] -# for votes in the range of ($index+0.5) .. ($index+1.4) -sub dbVoteStats { - my($self, $col, $id, $ign) = @_; - my $r = [ map [0,0], 0..9 ]; - $r->[$_->{idx}] = [ $_->{votes}, $_->{total} ] for (@{$self->dbAll(q| - SELECT (vote::numeric/10)::int-1 AS idx, COUNT(vote) as votes, SUM(vote) AS total - FROM ulist_vns uv - WHERE uv.vote IS NOT NULL AND NOT EXISTS(SELECT 1 FROM users u WHERE u.id = uv.uid AND u.ign_votes) - AND uv.vid = ? - GROUP BY (vote::numeric/10)::int|, - $id - )}); - return $r; -} - -1; - diff --git a/lib/VNDB/ExtLinks.pm b/lib/VNDB/ExtLinks.pm index 2d057847..00472abd 100644 --- a/lib/VNDB/ExtLinks.pm +++ b/lib/VNDB/ExtLinks.pm @@ -218,7 +218,7 @@ sub enrich_extlinks { my sub w { return if !$obj->{l_wikidata}; my($v, $fmt, $label) = ($w->{$obj->{l_wikidata}}{$_[0]}, @{$WIKIDATA{$_[0]}}{'fmt', 'label'}); - push @links, map [ $label, ref $fmt ? $fmt->($_) : sprintf $fmt, $_ ], ref $v ? @$v : $v ? $v : () + push @links, map [ $label, ref $fmt ? $fmt->($_) : sprintf($fmt, $_), undef ], ref $v ? @$v : $v ? $v : () } my sub l { my($f, $price) = @_; @@ -242,7 +242,7 @@ sub enrich_extlinks { w 'howlongtobeat'; w 'igdb_game'; l 'l_renai'; - push @links, [ 'VNStat', sprintf 'https://vnstat.net/novel/%d', $obj->{id} ] if $obj->{c_votecount}>=20; + push @links, [ 'VNStat', sprintf('https://vnstat.net/novel/%d', $obj->{id}), undef ] if $obj->{c_votecount}>=20; } # Release links @@ -250,7 +250,7 @@ sub enrich_extlinks { l 'l_egs'; l 'l_erotrail'; l 'l_steam'; - push @links, [ 'SteamDB', sprintf 'https://steamdb.info/app/%d/info', $obj->{l_steam} ] if $obj->{l_steam}; + push @links, [ 'SteamDB', sprintf('https://steamdb.info/app/%d/info', $obj->{l_steam}), undef ] if $obj->{l_steam}; l 'l_dlsite', $obj->{l_dlsite_price}; l 'l_dlsiteen', $obj->{l_dlsiteen_price}; l 'l_gog'; @@ -289,7 +289,7 @@ sub enrich_extlinks { w 'mobygames_company'; w 'gamefaqs_company'; w 'doujinshi_author'; - push @links, [ 'VNStat', sprintf 'https://vnstat.net/developer/%d', $obj->{id} ]; + push @links, [ 'VNStat', sprintf('https://vnstat.net/developer/%d', $obj->{id}), undef ]; } $obj->{extlinks} = \@links diff --git a/lib/VNDB/Handler/Chars.pm b/lib/VNDB/Handler/Chars.pm index 95430108..ee2452f9 100644 --- a/lib/VNDB/Handler/Chars.pm +++ b/lib/VNDB/Handler/Chars.pm @@ -7,9 +7,8 @@ use TUWF ':html', 'uri_escape'; use Exporter 'import'; use VNDB::Func; use VNDB::Types; -use List::Util 'min'; -our @EXPORT = ('charOps', 'charBrowseTable'); +our @EXPORT = ('charBrowseTable'); TUWF::register( qr{c(?:([1-9]\d*)(?:\.([1-9]\d*))?/(edit|copy)|/new)} @@ -18,31 +17,6 @@ TUWF::register( ); -sub charOps { - my($self, $sexual, $blockId) = @_; - $blockId ||= 'charops_block'; - my $spoil = $self->authPref('spoilers')||0; - - if($sexual) { - my $id_sex = $blockId.'_sex'; - input type => 'checkbox', class => 'visuallyhidden sexual_check', id => $id_sex, ($self->authPref('traits_sexual') ? (checked => 'checked') : ()); - label for => $id_sex, class => 'lst sec', 'Show sexual traits'; - } - - my $id_2 = $blockId.'_2'; - input type => 'radio', class => 'visuallyhidden radio_spoil2', name => $blockId, id => $id_2, $spoil == 2 ? (checked => 'checked') : (); - label for => $id_2, $sexual ? () : (class => 'lst'), 'Spoil me!'; - - my $id_1 = $blockId.'_1'; - input type => 'radio', class => 'visuallyhidden radio_spoil1', name => $blockId, id => $id_1, $spoil == 1 ? (checked => 'checked') : (); - label for => $id_1, 'Show minor spoilers'; - - my $id_0 = $blockId.'_0'; - input type => 'radio', class => 'visuallyhidden radio_spoil0', name => $blockId, id => $id_0, $spoil == 0 ? (checked => 'checked') : (); - label for => $id_0, 'Hide spoilers'; -} - - sub edit { my($self, $id, $rev, $copy) = @_; diff --git a/lib/VNDB/Handler/ULists.pm b/lib/VNDB/Handler/ULists.pm deleted file mode 100644 index 03c079b1..00000000 --- a/lib/VNDB/Handler/ULists.pm +++ /dev/null @@ -1,51 +0,0 @@ - -package VNDB::Handler::ULists; - -use strict; -use warnings; -use TUWF ':xml'; -use VNDB::Func; -use VNDB::Types; - - -TUWF::register( - qr{r([1-9]\d*)/list}, \&rlist_e, - qr{xml/rlist.xml}, \&rlist_e, -); - - -sub rlist_e { - my($self, $id) = @_; - - my $rid = $id; - if(!$rid) { - my $f = $self->formValidate({ get => 'id', required => 1, template => 'id' }); - return $self->resNotFound if $f->{_err}; - $rid = $f->{id}; - } - - my $uid = $self->authInfo->{id}; - return $self->htmlDenied() if !$uid; - - return if !$self->authCheckCode; - my $f = $self->formValidate( - { get => 'e', required => 1, enum => [ -1, keys %RLIST_STATUS ] }, - { get => 'ref', required => 0, default => "/r$rid" } - ); - return $self->resNotFound if $f->{_err}; - - $self->dbRListDel($uid, $rid) if $f->{e} == -1; - $self->dbRListAdd($uid, $rid, $f->{e}) if $f->{e} >= 0; - - if($id) { - $self->resRedirect($f->{ref}, 'temp'); - } else { - # doesn't really matter what we return, as long as it's XML - $self->resHeader('Content-type' => 'text/xml'); - xml; - tag 'done', ''; - } -} - -1; - diff --git a/lib/VNDB/Handler/VNPage.pm b/lib/VNDB/Handler/VNPage.pm index 30ddb43c..cc0c7da4 100644 --- a/lib/VNDB/Handler/VNPage.pm +++ b/lib/VNDB/Handler/VNPage.pm @@ -3,19 +3,15 @@ package VNDB::Handler::VNPage; use strict; use warnings; -use TUWF ':html', 'xml_escape'; +use TUWF ':html'; use VNDB::Func; use VNDB::Types; -use VNDB::ExtLinks; -use List::Util 'min'; -use POSIX 'strftime'; TUWF::register( qr{v/rand} => \&rand, qr{v([1-9]\d*)/releases} => \&releases, qr{v([1-9]\d*)/staff} => sub { $_[0]->resRedirect("/v$_[1]#staff") }, - qr{v([1-9]\d*)(?:\.([1-9]\d*))?} => \&page, ); @@ -302,717 +298,6 @@ sub _releases_table { } -sub page { - my($self, $vid, $rev) = @_; - - my $char = $rev && $rev eq 'chars'; # XXX: Not used anymore; implemented in VNWeb::VN::Page. - $rev = undef if $char; - - my $method = $rev ? 'dbVNGetRev' : 'dbVNGet'; - my $v = $self->$method( - id => $vid, - what => 'extended anime relations screenshots rating ranking staff'.($rev ? ' seiyuu' : ''), - $rev ? (rev => $rev) : (), - )->[0]; - return $self->resNotFound if !$v->{id}; - - my $r = $self->dbReleaseGet(vid => $vid, what => 'extended links vns producers platforms media', results => 200); - - enrich_extlinks v => $v; - enrich_extlinks r => $r; - - my $metadata = { - 'og:title' => $v->{title}, - 'og:description' => bb2text $v->{desc}, - }; - - if($v->{image} && !$v->{img_nsfw}) { - $metadata->{'og:image'} = imgurl(cv => $v->{image}); - } elsif(my ($ss) = grep !$_->{nsfw}, @{$v->{screenshots}}) { - $metadata->{'og:image'} = imgurl(st => $ss->{id}); - } - - $self->htmlHeader(title => $v->{title}, noindex => $rev, metadata => $metadata); - $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', lang_attr($v->{c_olang}), $v->{original} if $v->{original}; - - div class => 'vndetails'; - - # image - div class => 'vnimg'; - if(!$v->{image}) { - p 'No image uploaded yet'; - } else { - if($v->{img_nsfw}) { - p class => 'nsfw_pic'; - input id => 'nsfw_chk', type => 'checkbox', class => 'visuallyhidden', $self->authPref('show_nsfw') ? (checked => 'checked') : (); - label for => 'nsfw_chk'; - span id => 'nsfw_show'; - txt 'This image has been flagged as Not Safe For Work.'; - br; br; - span class => 'fake_link', 'Show me anyway'; - br; br; - txt '(This warning can be disabled in your account)'; - end; - span id => 'nsfw_hid'; - img src => imgurl(cv => $v->{image}), alt => $v->{title}; - i 'Flagged as NSFW'; - end; - end; - end; - } else { - img src => imgurl(cv => $v->{image}), alt => $v->{title}; - } - } - end 'div'; # /vnimg - - # general info - table class => 'stripe'; - Tr; - td class => 'key', 'Title'; - td $v->{title}; - end; - if($v->{original}) { - Tr; - td 'Original title'; - td lang_attr($v->{c_olang}), $v->{original}; - end; - } - if($v->{alias}) { - $v->{alias} =~ s/\n/, /g; - Tr; - td 'Aliases'; - td $v->{alias}; - end; - } - if($v->{length}) { - Tr; - td 'Length'; - td fmtvnlen $v->{length}, 1; - end; - } - - _producers($self, $r); - _relations($self, $v) if @{$v->{relations}}; - - if($v->{extlinks}->@*) { - Tr; - td 'Links'; - td; - for($v->{extlinks}->@*) { - a href => $_->[1], $_->[0]; - txt ', ' if $_ ne $v->{extlinks}[$#{$v->{extlinks}}]; - } - end; - end; - } - _affiliate_links($self, $r); - - _anime($self, $v) if @{$v->{anime}}; - - _useroptions($self, $v, $r) if $self->authInfo->{id}; - - Tr class => 'nostripe'; - td class => 'vndesc', colspan => 2; - h2 'Description'; - p; - lit $v->{desc} ? bb2html $v->{desc} : '-'; - end; - end; - end; - - end 'table'; - end 'div'; - div class => 'clearfloat', style => 'height: 5px', ''; # otherwise the tabs below aren't positioned correctly - - # tags - my $t = $self->dbTagStats(vid => $v->{id}, sort => 'rating', reverse => 1, minrating => 0, results => 999, state => 2); - if(@$t) { - div id => 'tagops'; - for (keys %TAG_CATEGORY) { - input id => "cat_$_", type => 'checkbox', class => 'visuallyhidden', - ($self->authInfo->{id} ? $self->authPref("tags_$_") : $_ ne 'ero') ? (checked => 'checked') : (); - label for => "cat_$_", lc $TAG_CATEGORY{$_}; - } - my $spoiler = $self->authPref('spoilers') || 0; - input id => 'tag_spoil_none', type => 'radio', class => 'visuallyhidden', name => 'tag_spoiler', $spoiler == 0 ? (checked => 'checked') : (); - label for => 'tag_spoil_none', class => 'sec', lc 'Hide spoilers'; - input id => 'tag_spoil_some', type => 'radio', class => 'visuallyhidden', name => 'tag_spoiler', $spoiler == 1 ? (checked => 'checked') : (); - label for => 'tag_spoil_some', lc 'Show minor spoilers'; - input id => 'tag_spoil_all', type => 'radio', class => 'visuallyhidden', name => 'tag_spoiler', $spoiler == 2 ? (checked => 'checked') : (); - label for => 'tag_spoil_all', lc 'Spoil me!'; - - input id => 'tag_toggle_summary', type => 'radio', class => 'visuallyhidden', name => 'tag_all', $self->authPref('tags_all') ? () : (checked => 'checked'); - label for => 'tag_toggle_summary', class => 'sec', lc 'summary'; - input id => 'tag_toggle_all', type => 'radio', class => 'visuallyhidden', name => 'tag_all', $self->authPref('tags_all') ? (checked => 'checked') : (); - label for => 'tag_toggle_all', class => 'lst', lc 'all'; - div id => 'vntags'; - my %counts = (); - for (@$t) { - my $cnt0 = $counts{$_->{cat} . '0'} || 0; - my $cnt1 = $counts{$_->{cat} . '1'} || 0; - my $cnt2 = $counts{$_->{cat} . '2'} || 0; - my $spoil = $_->{spoiler} > 1.3 ? 2 : $_->{spoiler} > 0.4 ? 1 : 0; - SWITCH: { - $counts{$_->{cat} . '2'} = ++$cnt2; - if ($spoil == 2) { last SWITCH; } - $counts{$_->{cat} . '1'} = ++$cnt1; - if ($spoil == 1) { last SWITCH; } - $counts{$_->{cat} . '0'} = ++$cnt0; - } - my $cut = $cnt0 > 15 ? ' cut cut2 cut1 cut0' : ($cnt1 > 15 ? ' cut cut2 cut1' : ($cnt2 > 15 ? ' cut cut2' : '')); - span class => sprintf 'tagspl%d cat_%s%s', $spoil, $_->{cat}, $cut; - a href => "/g$_->{id}", style => sprintf('font-size: %dpx', $_->{rating}*3.5+6), $_->{name}; - b class => 'grayedout', sprintf ' %.1f', $_->{rating}; - end; - txt ' '; - } - end; - end; - } - end 'div'; # /mainbox - - my $chars = $self->dbCharGet(vid => $v->{id}, what => "seiyuu vns($v->{id})".($char ? ' extended traits' : ''), results => 500); - if(@$chars || $self->authCan('edit')) { - clearfloat; # fix tabs placement when tags are hidden - div class => 'maintabs'; - ul; - if(@$chars) { - li class => (!$char ? ' tabselected' : ''); a href => "/v$v->{id}#main", name => 'main', 'main'; end; - li class => ($char ? ' tabselected' : ''); a href => "/v$v->{id}/chars#chars", name => 'chars', 'characters'; end; - } - end; - ul; - if($self->authCan('edit')) { - li; a href => "/v$v->{id}/add", 'add release'; end; - li; a href => "/c/new?vid=$v->{id}", 'add character'; end; - } - end; - end; - } - - if($char) { - #_chars($self, $chars, $v); - } else { - _releases($self, $v, $r); - _staff($self, $v); - _charsum($self, $chars, $v); - _stats($self, $v); - _screenshots($self, $v, $r) if @{$v->{screenshots}}; - } - - $self->htmlFooter(v2rwjs => $self->authInfo->{id}); -} - - -sub _revision { - my($self, $v, $rev) = @_; - return if !$rev; - - my $prev = $rev && $rev > 1 && $self->dbVNGetRev( - id => $v->{id}, rev => $rev-1, what => 'extended anime relations screenshots staff seiyuu' - )->[0]; - - $self->htmlRevision('v', $prev, $v, - [ title => 'Title (romaji)', diff => 1 ], - [ original => 'Original title', diff => 1 ], - [ alias => 'Alias', diff => qr/[ ,\n\.]/ ], - [ desc => 'Description', diff => qr/[ ,\n\.]/ ], - [ length => 'Length', serialize => sub { fmtvnlen $_[0] } ], - [ l_wp => 'Wikipedia link', htmlize => sub { - $_[0] ? sprintf '<a href="http://en.wikipedia.org/wiki/%s">%1$s</a>', xml_escape $_[0] : '[empty]' - }], - [ l_wikidata => 'Wikidata ID', htmlize => sub { $_[0] ? sprintf '<a href="https://www.wikidata.org/wiki/Q%d">Q%1$d</a>', $_[0] : '[empty]' } ], - [ l_encubed => 'Encubed tag', htmlize => sub { - $_[0] ? sprintf '<a href="http://novelnews.net/tag/%s/">%1$s</a>', xml_escape $_[0] : '[empty]' - }], - [ l_renai => 'Renai.us link', htmlize => sub { - $_[0] ? sprintf '<a href="https://renai.us/game/%s">%1$s</a>', xml_escape $_[0] : '[empty]' - }], - [ credits => 'Credits', join => '<br />', split => sub { - my @r = map sprintf('<a href="/s%d" title="%s">%s</a> [%s]%s', $_->{id}, - xml_escape($_->{original}||$_->{name}), xml_escape($_->{name}), xml_escape($CREDIT_TYPE{$_->{role}}), - $_->{note} ? ' ['.xml_escape($_->{note}).']' : ''), - sort { $a->{id} <=> $b->{id} || $a->{role} cmp $b->{role} } @{$_[0]}; - return @r ? @r : ('[empty]'); - }], - [ seiyuu => 'Seiyuu', join => '<br />', split => sub { - my @r = map sprintf('<a href="/s%d" title="%s">%s</a> as <a href="/c%d">%s</a>%s', - $_->{id}, xml_escape($_->{original}||$_->{name}), xml_escape($_->{name}), $_->{cid}, xml_escape($_->{cname}), - $_->{note} ? ' ['.xml_escape($_->{note}).']' : ''), - sort { $a->{id} <=> $b->{id} || $a->{cid} <=> $b->{cid} || $a->{note} cmp $b->{note} } @{$_[0]}; - return @r ? @r : ('[empty]'); - }], - [ relations => 'Relations', join => '<br />', split => sub { - my @r = map sprintf('[%s] %s: <a href="/v%d" title="%s">%s</a>', - $_->{official} ? 'official' : 'unofficial', $VN_RELATION{$_->{relation}}{txt}, - $_->{id}, xml_escape($_->{original}||$_->{title}), xml_escape shorten $_->{title}, 40 - ), sort { $a->{id} <=> $b->{id} } @{$_[0]}; - return @r ? @r : ('[empty]'); - }], - [ 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 : ('[empty]'); - }], - [ screenshots => 'Screenshots', join => '<br />', split => sub { - my @r = map sprintf('[%s] <a href="%s" data-iv="%dx%d">%d</a> (%s)', - $_->{rid} ? qq|<a href="/r$_->{rid}">r$_->{rid}</a>| : 'no release', - imgurl(sf => $_->{id}), $_->{width}, $_->{height}, $_->{id}, - $_->{nsfw} ? 'Not safe' : 'Safe' - ), @{$_[0]}; - return @r ? @r : ('[empty]'); - }], - [ image => 'Image', htmlize => sub { - my $url = imgurl(cv => $_[0]); - if($_[0]) { - return $_[1]->{img_nsfw} && !$self->authPref('show_nsfw') ? "<a href=\"$url\">(NSFW)</a>" : "<img src=\"$url\" />"; - } else { - return 'No image'; - } - }], - [ img_nsfw => 'Image NSFW', serialize => sub { $_[0] ? 'Not safe' : 'Safe' } ], - ); -} - - -sub _producers { - my($self, $r) = @_; - - my %lang; - my @lang = grep !$lang{$_}++, map @{$_->{languages}}, @$r; - - if(grep $_->{developer}, map @{$_->{producers}}, @$r) { - my %dev = map $_->{developer} ? ($_->{id} => $_) : (), map @{$_->{producers}}, @$r; - my @dev = sort { $a->{name} cmp $b->{name} } values %dev; - Tr; - td 'Developer'; - td; - for (@dev) { - a href => "/p$_->{id}", title => $_->{original}||$_->{name}, shorten $_->{name}, 30; - txt ' & ' if $_ != $dev[$#dev]; - } - end; - end; - } - - if(grep $_->{publisher}, map @{$_->{producers}}, @$r) { - Tr; - td 'Publishers'; - td; - for my $l (@lang) { - my %p = map $_->{publisher} ? ($_->{id} => $_) : (), map @{$_->{producers}}, grep grep($_ eq $l, @{$_->{languages}}), @$r; - my @p = sort { $a->{name} cmp $b->{name} } values %p; - next if !@p; - cssicon "lang $l", $LANGUAGE{$l}; - for (@p) { - a href => "/p$_->{id}", title => $_->{original}||$_->{name}, shorten $_->{name}, 30; - txt ' & ' if $_ != $p[$#p]; - } - br; - } - end; - end 'tr'; - } -} - - -sub _relations { - my($self, $v) = @_; - - my %rel; - push @{$rel{$_->{relation}}}, $_ - for (sort { $a->{title} cmp $b->{title} } @{$v->{relations}}); - - - Tr; - td 'Relations'; - td class => 'relations'; - dl; - for(sort keys %rel) { - dt $VN_RELATION{$_}{txt}; - dd; - for (@{$rel{$_}}) { - b class => 'grayedout', '[unofficial] ' if !$_->{official}; - a href => "/v$_->{id}", title => $_->{original}||$_->{title}, shorten $_->{title}, 40; - br; - } - end; - } - end; - end; - end 'tr'; -} - - -sub _anime { - my($self, $v) = @_; - - Tr; - td 'Related anime'; - td class => 'anime'; - for (sort { ($a->{year}||9999) <=> ($b->{year}||9999) } @{$v->{anime}}) { - if(!$_->{lastfetch} || !$_->{year} || !$_->{title_romaji}) { - b; - lit sprintf '[no information available at this time: <a href="http://anidb.net/a%d">%1$d</a>]', $_->{id}; - end; - } else { - b; - txt '['; - a href => "http://anidb.net/a$_->{id}", title => 'AniDB', 'DB'; - # AnimeNFO links seem to be broken at the moment. TODO: Completely remove? - #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; - abbr title => $_->{title_kanji}||$_->{title_romaji}, shorten $_->{title_romaji}, 50; - b ' ('.(defined $_->{type} ? $ANIME_TYPE{$_->{type}}{txt}.', ' : '').$_->{year}.')'; - br; - } - } - end; - end 'tr'; -} - - -sub _useroptions { - my($self, $v, $r) = @_; - - # Voting option is hidden if nothing has been released yet - my $minreleased = min grep $_, map $_->{released}, @$r; - - my $labels = tuwf->dbAlli( - 'SELECT l.id, l.label, l.private, uvl.vid IS NOT NULL as assigned - FROM ulist_labels l - LEFT JOIN ulist_vns_labels uvl ON uvl.uid = l.uid AND uvl.lbl = l.id AND uvl.vid =', \$v->{id}, ' - WHERE l.uid =', \$self->authInfo->{id}, ' - ORDER BY CASE WHEN l.id < 10 THEN l.id ELSE 10 END, l.label' - ); - my $lst = tuwf->dbRowi('SELECT vid, vote, notes FROM ulist_vns WHERE uid =', \$self->authInfo->{id}, 'AND vid =', \$v->{id}); - - Tr class => 'nostripe'; - td colspan => 2; - VNWeb::HTML::elm_('UList.VNPage', undef, { - uid => 1*$self->authInfo->{id}, - vid => 1*$v->{id}, - onlist => $lst->{vid}?\1:\0, - canvote => $minreleased && $minreleased < strftime('%Y%m%d', gmtime) ? \1 : \0, - vote => fmtvote($lst->{vote}).'', - notes => $lst->{notes}||'', - labels => [ map +{ id => 1*$_->{id}, label => $_->{label}, private => $_->{private}?\1:\0 }, @$labels ], - selected => [ map $_->{id}, grep $_->{assigned}, @$labels ], - }); - end; - end; -} - - -sub _affiliate_links { - my($self, $r) = @_; - - # If the same shop link has been added to multiple releases, use the 'first' matching type in this list. - my @type = ('bundle', '', 'partial', 'trial', 'patch'); - - # url => [$title, $url, $price, $type] - my %links; - for my $rel (@$r) { - my $type = $rel->{patch} ? 4 : - $rel->{type} eq 'trial' ? 3 : - $rel->{type} eq 'partial' ? 2 : - @{$rel->{vn}} > 1 ? 0 : 1; - - for my $l (grep $_->[2], $rel->{extlinks}->@*) { - $links{$l->[1]} = [ @$l, min $type, $links{$l->[1]}[3]||9 ]; - } - } - return if !keys %links; - - use utf8; - Tr id => 'buynow'; - td 'Shops'; - td; - for my $l (sort { $a->[0] cmp $b->[0] || $a->[2] cmp $b->[2] } values %links) { - b class => 'standout', 'ยป '; - a href => $l->[1]; - txt $l->[2]; - b class => 'grayedout', " @ "; - txt $l->[0]; - b class => 'grayedout', " ($type[$l->[3]])" if $l->[3] != 1; - end; - br; - } - end; - end; -} - - -sub _releases { - my($self, $v, $r) = @_; - - div class => 'mainbox releases'; - 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->dbRListGet(uid => $self->authInfo->{id}, rid => [map $_->{id}, @$r]); - for my $i (@$l) { - [grep $i->{rid} == $_->{id}, @$r]->[0]{ulist} = $i; - } - div id => 'vnrlist_code', class => 'hidden', $self->authGetCode('/xml/rlist.xml'); - } - - my %lang; - my @lang = grep !$lang{$_}++, map @{$_->{languages}}, @$r; - - table; - for my $l (@lang) { - Tr class => 'lang'; - td colspan => 7; - cssicon "lang $l", $LANGUAGE{$l}; - txt $LANGUAGE{$l}; - end; - end; - for my $rel (grep grep($_ eq $l, @{$_->{languages}}), @$r) { - Tr; - td class => 'tc1'; lit fmtdatestr $rel->{released}; end; - td class => 'tc2', $rel->{minage} < 0 ? '' : minage $rel->{minage}; - td class => 'tc3'; - for (sort @{$rel->{platforms}}) { - next if $_ eq 'oth'; - cssicon $_, $PLATFORM{$_}; - } - cssicon "rt$rel->{type}", $rel->{type}; - end; - td class => 'tc4'; - a href => "/r$rel->{id}", title => $rel->{original}||$rel->{title}, $rel->{title}; - b class => 'grayedout', ' (patch)' if $rel->{patch}; - end; - - td class => 'tc_icons'; - _release_icons($self, $rel); - end; - - td class => 'tc5'; - if($self->authInfo->{id}) { - a href => "/r$rel->{id}", id => "rlsel_$rel->{id}", class => 'vnrlsel', - $rel->{ulist} ? $RLIST_STATUS{ $rel->{ulist}{status} } : '--'; - } else { - txt ' '; - } - end; - td class => 'tc6'; - $self->releaseExtLinks($rel); - end; - end 'tr'; - } - } - end 'table'; - end 'div'; -} - - -# Creates an small sized img inside an abbr tag. Used for per-release information icons. -sub _release_icon { - my($class, $title, $img) = @_; - abbr class => "release_icons_container release_icon_$class", title => $title; - img src=> "$TUWF::OBJ->{url_static}/f/$img.svg", class => "release_icons", alt => $title; - end; -} - -sub _release_icons { - my($self, $rel) = @_; - - # Voice column - my $voice = $rel->{voiced}; - _release_icon $VOICED{$voice}{icon}, $VOICED{$voice}{txt}, 'voiced' if $voice; - - # Animations columns - my $story_anim = $rel->{ani_story}; - _release_icon $ANIMATED{$story_anim}{story_icon}, "Story: $ANIMATED{$story_anim}{txt}", 'story_animated' if $story_anim; - - my $ero_anim = $rel->{ani_ero}; - _release_icon $ANIMATED{$ero_anim}{ero_icon}, "Ero: $ANIMATED{$ero_anim}{txt}", 'ero_animated' if $ero_anim; - - # Cost column - _release_icon 'freeware', 'Freeware', 'free' if $rel->{freeware}; - _release_icon 'nonfree', 'Non-free', 'nonfree' unless $rel->{freeware}; - - # Publisher type column - if(!$rel->{patch}) { - _release_icon 'doujin', 'Doujin', 'doujin' if $rel->{doujin}; - _release_icon 'commercial', 'Commercial', 'commercial' unless $rel->{doujin}; - } - - # Resolution column - my $resolution = $rel->{resolution}; - if($resolution ne 'unknown') { - my $resolution_type = $resolution eq 'nonstandard' ? 'custom' : $RESOLUTION{$resolution}{cat} eq 'widescreen' ? '16-9' : '4-3'; - # Ugly workaround: PC-98 has non-square pixels, thus not widescreen - $resolution_type = '4-3' if $resolution_type eq '16-9' && grep $_ eq 'p98', @{$rel->{platforms}}; - _release_icon "res$resolution_type", $RESOLUTION{$resolution}{txt}, "resolution_$resolution_type"; - } - - # Media column - if(@{$rel->{media}}) { - my $icon = $MEDIUM{ $rel->{media}[0]{medium} }{icon}; - my $media_detail = join ', ', map fmtmedia($_->{medium}, $_->{qty}), @{$rel->{media}}; - _release_icon $icon, $media_detail, $icon; - } - - _release_icon 'uncensor', 'Uncensored', 'uncensor' if $rel->{uncensored}; - - # Notes column - _release_icon 'notes', bb2text($rel->{notes}), 'notes' if $rel->{notes}; -} - - -sub _screenshots { - my($self, $v, $r) = @_; - - input id => 'nsfwhide_chk', type => 'checkbox', class => 'visuallyhidden', $self->authPref('show_nsfw') ? (checked => 'checked') : (); - div class => 'mainbox', id => 'screenshots'; - - if(grep $_->{nsfw}, @{$v->{screenshots}}) { - p class => 'nsfwtoggle'; - txt 'Showing '; - i id => 'nsfwshown', scalar grep(!$_->{nsfw}, @{$v->{screenshots}}); - span class => 'nsfw', scalar @{$v->{screenshots}}; - txt sprintf ' out of %d screenshot%s. ', scalar @{$v->{screenshots}}, @{$v->{screenshots}} == 1 ? '' : 's'; - label for => 'nsfwhide_chk', class => 'fake_link', 'show/hide NSFW'; - end; - } - - h1 'Screenshots'; - - for my $rel (@$r) { - my @scr = grep $_->{rid} && $rel->{id} == $_->{rid}, @{$v->{screenshots}}; - next if !@scr; - p class => 'rel'; - cssicon "lang $_", $LANGUAGE{$_} for (@{$rel->{languages}}); - cssicon $_, $PLATFORM{$_} for (@{$rel->{platforms}}); - a href => "/r$rel->{id}", $rel->{title}; - end; - div class => 'scr'; - for (@scr) { - my($w, $h) = imgsize($_->{width}, $_->{height}, @{$self->{scr_size}}); - a href => imgurl(sf => $_->{id}), - class => sprintf('scrlnk%s', $_->{nsfw} ? ' nsfw':''), - 'data-iv' => "$_->{width}x$_->{height}:scr"; - img src => imgurl(st => $_->{id}), - width => $w, height => $h, alt => "Screenshot #$_->{id}"; - end; - } - end; - } - end 'div'; -} - - -sub _stats { - my($self, $v) = @_; - - my $stats = $self->dbVoteStats(vid => $v->{id}, 1); - div class => 'mainbox'; - h1 'User stats'; - if(!grep $_->[0] > 0, @$stats) { - p 'Nobody has voted on this visual novel yet...'; - } else { - $self->htmlVoteStats(v => $v, $stats); - } - end; -} - - -sub _charspoillvl { - my($vid, $c) = @_; - my $minspoil = 5; - $minspoil = $_->{vid} == $vid && $_->{spoil} < $minspoil ? $_->{spoil} : $minspoil - for(@{$c->{vns}}); - return $minspoil; -} - - -sub _charsum { - my($self, $l, $v) = @_; - return if !@$l; - - my(@l, %done, $has_spoilers); - for my $r (keys %CHAR_ROLE) { - last if $r eq 'appears'; - for (grep grep($_->{role} eq $r, @{$_->{vns}}) && !$done{$_->{id}}++, @$l) { - $_->{role} = $r; - $has_spoilers = $has_spoilers || _charspoillvl $v->{id}, $_; - push @l, $_; - } - } - - div class => 'mainbox charsum summarize charops', 'data-summarize-height' => 200, id => 'charops'; - $self->charOps(0, 'charsum') if $has_spoilers; - h1 'Character summary'; - div class => 'charsum_list'; - for my $c (@l) { - div class => 'charsum_bubble'.($has_spoilers ? ' '.charspoil(_charspoillvl $v->{id}, $c) : ''); - div class => 'name'; - i $CHAR_ROLE{$c->{role}}{txt}; - cssicon "gen $c->{gender}", $GENDER{$c->{gender}} if $c->{gender} ne 'unknown'; - a href => "/c$c->{id}", title => $c->{original}||$c->{name}, $c->{name}; - end; - if(@{$c->{seiyuu}}) { - div class => 'actor'; - txt 'Voiced by'; - @{$c->{seiyuu}} > 1 ? br : txt ' '; - for my $s (sort { $a->{name} cmp $b->{name} } @{$c->{seiyuu}}) { - a href => "/s$s->{sid}", title => $s->{original}||$s->{name}, $s->{name}; - b class => 'grayedout', $s->{note} if $s->{note}; - br; - } - end; - } - end; - } - end; - end; -} - - -sub _staff { - my ($self, $v) = @_; - return if !@{$v->{credits}}; - - div class => 'mainbox staff summarize', 'data-summarize-height' => 200, id => 'staff'; - h1 'Staff'; - for my $r (keys %CREDIT_TYPE) { - my @s = grep $_->{role} eq $r, @{$v->{credits}}; - next if !@s; - ul; - li; b $CREDIT_TYPE{$r}; end; - for(@s) { - li; - a href => "/s$_->{id}", title => $_->{original}||$_->{name}, $_->{name}; - b class => 'grayedout', $_->{note} if $_->{note}; - end; - } - end; - } - clearfloat; - end; -} 1; diff --git a/lib/VNDB/Types.pm b/lib/VNDB/Types.pm index dfb187f4..573562a2 100644 --- a/lib/VNDB/Types.pm +++ b/lib/VNDB/Types.pm @@ -182,20 +182,20 @@ hash TAG_CATEGORY => hash ANIMATED => - 0 => { txt => 'Unknown', story_icon => 'unknown', ero_icon => 'unknown' }, - 1 => { txt => 'No animations', story_icon => 'story_not_animated', ero_icon => 'ero_not_animated' }, - 2 => { txt => 'Simple animations', story_icon => 'story_simple_animated', ero_icon => 'ero_simple_animated' }, - 3 => { txt => 'Some fully animated scenes', story_icon => 'story_some_fully_animated', ero_icon => 'ero_some_fully_animated' }, - 4 => { txt => 'All scenes fully animated', story_icon => 'story_all_fully_animated', ero_icon => 'ero_all_fully_animated' }; + 0 => { txt => 'Unknown' }, + 1 => { txt => 'No animations' }, + 2 => { txt => 'Simple animations' }, + 3 => { txt => 'Some fully animated scenes' }, + 4 => { txt => 'All scenes fully animated' }; hash VOICED => - 0 => { txt => 'Unknown', icon => 'unknown' }, - 1 => { txt => 'Not voiced', icon => 'not_voiced' }, - 2 => { txt => 'Only ero scenes voiced', icon => 'ero_voiced' }, - 3 => { txt => 'Partially voiced', icon => 'partially_voiced' }, - 4 => { txt => 'Fully voiced', icon => 'fully_voiced' }; + 0 => { txt => 'Unknown' }, + 1 => { txt => 'Not voiced' }, + 2 => { txt => 'Only ero scenes voiced' }, + 3 => { txt => 'Partially voiced' }, + 4 => { txt => 'Fully voiced' }; diff --git a/lib/VNDB/Util/CommonHTML.pm b/lib/VNDB/Util/CommonHTML.pm index 03b1bd28..c052f726 100644 --- a/lib/VNDB/Util/CommonHTML.pm +++ b/lib/VNDB/Util/CommonHTML.pm @@ -12,7 +12,7 @@ use POSIX 'ceil'; our @EXPORT = qw| htmlMainTabs htmlDenied htmlHiddenMessage htmlRevision - htmlEditMessage htmlItemMessage htmlVoteStats htmlSearchBox + htmlEditMessage htmlItemMessage htmlSearchBox |; @@ -214,88 +214,6 @@ sub htmlItemMessage { } -# generates two tables, one with a vote graph, other with recent votes -# Only supports $type eq 'v' now. -sub htmlVoteStats { - my($self, $type, $obj, $stats) = @_; - - my($max, $count, $total) = (0, 0, 0); - for (0..$#$stats) { - $max = $stats->[$_][0] if $stats->[$_][0] > $max; - $count += $stats->[$_][0]; - $total += $stats->[$_][1]; - } - div class => 'votestats'; - table class => 'votegraph'; - thead; Tr; - td colspan => 2, 'Vote stats'; - end; end; - tfoot; Tr; - td colspan => 2, sprintf '%d vote%s total, average %.2f%s', $count, $count == 1 ? '' : 's', $total/$count/10, - $type eq 'v' ? ' ('.fmtrating(ceil($total/$count/10-1)||1).')' : ''; - end; end; - for (reverse 0..$#$stats) { - Tr; - td class => 'number', $_+1; - td class => 'graph'; - div style => 'width: '.($stats->[$_][0]/$max*250).'px', ' '; - txt $stats->[$_][0]; - end; - end; - } - end 'table'; - - my $recent = $self->dbAlli(' - SELECT uv.vote,', VNWeb::DB::sql_totime('uv.vote_date '), 'as date, ', VNWeb::DB::sql_user(), ' - , NOT EXISTS(SELECT 1 FROM ulist_vns_labels uvl JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl WHERE uvl.uid = uv.uid AND uvl.vid = uv.vid AND NOT ul.private) AS hide_list - FROM ulist_vns uv - JOIN users u ON u.id = uv.uid - WHERE uv.vid =', \$obj->{id}, 'AND uv.vote IS NOT NULL - AND NOT EXISTS(SELECT 1 FROM users u WHERE u.id = uv.uid AND u.ign_votes) - ORDER BY uv.vote_date DESC - LIMIT', \8 - ); - - if(@$recent) { - table class => 'recentvotes stripe'; - thead; Tr; - td colspan => 3; - txt 'Recent votes'; - b; - txt '('; - a href => "/$type$obj->{id}/votes", 'show all'; - txt ')'; - end; - end; - end; end; - for (@$recent) { - Tr; - td; - if($_->{hide_list}) { - b class => 'grayedout', 'hidden'; - } else { - VNWeb::HTML::user_($_); - } - end; - td fmtvote $_->{vote}; - td fmtdate $_->{date}; - end; - } - end 'table'; - } - - clearfloat; - if($type eq 'v' && $obj->{c_votecount}) { - div; - h3 'Ranking'; - p sprintf 'Popularity: ranked #%d with a score of %.2f', $obj->{p_ranking}, ($obj->{c_popularity}||0)*100; - p sprintf 'Bayesian rating: ranked #%d with a rating of %.2f', $obj->{r_ranking}, $obj->{c_rating}/10; - end; - } - end 'div'; -} - - sub htmlSearchBox { shift; VNWeb::HTML::searchbox_(@_); } diff --git a/lib/VNWeb/HTML.pm b/lib/VNWeb/HTML.pm index c90945fe..369db474 100644 --- a/lib/VNWeb/HTML.pm +++ b/lib/VNWeb/HTML.pm @@ -477,7 +477,7 @@ sub _revision_header_ { sub _revision_fmtval_ { - my($opt, $val) = @_; + my($opt, $val, $obj) = @_; return i_ '[empty]' if !defined $val || !length $val || (defined $opt->{empty} && $val eq $opt->{empty}); return lit_ html_escape $val if !$opt->{fmt}; if(ref $opt->{fmt} eq 'HASH') { @@ -486,12 +486,12 @@ sub _revision_fmtval_ { } return txt_ $val ? 'True' : 'False' if $opt->{fmt} eq 'bool'; local $_ = $val; - $opt->{fmt}->(); + $opt->{fmt}->($obj); } sub _revision_fmtcol_ { - my($opt, $i, $l) = @_; + my($opt, $i, $l, $obj) = @_; my $ctx = 100; # Number of characters of context in textual diffs my sub sep_ { b_ class => 'standout', '<...>' }; # Context separator @@ -526,11 +526,11 @@ sub _revision_fmtcol_ { } } elsif(@$l > 1 && $i == 2 && ($ch eq '+' || $ch eq 'c')) { - b_ class => 'diff_add', sub { _revision_fmtval_ $opt, $val } + b_ class => 'diff_add', sub { _revision_fmtval_ $opt, $val, $obj } } elsif(@$l > 1 && $i == 1 && ($ch eq '-' || $ch eq 'c')) { - b_ class => 'diff_del', sub { _revision_fmtval_ $opt, $val } + b_ class => 'diff_del', sub { _revision_fmtval_ $opt, $val, $obj } } elsif($ch eq 'c' || $ch eq 'u' || @$l == 1) { - _revision_fmtval_ $opt, $val; + _revision_fmtval_ $opt, $val, $obj; } }, @$l; }; @@ -578,8 +578,8 @@ sub _revision_diff_ { tr_ sub { td_ $name; - _revision_fmtcol_ \%opt, 1, $l; - _revision_fmtcol_ \%opt, 2, $l; + _revision_fmtcol_ \%opt, 1, $l, $old; + _revision_fmtcol_ \%opt, 2, $l, $new; } } diff --git a/lib/VNWeb/Prelude.pm b/lib/VNWeb/Prelude.pm index cd95c046..87810ac7 100644 --- a/lib/VNWeb/Prelude.pm +++ b/lib/VNWeb/Prelude.pm @@ -71,6 +71,7 @@ sub import { no strict 'refs'; *{$c.'::RE'} = *RE; *{$c.'::in'} = \∈ + *{$c.'::idcmp'} = \&idcmp; } @@ -113,4 +114,12 @@ sub in { 0 } + +# Compare two vndbids, using proper numeric order +sub idcmp($$) { + my($a1, $a2) = $_[0] =~ /^([a-z]+)([0-9]+)$/; + my($b1, $b2) = $_[1] =~ /^([a-z]+)([0-9]+)$/; + $a1 cmp $b1 || $a2 <=> $b2 +} + 1; diff --git a/lib/VNWeb/Releases/Lib.pm b/lib/VNWeb/Releases/Lib.pm new file mode 100644 index 00000000..a5853504 --- /dev/null +++ b/lib/VNWeb/Releases/Lib.pm @@ -0,0 +1,24 @@ +package VNWeb::Releases::Lib; + +use VNWeb::Prelude; +use Exporter 'import'; + +our @EXPORT = qw/release_extlinks_/; + + +# Generate the html for an 'external links' dropdown, assumes enrich_extlinks() has already been called on this object. +sub release_extlinks_ { + my($r, $id) = @_; + return if !$r->{extlinks}->@*; + my $has_dd = $r->{extlinks}->@* > ($r->{website} ? 1 : 0); + my sub icon_ { + a_ href => $r->{website}||'#', sub { + txt_ scalar $r->{extlinks}->@* if $has_dd; + abbr_ class => 'icons external', title => 'External link', ''; + } + } + elm_ ReleaseExtLinks => undef, [ ''.($id||$r->{id}), $r->{extlinks} ], \&icon_ if $has_dd; + icon_ if !$has_dd; +} + +1; diff --git a/lib/VNWeb/Releases/Page.pm b/lib/VNWeb/Releases/Page.pm index d0c6d620..77a50f27 100644 --- a/lib/VNWeb/Releases/Page.pm +++ b/lib/VNWeb/Releases/Page.pm @@ -193,7 +193,7 @@ sub _infotable_ { td_ sub { div_ class => 'elm_dd_input', style => 'width: 150px', sub { my $d = tuwf->dbVali('SELECT status FROM rlists WHERE', { rid => $r->{id}, uid => auth->uid }); - elm_ 'UList.ReleaseEdit', $VNWeb::User::Lists::RLIST_STATUS, { rid => $r->{id}, uid => auth->uid, status => $d }; + elm_ 'UList.ReleaseEdit', $VNWeb::User::Lists::RLIST_STATUS, { rid => $r->{id}, uid => auth->uid, status => $d, empty => 'not on your list' }; } }; } if auth; diff --git a/lib/VNWeb/User/Lists.pm b/lib/VNWeb/User/Lists.pm index d4a5b800..dbfd0aea 100644 --- a/lib/VNWeb/User/Lists.pm +++ b/lib/VNWeb/User/Lists.pm @@ -218,9 +218,11 @@ our $RLIST_STATUS = form_compile any => { uid => { id => 1 }, rid => { id => 1 }, status => { required => 0, uint => 1, enum => \%RLIST_STATUS }, # undef meaning delete + empty => { required => 0, default => '' }, # An 'out' field }; elm_api UListRStatus => undef, $RLIST_STATUS, sub { my($data) = @_; + delete $data->{empty}; return elm_Unauth if !own $data->{uid}; if(!defined $data->{status}) { tuwf->dbExeci('DELETE FROM rlists WHERE uid =', \$data->{uid}, 'AND rid =', \$data->{rid}) diff --git a/lib/VNWeb/VN/Page.pm b/lib/VNWeb/VN/Page.pm index 89903d62..0d2d440d 100644 --- a/lib/VNWeb/VN/Page.pm +++ b/lib/VNWeb/VN/Page.pm @@ -1,6 +1,8 @@ package VNWeb::VN::Page; use VNWeb::Prelude; +use VNWeb::Releases::Lib 'release_extlinks_'; +use VNDB::Func 'fmtrating'; use POSIX 'strftime'; @@ -25,6 +27,22 @@ sub enrich_vn { } +# Enrich everything necessary for rev_() (includes enrich_vn()) +sub enrich_item { + my($v) = @_; + enrich_vn $v; + enrich_merge aid => 'SELECT id AS sid, aid, name, original FROM staff_alias WHERE aid IN', $v->{staff}, $v->{seiyuu}; + enrich_merge cid => 'SELECT id AS cid, name AS char_name, original AS char_original FROM chars WHERE id IN', $v->{seiyuu}; + enrich_merge scr => 'SELECT id AS scr, width, height FROM images WHERE id IN', $v->{screenshots}; + + $v->{relations} = [ sort { $a->{vid} <=> $b->{vid} } $v->{relations}->@* ]; + $v->{anime} = [ sort { $a->{aid} <=> $b->{aid} } $v->{anime}->@* ]; + $v->{staff} = [ sort { $a->{aid} <=> $b->{aid} || $a->{role} cmp $b->{role} } $v->{staff}->@* ]; + $v->{seiyuu} = [ sort { $a->{aid} <=> $b->{aid} || $a->{cid} <=> $b->{cid} || $a->{note} cmp $b->{note} } $v->{seiyuu}->@* ]; + $v->{screenshots} = [ sort { idcmp($a->{scr}, $b->{scr}) } $v->{screenshots}->@* ]; +} + + sub og { my($v) = @_; +{ @@ -35,6 +53,48 @@ sub og { } +sub rev_ { + my($v) = @_; + revision_ v => $v, \&enrich_item, + [ title => 'Title (romaji)' ], + [ original => 'Original title' ], + [ alias => 'Alias' ], + [ desc => 'Description' ], + [ length => 'Length', fmt => \%VN_LENGTH ], + [ staff => 'Credits', fmt => sub { + a_ href => "/s$_->{sid}", title => $_->{original}||$_->{name}, $_->{name}; + txt_ " [$CREDIT_TYPE{$_->{role}}]"; + txt_ " [$_->{note}]" if $_->{note}; + }], + [ seiyuu => 'Seiyuu', fmt => sub { + a_ href => "/s$_->{sid}", title => $_->{original}||$_->{name}, $_->{name}; + txt_ ' as '; + a_ href => "/c$_->{cid}", title => $_->{char_original}||$_->{char_name}, $_->{char_name}; + txt_ " [$_->{note}]" if $_->{note}; + }], + [ relations => 'Relations', fmt => sub { + txt_ sprintf '[%s] %s: ', $_->{official} ? 'official' : 'unofficial', $VN_RELATION{$_->{relation}}{txt}; + a_ href => "/v$_->{vid}", title => $_->{original}||$_->{title}, $_->{title}; + }], + [ anime => 'Anime', fmt => sub { a_ href => "https://anidb.net/anime/$_->{aid}", "a$_->{aid}" }], + [ screenshots => 'Screenshots', fmt => sub { + txt_ '['; + a_ href => "/r$_->{rid}", "r$_->{rid}" if $_->{rid}; + txt_ 'no release' if !$_->{rid}; + txt_ '] '; + a_ href => tuwf->imgurl($_->{scr}), 'data-iv' => "$_->{width}x$_->{height}", $_->{scr}; + txt_ $_->{nsfw} ? ' (Not safe)' : ' (Safe)'; + }], + [ image => 'Image', fmt => sub { + !viewget->{show_nsfw} && $_[0]{img_nsfw} + ? a_ href => tuwf->imgurl($_), '(NSFW)' + : img_ src => tuwf->imgurl($_) + } ], + [ img_nsfw => 'Image NSFW', fmt => sub { txt_ $_ ? 'Not safe' : 'Safe' } ], + revision_extlinks 'v' +} + + sub infobox_img_ { my($v) = @_; p_ 'No image uploaded yet.' if !$v->{image}; @@ -342,13 +402,323 @@ sub tabs_ { return if !$haschars && !auth->permEdit; div_ class => 'maintabs', sub { ul_ sub { - li_ class => (!$char ? ' tabselected' : ''), sub { a_ href => "/v$v->{id}#main", name => 'main', 'main' }; - li_ class => ($char ? ' tabselected' : ''), sub { a_ href => "/v$v->{id}/chars#chars", name => 'chars', 'characters' }; - } if $haschars; + if($haschars) { + li_ class => (!$char ? ' tabselected' : ''), sub { a_ href => "/v$v->{id}#main", name => 'main', 'main' }; + li_ class => ($char ? ' tabselected' : ''), sub { a_ href => "/v$v->{id}/chars#chars", name => 'chars', 'characters' }; + } + }; ul_ sub { - li_ sub { a_ href => "/v$v->{id}/add", 'add release' }; - li_ sub { a_ href => "/c/new?vid=$v->{id}", 'add character' }; - } if auth->permEdit; + if(auth->permEdit) { + li_ sub { a_ href => "/v$v->{id}/add", 'add release' }; + li_ sub { a_ href => "/c/new?vid=$v->{id}", 'add character' }; + } + }; + } +} + + +sub releases_ { + my($v) = @_; + + # TODO: Organize a long list of releases a bit better somehow? Collapsable language sections? + + enrich_merge id => ' + SELECT id, title, original, notes, minage, freeware, doujin, resolution, voiced, ani_story, ani_ero, uncensored + FROM releases WHERE id IN', $v->{releases}; + enrich_merge id => sql('SELECT rid as id, status as rlist_status FROM rlists WHERE uid =', \auth->uid, 'AND rid IN'), $v->{releases} if auth; + enrich_flatten lang => id => id => 'SELECT id, lang FROM releases_lang WHERE id IN', $v->{releases}; + enrich_flatten platforms => id => id => 'SELECT id, platform FROM releases_platforms WHERE id IN', $v->{releases}; + enrich media => id => id => 'SELECT id, medium, qty FROM releases_media WHERE id IN', $v->{releases}; + + $v->{releases} = [ sort { $a->{released} <=> $b->{released} || $a->{id} <=> $b->{id} } $v->{releases}->@* ]; + my %lang; + my @lang = grep !$lang{$_}++, map $_->{lang}->@*, $v->{releases}->@*; + + my sub icon_ { + my($img, $label, $class) = @_; + $class = $class ? " release_icon_$class" : ''; + img_ src => config->{url_static}."/f/$img.svg", class => "release_icons$class", title => $label; + } + + my sub icons_ { + my($r) = @_; + icon_ 'voiced', $VOICED{$r->{voiced}}{txt}, "voiced$r->{voiced}" if $r->{voiced}; + icon_ 'story_animated', "Story: $ANIMATED{$r->{ani_story}}{txt}", "anim$r->{ani_story}" if $r->{ani_story}; + icon_ 'ero_animated', "Ero: $ANIMATED{$r->{ani_ero}}{txt}", "anim$r->{ani_ero}" if $r->{ani_ero}; + icon_ 'free', 'Freeware' if $r->{freeware}; + icon_ 'nonfree', 'Non-free' if !$r->{freeware}; + icon_ 'doujin', 'Doujin' if !$r->{patch} && $r->{doujin}; + icon_ 'commercial', 'Commercial' if !$r->{patch} && !$r->{doujin}; + if($r->{resolution} ne 'unknown') { + my $type = $r->{resolution} eq 'nonstandard' ? 'custom' : $RESOLUTION{$r->{resolution}}{cat} eq 'widescreen' ? '16-9' : '4-3'; + # Ugly workaround: PC-98 has non-square pixels, thus not widescreen + $type = '4-3' if $type eq '16-9' && grep $_ eq 'p98', $r->{platforms}->@*; + icon_ "resolution_$type", $RESOLUTION{$r->{resolution}}{txt}; + } + icon_ $MEDIUM{ $r->{media}[0]{medium} }{icon}, join ', ', map fmtmedia($_->{medium}, $_->{qty}), $r->{media}->@* if $r->{media}->@*; + icon_ 'uncensor', 'Uncensored' if $r->{uncensored}; + icon_ 'notes', bb2text $r->{notes} if $r->{notes}; + } + + my sub lang_ { + my($lang) = @_; + tr_ class => 'lang', sub { + td_ colspan => 7, sub { + abbr_ class => "icons lang $lang", title => $LANGUAGE{$lang}, ''; + txt_ $LANGUAGE{$lang}; + } + }; + tr_ sub { + td_ class => 'tc1', sub { rdate_ $_->{released} }; + td_ class => 'tc2', $_->{minage} < 0 ? '' : minage $_->{minage}; + td_ class => 'tc3', sub { + abbr_ class => "icons $_", title => $PLATFORM{$_}, '' for grep $_ ne 'oth', $_->{platforms}->@*; + abbr_ class => "icons rt$_->{type}", title => $_->{type}, ''; + }; + td_ class => 'tc4', sub { + a_ href => "/r$_->{id}", title => $_->{original}||$_->{title}, $_->{title}; + b_ class => 'grayedout', ' (patch)' if $_->{patch}; + }; + td_ class => 'tc_icons', sub { icons_ $_ }; + td_ class => 'tc5 elm_dd_left', sub { + elm_ 'UList.ReleaseEdit', $VNWeb::User::Lists::RLIST_STATUS, { rid => $_->{id}, uid => auth->uid, status => $_->{rlist_status}, empty => '--' } if auth; + }; + td_ class => 'tc6', sub { release_extlinks_ $_, "$lang$_->{id}" }; + } for grep grep($_ eq $lang, $_->{lang}->@*), $v->{releases}->@*; + } + + div_ class => 'mainbox releases', sub { + h1_ 'Releases'; + if(!$v->{releases}->@*) { + p_ 'We don\'t have any information about releases of this visual novel yet...'; + } else { + table_ sub { lang_ $_ for @lang }; + } + } +} + + +sub staff_ { + my($v) = @_; + + # XXX: The staff listing is included in the page 3 times, for 3 different + # layouts. A better approach to get the same layout is to add the boxes to + # the HTML once with classes indicating the box position (e.g. + # "4col-col1-row1 3col-col2-row1" etc) and then using CSS to position the + # box appropriately. My attempts to do this have failed, however. The + # layouting can also be done in JS, but that's not my preferred option. + + # Step 1: Get a list of 'boxes'; Each 'box' represents a role with a list of staff entries. + # @boxes = [ $height, $roleimp, $html ] + my %roles; + push $roles{$_->{role}}->@*, $_ for $v->{staff}->@*; + my $i=0; + my @boxes = + sort { $b->[0] <=> $a->[0] || $a->[1] <=> $b->[1] } + map [ 2+$roles{$_}->@*, $i++, + xml_string sub { + li_ class => 'vnstaff_head', $CREDIT_TYPE{$_}; + li_ sub { + a_ href => "/s$_->{sid}", title => $_->{original}||$_->{name}, $_->{name}; + b_ title => $_->{note}, class => 'grayedout', $_->{note} if $_->{note}; + } for sort { $a->{name} cmp $b->{name} } $roles{$_}->@*; + } + ], grep $roles{$_}, keys %CREDIT_TYPE; + + # Step 2. Assign boxes to columns for 2 to 4 column layouts, + # efficiently packing the boxes to use the least vertical space, + # sorting the columns and boxes within columns by role importance. + # (There is no 1-column layout, that's just the 2-column layout stacked with css) + my @cols = map [map [0,99,[]], 1..$_], 2..4; # [ $height, $min_roleimp, $boxes ] for each column in each layout + for my $c (@cols) { + for (@boxes) { + my $smallest = $c->[0]; + $c->[$_][0] < $smallest->[0] && ($smallest = $c->[$_]) for 1..$#$c; + $smallest->[0] += $_->[0]; + $smallest->[1] = $_->[1] if $_->[1] < $smallest->[1]; + push $smallest->[2]->@*, $_; + } + $_->[2] = [ sort { $a->[1] <=> $b->[1] } $_->[2]->@* ] for @$c; + @$c = sort { $a->[1] <=> $b->[1] } @$c; + } + + div_ class => 'mainbox', 'data-mainbox-summarize' => 200, sub { + h1_ 'Staff'; + div_ class => sprintf('vnstaff vnstaff-%d', scalar @$_), sub { + ul_ sub { + lit_ $_->[2] for $_->[2]->@*; + } for @$_ + } for @cols; + } if $v->{staff}->@*; +} + + +sub charsum_ { + my($v) = @_; + + my $spoil = viewget->{spoilers}; + my $c = tuwf->dbAlli(' + SELECT c.id, c.name, c.original, c.gender, v.role + FROM chars c + JOIN (SELECT id, MIN(role) FROM chars_vns WHERE role <> \'appears\' AND spoil <=', \$spoil, 'AND vid =', \$v->{id}, 'GROUP BY id) v(id,role) ON c.id = v.id + WHERE NOT c.hidden + ORDER BY v.role, c.name, c.id' + ); + return if !@$c; + enrich seiyuu => id => cid => sub { sql(' + SELECT vs.cid, sa.id, sa.name, sa.original, vs.note + FROM vn_seiyuu vs + JOIN staff_alias sa ON sa.aid = vs.aid + WHERE vs.id =', \$v->{id}, 'AND vs.cid IN', $_, ' + ORDER BY sa.name' + ) }, $c; + + div_ class => 'mainbox', 'data-mainbox-summarize' => 200, sub { + p_ class => 'mainopts', sub { + a_ href => "/v$v->{id}/chars#chars", 'Full character list'; + }; + h1_ 'Character summary'; + div_ class => 'charsum_list', sub { + div_ class => 'charsum_bubble', sub { + div_ class => 'name', sub { + i_ $CHAR_ROLE{$_->{role}}{txt}; + abbr_ class => "icons gen $_->{gender}", title => $GENDER{$_->{gender}}, '' if $_->{gender} ne 'unknown'; + a_ href => "/c$_->{id}", title => $_->{original}||$_->{name}, $_->{name}; + }; + div_ class => 'actor', sub { + txt_ 'Voiced by'; + $_->{seiyuu}->@* > 1 ? br_ : txt_ ' '; + join_ \&br_, sub { + a_ href => "/s$_->{id}", title => $_->{original}||$_->{name}, $_->{name}; + b_ class => 'grayedout', $_->{note} if $_->{note}; + }, $_->{seiyuu}->@*; + } if $_->{seiyuu}->@*; + } for @$c; + }; + }; +} + + +sub stats_ { + my($v) = @_; + + my $stats = tuwf->dbAlli(' + SELECT (uv.vote::numeric/10)::int AS idx, COUNT(uv.vote) as votes, SUM(uv.vote) AS total + FROM ulist_vns uv + WHERE uv.vote IS NOT NULL + AND NOT EXISTS(SELECT 1 FROM users u WHERE u.id = uv.uid AND u.ign_votes) + AND uv.vid =', \$v->{id}, ' + GROUP BY (uv.vote::numeric/10)::int' + ); + my $sum = sum map $_->{total}, @$stats; + my $max = max map $_->{votes}, @$stats; + my $num = sum map $_->{votes}, @$stats; + + my $recent = @$stats && tuwf->dbAlli(' + SELECT uv.vote,', sql_totime('uv.vote_date'), 'as date, ', sql_user(), ' + , NOT EXISTS(SELECT 1 FROM ulist_vns_labels uvl JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl WHERE uvl.uid = uv.uid AND uvl.vid = uv.vid AND NOT ul.private) AS hide_list + FROM ulist_vns uv + JOIN users u ON u.id = uv.uid + WHERE uv.vid =', \$v->{id}, 'AND uv.vote IS NOT NULL + AND NOT EXISTS(SELECT 1 FROM users u WHERE u.id = uv.uid AND u.ign_votes) + ORDER BY uv.vote_date DESC + LIMIT', \8 + ); + + my $rank = $v->{c_votecount} && tuwf->dbRowi(' + SELECT c_rating, c_popularity + , (SELECT COUNT(*)+1 FROM vn iv WHERE NOT iv.hidden AND iv.c_popularity > COALESCE(v.c_popularity, 0.0)) AS pop_rank + , (SELECT COUNT(*)+1 FROM vn iv WHERE NOT iv.hidden AND iv.c_rating > COALESCE(v.c_rating, 0.0)) AS rating_rank + FROM vn v WHERE id =', \$v->{id} + ); + + my sub votestats_ { + table_ class => 'votegraph', sub { + thead_ sub { tr_ sub { td_ colspan => 2, 'Vote stats' } }; + tfoot_ sub { tr_ sub { td_ colspan => 2, sprintf '%d vote%s total, average %.2f (%s)', $num, $num == 1 ? '' : 's', $sum/$num/10, fmtrating(ceil($sum/$num/10-1)||1) } }; + tr_ sub { + my $num = $_; + my $votes = [grep $num == $_->{idx}, @$stats]->[0]{votes} || 0; + td_ class => 'number', $num; + td_ class => 'graph', sub { + div_ style => sprintf('width: %dpx', ($votes||0)/$max*250), ' '; + txt_ $votes||0; + }; + } for (reverse 1..10); + }; + + table_ class => 'recentvotes stripe', sub { + thead_ sub { tr_ sub { td_ colspan => 3, sub { + txt_ 'Recent votes'; + b_ sub { + txt_ '('; + a_ href => "/v$v->{id}/votes", 'show all'; + txt_ ')'; + } + } } }; + tr_ sub { + td_ sub { + b_ class => 'grayedout', 'hidden' if $_->{hide_list}; + user_ $_ if !$_->{hide_list}; + }; + td_ fmtvote $_->{vote}; + td_ fmtdate $_->{date}; + } for @$recent; + } if $recent && @$recent; + + clearfloat_; + div_ sub { + h3_ 'Ranking'; + p_ sprintf 'Popularity: ranked #%d with a score of %.2f', $rank->{pop_rank}, ($rank->{c_popularity}||0)*100; + p_ sprintf 'Bayesian rating: ranked #%d with a rating of %.2f', $rank->{rating_rank}, $rank->{c_rating}/10; + } if $v->{c_votecount}; + } + + div_ class => 'mainbox', sub { + h1_ 'User stats'; + if(!@$stats) { + p_ 'Nobody has voted on this visual novel yet...'; + } else { + div_ class => 'votestats', \&votestats_; + } + } +} + + +sub screenshots_ { + my($v) = @_; + my $s = $v->{screenshots}; + return if !@$s; + + my %rel; + push $rel{$_->{rid}}->@*, $_ for @$s; + + input_ id => 'nsfwhide_chk', type => 'checkbox', class => 'visuallyhidden', auth->pref('show_nsfw') ? (checked => 'checked') : (); + div_ class => 'mainbox', id => 'screenshots', sub { + + p_ class => 'nsfwtoggle', sub { + txt_ 'Showing '; + i_ id => 'nsfwshown', scalar grep !$_->{nsfw}, @$s; + span_ class => 'nsfw', scalar @$s; + txt_ sprintf ' out of %d screenshot%s. ', scalar @$s, @$s == 1 ? '' : 's'; + label_ for => 'nsfwhide_chk', class => 'fake_link', 'show/hide NSFW'; + } if grep $_->{nsfw}, @$s; + + h1_ 'Screenshots'; + + for my $r (grep $rel{$_->{id}}, $v->{releases}->@*) { + p_ class => 'rel', sub { + abbr_ class => "icons lang $_", title => $LANGUAGE{$_}, '' for $r->{languages}->@*; + abbr_ class => "icons $_", title => $PLATFORM{$_}, '' for $r->{platforms}->@*; + a_ href => "/r$r->{id}", $r->{title}; + }; + div_ class => 'scr', sub { + a_ href => tuwf->imgurl($_->{scr}), class => sprintf('scrlnk%s', $_->{nsfw} ? ' nsfw':''), 'data-iv' => "$_->{width}x$_->{height}:scr", sub { + my($w, $h) = imgsize $_->{width}, $_->{height}, tuwf->{scr_size}->@*; + img_ src => tuwf->imgurl($_->{scr}, 1), width => $w, height => $h, alt => "Screenshot #$_->{scr}"; + } for $rel{$r->{id}}->@*; + } + } } } @@ -393,6 +763,26 @@ sub chars_ { } +TUWF::get qr{/$RE{vrev}}, sub { + my $v = db_entry v => tuwf->capture('id'), tuwf->capture('rev'); + return tuwf->resNotFound if !$v; + + enrich_item $v; + + framework_ title => $v->{title}, index => !tuwf->capture('rev'), type => 'v', dbobj => $v, hiddenmsg => 1, og => og($v), + sub { + rev_ $v if tuwf->capture('rev'); + infobox_ $v; + tabs_ $v, 0; + releases_ $v; + staff_ $v; + charsum_ $v; + stats_ $v; + screenshots_ $v; + }; +}; + + TUWF::get qr{/$RE{vid}/chars}, sub { my $v = db_entry v => tuwf->capture('id'); return tuwf->resNotFound if !$v; |