summaryrefslogtreecommitdiff
path: root/lib/VNWeb/VN/Page.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/VNWeb/VN/Page.pm')
-rw-r--r--lib/VNWeb/VN/Page.pm782
1 files changed, 549 insertions, 233 deletions
diff --git a/lib/VNWeb/VN/Page.pm b/lib/VNWeb/VN/Page.pm
index ed4f6e75..6262fcc1 100644
--- a/lib/VNWeb/VN/Page.pm
+++ b/lib/VNWeb/VN/Page.pm
@@ -3,44 +3,96 @@ package VNWeb::VN::Page;
use VNWeb::Prelude;
use VNWeb::Releases::Lib;
use VNWeb::Images::Lib qw/image_flagging_display image_ enrich_image_obj/;
+use VNWeb::ULists::Lib 'ulists_widget_full_data';
use VNDB::Func 'fmtrating';
-# Enrich everything necessary to at least render infobox_().
-# Also used by Chars::VNTab & Reviews::VNTab
+# Enrich everything necessary to at least render infobox_() and tabs_().
+# Also used by Chars::VNTab, Reviews::VNTab and VN::Quotes
sub enrich_vn {
- my($v) = @_;
- enrich_merge id => 'SELECT id, c_votecount, c_olang::text[] AS c_olang FROM vn WHERE id IN', $v;
- enrich_merge vid => 'SELECT id AS vid, title, original FROM vn WHERE id IN', $v->{relations};
+ my($v, $revonly) = @_;
+ $v->{title} = titleprefs_obj $v->{olang}, $v->{titles};
+ enrich_merge id => 'SELECT id, c_votecount, c_length, c_lengthnum FROM vn WHERE id IN', $v;
+ enrich_merge vid => sql('SELECT id AS vid, title, sorttitle, c_released FROM', vnt, 'v WHERE id IN'), $v->{relations};
enrich_merge aid => 'SELECT id AS aid, title_romaji, title_kanji, year, type, ann_id, lastfetch FROM anime WHERE id IN', $v->{anime};
- enrich_extlinks v => $v;
+ enrich_extlinks v => 0, $v;
enrich_image_obj image => $v;
enrich_image_obj scr => $v->{screenshots};
+ # The queries below are not relevant for revisions
+ return if $revonly;
+
# This fetches rather more information than necessary for infobox_(), but it'll have to do.
# (And we'll need it for the releases tab anyway)
$v->{releases} = tuwf->dbAlli('
- SELECT r.id, r.type, r.patch, r.released, r.gtin,', sql_extlinks(r => 'r.'), '
+ SELECT r.id, rv.rtype, r.patch, r.released, r.gtin,', sql_extlinks(r => 'r.'), '
, (SELECT COUNT(*) FROM releases_vn rv WHERE rv.id = r.id) AS num_vns
FROM releases r
JOIN releases_vn rv ON rv.id = r.id
WHERE NOT r.hidden AND rv.vid =', \$v->{id}
);
- enrich_extlinks r => $v->{releases};
+ enrich_extlinks r => 0, $v->{releases};
+
+ $v->{reviews} = tuwf->dbRowi('
+ SELECT COUNT(*) FILTER(WHERE isfull) AS full, COUNT(*) FILTER(WHERE NOT isfull) AS mini, COUNT(*) AS total
+ FROM reviews
+ WHERE NOT c_flagged AND vid =', \$v->{id}
+ );
+ $v->{tags} = !prefs()->{has_tagprefs} ? tuwf->dbAlli('
+ SELECT t.id, t.name, t.cat, tv.rating, tv.count, tv.spoiler, tv.lie
+ FROM tags t
+ JOIN tags_vn_direct tv ON t.id = tv.tag
+ WHERE tv.vid =', \$v->{id}, '
+ ORDER BY rating DESC, t.name'
+ ) : tuwf->dbAlli(
+ # Monster of a query, but tag overrides are a bit complicated:
+ # - We need to find the shortest path from a tag applied to the VN to a
+ # parent in users_prefs_tags, and use those preferences. That's what
+ # tag_direct does.
+ # - If the user has a tag marked as "Always show" but hasn't checked
+ # "also apply to child tags", then we need to look for any child tags
+ # and inject their parent if said parent hasn't been directly applied.
+ # That's what tag_indirect does.
+ 'WITH RECURSIVE tag_overrides (tid, spoil, color, childs, lvl) AS (
+ SELECT tid, spoil, color, childs, 0 FROM users_prefs_tags WHERE id =', \auth->uid, '
+ UNION ALL
+ SELECT tp.id, x.spoil, x.color, true, lvl+1
+ FROM tag_overrides x
+ JOIN tags_parents tp ON tp.parent = x.tid
+ WHERE x.childs
+ ), tag_overrides_grouped (tid, spoil, color) AS (
+ SELECT DISTINCT ON(tid) tid, spoil, color FROM tag_overrides ORDER BY tid, lvl
+ ), tag_direct (tid, rating, count, spoiler, lie, override, color) AS (
+ SELECT t.tag, t.rating, t.count, t.spoiler, t.lie, x.spoil, x.color
+ FROM tags_vn_direct t
+ LEFT JOIN tag_overrides_grouped x ON x.tid = t.tag
+ WHERE t.vid =', \$v->{id}, 'AND x.spoil IS DISTINCT FROM 1+1+1
+ ), tag_indirect (tid, rating, count, spoiler, lie, override, color) AS (
+ SELECT t.tag, t.rating, 0::smallint, t.spoiler, t.lie, x.spoil, x.color
+ FROM tags_vn_inherit t
+ JOIN users_prefs_tags x ON x.tid = t.tag
+ WHERE t.vid =', \$v->{id}, 'AND x.id =', \auth->uid, 'AND NOT x.childs AND x.spoil = 0
+ AND NOT EXISTS(SELECT 1 FROM tag_direct d WHERE d.tid = t.tag)
+ ) SELECT t.id, t.name, t.cat, d.rating, d.count, d.spoiler, d.lie, d.override, d.color
+ FROM tags t
+ JOIN (SELECT * FROM tag_direct UNION ALL SELECT * FROM tag_indirect) d ON d.tid = t.id
+ ORDER BY d.rating DESC, t.name'
+ );
}
# 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};
+ my($v, $full) = @_;
+ enrich_vn $v, !$full;
+ enrich_merge aid => sql('SELECT id AS sid, aid, title FROM', staff_aliast, 's WHERE aid IN'), $v->{staff}, $v->{seiyuu};
+ enrich_merge cid => sql('SELECT id AS cid, title AS char_title FROM', charst, 'c WHERE id IN'), $v->{seiyuu};
- $v->{relations} = [ sort { $a->{vid} <=> $b->{vid} } $v->{relations}->@* ];
+ $v->{relations} = [ sort { idcmp($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->{editions} = [ sort { ($a->{lang}||'') cmp ($b->{lang}||'') || $b->{official} cmp $a->{official} || $a->{name} cmp $b->{name} } $v->{editions}->@* ];
+ $v->{staff} = [ sort { ($a->{eid}//-1) <=> ($b->{eid}//-1) || $a->{aid} <=> $b->{aid} || $a->{role} cmp $b->{role} } $v->{staff}->@* ];
+ $v->{seiyuu} = [ sort { $a->{aid} <=> $b->{aid} || idcmp($a->{cid}, $b->{cid}) || $a->{note} cmp $b->{note} } $v->{seiyuu}->@* ];
$v->{screenshots} = [ sort { idcmp($a->{scr}{id}, $b->{scr}{id}) } $v->{screenshots}->@* ];
}
@@ -48,57 +100,94 @@ sub enrich_item {
sub og {
my($v) = @_;
+{
- description => bb_format($v->{desc}, text => 1),
- image => $v->{image} && !$v->{image}{sexual} && !$v->{image}{violence} ? tuwf->imgurl($v->{image}{id}) :
- [map $_->{scr}{sexual}||$_->{scr}{violence}?():(tuwf->imgurl($_->{scr}{id})), $v->{screenshots}->@*]->[0]
+ description => bb_format($v->{description}, text => 1),
+ image => $v->{image} && !$v->{image}{sexual} && !$v->{image}{violence} ? imgurl($v->{image}{id}) :
+ [map $_->{scr}{sexual}||$_->{scr}{violence}?():(imgurl($_->{scr}{id})), $v->{screenshots}->@*]->[0]
}
}
+sub prefs {
+ state $default = {
+ vnrel_langs => \%LANGUAGE, vnrel_olang => 1, vnrel_mtl => 0,
+ staffed_langs => \%LANGUAGE, staffed_olang => 1, staffed_unoff => 0,
+ has_tagprefs => 0,
+ };
+ tuwf->req->{vnpage_prefs} //= auth ? do {
+ my $v = tuwf->dbRowi('
+ SELECT vnrel_langs::text[], vnrel_olang, vnrel_mtl
+ , staffed_langs::text[], staffed_olang, staffed_unoff
+ , EXISTS(SELECT 1 FROM users_prefs_tags WHERE id =', \auth->uid, ') AS has_tagprefs
+ FROM users_prefs
+ WHERE id =', \auth->uid
+ );
+ $v->{vnrel_langs} = $v->{vnrel_langs} ? { map +($_,1), $v->{vnrel_langs}->@* } : \%LANGUAGE;
+ $v->{staffed_langs} = $v->{staffed_langs} ? { map +($_,1), $v->{staffed_langs}->@* } : \%LANGUAGE;
+ $v
+ } : $default;
+}
+
+
# The voting and review options are hidden if nothing has been released yet.
sub canvote {
my($v) = @_;
- my $minreleased = min grep $_, map $_->{released}, $v->{releases}->@*;
- $minreleased && $minreleased <= strftime('%Y%m%d', gmtime)
+ $v->{_canvote} //= do {
+ my $minreleased = min grep $_, map $_->{released}, $v->{releases}->@*;
+ $minreleased && $minreleased <= strftime('%Y%m%d', gmtime)
+ };
}
sub rev_ {
my($v) = @_;
- revision_ v => $v, \&enrich_item,
- [ title => 'Title (romaji)' ],
- [ original => 'Original title' ],
+ revision_ $v, \&enrich_item,
+ [ titles => 'Title(s)', txt => sub {
+ "[$_->{lang}] $_->{title}".($_->{latin} ? " / $_->{latin}" : '').($_->{official} ? '' : ' (unofficial)')
+ }],
[ alias => 'Alias' ],
- [ desc => 'Description' ],
+ [ olang => 'Original language', fmt => \%LANGUAGE ],
+ [ description => 'Description' ],
+ [ devstatus => 'Development status',fmt => \%DEVSTATUS ],
[ length => 'Length', fmt => \%VN_LENGTH ],
+ [ editions => 'Editions', fmt => sub {
+ abbr_ class => "icon-lang-$_->{lang}", title => $LANGUAGE{$_->{lang}}{txt}, '' if $_->{lang};
+ txt_ $_->{name};
+ small_ ' (unofficial)' if !$_->{official};
+ }],
[ staff => 'Credits', fmt => sub {
- a_ href => "/s$_->{sid}", title => $_->{original}||$_->{name}, $_->{name} if $_->{sid};
- b_ class => 'grayedout', '[removed alias]' if !$_->{sid};
+ my $eid = $_->{eid};
+ my $e = defined $eid && (grep $eid == $_->{eid}, $_[0]{editions}->@*)[0];
+ txt_ "[$e->{name}] " if $e;
+ a_ href => "/$_->{sid}", tattr $_ if $_->{sid};
+ small_ '[removed alias]' if !$_->{sid};
txt_ " [$CREDIT_TYPE{$_->{role}}]";
txt_ " [$_->{note}]" if $_->{note};
}],
[ seiyuu => 'Seiyuu', fmt => sub {
- a_ href => "/s$_->{sid}", title => $_->{original}||$_->{name}, $_->{name} if $_->{sid};
- b_ class => 'grayedout', '[removed alias]' if !$_->{sid};
+ a_ href => "/$_->{sid}", tattr $_ if $_->{sid};
+ small_ '[removed alias]' if !$_->{sid};
txt_ ' as ';
- a_ href => "/c$_->{cid}", title => $_->{char_original}||$_->{char_name}, $_->{char_name};
+ a_ href => "/$_->{cid}", tattr $_->{char_title};
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};
+ a_ href => "/$_->{vid}", tattr $_;
}],
[ anime => 'Anime', fmt => sub { a_ href => "https://anidb.net/anime/$_->{aid}", "a$_->{aid}" }],
[ screenshots => 'Screenshots', fmt => sub {
+ my $rev = $_[0]{chid} == $v->{chid} ? 'new' : 'old';
txt_ '[';
- a_ href => "/r$_->{rid}", "r$_->{rid}" if $_->{rid};
+ a_ href => "/$_->{rid}", $_->{rid} if $_->{rid};
txt_ 'no release' if !$_->{rid};
txt_ '] ';
- a_ href => tuwf->imgurl($_->{scr}{id}), 'data-iv' => "$_->{scr}{width}x$_->{scr}{height}::$_->{scr}{sexual}$_->{scr}{violence}$_->{scr}{votecount}", $_->{scr}{id};
- txt_ ' [';
- a_ href => "/img/$_->{scr}{id}", image_flagging_display $_->{scr};
+ a_ href => imgurl($_->{scr}{id}), 'data-iv' => "$_->{scr}{width}x$_->{scr}{height}:$rev:$_->{scr}{sexual}$_->{scr}{violence}$_->{scr}{votecount}", $_->{scr}{id};
+ txt_ " [$_->{scr}{width}x$_->{scr}{height}; ";
+ a_ href => "/$_->{scr}{id}", image_flagging_display $_->{scr} if auth;
+ span_ image_flagging_display $_->{scr} if !auth;
txt_ '] ';
- b_ class => 'grayedout', sprintf 'old flag: %s', $_->{nsfw} ? 'NSFW' : 'Safe';
+ # The old NSFW flag has been removed around 2020-07-14, so not relevant for edits made later on.
+ small_ sprintf 'old flag: %s', $_->{nsfw} ? 'NSFW' : 'Safe' if $_[0]{rev_added} < 1594684800;
}],
[ image => 'Image', fmt => sub { image_ $_ } ],
[ img_nsfw => 'Image NSFW (unused)', fmt => sub { txt_ $_ ? 'Not safe' : 'Safe' } ],
@@ -111,66 +200,132 @@ sub infobox_relations_ {
return if !$v->{relations}->@*;
my %rel;
- push $rel{$_->{relation}}->@*, $_ for sort { $a->{title} cmp $b->{title} } $v->{relations}->@*;
+ push $rel{$_->{relation}}->@*, $_ for sort { $b->{official} <=> $a->{official} || $a->{c_released} <=> $b->{c_released} || $a->{sorttitle} cmp $b->{sorttitle} } $v->{relations}->@*;
+ my $unoffcount = grep !$_->{official}, $v->{relations}->@*;
tr_ sub {
td_ 'Relations';
- td_ class => 'relations', sub { dl_ sub {
- for(sort keys %rel) {
- dt_ $VN_RELATION{$_}{txt};
- dd_ sub {
- join_ \&br_, sub {
- b_ class => 'grayedout', '[unofficial] ' if !$_->{official};
- a_ href => "/v$_->{vid}", title => $_->{original}||$_->{title}, shorten $_->{title}, 40;
- }, $rel{$_}->@*;
+ td_ class => 'relations linkradio', sub {
+ if($unoffcount >= 3) {
+ input_ type => 'checkbox', id => 'unoffrelations', class => 'hidden';
+ label_ for => 'unoffrelations', "unofficial ($unoffcount)";
+ }
+ dl_ sub {
+ for(sort keys %rel) {
+ my @allunoff = (!grep $_->{official}, $rel{$_}->@*) ? (class => 'unofficial') : ();
+ dt_ @allunoff, $VN_RELATION{$_}{txt};
+ dd_ @allunoff, sub {
+ p_ class => $_->{official} ? undef : 'unofficial', sub {
+ small_ '[unofficial] ' if !$_->{official};
+ a_ href => "/$_->{vid}", tattr $_;
+ } for $rel{$_}->@*;
+ }
}
}
- }}
+ }
}
}
+sub infobox_length_ {
+ my($v) = @_;
+
+ tr_ sub {
+ td_ 'Play time';
+ td_ sub {
+ # Cached number, which means this VN has counted votes
+ if($v->{c_lengthnum}) {
+ my $m = $v->{c_length};
+ txt_ +(grep $m >= $_->{low} && $m < $_->{high}, values %VN_LENGTH)[0]{txt}.' (';
+ vnlength_ $m;
+ txt_ ' from ';
+ a_ href => "/$v->{id}/lengthvotes", sprintf '%d vote%s', $v->{c_lengthnum}, $v->{c_length}==1?'':'s';
+ txt_ ')';
+ # No cached number so no counted votes; fall back to old 'length' field and display number of uncounted votes
+ } else {
+ my $uncounted = tuwf->dbVali('SELECT count(*) FROM vn_length_votes WHERE vid =', \$v->{id}, 'AND NOT private');
+ txt_ $VN_LENGTH{$v->{length}}{txt};
+ if ($v->{length} || $uncounted) {
+ lit_ ' (';
+ txt_ $VN_LENGTH{$v->{length}}{time} if $v->{length};
+ lit_ ', ' if $v->{length} && $uncounted;
+ a_ href => "/$v->{id}/lengthvotes", sprintf '%d uncounted vote%s', $uncounted, $uncounted == 1 ? '' : 's' if $uncounted;
+ lit_ ')';
+ }
+ }
+ if (VNWeb::VN::Length::can_vote()) {
+ my $my = tuwf->dbRowi('SELECT rid::text[] AS rid, length, speed, private, notes FROM vn_length_votes WHERE vid =', \$v->{id}, 'AND uid =', \auth->uid);
+ elm_ VNLengthVote => $VNWeb::VN::Length::LENGTHVOTE, {
+ uid => auth->uid, vid => $v->{id},
+ vote => $my->{rid}?$my:undef,
+ maycount => $v->{devstatus} != 1,
+ }, sub { span_ @_, ''};
+ }
+ };
+ };
+}
+
+
sub infobox_producers_ {
my($v) = @_;
my $p = tuwf->dbAlli('
- SELECT p.id, p.name, p.original, rl.lang, bool_or(rp.developer) as developer, bool_or(rp.publisher) as publisher
+ SELECT p.id, p.title, p.sorttitle, rl.lang, bool_or(rp.developer) as developer, bool_or(rp.publisher) as publisher, min(rv.rtype) as rtype, bool_or(r.official) as official
FROM releases_vn rv
JOIN releases r ON r.id = rv.id
- JOIN releases_lang rl ON rl.id = rv.id
+ JOIN releases_titles rl ON rl.id = rv.id
JOIN releases_producers rp ON rp.id = rv.id
- JOIN producers p ON p.id = rp.pid
- WHERE NOT r.hidden AND rv.vid =', \$v->{id}, '
- GROUP BY p.id, p.name, p.original, rl.lang
- ORDER BY MIN(r.released), p.name
+ JOIN', producerst, 'p ON p.id = rp.pid
+ WHERE NOT r.hidden AND (r.official OR NOT rl.mtl) AND rv.vid =', \$v->{id}, '
+ GROUP BY p.id, p.title, p.sorttitle, rl.lang
+ ORDER BY NOT bool_or(r.official), MIN(r.released), p.sorttitle
');
return if !@$p;
+ my $hasfull = grep $_->{rtype} eq 'complete', @$p;
my %dev;
- my @dev = grep $_->{developer} && !$dev{$_->{id}}++, @$p;
+ my @dev = grep $_->{developer} && (!$hasfull || $_->{rtype} ne 'trial') && !$dev{$_->{id}}++, @$p;
tr_ sub {
td_ 'Developer';
td_ sub {
- join_ ' & ', sub { a_ href => "/p$_->{id}", title => $_->{original}||$_->{name}, $_->{name}; }, @dev;
+ join_ ' & ', sub { a_ href => "/$_->{id}", tattr $_ }, @dev;
};
} if @dev;
my(%lang, @lang, $lang);
- for(grep $_->{publisher}, @$p) {
+ for(grep $_->{publisher} && (!$hasfull || $_->{rtype} ne 'trial'), @$p) {
push @lang, $_->{lang} if !$lang{$_->{lang}};
push $lang{$_->{lang}}->@*, $_;
}
+ return if !keys %lang;
+
+ use sort 'stable';
+ @lang = sort { ($b eq $v->{olang}) cmp ($a eq $v->{olang}) } @lang;
+
+ # Merge multiple languages into one group if the publishers are the same.
+ my @nlang = (shift @lang);
+ my $last = join ';', sort map $_->{id}, $lang{$nlang[0]}->@*;
+ for (@lang) {
+ my $cids = join ';', sort map $_->{id}, $lang{$_}->@*;
+ if($last eq $cids) {
+ $nlang[$#nlang] .= ";$_";
+ } else {
+ push @nlang, $_;
+ }
+ $last = $cids;
+ }
tr_ sub {
td_ 'Publishers';
td_ sub {
join_ \&br_, sub {
- abbr_ class => "icons lang $_", title => $LANGUAGE{$_}, '';
- join_ ' & ', sub { a_ href => "/p$_->{id}", title => $_->{original}||$_->{name}, $_->{name} }, $lang{$_}->@*;
- }, @lang;
+ my @l = split /;/;
+ abbr_ class => "icon-lang-$_", title => $LANGUAGE{$_}{txt}, '' for @l;
+ join_ ' & ', sub { a_ href => "/$_->{id}", $_->{official} ? () : (class => 'grayedout'), tattr $_ }, $lang{$l[0]}->@*;
+ }, @nlang;
}
- } if keys %lang;
+ };
}
@@ -183,25 +338,26 @@ sub infobox_affiliates_ {
# url => [$title, $url, $price, $type]
my %links;
for my $rel ($v->{releases}->@*) {
- my $type = $rel->{patch} ? 4 :
- $rel->{type} eq 'trial' ? 3 :
- $rel->{type} eq 'partial' ? 2 :
- $rel->{num_vns} > 1 ? 0 : 1;
+ my $type = $rel->{patch} ? 4 :
+ $rel->{rtype} eq 'trial' ? 3 :
+ $rel->{rtype} eq 'partial' ? 2 :
+ $rel->{num_vns} > 1 ? 0 : 1;
- $links{$_->[1]} = [ @$_, min $type, $links{$_->[1]}[3]||9 ] for grep $_->[2], $rel->{extlinks}->@*;
+ $links{$_->{url2}} = [ @{$_}{qw/label url2 price/}, min $type, $links{$_->{url2}}[3]||9 ] for grep $_->{price}, $rel->{extlinks}->@*;
}
return if !keys %links;
tr_ id => 'buynow', sub {
td_ 'Shops';
td_ sub {
+ small_ class => 'ad', 'sponsored links';
join_ \&br_, sub {
- b_ class => 'standout', '» ';
+ b_ '» ';
a_ href => $_->[1], sub {
txt_ $_->[2];
- b_ class => 'grayedout', ' @ ';
+ small_ ' @ ';
txt_ $_->[0];
- b_ class => 'grayedout', " ($type[$_->[3]])" if $_->[3] != 1;
+ small_ " ($type[$_->[3]])" if $_->[3] != 1;
};
}, sort { $a->[0] cmp $b->[0] || $a->[2] cmp $b->[2] } values %links;
}
@@ -216,13 +372,13 @@ sub infobox_anime_ {
td_ 'Related anime';
td_ class => 'anime', sub { join_ \&br_, sub {
if(!$_->{lastfetch} || !$_->{year} || !$_->{title_romaji}) {
- b_ sub {
+ span_ sub {
txt_ '[no information available at this time: ';
a_ href => 'https://anidb.net/anime/'.$_->{aid}, "a$_->{aid}";
txt_ ']';
};
} else {
- b_ sub {
+ span_ sub {
txt_ '[';
a_ href => "https://anidb.net/anime/$_->{aid}", title => 'AniDB', 'DB';
if($_->{ann_id}) {
@@ -232,7 +388,7 @@ sub infobox_anime_ {
txt_ '] ';
};
abbr_ title => $_->{title_kanji}||$_->{title_romaji}, shorten $_->{title_romaji}, 50;
- b_ ' ('.(defined $_->{type} ? $ANIME_TYPE{$_->{type}}{txt}.', ' : '').$_->{year}.')';
+ span_ ' ('.(defined $_->{type} ? $ANIME_TYPE{$_->{type}}{txt}.', ' : '').$_->{year}.')';
}
}, sort { ($a->{year}||9999) <=> ($b->{year}||9999) } $v->{anime}->@* }
}
@@ -241,113 +397,114 @@ sub infobox_anime_ {
sub infobox_tags_ {
my($v) = @_;
- my $rating = 'avg(CASE WHEN tv.ignore OR (u.id IS NOT NULL AND NOT u.perm_tag) THEN NULL ELSE tv.vote END)';
- my $tags = tuwf->dbAlli("
- SELECT t.id, t.name, t.cat, count(*) as cnt, $rating as rating
- , coalesce(avg(CASE WHEN tv.ignore OR (u.id IS NOT NULL AND NOT u.perm_tag) THEN NULL ELSE tv.spoiler END), t.defaultspoil) as spoiler
- FROM tags t
- JOIN tags_vn tv ON tv.tag = t.id
- LEFT JOIN users u ON u.id = tv.uid
- WHERE t.state = 1+1 AND tv.vid =", \$v->{id}, "
- GROUP BY t.id, t.name, t.cat
- HAVING $rating > 0
- ORDER BY rating DESC"
- );
- return if !@$tags;
-
div_ id => 'tagops', sub {
- debug_ $tags;
- for (keys %TAG_CATEGORY) {
- input_ id => "cat_$_", type => 'checkbox', class => 'visuallyhidden',
+ debug_ $v->{tags};
+ my @ero = grep($_->{cat} eq 'ero', $v->{tags}->@*) ? ('ero') : ();
+ for ('cont', @ero, 'tech') {
+ input_ id => "cat_$_", type => 'checkbox', class => 'hidden',
(auth ? auth->pref("tags_$_") : $_ ne 'ero') ? (checked => 'checked') : ();
label_ for => "cat_$_", lc $TAG_CATEGORY{$_};
}
my $spoiler = auth->pref('spoilers') || 0;
- input_ id => 'tag_spoil_none', type => 'radio', class => 'visuallyhidden', name => 'tag_spoiler', $spoiler == 0 ? (checked => 'checked') : ();
+ input_ id => 'tag_spoil_none', type => 'radio', class => 'hidden', name => 'tag_spoiler', $spoiler == 0 ? (checked => 'checked') : ();
label_ for => 'tag_spoil_none', class => 'sec', 'hide spoilers';
- input_ id => 'tag_spoil_some', type => 'radio', class => 'visuallyhidden', name => 'tag_spoiler', $spoiler == 1 ? (checked => 'checked') : ();
+ input_ id => 'tag_spoil_some', type => 'radio', class => 'hidden', name => 'tag_spoiler', $spoiler == 1 ? (checked => 'checked') : ();
label_ for => 'tag_spoil_some', 'show minor spoilers';
- input_ id => 'tag_spoil_all', type => 'radio', class => 'visuallyhidden', name => 'tag_spoiler', $spoiler == 2 ? (checked => 'checked') : ();
+ input_ id => 'tag_spoil_all', type => 'radio', class => 'hidden', name => 'tag_spoiler', $spoiler == 2 ? (checked => 'checked') : ();
label_ for => 'tag_spoil_all', 'spoil me!';
- input_ id => 'tag_toggle_summary', type => 'radio', class => 'visuallyhidden', name => 'tag_all', auth->pref('tags_all') ? () : (checked => 'checked');
+ input_ id => 'tag_toggle_summary', type => 'radio', class => 'hidden', name => 'tag_all', auth->pref('tags_all') ? () : (checked => 'checked');
label_ for => 'tag_toggle_summary', class => 'sec', 'summary';
- input_ id => 'tag_toggle_all', type => 'radio', class => 'visuallyhidden', name => 'tag_all', auth->pref('tags_all') ? (checked => 'checked') : ();
+ input_ id => 'tag_toggle_all', type => 'radio', class => 'hidden', name => 'tag_all', auth->pref('tags_all') ? (checked => 'checked') : ();
label_ for => 'tag_toggle_all', class => 'lst', 'all';
div_ id => 'vntags', sub {
my %counts = map +($_,[0,0,0]), keys %TAG_CATEGORY;
join_ ' ', sub {
- my $spoil = $_->{spoiler} > 1.3 ? 2 : $_->{spoiler} > 0.4 ? 1 : 0;
+ my $spoil = $_->{override}//$_->{spoiler};
my $cnt = $counts{$_->{cat}};
$cnt->[2]++;
$cnt->[1]++ if $spoil < 2;
$cnt->[0]++ if $spoil < 1;
- my $cut = $cnt->[0] > 15 ? ' cut cut2 cut1 cut0' : $cnt->[1] > 15 ? ' cut cut2 cut1' : $cnt->[2] > 15 ? ' cut cut2' : '';
+ my $cut = defined $_->{override} ? '' : $cnt->[0] > 15 ? ' cut cut2 cut1 cut0' : $cnt->[1] > 15 ? ' cut cut2 cut1' : $cnt->[2] > 15 ? ' cut cut2' : '';
span_ class => "tagspl$spoil cat_$_->{cat} $cut", sub {
- a_ href => "/g$_->{id}", style => sprintf('font-size: %dpx', $_->{rating}*3.5+6), $_->{name};
- spoil_ $spoil;
- b_ class => 'grayedout', sprintf ' %.1f', $_->{rating};
+ a_ href => "/$_->{id}",
+ mkclass(defined $_->{override} ? 'lieo' : 'lie', $_->{lie},
+ $_->{color} ? ($_->{color}, $_->{color} =~ /standout|grayedout/ ? 1 : 0) : ()),
+ style => sprintf('font-size: %dpx', $_->{rating}*3.5+6)
+ .(($_->{color}//'') =~ /^#/ ? "; color: $_->{color}" : ''),
+ $_->{name};
+ spoil_ $_->{spoiler};
+ small_ sprintf ' %.1f', $_->{rating};
}
- }, @$tags;
+ }, $v->{tags}->@*;
}
}
}
-sub infobox_useroptions_ {
- my($v) = @_;
- return if !auth;
-
- 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 =', \auth->uid, '
- 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 =', \auth->uid, 'AND vid =', \$v->{id});
- my $review = tuwf->dbVali('SELECT id FROM reviews WHERE uid =', \auth->uid, 'AND vid =', \$v->{id});
-
- tr_ class => 'nostripe', sub {
- td_ colspan => 2, sub {
- elm_ 'UList.VNPage', $VNWeb::ULists::Elm::VNPAGE, {
- uid => auth->uid,
- vid => $v->{id},
- onlist => $lst->{vid}||0,
- canvote => canvote($v),
- vote => fmtvote($lst->{vote}),
- notes => $lst->{notes}||'',
- review => $review,
- canreview=> $review || (canvote($v) && can_edit(w => {})),
- labels => $labels,
- selected => [ map $_->{id}, grep $_->{assigned}, @$labels ],
+# Also used by Chars::VNTab & Reviews::VNTab
+sub infobox_ {
+ my($v, $notags) = @_;
+
+ sub tlang_ {
+ my($t) = @_;
+ tr_ mkclass(title => 1, grayedout => !$t->{official}), sub {
+ td_ sub {
+ abbr_ class => "icon-lang-$t->{lang}", title => $LANGUAGE{$t->{lang}}{txt}, '';
};
+ td_ sub {
+ span_ tlang($t->{lang}, $t->{title}), $t->{title};
+ if($t->{latin}) {
+ br_;
+ txt_ $t->{latin};
+ }
+ }
}
}
-}
+ article_ sub {
+ itemmsg_ $v;
+ h1_ tlang($v->{title}[0], $v->{title}[1]), $v->{title}[1];
+ h2_ class => 'alttitle', tlang(@{$v->{title}}[2,3]), $v->{title}[3] if $v->{title}[3] && $v->{title}[3] ne $v->{title}[1];
+
+ div_ class => 'warning', sub {
+ h2_ 'No releases';
+ p_ sub {
+ txt_ 'This entry does not have any releases associated with it yet. Please ';
+ a_ href => "/$v->{id}/add", 'add a release entry';
+ txt_ ' if you have information about this visual novel.';
+ br_;
+ txt_ '(A release entry should be present even if nothing has been
+ released yet, in that case it can just be a placeholder for a
+ future release)';
+ };
+ } if !$v->{hidden} && auth->permEdit && !$v->{releases}->@*;
-# Also used by Chars::VNTab & Reviews::VNTab
-sub infobox_ {
- my($v) = @_;
- div_ class => 'mainbox', sub {
- itemmsg_ v => $v;
- h1_ $v->{title};
- h2_ class => 'alttitle', lang_attr($v->{c_olang}), $v->{original} if $v->{original};
+ p_ class => 'center standout', sub { lit_ config->{special_games}{$v->{id}}; br_; br_ } if config->{special_games}{$v->{id}};
div_ class => 'vndetails', sub {
- div_ class => 'vnimg', sub { image_ $v->{image}, alt => $v->{title}; };
+ div_ class => 'vnimg', sub { image_ $v->{image}, alt => $v->{title}[1]; };
table_ class => 'stripe', sub {
tr_ sub {
- td_ class => 'key', 'Title';
- td_ sub { txt_ $v->{title}; debug_ $v; };
- };
-
+ td_ 'Title';
+ td_ sub {
+ table_ sub { tlang_ $v->{titles}[0] };
+ };
+ } if $v->{titles}->@* == 1;
tr_ sub {
- td_ 'Original title';
- td_ lang_attr($v->{c_olang}), $v->{original};
- } if $v->{original};
+ td_ class => 'titles', colspan => 2, sub {
+ details_ sub {
+ summary_ sub {
+ div_ 'Titles';
+ table_ sub { tlang_ grep $_->{lang} eq $v->{olang}, $v->{titles}->@* };
+ };
+ table_ sub {
+ tlang_ $_ for grep $_->{lang} ne $v->{olang}, sort { $b->{official} cmp $a->{official} || $a->{lang} cmp $b->{lang} } $v->{titles}->@*;
+ };
+ };
+ };
+ } if $v->{titles}->@* > 1;
tr_ sub {
td_ 'Aliases';
@@ -355,64 +512,76 @@ sub infobox_ {
} if $v->{alias};
tr_ sub {
- td_ 'Length';
- td_ "$VN_LENGTH{$v->{length}}{txt} ($VN_LENGTH{$v->{length}}{time})";
- } if $v->{length};
-
+ td_ 'Status';
+ td_ sub {
+ txt_ 'In development' if $v->{devstatus} == 1;
+ txt_ 'Unfinished, no ongoing development' if $v->{devstatus} == 2;
+ };
+ } if $v->{devstatus};
+
+ infobox_length_ $v;
infobox_producers_ $v;
infobox_relations_ $v;
tr_ sub {
td_ 'Links';
- td_ sub { join_ ', ', sub { a_ href => $_->[1], $_->[0] }, $v->{extlinks}->@* };
+ td_ sub { join_ ', ', sub { a_ href => $_->{url2}, $_->{label} }, $v->{extlinks}->@* };
} if $v->{extlinks}->@*;
infobox_affiliates_ $v;
infobox_anime_ $v;
- infobox_useroptions_ $v;
+
+ tr_ class => 'nostripe', sub {
+ td_ colspan => 2, sub {
+ elm_ 'UList.VNPage', $VNWeb::ULists::Elm::WIDGET,
+ ulists_widget_full_data $v, auth->uid, 1, canvote $v;
+ }
+ } if auth;
tr_ class => 'nostripe', sub {
td_ class => 'vndesc', colspan => 2, sub {
h2_ 'Description';
- p_ sub { lit_ $v->{desc} ? bb_format $v->{desc} : '-' };
+ p_ sub { lit_ $v->{description} ? bb_format $v->{description} : '-' };
+ debug_ $v;
}
}
}
};
div_ class => 'clearfloat', style => 'height: 5px', ''; # otherwise the tabs below aren't positioned correctly
- infobox_tags_ $v;
+ infobox_tags_ $v if $v->{tags}->@* && !$notags;
}
}
-# Also used by Chars::VNTab & Reviews::VNTab
+# Also used by Chars::VNTab, Reviews::VNTab and VN::Quotes
sub tabs_ {
my($v, $tab) = @_;
my $chars = tuwf->dbVali('SELECT COUNT(DISTINCT c.id) FROM chars c JOIN chars_vns cv ON cv.id = c.id WHERE NOT c.hidden AND cv.vid =', \$v->{id});
- my $reviews = tuwf->dbRowi('SELECT COUNT(*) FILTER(WHERE isfull) AS full, COUNT(*) FILTER(WHERE NOT isfull) AS mini FROM reviews WHERE NOT c_flagged AND vid =', \$v->{id});
+ my $quotes = tuwf->dbVali('SELECT COUNT(*) FROM quotes WHERE NOT hidden AND vid =', \$v->{id});
- return if !$chars && !$reviews->{full} && !$reviews->{mini} && !auth->permEdit && !auth->permReview;
$tab ||= '';
- div_ class => 'maintabs', sub {
- ul_ sub {
- li_ class => ($tab eq '' ? ' tabselected' : ''), sub { a_ href => "/v$v->{id}#main", name => 'main', 'main' } if $chars || $reviews;
- li_ class => ($tab eq 'chars' ? ' tabselected' : ''), sub { a_ href => "/v$v->{id}/chars#chars", name => 'chars', "characters ($chars)" } if $chars;
- if($reviews->{mini} > 4 || $tab eq 'minireviews' || $tab eq 'fullreviews') {
- li_ class => ($tab eq 'minireviews'?' tabselected' : ''), sub { a_ href => "/v$v->{id}/minireviews#review", name => 'review', "mini reviews ($reviews->{mini})" } if $reviews->{mini};
- li_ class => ($tab eq 'fullreviews'?' tabselected' : ''), sub { a_ href => "/v$v->{id}/fullreviews#review", name => 'review', "full reviews ($reviews->{full})" } if $reviews->{full};
- } elsif($reviews->{mini} || $reviews->{full}) {
- li_ class => ($tab =~ /reviews/ ?' tabselected':''), sub { a_ href => "/v$v->{id}/reviews#review", name => 'review', sprintf 'reviews (%d)', $reviews->{mini}+$reviews->{full} };
+ nav_ sub {
+ menu_ sub {
+ li_ class => ($tab eq '' ? ' tabselected' : ''), sub { a_ href => "/$v->{id}#main", name => 'main', 'main' };
+ li_ class => ($tab eq 'tags' ? ' tabselected' : ''), sub { a_ href => "/$v->{id}/tags#tags", name => 'tags', 'tags' };
+ li_ class => ($tab eq 'chars' ? ' tabselected' : ''), sub { a_ href => "/$v->{id}/chars#chars", name => 'chars', "characters ($chars)" } if $chars;
+ if($v->{reviews}{mini} > 4 || $tab eq 'minireviews' || $tab eq 'fullreviews') {
+ li_ class => ($tab eq 'minireviews'?' tabselected' : ''), sub { a_ href => "/$v->{id}/minireviews#review", name => 'review', "mini reviews ($v->{reviews}{mini})" } if $v->{reviews}{mini};
+ li_ class => ($tab eq 'fullreviews'?' tabselected' : ''), sub { a_ href => "/$v->{id}/fullreviews#review", name => 'review', "full reviews ($v->{reviews}{full})" } if $v->{reviews}{full};
+ } elsif($v->{reviews}{mini} || $v->{reviews}{full}) {
+ li_ class => ($tab =~ /reviews/ ?' tabselected':''), sub { a_ href => "/$v->{id}/reviews#review", name => 'review', sprintf 'reviews (%d)', $v->{reviews}{total} };
}
+ li_ class => ($tab eq 'quotes' ? ' tabselected' : ''), sub { a_ href => "/$v->{id}/quotes#quotes", name => 'quotes', "quotes ($quotes)" };
};
- ul_ sub {
+ menu_ sub {
if(auth && canvote $v) {
my $id = tuwf->dbVali('SELECT id FROM reviews WHERE vid =', \$v->{id}, 'AND uid =', \auth->uid);
- li_ sub { a_ href => "/v$v->{id}/addreview", 'add review' } if !$id && can_edit w => {};
+ li_ sub { a_ href => "/$v->{id}/addreview", 'add review' } if !$id && can_edit w => {};
li_ sub { a_ href => "/$id/edit", 'edit review' } if $id;
}
if(auth->permEdit) {
- li_ sub { a_ href => "/v$v->{id}/add", 'add release' };
- li_ sub { a_ href => "/v$v->{id}/addchar", 'add character' };
+ li_ sub { a_ href => "/$v->{id}/add", 'add release' };
+ li_ sub { a_ href => "/$v->{id}/addchar", 'add character' };
}
};
}
@@ -422,37 +591,50 @@ sub tabs_ {
sub releases_ {
my($v) = @_;
- # TODO: Organize a long list of releases a bit better somehow? Collapsable language sections?
-
enrich_release $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}->@*;
+ $v->{releases} = sort_releases $v->{releases};
+
+ my(%lang, %langrel, %langmtl);
+ for my $r ($v->{releases}->@*) {
+ for ($r->{titles}->@*) {
+ push $lang{$_->{lang}}->@*, $r;
+ $langmtl{$_->{lang}} = ($langmtl{$_->{lang}}//1) && $_->{mtl};
+ }
+ }
+ $langrel{$_} = min map $_->{released}, $lang{$_}->@* for keys %lang;
+ my @lang = sort { $langrel{$a} <=> $langrel{$b} || ($b eq $v->{olang}) cmp ($a eq $v->{olang}) || $a cmp $b } keys %lang;
+ my $pref = prefs;
my sub lang_ {
my($lang) = @_;
- tr_ class => 'lang', sub {
- td_ colspan => 7, sub {
- abbr_ class => "icons lang $lang", title => $LANGUAGE{$lang}, '';
- txt_ $LANGUAGE{$lang};
- }
+ my $ropt = { id => $lang, lang => $lang };
+ my $mtl = $langmtl{$lang};
+ my $open = ($pref->{vnrel_olang} && $lang eq $v->{olang} && !$mtl) || ($pref->{vnrel_langs}{$lang} && (!$mtl || $pref->{vnrel_mtl}));
+ details_ open => $open?'open':undef, sub {
+ summary_ $mtl ? (class => 'mtl') : (), sub {
+ abbr_ class => "icon-lang-$lang".($mtl?' mtl':''), title => $LANGUAGE{$lang}{txt}, '';
+ txt_ $LANGUAGE{$lang}{txt};
+ small_ sprintf ' (%d)', scalar $lang{$lang}->@*;
+ };
+ table_ class => 'releases', sub {
+ release_row_ $_, $ropt for $lang{$lang}->@*;
+ };
};
- release_row_ $_, $lang for grep grep($_ eq $lang, $_->{lang}->@*), $v->{releases}->@*;
}
- div_ class => 'mainbox', sub {
+ article_ class => 'vnreleases', sub {
h1_ 'Releases';
if(!$v->{releases}->@*) {
p_ 'We don\'t have any information about releases of this visual novel yet...';
} else {
- table_ class => 'releases', sub { lang_ $_ for @lang };
+ lang_ $_ for @lang;
}
}
}
-sub staff_ {
- my($v) = @_;
+sub staff_cols_ {
+ my($lst) = @_;
# 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
@@ -464,7 +646,7 @@ sub staff_ {
# 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 grep $_->{sid}, $v->{staff}->@*;
+ push $roles{$_->{role}}->@*, $_ for grep $_->{sid}, @$lst;
my $i=0;
my @boxes =
sort { $b->[0] <=> $a->[0] || $a->[1] <=> $b->[1] }
@@ -472,9 +654,9 @@ sub staff_ {
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{$_}->@*;
+ a_ href => "/$_->{sid}", tattr $_;
+ small_ $_->{note} if $_->{note};
+ } for sort { $a->{title}[1] cmp $b->{title}[1] } $roles{$_}->@*;
}
], grep $roles{$_}, keys %CREDIT_TYPE;
@@ -495,14 +677,45 @@ sub staff_ {
@$c = sort { $a->[1] <=> $b->[1] } @$c;
}
- div_ class => 'mainbox', 'data-mainbox-summarize' => 200, sub {
+ div_ class => sprintf('vnstaff-%d', scalar @$_), sub {
+ ul_ sub {
+ lit_ $_->[2] for $_->[2]->@*;
+ } for @$_
+ } for @cols;
+}
+
+
+sub staff_ {
+ my($v) = @_;
+ return if !$v->{staff}->@*;
+
+ my %staff;
+ push $staff{ $_->{eid} // '' }->@*, $_ for $v->{staff}->@*;
+ my $pref = prefs;
+
+ article_ class => 'vnstaff', id => 'staff', sub {
h1_ 'Staff';
- div_ class => sprintf('vnstaff vnstaff-%d', scalar @$_), sub {
- ul_ sub {
- lit_ $_->[2] for $_->[2]->@*;
- } for @$_
- } for @cols;
- } if $v->{staff}->@*;
+ if (!$v->{editions}->@*) {
+ staff_cols_ $v->{staff};
+ return;
+ }
+ for my $e (undef, $v->{editions}->@*) {
+ my $lst = $staff{ $e ? $e->{eid} : '' };
+ next if !$lst;
+ my $lang = ($e && $e->{lang}) || $v->{olang};
+ my $unoff = $e && !$e->{official};
+ my $open = ($pref->{staffed_olang} && !$e) || ($pref->{staffed_langs}{$lang} && (!$unoff || $pref->{staffed_unoff}));
+ details_ open => $open?'open':undef, sub {
+ summary_ sub {
+ abbr_ class => "icon-lang-$e->{lang}", title => $LANGUAGE{$e->{lang}}{txt}, '' if $e && $e->{lang};
+ txt_ 'Original edition' if !$e;
+ txt_ $e->{name} if $e;
+ small_ ' (unofficial)' if $unoff;
+ };
+ staff_cols_ $lst;
+ };
+ }
+ };
}
@@ -511,39 +724,41 @@ sub charsum_ {
my $spoil = viewget->{spoilers};
my $c = tuwf->dbAlli('
- SELECT c.id, c.name, c.original, c.gender, v.role
- FROM chars c
+ SELECT c.id, c.title, c.gender, v.role
+ FROM', charst, '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
+ SELECT vs.cid, sa.id, sa.title, vs.note
FROM vn_seiyuu vs
- JOIN staff_alias sa ON sa.aid = vs.aid
+ JOIN', staff_aliast, 'sa ON sa.aid = vs.aid
WHERE vs.id =', \$v->{id}, 'AND vs.cid IN', $_, '
- ORDER BY sa.name'
+ ORDER BY sa.sorttitle'
) }, $c;
- div_ class => 'mainbox', 'data-mainbox-summarize' => 200, sub {
+ article_ 'data-mainbox-summarize' => 210, sub {
p_ class => 'mainopts', sub {
- a_ href => "/v$v->{id}/chars#chars", 'Full character list';
+ a_ href => "/$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};
+ span_ sub {
+ abbr_ class => "icon-gen-$_->{gender}", title => $GENDER{$_->{gender}}, '' if $_->{gender} ne 'unknown';
+ a_ href => "/$_->{id}", tattr $_;
+ };
+ em_ $CHAR_ROLE{$_->{role}}{txt};
};
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};
+ a_ href => "/$_->{id}", tattr $_;
+ small_ $_->{note} if $_->{note};
}, $_->{seiyuu}->@*;
} if $_->{seiyuu}->@*;
} for @$c;
@@ -568,22 +783,27 @@ sub 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
+ SELECT uv.vote, uv.c_private,', sql_totime('uv.vote_date'), 'as date, ', sql_user(), '
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
+ LIMIT', \($v->{reviews}{total} ? 7 : 8)
);
- my $rank = $v->{c_votecount} && tuwf->dbRowi('SELECT c_rating, c_popularity, c_pop_rank, c_rat_rank FROM vn v WHERE id =', \$v->{id});
+ my $rank = $v->{c_votecount} && tuwf->dbRowi('SELECT c_average, c_rating, c_pop_rank, c_rat_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) } };
+ tfoot_ sub { tr_ sub { td_ colspan => 2, sub {
+ txt_ sprintf '%d vote%s%s', $num, $num == 1 ? '' : 's', $rank && $rank->{c_pop_rank} ? sprintf ' (rank %d)', $rank->{c_pop_rank} : '';
+ br_;
+ txt_ sprintf '%.02f average (%s%s)', $sum/$num/10,
+ $rank && $rank->{c_rating} && $rank->{c_rating} != $rank->{c_average} ? sprintf '%.02f weighted, ', $rank->{c_rating}/100 : '',
+ $rank && $rank->{c_rat_rank} ? sprintf('rank %d', $rank->{c_rat_rank}) : 'unranked';
+ } } };
tr_ sub {
my $num = $_;
my $votes = [grep $num == $_->{idx}, @$stats]->[0]{votes} || 0;
@@ -598,31 +818,28 @@ sub stats_ {
table_ class => 'recentvotes stripe', sub {
thead_ sub { tr_ sub { td_ colspan => 3, sub {
txt_ 'Recent votes';
- b_ sub {
+ span_ sub {
txt_ '(';
- a_ href => "/v$v->{id}/votes", 'show all';
+ a_ href => "/$v->{id}/votes", 'show all';
txt_ ')';
}
} } };
+ tfoot_ sub { tr_ sub { td_ colspan => 3, sub {
+ a_ href => "/$v->{id}/reviews#review", sprintf'%d review%s »', $v->{reviews}{total}, $v->{reviews}{total}==1?'':'s';
+ } } } if $v->{reviews}{total};
tr_ sub {
td_ sub {
- b_ class => 'grayedout', 'hidden' if $_->{hide_list};
- user_ $_ if !$_->{hide_list};
+ small_ 'hidden' if $_->{c_private};
+ user_ $_ if !$_->{c_private};
};
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->{c_pop_rank}, $rank->{c_popularity}*100 if defined $rank->{c_popularity};
- p_ sprintf 'Bayesian rating: ranked #%d with a rating of %.2f', $rank->{c_rat_rank}, $rank->{c_rating}/10;
- } if $v->{c_votecount};
}
- div_ class => 'mainbox', sub {
+ article_ id => 'stats', sub {
h1_ 'User stats';
if(!@$stats) {
p_ 'Nobody has voted on this visual novel yet...';
@@ -651,34 +868,34 @@ sub screenshots_ {
my %rel;
push $rel{$_->{rid}}->@*, $_ for grep $_->{rid}, @$s;
- input_ name => 'scrhide_s', id => "scrhide_s$_", type => 'radio', class => 'visuallyhidden', $sexs == $_ ? (checked => 'checked') : () for 0..2;
- input_ name => 'scrhide_v', id => "scrhide_v$_", type => 'radio', class => 'visuallyhidden', $vios == $_ ? (checked => 'checked') : () for 0..2;
- div_ class => 'mainbox', id => 'screenshots', sub {
+ input_ name => 'scrhide_s', id => "scrhide_s$_", type => 'radio', class => 'hidden', $sexs == $_ ? (checked => 'checked') : () for 0..2;
+ input_ name => 'scrhide_v', id => "scrhide_v$_", type => 'radio', class => 'hidden', $vios == $_ ? (checked => 'checked') : () for 0..2;
+ article_ id => 'screenshots', sub {
p_ class => 'mainopts', sub {
- if($sex[1] || $sex[2]) {
+ if($sexp < 0 || $sex[1] || $sex[2]) {
label_ for => 'scrhide_s0', class => 'fake_link', "Safe ($sex[0])";
label_ for => 'scrhide_s1', class => 'fake_link', "Suggestive ($sex[1])" if $sex[1];
label_ for => 'scrhide_s2', class => 'fake_link', "Explicit ($sex[2])" if $sex[2];
}
- b_ class => 'grayedout', ' | ' if ($sex[1] || $sex[2]) && ($vio[1] || $vio[2]);
+ small_ ' | ' if ($sexp < 0 || $sex[1] || $sex[2]) && ($vio[1] || $vio[2]);
if($vio[1] || $vio[2]) {
label_ for => 'scrhide_v0', class => 'fake_link', "Tame ($vio[0])";
label_ for => 'scrhide_v1', class => 'fake_link', "Violent ($vio[1])" if $vio[1];
label_ for => 'scrhide_v2', class => 'fake_link', "Brutal ($vio[2])" if $vio[2];
}
- } if $sex[1] || $sex[2] || $vio[1] || $vio[2];
+ } if $sexp < 0 || $sex[1] || $sex[2] || $vio[1] || $vio[2];
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};
+ abbr_ class => "icon-lang-$_->{lang}", title => $LANGUAGE{$_->{lang}}{txt}, '' for $r->{titles}->@*;
+ platform_ $_ for $r->{platforms}->@*;
+ a_ href => "/$r->{id}", tattr $r;
};
div_ class => 'scr', sub {
- a_ href => tuwf->imgurl($_->{scr}{id}),
+ a_ href => imgurl($_->{scr}{id}),
'data-iv' => "$_->{scr}{width}x$_->{scr}{height}:scr:$_->{scr}{sexual}$_->{scr}{violence}$_->{scr}{votecount}",
mkclass(
scrlnk => 1,
@@ -689,8 +906,8 @@ sub screenshots_ {
nsfw => $_->{scr}{sexual} || $_->{scr}{violence},
),
sub {
- my($w, $h) = imgsize $_->{scr}{width}, $_->{scr}{height}, tuwf->{scr_size}->@*;
- img_ src => tuwf->imgurl($_->{scr}{id}, 1), width => $w, height => $h, alt => "Screenshot $_->{scr}{id}";
+ my($w, $h) = imgsize $_->{scr}{width}, $_->{scr}{height}, config->{scr_size}->@*;
+ img_ src => imgurl($_->{scr}{id}, 't'), width => $w, height => $h, alt => "Screenshot $_->{scr}{id}";
} for $rel{$r->{id}}->@*;
}
}
@@ -698,13 +915,97 @@ sub screenshots_ {
}
+sub tags_ {
+ my($v) = @_;
+ if(!$v->{tags}->@*) {
+ article_ sub {
+ h1_ 'Tags';
+ p_ 'This VN has no tags assigned to it (yet).';
+ };
+ return;
+ }
+
+ my %tags = map +($_->{id},$_), $v->{tags}->@*;
+ my $parents = tuwf->dbAlli("
+ WITH RECURSIVE parents (tag, child) AS (
+ SELECT tag::vndbid, NULL::vndbid FROM (VALUES", sql_join(',', map sql('(',\$_,')'), keys %tags), ") AS x(tag)
+ UNION
+ SELECT tp.parent, tp.id FROM tags_parents tp, parents a WHERE a.tag = tp.id AND tp.main
+ ) SELECT * FROM parents WHERE child IS NOT NULL"
+ );
+
+ for(@$parents) {
+ $tags{$_->{tag}} ||= { id => $_->{tag} };
+ push $tags{$_->{tag}}{childs}->@*, $_->{child};
+ $tags{$_->{child}}{notroot} = 1;
+ }
+ enrich_merge id => 'SELECT id, name, cat FROM tags WHERE id IN', grep !$_->{name}, values %tags;
+ my @roots = sort { $a->{name} cmp $b->{name} } grep !$_->{notroot}, values %tags;
+
+ # Calculate rating and spoiler for parent tags.
+ my sub scores {
+ my($t) = @_;
+ return if !$t->{childs};
+ __SUB__->($tags{$_}) for $t->{childs}->@*;
+ $t->{inherited} = 1 if !defined $t->{rating};
+ $t->{spoiler} //= min map $tags{$_}{spoiler}, $t->{childs}->@*;
+ $t->{override} //= min map $tags{$_}{override}//$tags{$_}{spoiler}, $t->{childs}->@* if grep defined($tags{$_}{override}), $t->{childs}->@*;
+ $t->{rating} //= sum(map $tags{$_}{rating}, $t->{childs}->@*) / $t->{childs}->@*;
+ }
+ scores $_ for @roots;
+
+ my $view = viewget;
+ my sub rec {
+ my($lvl, $t) = @_;
+ return if ($t->{override}//$t->{spoiler}) > $view->{spoilers};
+ li_ class => "tagvnlist-top", sub {
+ h3_ sub { a_ href => "/$t->{id}", $t->{name} }
+ } if !$lvl;
+
+ li_ $lvl == 1 ? (class => 'tagvnlist-parent') : $t->{inherited} ? (class => 'tagvnlist-inherited') : (), sub {
+ VNWeb::TT::Lib::tagscore_($t->{rating}, $t->{inherited});
+ small_ '━━'x($lvl-1).' ' if $lvl > 1;
+ a_ href => "/$t->{id}", mkclass(
+ $t->{color} ? ($t->{color}, $t->{color} =~ /standout|grayedout/ ? 1 : 0) : (),
+ lie => $t->{lie} && ($view->{spoilers} > 1 || defined $t->{override}),
+ parent => !$t->{rating}
+ ), ($t->{color}//'') =~ /^#/ ? (style => "color: $t->{color}") : (),
+ $t->{name};
+ spoil_ $t->{spoiler};
+ a_ href => "/g/links?v=$v->{id}&t=$t->{id}", class => 'grayedout', " ($t->{count})" if $t->{count};
+ } if $lvl;
+
+ if($t->{childs}) {
+ __SUB__->($lvl+1, $_) for sort { $a->{name} cmp $b->{name} } map $tags{$_}, $t->{childs}->@*;
+ }
+ }
+
+ article_ sub {
+ my $max_spoil = max map $_->{lie}?2:$_->{spoiler}, values %tags;
+ p_ class => 'mainopts', sub {
+ if($max_spoil) {
+ a_ mkclass(checked => $view->{spoilers} == 0), href => '?view='.viewset(spoilers=>0).'#tags', 'Hide spoilers';
+ a_ mkclass(checked => $view->{spoilers} == 1), href => '?view='.viewset(spoilers=>1).'#tags', 'Show minor spoilers';
+ a_ mkclass(standout =>$view->{spoilers} == 2), href => '?view='.viewset(spoilers=>2).'#tags', 'Spoil me!' if $max_spoil == 2;
+ }
+ } if $max_spoil;
+
+ h1_ 'Tags';
+ ul_ class => 'vntaglist', sub {
+ rec 0, $_ for @roots;
+ };
+ debug_ \%tags;
+ };
+}
+
+
TUWF::get qr{/$RE{vrev}}, sub {
- my $v = db_entry v => tuwf->capture('id'), tuwf->capture('rev');
+ my $v = db_entry tuwf->captures('id', 'rev');
return tuwf->resNotFound if !$v;
- enrich_item $v;
+ enrich_item $v, 1;
- framework_ title => $v->{title}, index => !tuwf->capture('rev'), type => 'v', dbobj => $v, hiddenmsg => 1, js => 1, og => og($v),
+ framework_ title => $v->{title}[1], index => !tuwf->capture('rev'), dbobj => $v, hiddenmsg => 1, js => 1, og => og($v),
sub {
rev_ $v if tuwf->capture('rev');
infobox_ $v;
@@ -717,4 +1018,19 @@ TUWF::get qr{/$RE{vrev}}, sub {
};
};
+
+TUWF::get qr{/$RE{vid}/tags}, sub {
+ my $v = db_entry tuwf->capture('id');
+ return tuwf->resNotFound if !$v;
+
+ enrich_vn $v;
+
+ framework_ title => $v->{title}[1], index => 1, dbobj => $v, hiddenmsg => 1,
+ sub {
+ infobox_ $v, 1;
+ tabs_ $v, 'tags';
+ tags_ $v;
+ };
+};
+
1;