diff options
Diffstat (limited to 'lib/VN3/VN')
-rw-r--r-- | lib/VN3/VN/Edit.pm | 187 | ||||
-rw-r--r-- | lib/VN3/VN/JS.pm | 46 | ||||
-rw-r--r-- | lib/VN3/VN/Lib.pm | 20 | ||||
-rw-r--r-- | lib/VN3/VN/Page.pm | 631 |
4 files changed, 0 insertions, 884 deletions
diff --git a/lib/VN3/VN/Edit.pm b/lib/VN3/VN/Edit.pm deleted file mode 100644 index bee48a5f..00000000 --- a/lib/VN3/VN/Edit.pm +++ /dev/null @@ -1,187 +0,0 @@ -package VN3::VN::Edit; - -use VN3::Prelude; -use VN3::VN::Lib; - - -my $FORM = { - alias => { required => 0, default => '', maxlength => 500 }, - anime => { maxlength => 50, sort_keys => 'aid', aoh =>{ - aid => { id => 1 } - } }, - desc => { required => 0, default => '', maxlength => 10240 }, - image => { required => 0, default => 0, id => 1 }, # X - img_nsfw => { anybool => 1 }, - hidden => { anybool => 1 }, - l_encubed => { required => 0, default => '', maxlength => 100 }, - l_renai => { required => 0, default => '', maxlength => 100 }, - l_wp => { required => 0, default => '', maxlength => 150 }, - length => { vn_length => 1 }, - locked => { anybool => 1 }, - original => { required => 0, default => '', maxlength => 250 }, - relations => { maxlength => 50, sort_keys => 'vid', aoh => { - vid => { id => 1 }, # X - relation => { vn_relation => 1 }, - official => { anybool => 1 }, - title => { _when => 'out' }, - } }, - screenshots => { maxlength => 10, sort_keys => 'scr', aoh => { - scr => { id => 1 }, # X - rid => { id => 1 }, # X - nsfw => { anybool => 1 }, - width => { _when => 'out', uint => 1 }, - height => { _when => 'out', uint => 1 }, - } }, - seiyuu => { sort_keys => ['aid','cid'], aoh => { - aid => { id => 1 }, # X - cid => { id => 1 }, # X - note => { required => 0, default => '', maxlength => 250 }, - id => { _when => 'out', id => 1 }, - name => { _when => 'out' }, - } }, - staff => { sort_keys => ['aid','role'], aoh => { - aid => { id => 1 }, # X - role => { staff_role => 1 }, - note => { required => 0, default => '', maxlength => 250 }, - id => { _when => 'out', id => 1 }, - name => { _when => 'out' }, - } }, - title => { maxlength => 250 }, - - id => { _when => 'out', required => 0, id => 1 }, - authmod => { _when => 'out', anybool => 1 }, - editsum => { _when => 'in out', editsum => 1 }, - chars => { _when => 'out', aoh => { - id => { id => 1 }, - name => {}, - } }, - releases => { _when => 'out', aoh => { - id => { id => 1 }, - title => {}, - original => {}, - display => {}, - resolution=> {}, - } }, -}; - -my $FORM_OUT = form_compile out => $FORM; -my $FORM_IN = form_compile in => $FORM; -my $FORM_CMP = form_compile cmp => $FORM; - -elm_form VNEdit => $FORM_OUT, $FORM_IN; - - -TUWF::get qr{/$VREV_RE/edit} => sub { - my $vn = entry v => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound; - return tuwf->resDenied if !can_edit v => $vn; - - enrich aid => q{SELECT id, aid, name FROM staff_alias WHERE aid IN} => $vn->{staff}, $vn->{seiyuu}; - enrich vid => q{SELECT id AS vid, title FROM vn WHERE id IN} => $vn->{relations}; - enrich scr => q{SELECT id AS scr, width, height FROM screenshots WHERE id IN}, $vn->{screenshots}; - $vn->{chars} = tuwf->dbAlli('SELECT id, name FROM chars c WHERE id IN(SELECT id FROM chars_vns WHERE vid =', \$vn->{id}, ') ORDER BY name'); - - $vn->{releases} = tuwf->dbAlli('SELECT id, title, original, resolution FROM releases WHERE id IN(SELECT id FROM releases_vn WHERE vid =', \$vn->{id}, ') ORDER BY id'); - enrich_list1 lang => id => id => q{SELECT id, lang FROM releases_lang WHERE id IN}, $vn->{releases}; - $_->{display} = sprintf '[%s] %s (r%d)', join(',', @{ delete $_->{lang} }), $_->{title}, $_->{id} for @{$vn->{releases}}; - - $vn->{authmod} = auth->permDbmod; - $vn->{editsum} = $vn->{chrev} == $vn->{maxrev} ? '' : "Reverted to revision v$vn->{id}.$vn->{chrev}"; - - Framework index => 0, title => "Edit $vn->{title}", - top => sub { - Div class => 'col-md', sub { - EntryEdit v => $vn; - Div class => 'detail-page-title', sub { - Txt $vn->{title}; - Debug $vn; - }; - TopNav edit => $vn; - }; - }, sub { - FullPageForm module => 'VNEdit.Main', data => $vn, schema => $FORM_OUT, sections => [ - general => 'General info', - staff => 'Staff', - cast => 'Cast', - relations => 'Relations', - screenshots => 'Screenshots', - ]; - }; -}; - - -TUWF::get '/v/add', sub { - return tuwf->resDenied if !auth->permEdit; - Framework index => 0, title => 'Add a new visual novel', narrow => 1, sub { - Div class => 'row', sub { - Div class => 'col-md col-md--1', sub { Div 'data-elm-module' => 'VNEdit.New', '' }; - }; - }; -}; - - -json_api qr{/(?:$VID_RE/edit|v/add)}, $FORM_IN, sub { - my $data = shift; - my $new = !tuwf->capture('id'); - my $vn = $new ? { id => 0 } : entry v => tuwf->capture('id') or return tuwf->resNotFound; - - return $elm_Unauth->() if !can_edit v => $vn; - - if(!auth->permDbmod) { - $data->{hidden} = $vn->{hidden}||0; - $data->{locked} = $vn->{locked}||0; - } - - # Elm doesn't actually verify this one - die "Image not found" if $data->{image} && !-e tuwf->imgpath(cv => $data->{image}); - - die "Relation with self" if grep $_->{vid} == $vn->{id}, @{$data->{relations}}; - validate_dbid 'SELECT id FROM vn WHERE id IN', map $_->{vid}, @{$data->{relations}}; - validate_dbid 'SELECT id FROM screenshots WHERE id IN', map $_->{scr}, @{$data->{screenshots}}; - validate_dbid sql('SELECT DISTINCT id FROM releases_vn WHERE vid =', \$vn->{id}, ' AND id IN'), map $_->{rid}, @{$data->{screenshots}}; - validate_dbid 'SELECT aid FROM staff_alias WHERE aid IN', map $_->{aid}, @{$data->{seiyuu}}, @{$data->{staff}}; - validate_dbid sql('SELECT DISTINCT id FROM chars_vns WHERE vid =', \$vn->{id}, ' AND id IN'), map $_->{cid}, @{$data->{seiyuu}}; - - $data->{desc} = bb_subst_links $data->{desc}; - return $elm_Unchanged->() if !$new && !form_changed $FORM_CMP, $data, $vn; - - my($id,undef,$rev) = update_entry v => $vn->{id}, $data; - - update_reverse($id, $rev, $vn, $data); - - $elm_Changed->($id, $rev); -}; - - -sub update_reverse { - my($id, $rev, $old, $new) = @_; - - my %old = map +($_->{vid}, $_), $old->{relations} ? @{$old->{relations}} : (); - my %new = map +($_->{vid}, $_), @{$new->{relations}}; - - # Updates to be performed, vid => { vid => x, relation => y, official => z } or undef if the relation should be removed. - my %upd; - - for my $i (keys %old, keys %new) { - if($old{$i} && !$new{$i}) { - $upd{$i} = undef; - } elsif(!$old{$i} || $old{$i}{relation} ne $new{$i}{relation} || !$old{$i}{official} != !$new{$i}{official}) { - $upd{$i} = { - vid => $id, - relation => vn_relation_reverse($new{$i}{relation}), - official => $new{$i}{official} - }; - } - } - - for my $i (keys %upd) { - my $v = entry v => $i; - $v->{relations} = [ - $upd{$i} ? $upd{$i} : (), - grep $_->{vid} != $id, @{$v->{relations}} - ]; - $v->{editsum} = "Reverse relation update caused by revision v$id.$rev"; - update_entry v => $i, $v, 1; - } -} - -1; diff --git a/lib/VN3/VN/JS.pm b/lib/VN3/VN/JS.pm deleted file mode 100644 index ec98b768..00000000 --- a/lib/VN3/VN/JS.pm +++ /dev/null @@ -1,46 +0,0 @@ -package VN3::VN::JS; - -use VN3::Prelude; - - -my $elm_VNResult = elm_api VNResult => { aoh => { - id => { id => 1 }, - title => {}, - original => {}, - hidden => { anybool => 1 }, -}}; - - -json_api '/js/vn.json', { - search => { type => 'array', scalar => 1, minlength => 1, values => { maxlength => 500 } }, - hidden => { anybool => 1 } -}, sub { - my $data = shift; - - my $r = tuwf->dbAlli( - 'SELECT v.id, v.title, v.original, v.hidden', - 'FROM (', (sql_join 'UNION ALL', map { - my $qs = s/[%_]//gr; - my @q = normalize_query $_; - +( - # ID search - /^$VID_RE$/ ? (sql 'SELECT 1, id FROM vn WHERE id =', \"$1") : (), - # prefix match - sql('SELECT 2, id FROM vn WHERE title ILIKE', \"$qs%"), - # substring match - @q ? (sql 'SELECT 3, id FROM vn WHERE', sql_and map sql('c_search ILIKE', \"%$_%"), @q) : () - ) - } @{$data->{search}}), - ') AS vt (ord, id)', - 'JOIN vn v ON v.id = vt.id', - $data->{hidden} ? () : ('WHERE NOT v.hidden'), - 'GROUP BY v.id, v.title, v.original', - 'ORDER BY MIN(vt.ord), v.title', - 'LIMIT 20' - ); - - $elm_VNResult->($r); -}; - -1; - diff --git a/lib/VN3/VN/Lib.pm b/lib/VN3/VN/Lib.pm deleted file mode 100644 index 9571cef8..00000000 --- a/lib/VN3/VN/Lib.pm +++ /dev/null @@ -1,20 +0,0 @@ -package VN3::VN::Lib; - -use VN3::Prelude; - -our @EXPORT = qw/TopNav/; - - -sub TopNav { - my($page, $v) = @_; - - my $rg = exists $v->{rgraph} ? $v->{rgraph} : tuwf->dbVali('SELECT rgraph FROM vn WHERE id=', \$v->{id}); - - Div class => 'nav raised-top-nav', sub { - Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'details'), sub { A href => "/v$v->{id}", class => 'nav__link', 'Details'; }; - Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'discussions'),sub { A href => "/t/v$v->{id}", class => 'nav__link', 'Discussions'; }; # TODO: count - Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'relations'), sub { A href => "/v$v->{id}/rg", class => 'nav__link', 'Relations'; } if $rg; - }; -} - -1; diff --git a/lib/VN3/VN/Page.pm b/lib/VN3/VN/Page.pm deleted file mode 100644 index a09bbeb9..00000000 --- a/lib/VN3/VN/Page.pm +++ /dev/null @@ -1,631 +0,0 @@ -package VN3::VN::Page; - -use VN3::Prelude; -use VN3::VN::Lib; - - -TUWF::get '/v/rand', sub { - # TODO: Apply stored filters? - my $vid = tuwf->dbVal('SELECT id FROM vn WHERE NOT hidden ORDER BY RANDOM() LIMIT 1'); - tuwf->resRedirect("/v$vid", 'temp'); -}; - - -sub CVImage { - my($vn, $class, $class_sfw, $class_nsfw) = @_; - return if !$vn->{image}; - - my $img = tuwf->imgurl(cv => $vn->{image}); - my $nsfw = tuwf->conf->{url_static}.'/v3/nsfw.svg'; - Img class => $class.' '.($vn->{img_nsfw} ? $class_nsfw : $class_sfw), - !$vn->{img_nsfw} ? (src => $img) - : auth->pref('show_nsfw') ? (src => $img, 'data-toggle-img' => $nsfw) - : (src => $nsfw, 'data-toggle-img' => $img); -} - - -sub Top { - my $vn = shift; - Div class => 'fixed-size-left-sidebar-md', ''; - Div class => 'col-md', sub { - Div class => 'vn-header', sub { - EntryEdit v => $vn; - CVImage $vn, 'page-header-img-mobile img img--rounded d-md-none', '', 'nsfw-outline'; - Div class => 'vn-header__title', $vn->{title}; - Div class => 'vn-header__original-title', $vn->{original} if $vn->{original}; - Div class => 'vn-header__details', sub { - Txt $vn->{c_rating} ? sprintf '%.1f ', $vn->{c_rating}/10 : '-'; - Div class => 'vn-header__sep', ''; - Txt vn_length_time $vn->{length}; - Div class => 'vn-header__sep', ''; - Txt join ', ', map $LANGUAGE{$_}, @{$vn->{c_languages}}; - Debug $vn; - }; - }; - TopNav details => $vn; - }; -} - - -sub SidebarProd { - my $vn = shift; - - my $prod = tuwf->dbAlli(q{ - SELECT p.id, p.name, p.original, bool_or(rp.developer) AS dev, bool_or(rp.publisher) AS pub - FROM releases r - JOIN releases_producers rp ON rp.id = r.id - JOIN releases_vn rv ON rv.id = r.id - JOIN producers p ON rp.pid = p.id - WHERE rv.vid =}, \$vn->{id}, q{ - AND NOT r.hidden - GROUP BY p.id, p.name, p.original - ORDER BY p.name - }); - - my $Fmt = sub { - my($single, $multi, @lst) = @_; - - Dt @lst == 1 ? $single : $multi; - Dd sub { - Join ', ', sub { - A href => "/p$_[0]{id}", title => $_[0]{original}||$_[0]{name}, $_[0]{name} - }, @lst; - }; - }; - - $Fmt->('Developer', 'Developers', grep $_->{dev}, @$prod); - $Fmt->('Publisher', 'Publishers', grep $_->{pub}, @$prod); -} - - -sub SidebarRel { - my $vn = shift; - return if !@{$vn->{relations}}; - - Dt 'Relations'; - Dd sub { - Dl sub { - for my $type (keys %VN_RELATION) { - my @rel = grep $_->{relation} eq $type, @{$vn->{relations}}; - next if !@rel; - Dt vn_relation_display $type; - Dd class => 'single-line-md', sub { - Span 'unofficial ' if !$_->{official}; - A href => "/v$_->{vid}", title => $_->{original}||$_->{title}, $_->{title}; - } for @rel; - } - } - } -} - - -sub Sidebar { - my $vn = shift; - - CVImage $vn, 'img img--fit img--rounded d-none d-md-block vn-img-desktop', 'elevation-1', 'elevation-1-nsfw' if $vn->{image}; - Div class => 'vn-image-placeholder img--rounded elevation-1 d-none d-md-block vn-img-desktop', sub { - Div class => 'vn-image-placeholder__icon', sub { - Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/camera-alt.svg'; - } - } if !$vn->{image}; - - Div class => 'add-to-list elevated-button elevation-1', sub { - Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/plus.svg'; - Txt 'Add to my list'; - }; - - Dl class => 'vn-page__dl', sub { - if($vn->{original}) { - Dt 'Original Title'; - Dd $vn->{original}; - } - - Dt 'Main Title'; - Dd $vn->{title}; - - if($vn->{alias}) { - Dt 'Aliases'; - Dd $vn->{alias} =~ s/\n/, /gr; - } - - if($vn->{length}) { - Dt 'Length'; - Dd vn_length_display $vn->{length}; - } - - SidebarProd $vn; - SidebarRel $vn; - - # TODO: Affiliate links - # TODO: Anime - }; -} - - -sub Tags { - my $vn = shift; - - my $tag_rating = 'avg(CASE WHEN tv.ignore THEN NULL ELSE tv.vote END)'; - my $tags = tuwf->dbAlli(qq{ - SELECT tv.tag, t.name, t.cat, count(*) as cnt, $tag_rating as rating, - COALESCE(avg(CASE WHEN tv.ignore THEN NULL ELSE tv.spoiler END), t.defaultspoil) as spoiler - FROM tags_vn tv - JOIN tags t ON tv.tag = t.id - WHERE tv.vid =}, \$vn->{id}, qq{ - AND t.state = 1+1 - GROUP BY tv.tag, t.name, t.cat, t.defaultspoil - HAVING $tag_rating > 0 - ORDER BY $tag_rating DESC - }); - - my $spoil = auth->pref('spoilers') || 0; - my $cat = auth->pref('tags_cat') || 'cont,tech'; - my %cat = map +($_, !!($cat =~ /$_/)), qw/cont ero tech/; - - Div mkclass( - 'tag-summary__tags' => 1, - 'tag-summary--collapsed' => 1, - 'tag-summary--hide-spoil-1' => $spoil < 1, - 'tag-summary--hide-spoil-2' => $spoil < 2, - map +("tag-summary--hide-$_", !$cat{$_}), keys %cat - ), sub { - for my $tag (@$tags) { - Div class => sprintf( - 'tag-summary__tag tag-summary__tag--%s tag-summary__tag--spoil-%d', - $tag->{cat}, $tag->{spoiler} > 1.3 ? 2 : $tag->{spoiler} > 0.4 ? 1 : 0 - ), sub { - A href => "/g$tag->{tag}", class => 'link--subtle', $tag->{name}; - Div class => 'tag-summary__tag-meter', style => sprintf('width: %dpx', $tag->{rating}*10), ''; - }; - } - }; - - Div class => 'tag-summary__options', sub { - Div class => 'tag-summary__options-left', sub { - A href => 'javascript:;', class => 'link--subtle d-none tag-summary__show-all', sub { - Span class => 'caret caret--pre', ''; - Txt ' Show all tags'; - }; - Debug $tags; - }; - Div class => 'tag-summary__options-right', sub { - Div class => 'tag-summary__option dropdown', sub { - A href => 'javascript:;', class => 'link--subtle dropdown__toggle', sub { - Span class => 'tag-summary_option--spoil', spoil_display $spoil; - Lit ' '; - Span class => 'caret', ''; - }; - Div class => 'dropdown-menu', sub { - A class => 'dropdown-menu__item tag-summary_option--spoil-0', href => 'javascript:;', spoil_display 0; - A class => 'dropdown-menu__item tag-summary_option--spoil-1', href => 'javascript:;', spoil_display 1; - A class => 'dropdown-menu__item tag-summary_option--spoil-2', href => 'javascript:;', spoil_display 2; - }; - }; - Div class => 'tag-summary__option', sub { Switch 'Content', $cat{cont}, 'tag-summary__option--cont' => 1; }; - Div class => 'tag-summary__option', sub { Switch 'Sexual', $cat{ero}, 'tag-summary__option--ero' => 1; }; - Div class => 'tag-summary__option', sub { Switch 'Technical', $cat{tech}, 'tag-summary__option--tech' => 1; }; - }; - }; -} - - -sub Releases { - my $vn = shift; - - my %lang; - my @lang = grep !$lang{$_}++, map @{$_->{lang}}, @{$vn->{releases}}; - - for my $lang (@lang) { - Div class => 'relsm__language', sub { - Lang $lang; - Txt " $LANGUAGE{$lang}"; - }; - Div class => 'relsm__table', sub { - Div class => 'relsm__rel', sub { - my $rel = $_; - - Div class => 'relsm__rel-col relsm__rel-date tabular-nums', sub { ReleaseDate $rel->{released}; }; - A class => 'relsm__rel-col relsm__rel-name', href => "/r$rel->{id}", title => $rel->{original}||$rel->{title}, $rel->{title}; - Div class => 'relsm__rel-col relsm__rel-platforms', sub { Platform $_ for @{$rel->{platforms}} }; - Div class => 'relsm__rel-col relsm__rel-mylist', sub { - # TODO: Make this do something - Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/plus-circle.svg'; - }; - if($rel->{website}) { - Div class => 'relsm__rel-col relsm__rel-link', sub { - A href => $rel->{website}, 'Link'; - }; - } else { - Div class => 'relsm__rel-col relsm__rel-link relsm__rel-link--none', 'Link'; - } - - # TODO: Age rating - # TODO: Release type - # TODO: Release icons - } for grep grep($_ eq $lang, @{$_->{lang}}), @{$vn->{releases}}; - } - } -} - - -sub Staff { - my $vn = shift; - return if !@{$vn->{staff}}; - - my $Role = sub { - my $role = shift; - my @staff = grep $_->{role} eq $role, @{$vn->{staff}}; - return if !@staff; - - Div class => 'staff-credits__section', sub { - Div class => 'staff-credits__section-title', $CREDIT_TYPE{$role}; - Div class => 'staff-credits__item', sub { - A href => "/s$_->{id}", title => $_->{original}||$_->{name}, $_->{name}; - Span class => 'staff-credits__note', " $_->{note}" if $_->{note}; - } for (@staff); - }; - }; - - Div class => 'section', id => 'staff', sub { - H2 class => 'section__title', 'Staff'; - Div class => 'staff-credits js-columnize', 'data-columns' => 3, sub { - $Role->($_) for keys %CREDIT_TYPE; - }; - }; -} - - -sub Gallery { - my $vn = shift; - - return if !@{$vn->{screenshots}}; - my $show = auth->pref('show_nsfw'); - - Div mkclass(section => 1, gallery => 1, 'gallery--show-r18' => $show), id => 'gallery', sub { - H2 class => 'section__title', sub { - Switch '18+', $show, 'gallery-r18-toggle' => 1 if grep $_->{nsfw}, @{$vn->{screenshots}}; - Txt 'Gallery'; - }; - - # TODO: Thumbnails are being upscaled, we should probably recreate all thumbnails at higher resolution - - Div class => 'gallery__section', sub { - for my $s (@{$vn->{screenshots}}) { - my $r = (grep $_->{id} == $s->{rid}, @{$vn->{releases}})[0]; - my $meta = { - width => 1*$s->{width}, - height => 1*$s->{height}, - rel => { - id => 1*$s->{rid}, - title => $r->{title}, - lang => $r->{lang}, - plat => $r->{platforms}, - } - }; - - A mkclass('gallery__image-link' => 1, 'gallery__image--r18' => $s->{nsfw}), - 'data-lightbox-nfo' => JSON::XS->new->encode($meta), - href => tuwf->imgurl(sf => $s->{scr}), - sub { - Img mkclass(gallery__image => 1, 'nsfw-outline' => $s->{nsfw}), src => tuwf->imgurl(st => $s->{scr}); - } - } - } - }; -} - - -sub CharacterList { - my($vn, $roles, $first_char) = @_; - - # TODO: Implement spoiler & sexual stuff settings - # TODO: Make long character lists collapsable - - Div class => 'character-browser__top-item dropdown', sub { - A href => 'javascript:;', class => 'link--subtle dropdown__toggle', sub { - Txt spoil_display 0; - Lit ' '; - Span class => 'caret', ''; - }; - Div class => 'dropdown-menu', sub { - A class => 'dropdown-menu__item', href => 'javascript:;', spoil_display 0; - A class => 'dropdown-menu__item', href => 'javascript:;', spoil_display 1; - A class => 'dropdown-menu__item', href => 'javascript:;', spoil_display 2; - }; - }; - Div class => 'character-browser__top-item d-none d-md-block', sub { Switch 'Sexual traits', 0 }; - Div class => 'character-browser__top-item', sub { - A href => "/v$vn->{id}/chars", 'View all on one page'; - }; - - Div class => 'character-browser__list', sub { - Div class => 'character-browser__list-title', char_role_display $_, scalar @{$roles->{$_}}; - A mkclass('character-browser__char' => 1, 'character-browser__char--active' => $_->{id} == $first_char), - href => "/c$_->{id}", title => $_->{original}||$_->{name}, 'data-character' => $_->{id}, $_->{name} - for @{$roles->{$_}}; - } for grep @{$roles->{$_}}, keys %CHAR_ROLE; -} - - -sub CharacterInfo { - my $char = shift; - - Div class => 'row', sub { - Div class => 'col-md', sub { - # TODO: Gender & blood type - Div class => 'character__name', $char->{name}; - Div class => 'character__subtitle', $char->{original} if $char->{original}; - Div class => 'character__description serif', sub { - P sub { Lit bb2html $char->{desc}, 0, 1 }; - }; - }; - Div class => 'col-md character__image', sub { - Img class => 'img img--fit img--rounded', - src => tuwf->imgurl(ch => $char->{image}) - } if $char->{image}; - }; - - my(%groups, @groups); - for(@{$char->{traits}}) { - push @groups, $_->{gid} if !$groups{$_->{gid}}; - push @{$groups{$_->{gid}}}, $_; - } - - # Create a list of key/value things, so that we can neatly split them in - # two. The split occurs on the number of sections, so long sections can - # still cause some imbalance. - # TODO: Date of birth? - my @traits = ( - $char->{alias} ? sub { - Dt 'Aliases'; - Dd $char->{alias} =~ s/\n/, /gr; - } : (), - - $char->{weight} || $char->{height} || $char->{s_bust} || $char->{s_waist} || $char->{s_hip} ? sub { - Dt 'Measurements'; - Dd join ', ', - $char->{height} ? "Height: $char->{height}cm" : (), - $char->{weight} ? "Weight: $char->{weight}kg" : (), - $char->{s_bust} || $char->{s_waist} || $char->{s_hip} ? - sprintf 'Bust-Waist-Hips: %s-%s-%scm', $char->{s_bust}||'??', $char->{s_waist}||'??', $char->{s_hip}||'??' : (); - } : (), - - # TODO: Do something with spoiler settings. - (map { my $g = $_; sub { - Dt sub { A href => "/i$g", $groups{$g}[0]{group} }; - Dd sub { - Join ', ', sub { - A href => "/i$_[0]{tid}", $_[0]{name}; - }, @{$groups{$g}}; - }; - } } @groups), - - @{$char->{seiyuu}} ? sub { - Dt 'Voiced by'; - Dd sub { - my $prev = ''; - for my $s (sort { $a->{name} cmp $b->{name} } @{$char->{seiyuu}}) { - next if $s->{name} eq $prev; - A href => "/s$s->{id}", title => $s->{original}||$s->{name}, $s->{name}; - Txt ' ('.$s->{note}.')' if $s->{note}; - } - }; - } : (), - ); - - Div class => 'character__traits row mt-4', sub { - Dl class => 'col-md dl--horizontal', sub { $_->() for @traits[0..$#traits/2]; }; - Dl class => 'col-md dl--horizontal', sub { $_->() for @traits[$#traits/2+1..$#traits]; }; - } if @traits; -} - - -sub Characters { - my $vn = shift; - - # XXX: Fetching and rendering all character details on the VN page is a bit - # inefficient and bloats the HTML. We should probably load data from other - # characters on demand. - - my $chars = tuwf->dbAlli(q{ - SELECT id, name, original, alias, image, "desc", gender, s_bust, s_waist, s_hip, - b_month, b_day, height, weight, bloodt - FROM chars - WHERE NOT hidden - AND id IN(SELECT id FROM chars_vns WHERE vid =}, \$vn->{id}, q{) - ORDER BY name - }); - return if !@$chars; - - enrich_list releases => id => id => - sql('SELECT id, rid, spoil, role FROM chars_vns WHERE vid =', \$vn->{id}, ' AND id IN'), - $chars; - - # XXX: Just fetching this list takes ~10ms for a large VN (v92). I worry - # about formatting and displaying it on every page view. (This query can - # probably be sped up by grabbing/caching the group tag names separately, - # there are only 12 groups in the DB anyway). - enrich_list traits => id => id => sub {sql q{ - SELECT ct.id, ct.tid, ct.spoil, t.name, t.sexual, g.id AS gid, g.name AS group, g.order - FROM chars_traits ct - JOIN traits t ON t.id = ct.tid - JOIN traits g ON g.id = t.group - WHERE ct.id IN}, $_[0], q{ - ORDER BY g.order, t.name - }}, $chars; - - enrich_list seiyuu => id => cid => sub{sql q{ - SELECT va.id, vs.aid, vs.cid, vs.note, va.name, va.original - FROM vn_seiyuu_hist vs JOIN staff_alias va ON va.aid = vs.aid - WHERE vs.chid =}, \$vn->{chid} - }, $chars; - - my %done; - my %roles = map { - my $r = $_; - ($r, [ grep grep($_->{role} eq $r, @{$_->{releases}}) && !$done{$_->{id}}++, @$chars ]); - } keys %CHAR_ROLE; - - my($first_char) = map @{$roles{$_}} ? $roles{$_}[0]{id} : (), keys %CHAR_ROLE; - - Div class => 'section', id => 'characters', sub { - H2 class => 'section__title', sub { Txt 'Characters'; Debug \%roles }; - Div class => 'character-browser', sub { - Div class => 'row', sub { - Div class => 'fixed-size-left-sidebar-md', sub { - Div class => 'character-browser__top-items', sub { CharacterList $vn, \%roles, $first_char; } - }; - Div class => 'col-md col-md--3 d-none d-md-block', sub { - Div mkclass(character => 1, 'd-none' => $_->{id} != $first_char), 'data-character' => $_->{id}, - sub { CharacterInfo $_ } - for @$chars; - }; - }; - }; - }; -} - - -sub Stats { - my $vn = shift; - - my($has_data, $Dist) = VoteGraph v => $vn->{id}; - return if !$has_data; - - my $recent_votes = tuwf->dbAlli(q{ - SELECT v.vid, v.vote,}, sql_totime('v.date'), q{AS date, u.id, u.username - FROM votes v JOIN users u ON u.id = v.uid - WHERE NOT EXISTS(SELECT 1 FROM users_prefs WHERE uid = u.id AND key = 'hide_list') - AND NOT u.ign_votes - AND v.vid =}, \$vn->{id}, q{ - ORDER BY v.date DESC LIMIT 10 - }); - my $Recent = sub { - H4 'Recent votes'; - Div class => 'recent-votes', sub { - Table class => 'recent-votes__table tabular-numbs', sub { - Tbody sub { - Tr sub { - Td sub { A href => "/u$_->{id}", $_->{username}; }; - Td vote_display $_->{vote}; - Td date_display $_->{date}; - } for @$recent_votes; - }; - }; - Div class => 'final-text', sub { - A href => "/v$vn->{id}/votes", 'All votes'; - }; - }; - }; - - - my $popularity_rank = tuwf->dbVali( - 'SELECT COUNT(*)+1 FROM vn WHERE NOT hidden AND c_popularity >', - \($vn->{c_popularity}||0) - ); - my $rating_rank = tuwf->dbVali( - 'SELECT COUNT(*)+1 FROM vn WHERE NOT hidden AND c_rating >', - \($vn->{c_rating}||0) - ); - - my $Popularity = sub { - H4 'Ranking'; - Dl class => 'stats__ranking', sub { - Dt 'Popularity'; - Dd sprintf 'ranked #%d with a score of %.2f', $popularity_rank, 100*($vn->{c_popularity}||0); - Dt 'Bayesian rating'; - Dd sprintf 'ranked #%d with a rating of %.2f', $rating_rank, $vn->{c_rating}/10; - }; - Div class => 'final-text', sub { - A href => '/v/all', 'See best rated games'; - }; - }; - - - Div class => 'section stats', id => 'stats', sub { - H2 class => 'section__title', 'Stats'; - Div class => 'row semi-muted', sub { - Div class => 'stats__col col-md col-md-1', sub { - H4 'Vote distribution'; - $Dist->(); - }; - Div class => 'stats__col col-md col-md-1', $Recent if @$recent_votes; - Div class => 'stats__col col-md col-md-1', $Popularity; - }; - }; -} - - -sub Contents { - my $vn = shift; - - Div class => 'vn-page', sub { - Div class => 'row', sub { - Div class => 'col-md', sub { - Div class => 'row', sub { - Div class => 'fixed-size-left-sidebar-md vn-page__top-details', sub { Sidebar $vn }; - Div class => 'fixed-size-left-sidebar-md', ''; - Div class => 'col-md', sub { - Div class => 'description serif', id => 'about', sub { - P sub { Lit bb2html $vn->{desc}||'No description.' }; - }; - Div class => 'section', id => 'tags', sub { - Div class => 'tag-summary', sub { Tags $vn }; - }; - Div class => 'section', id => 'releases', sub { - H2 class => 'section__title', 'Releases'; - Div class => 'relsm', sub { Releases $vn }; - }; - Staff $vn; - Gallery $vn; - }; - }; - }; - }; - Div class => 'row', sub { - Div class => 'col-xxl', sub { - Characters $vn; - Stats $vn; - }; - }; - }; -} - - -TUWF::get qr{/$VREV_RE}, sub { - my $vn = entry v => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound; - return tuwf->resNotFound if !$vn->{id} || $vn->{hidden}; - - enrich id => q{SELECT id, rgraph, c_languages::text[], c_popularity, c_rating, c_votecount FROM vn WHERE id IN}, $vn; - enrich scr => q{SELECT id AS scr, width, height FROM screenshots WHERE id IN}, $vn->{screenshots}; - enrich vid => q{SELECT id AS vid, title, original FROM vn WHERE id IN}, $vn->{relations}; - enrich aid => q{SELECT aid, id, name, original FROM staff_alias WHERE aid IN}, $vn->{staff}; - - enrich_list releases => id => vid => sub {sql q{ - SELECT rv.vid, r.id, r.title, r.original, r.type, r.website, r.released, r.notes, - r.minage, r.patch, r.freeware, r.doujin, r.resolution, r.voiced, r.ani_story, r.ani_ero - FROM releases r - JOIN releases_vn rv ON r.id = rv.id - WHERE NOT r.hidden AND rv.vid IN}, $_[0], q{ - ORDER BY r.released - }}, $vn; - - enrich_list1 platforms => id => id => 'SELECT id, platform FROM releases_platforms WHERE id IN', $vn->{releases}; - enrich_list1 lang => id => id => 'SELECT id, lang FROM releases_lang WHERE id IN', $vn->{releases}; - enrich_list media => id => id => 'SELECT id, medium, qty FROM releases_media WHERE id IN', $vn->{releases}; - - Framework - og => { - description => bb2text($vn->{desc}), - $vn->{image} && !$vn->{img_nsfw} ? ( - image => tuwf->imgurl(cv => $vn->{image}) - ) : (($_) = grep !$_->{nsfw}, @{$vn->{screenshots}}) ? ( - image => tuwf->imgurl(st => $_->{scr}) - ) : () - }, - title => $vn->{title}, - top => sub { Top $vn }, - sub { Contents $vn }; -}; - -1; |