summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/VNDB/ExtLinks.pm13
-rw-r--r--lib/VNDB/Handler/Chars.pm168
-rw-r--r--lib/VNDB/Handler/VNPage.pm26
-rw-r--r--lib/VNWeb/Chars/Page.pm12
-rw-r--r--lib/VNWeb/VN/Page.pm409
5 files changed, 429 insertions, 199 deletions
diff --git a/lib/VNDB/ExtLinks.pm b/lib/VNDB/ExtLinks.pm
index 722bf10b..2d057847 100644
--- a/lib/VNDB/ExtLinks.pm
+++ b/lib/VNDB/ExtLinks.pm
@@ -6,7 +6,7 @@ use VNDB::Config;
use VNDB::Schema;
use Exporter 'import';
-our @EXPORT = ('enrich_extlinks', 'revision_extlinks', 'validate_extlinks');
+our @EXPORT = ('sql_extlinks', 'enrich_extlinks', 'revision_extlinks', 'validate_extlinks');
# column name in wikidata table => \%info
@@ -162,6 +162,15 @@ our %LINKS = (
);
+# Return a list of columns to fetch all external links for a database entry.
+sub sql_extlinks {
+ my($type, $prefix) = @_;
+ $prefix ||= '';
+ my $l = $LINKS{$type} || die "DB entry type $type has no links";
+ VNWeb::DB::sql_comma(map $prefix.$_, sort keys %$l)
+}
+
+
# Fetch a list of links to display at the given database entries, adds the
# following field to each object:
#
@@ -213,7 +222,7 @@ sub enrich_extlinks {
}
my sub l {
my($f, $price) = @_;
- my($v, $fmt, $fmt2, $label) = ($obj->{$f}, @{$l->{$f}}{'fmt', 'fmt2', 'label'});
+ my($v, $fmt, $fmt2, $label) = ($obj->{$f}, $l->{$f} ? @{$l->{$f}}{'fmt', 'fmt2', 'label'} : ());
push @links, map [ $label, sprintf(ref $fmt2 ? $fmt2->($obj) : $fmt2 || $fmt, $_), $price ], ref $v ? @$v : $v ? $v : ()
}
diff --git a/lib/VNDB/Handler/Chars.pm b/lib/VNDB/Handler/Chars.pm
index d1544981..95430108 100644
--- a/lib/VNDB/Handler/Chars.pm
+++ b/lib/VNDB/Handler/Chars.pm
@@ -9,7 +9,7 @@ use VNDB::Func;
use VNDB::Types;
use List::Util 'min';
-our @EXPORT = ('charOps', 'charTable', 'charBrowseTable');
+our @EXPORT = ('charOps', 'charBrowseTable');
TUWF::register(
qr{c(?:([1-9]\d*)(?:\.([1-9]\d*))?/(edit|copy)|/new)}
@@ -43,172 +43,6 @@ sub charOps {
}
-# Also used from Handler::VNPage
-sub charTable {
- my($self, $r, $link, $sep, $vn, $spoil) = @_;
- $spoil ||= 0;
-
- div class => 'chardetails '.charspoil($spoil).($sep ? ' charsep' : '');
-
- # image
- div class => 'charimg';
- if(!$r->{image}) {
- p 'No image uploaded yet';
- } else {
- img src => imgurl(ch => $r->{image}), alt => $r->{name};
- }
- end 'div';
-
- # info table
- table class => 'stripe';
- thead;
- Tr;
- td colspan => 2;
- if($link) {
- a href => "/c$r->{id}", style => 'margin-right: 10px; font-weight: bold', $r->{name};
- } else {
- b style => 'margin-right: 10px', $r->{name};
- }
- b class => 'grayedout', style => 'margin-right: 10px', $r->{original} if $r->{original};
- cssicon "gen $r->{gender}", $GENDER{$r->{gender}} if $r->{gender} ne 'unknown';
- span $BLOOD_TYPE{$r->{bloodt}} if $r->{bloodt} ne 'unknown';
- end;
- end;
- end;
-
- if($r->{alias}) {
- $r->{alias} =~ s/\n/, /g;
- Tr;
- td class => 'key', 'Aliases';
- td $r->{alias};
- end;
- }
- if(defined($r->{weight}) || $r->{height} || $r->{s_bust} || $r->{s_waist} || $r->{s_hip} || $r->{cup_size}) {
- Tr;
- td class => 'key', 'Measurements';
- td join ', ',
- $r->{height} ? "Height: $r->{height}cm" : (),
- defined($r->{weight}) ? "Weight: $r->{weight}kg" : (),
- $r->{s_bust} || $r->{s_waist} || $r->{s_hip} ?
- sprintf 'Bust-Waist-Hips: %s-%s-%scm', $r->{s_bust}||'??', $r->{s_waist}||'??', $r->{s_hip}||'??' : (),
- $r->{cup_size} ? "$CUP_SIZE{$r->{cup_size}} cup" : ();
- end;
- }
- if($r->{b_month} && $r->{b_day}) {
- Tr;
- td class => 'key', 'Birthday';
- td $r->{b_day}.' '.[qw{January February March April May June July August September October November December}]->[$r->{b_month}-1];
- end;
- }
- if(defined $r->{age}) {
- Tr;
- td class => 'key', 'Age';
- td $r->{age};
- end;
- }
-
- # traits
- my %groups;
- my @groups;
- for (@{$r->{traits}}) {
- my $g = $_->{group}||$_->{tid};
- push @groups, $g if !$groups{$g};
- push @{$groups{ $g }}, $_
- }
- for my $g (@groups) {
- Tr class => 'traitrow';
- td class => 'key'; a href => '/i'.($groups{$g}[0]{group}||$groups{$g}[0]{tid}), $groups{$g}[0]{groupname} || $groups{$g}[0]{name}; end;
- td;
- for (0..$#{$groups{$g}}) {
- my $t = $groups{$g}[$_];
- span class => charspoil($t->{spoil}).($t->{sexual} ? ' sexual' : '');
- txt ', ';
- a href => "/i$t->{tid}", $t->{name};
- end;
- }
- end;
- end;
- }
-
- # vns
- if(@{$r->{vns}} && (!$vn || $vn && (@{$r->{vns}} > 1 || $r->{vns}[0]{rid}))) {
- my %vns;
- push @{$vns{$_->{vid}}}, $_ for(sort { !defined($a->{rid})?1:!defined($b->{rid})?-1:$a->{rtitle} cmp $b->{rtitle} } @{$r->{vns}});
- Tr;
- td class => 'key', $vn ? 'Releases' : 'Visual novels';
- td;
- my $first = 0;
- for my $g (sort { $vns{$a}[0]{vntitle} cmp $vns{$b}[0]{vntitle} } keys %vns) {
- my @r = @{$vns{$g}};
- # special case: all releases, no exceptions
- if(!$vn && @r == 1 && !$r[0]{rid}) {
- span class => charspoil $r[0]{spoil};
- txt $CHAR_ROLE{$r[0]{role}}{txt}.' - ';
- a href => "/v$r[0]{vid}/chars", $r[0]{vntitle};
- br;
- end;
- next;
- }
- # otherwise, print VN title and list releases separately
- my $minspoil = 5;
- $minspoil = $minspoil > $_->{spoil} ? $_->{spoil} : $minspoil for (@r);
- span class => charspoil $minspoil;
- a href => "/v$r[0]{vid}/chars", $r[0]{vntitle} if !$vn;
- for(@r) {
- span class => charspoil $_->{spoil};
- br if !$vn || $_ != $r[0];
- b class => 'grayedout', '> ';
- txt $CHAR_ROLE{$_->{role}}{txt}.' - ';
- if($_->{rid}) {
- b class => 'grayedout', "r$_->{rid}:";
- a href => "/r$_->{rid}", $_->{rtitle};
- } else {
- txt 'All other releases';
- }
- end;
- }
- br;
- end;
- }
- end;
- end;
- }
-
- if(@{$r->{seiyuu}}) {
- Tr;
- td class => 'key', 'Voiced by';
- td;
- my $last_name = '';
- for my $s (sort { $a->{name} cmp $b->{name} } @{$r->{seiyuu}}) {
- next if $s->{name} eq $last_name;
- a href => "/s$s->{sid}", title => $s->{original}||$s->{name}, $s->{name};
- txt ' ('.$s->{note}.')' if $s->{note};
- br;
- $last_name = $s->{name};
- }
- end;
- end;
- }
-
- # description
- if($r->{desc}) {
- Tr class => 'nostripe';
- td class => 'chardesc', colspan => 2;
- h2 'Description';
- p;
- lit bb2html $r->{desc}, 0, 1;
- end;
- end;
- end;
- }
-
- end 'table';
- end;
- clearfloat;
-}
-
-
-
sub edit {
my($self, $id, $rev, $copy) = @_;
diff --git a/lib/VNDB/Handler/VNPage.pm b/lib/VNDB/Handler/VNPage.pm
index 2f35744f..3170fd30 100644
--- a/lib/VNDB/Handler/VNPage.pm
+++ b/lib/VNDB/Handler/VNPage.pm
@@ -14,7 +14,6 @@ use POSIX 'strftime';
TUWF::register(
qr{v/rand} => \&rand,
qr{v([1-9]\d*)/releases} => \&releases,
- qr{v([1-9]\d*)/(chars)} => \&page,
qr{v([1-9]\d*)/staff} => sub { $_[0]->resRedirect("/v$_[1]#staff") },
qr{v([1-9]\d*)(?:\.([1-9]\d*))?} => \&page,
);
@@ -306,7 +305,7 @@ sub _releases_table {
sub page {
my($self, $vid, $rev) = @_;
- my $char = $rev && $rev eq 'chars';
+ my $char = $rev && $rev eq 'chars'; # XXX: Not used anymore; implemented in VNWeb::VN::Page.
$rev = undef if $char;
my $method = $rev ? 'dbVNGetRev' : 'dbVNGet';
@@ -500,7 +499,7 @@ sub page {
}
if($char) {
- _chars($self, $chars, $v);
+ #_chars($self, $chars, $v);
} else {
_releases($self, $v, $r);
_staff($self, $v);
@@ -948,27 +947,6 @@ sub _charspoillvl {
}
-sub _chars {
- my($self, $l, $v) = @_;
- return if !@$l;
- my %done;
- my %rol;
- for my $r (keys %CHAR_ROLE) {
- $rol{$r} = [ grep grep($_->{role} eq $r, @{$_->{vns}}) && !$done{$_->{id}}++, @$l ];
- }
- div class => 'charops', id => 'charops';
- $self->charOps(1, 'chars');
- for my $r (keys %CHAR_ROLE) {
- next if !@{$rol{$r}};
- div class => 'mainbox';
- h1 $CHAR_ROLE{$r}{ @{$rol{$r}} > 1 ? 'plural' : 'txt' };
- $self->charTable($_, 1, $_ != $rol{$r}[0], 1, _charspoillvl $v->{id}, $_) for (@{$rol{$r}});
- end;
- }
- end;
-}
-
-
sub _charsum {
my($self, $l, $v) = @_;
return if !@$l;
diff --git a/lib/VNWeb/Chars/Page.pm b/lib/VNWeb/Chars/Page.pm
index 6e759d0b..5d366b07 100644
--- a/lib/VNWeb/Chars/Page.pm
+++ b/lib/VNWeb/Chars/Page.pm
@@ -30,12 +30,14 @@ sub enrich_item {
# Fetch multiple character entries with a format suitable for chartable_()
+# Also used by VN::Page
sub fetch_chars {
my($vid, $where) = @_;
my $l = tuwf->dbAlli('
SELECT id, name, original, alias, "desc", gender, b_month, b_day, s_bust, s_waist, s_hip, height, weight, bloodt, cup_size, age, image
- FROM chars WHERE NOT hidden AND (', $where, ')'
- );
+ FROM chars WHERE NOT hidden AND (', $where, ')
+ ORDER BY name
+ ');
enrich vns => id => id => sub { sql '
SELECT cv.id, cv.vid, cv.rid, cv.spoil, cv.role, v.title, v.original, r.title AS rtitle, r.original AS roriginal
@@ -99,9 +101,7 @@ sub _rev_ {
}
-# TODO: Also to be used by the character listing on VN pages; But it's not
-# currently compatible with VNDB::Handler::VNPage because that uses a different
-# spoiler hiding mechanism.
+# Also used by VN::Page
sub chartable_ {
my($c, $link, $sep, $vn) = @_;
my $view = viewget;
@@ -177,7 +177,7 @@ sub chartable_ {
br_ if !$vn;
join_ \&br_, sub {
b_ class => 'grayedout', '> ';
- txt_ $CHAR_ROLE{$v->{role}}{txt}.' - ';
+ txt_ $CHAR_ROLE{$_->{role}}{txt}.' - ';
if($_->{rid}) {
b_ class => 'grayedout', "r$_->{rid}:";
a_ href => "/r$_->{rid}", title => $_->{roriginal}||$_->{rtitle}, $_->{rtitle};
diff --git a/lib/VNWeb/VN/Page.pm b/lib/VNWeb/VN/Page.pm
new file mode 100644
index 00000000..832e7497
--- /dev/null
+++ b/lib/VNWeb/VN/Page.pm
@@ -0,0 +1,409 @@
+package VNWeb::VN::Page;
+
+use VNWeb::Prelude;
+use POSIX 'strftime';
+
+
+# Enrich everything necessary to at least render infobox_().
+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};
+ 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;
+
+ # 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 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};
+}
+
+
+sub og {
+ my($v) = @_;
+ +{
+ description => bb2text($v->{desc}),
+ image => $v->{image} && !$v->{img_nsfw} ? tuwf->imgurl($v->{image}) :
+ (map $_->{nsfw}?():(tuwf->imgurl($_->{scr})), $v->{screenshots}->@*)[0]
+ }
+}
+
+
+sub infobox_img_ {
+ my($v) = @_;
+ p_ 'No image uploaded yet.' if !$v->{image};
+ img_ src => tuwf->imgurl($v->{image}), alt => $v->{title} if $v->{image} && !$v->{img_nsfw};
+
+ p_ class => 'nsfw_pic', sub {
+ input_ id => 'nsfw_chk', type => 'checkbox', class => 'visuallyhidden', tuwf->authPref('show_nsfw') ? (checked => 'checked') : ();
+ label_ for => 'nsfw_chk', sub {
+ span_ id => 'nsfw_show', sub {
+ 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)';
+ };
+ span_ id => 'nsfw_hid', sub {
+ img_ src => tuwf->imgurl($v->{image}), alt => $v->{title};
+ i_ 'Flagged as NSFW';
+ };
+ };
+ } if $v->{image} && $v->{img_nsfw};
+}
+
+
+sub infobox_relations_ {
+ my($v) = @_;
+ return if !$v->{relations}->@*;
+
+ my %rel;
+ push $rel{$_->{relation}}->@*, $_ for sort { $a->{title} cmp $b->{title} } $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{$_}->@*;
+ }
+ }
+ }}
+ }
+}
+
+
+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
+ FROM releases_vn rv
+ JOIN releases r ON r.id = rv.id
+ JOIN releases_lang 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
+ ');
+ return if !@$p;
+
+ my $prev = 0;
+ my @dev = grep $_->{developer} && $prev != $_->{id} && ($prev = $_->{id}), @$p;
+
+ tr_ sub {
+ td_ 'Developer';
+ td_ sub {
+ join_ ' & ', sub { a_ href => "/p$_->{id}", title => $_->{original}||$_->{name}, $_->{name}; }, @dev;
+ };
+ } if @dev;
+
+ my(%lang, @lang, $lang);
+ for(grep $_->{publisher}, @$p) {
+ push @lang, $_->{lang} if !$lang{$_->{lang}};
+ push $lang{$_->{lang}}->@*, $_;
+ }
+
+ 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;
+ }
+ } if keys %lang;
+}
+
+
+sub infobox_affiliates_ {
+ my($v) = @_;
+
+ # 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 ($v->{releases}->@*) {
+ my $type = $rel->{patch} ? 4 :
+ $rel->{type} eq 'trial' ? 3 :
+ $rel->{type} eq 'partial' ? 2 :
+ $rel->{num_vns} > 1 ? 0 : 1;
+
+ $links{$_->[1]} = [ @$_, min $type, $links{$_->[1]}[3]||9 ] for grep $_->[2], $rel->{extlinks}->@*;
+ }
+ return if !keys %links;
+
+ tr_ id => 'buynow', sub {
+ td_ 'Shops';
+ td_ sub {
+ join_ \&br_, sub {
+ b_ class => 'standout', 'ยป ';
+ a_ href => $_->[1], sub {
+ txt_ $_->[2];
+ b_ class => 'grayedout', ' @ ';
+ txt_ $_->[0];
+ b_ class => 'grayedout', " ($type[$_->[3]])" if $_->[3] != 1;
+ };
+ }, sort { $a->[0] cmp $b->[0] || $a->[2] cmp $b->[2] } values %links;
+ }
+ }
+}
+
+
+sub infobox_anime_ {
+ my($v) = @_;
+ return if !$v->{anime}->@*;
+ tr_ sub {
+ td_ 'Related anime';
+ td_ class => 'anime', sub { join_ \&br_, sub {
+ if(!$_->{lastfetch} || !$_->{year} || !$_->{title_romaji}) {
+ b_ sub {
+ txt_ '[no information available at this time: ';
+ a_ href => 'https://anidb.net/anime/'.$_->{aid}, "a$_->{aid}";
+ txt_ ']';
+ };
+ } else {
+ b_ sub {
+ txt_ '[';
+ a_ href => "https://anidb.net/anime/$_->{aid}", title => 'AniDB', 'DB';
+ if($_->{ann_id}) {
+ txt_ '-';
+ a_ href => "http://www.animenewsnetwork.com/encyclopedia/anime.php?id=$_->{ann_id}", title => 'Anime News Network', 'ANN';
+ }
+ txt_ '] ';
+ };
+ abbr_ title => $_->{title_kanji}||$_->{title_romaji}, shorten $_->{title_romaji}, 50;
+ b_ ' ('.(defined $_->{type} ? $ANIME_TYPE{$_->{type}}{txt}.', ' : '').$_->{year}.')';
+ }
+ }, sort { ($a->{year}||9999) <=> ($b->{year}||9999) } $v->{anime}->@* }
+ }
+}
+
+
+sub infobox_tags_ {
+ my($v) = @_;
+ my $rating = 'avg(CASE WHEN tv.ignore 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 THEN NULL ELSE tv.spoiler END), t.defaultspoil) as spoiler
+ FROM tags t
+ JOIN tags_vn tv ON tv.tag = t.id
+ 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',
+ (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') : ();
+ 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') : ();
+ label_ for => 'tag_spoil_some', 'show minor spoilers';
+ input_ id => 'tag_spoil_all', type => 'radio', class => 'visuallyhidden', 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');
+ 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') : ();
+ 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 $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' : '';
+ span_ class => "tagspl$spoil cat_$_->{cat} $cut", sub {
+ a_ href => "/g$_->{id}", style => sprintf('font-size: %dpx', $_->{rating}*3.5+6), $_->{name};
+ b_ class => 'grayedout', sprintf ' %.1f', $_->{rating};
+ }
+ }, @$tags;
+ }
+ }
+}
+
+
+sub infobox_useroptions_ {
+ my($v) = @_;
+ return if !auth;
+
+ # Voting option is hidden if nothing has been released yet
+ my $minreleased = min grep $_, map $_->{released}, $v->{releases}->@*;
+
+ 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 FROM ulist_vns WHERE uid =', \auth->uid, 'AND vid =', \$v->{id});
+
+ tr_ class => 'nostripe', sub {
+ td_ colspan => 2, sub {
+ elm_ 'UList.VNPage', undef, { # TODO: Go through a TUWF::Validation schema
+ uid => 1*auth->uid,
+ vid => 1*$v->{id},
+ onlist => $lst->{vid}?\1:\0,
+ canvote => $minreleased && $minreleased < strftime('%Y%m%d', gmtime) ? \1 : \0,
+ vote => fmtvote($lst->{vote}).'',
+ labels => [ map +{ id => 1*$_->{id}, label => $_->{label}, private => $_->{private}?\1:\0 }, @$labels ],
+ selected => [ map $_->{id}, grep $_->{assigned}, @$labels ],
+ };
+ }
+ }
+}
+
+
+sub infobox_ {
+ my($v) = @_;
+ div_ class => 'mainbox', sub {
+ h1_ $v->{title};
+ h2_ class => 'alttitle', lang_attr($v->{c_olang}), $v->{original} if $v->{original};
+
+ div_ class => 'vndetails', sub {
+ div_ class => 'vnimg', sub { infobox_img_ $v };
+
+ table_ class => 'stripe', sub {
+ tr_ sub {
+ td_ class => 'key', 'Title';
+ td_ sub { txt_ $v->{title}; debug_ $v; };
+ };
+
+ tr_ sub {
+ td_ 'Original title';
+ td_ lang_attr($v->{c_olang}), $v->{original};
+ } if $v->{original};
+
+ tr_ sub {
+ td_ 'Aliases';
+ td_ $v->{alias} =~ s/\n/, /gr;
+ } if $v->{alias};
+
+ tr_ sub {
+ td_ 'Length';
+ td_ "$VN_LENGTH{$v->{length}}{txt} ($VN_LENGTH{$v->{length}}{time})";
+ } if $v->{length};
+
+ infobox_producers_ $v;
+ infobox_relations_ $v;
+
+ tr_ sub {
+ td_ 'Links';
+ td_ sub { join_ ', ', sub { a_ href => $_->[1], $_->[0] }, $v->{extlinks}->@* };
+ } if $v->{extlinks}->@*;
+
+ infobox_affiliates_ $v;
+ infobox_anime_ $v;
+ infobox_useroptions_ $v;
+
+ tr_ class => 'nostripe', sub {
+ td_ class => 'vndesc', colspan => 2, sub {
+ h2_ 'Description';
+ p_ sub { lit_ $v->{desc} ? bb2html $v->{desc} : '-' };
+ }
+ }
+ }
+ };
+ div_ class => 'clearfloat', style => 'height: 5px', ''; # otherwise the tabs below aren't positioned correctly
+ infobox_tags_ $v;
+ }
+}
+
+
+sub tabs_ {
+ my($v, $char) = @_;
+ # XXX: This query is kind of silly because we'll be fetching a list of characters regardless of which tab we have open.
+ my $haschars = tuwf->dbVali('SELECT 1 FROM chars c JOIN chars_vns cv ON cv.id = c.id WHERE NOT c.hidden AND cv.vid =', \$v->{id}, 'LIMIT 1');
+
+ return if !$haschars && !auth->perm('edit');
+ 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;
+ 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->perm('edit');
+ }
+}
+
+
+sub chars_ {
+ my($v) = @_;
+ my $view = viewget;
+ my $chars = VNWeb::Chars::Page::fetch_chars($v->{id}, sql('id IN(SELECT id FROM chars_vns WHERE vid =', \$v->{id}, ')'));
+ return if !@$chars;
+
+ my $max_spoil = max(
+ map max(
+ (map $_->{spoil}, $_->{traits}->@*),
+ (map $_->{spoil}, $_->{vns}->@*),
+ $_->{desc} =~ /\[spoiler\]/i ? 2 : 0,
+ ), @$chars
+ );
+ $chars = [ grep +grep($_->{spoil} <= $view->{spoilers}, $_->{vns}->@*), @$chars ];
+ my $has_sex = grep $_->{spoil} <= $view->{spoilers} && $_->{sexual}, map $_->{traits}->@*, @$chars;
+
+ my %done;
+ my $first = 0;
+ for my $r (keys %CHAR_ROLE) {
+ my @c = grep grep($_->{role} eq $r, $_->{vns}->@*) && !$done{$_->{id}}++, @$chars;
+ next if !@c;
+ div_ class => 'mainbox', sub {
+
+ p_ class => 'mainopts', sub {
+ if($max_spoil) {
+ a_ mkclass(checked => $view->{spoilers} == 0), href => '?view='.viewset(spoilers=>0).'#chars', 'Hide spoilers';
+ a_ mkclass(checked => $view->{spoilers} == 1), href => '?view='.viewset(spoilers=>1).'#chars', 'Show minor spoilers';
+ a_ mkclass(standout =>$view->{spoilers} == 2), href => '?view='.viewset(spoilers=>2).'#chars', 'Spoil me!' if $max_spoil == 2;
+ }
+ b_ class => 'grayedout', ' | ' if $has_sex && $max_spoil;
+ a_ mkclass(checked => $view->{traits_sexual}), href => '?view='.viewset(traits_sexual=>!$view->{traits_sexual}).'#chars', 'Show sexual traits' if $has_sex;
+ } if !$first++;
+
+ h1_ $CHAR_ROLE{$r}{ @c > 1 ? 'plural' : 'txt' };
+ VNWeb::Chars::Page::chartable_($_, 1, $_ != $c[0], 1) for @c;
+ }
+ }
+}
+
+
+TUWF::get qr{/$RE{vid}/chars}, sub {
+ my $v = db_entry v => tuwf->capture('id');
+ return tuwf->resNotFound if !$v;
+
+ enrich_vn $v;
+
+ framework_ title => $v->{title}, index => 1, type => 'v', dbobj => $v, hiddenmsg => 1, og => og($v),
+ sub {
+ infobox_ $v;
+ tabs_ $v, 1;
+ chars_ $v;
+ };
+};
+
+1;