diff options
Diffstat (limited to 'lib/VNWeb/VN/List.pm')
-rw-r--r-- | lib/VNWeb/VN/List.pm | 348 |
1 files changed, 256 insertions, 92 deletions
diff --git a/lib/VNWeb/VN/List.pm b/lib/VNWeb/VN/List.pm index 8bbfe858..42891f81 100644 --- a/lib/VNWeb/VN/List.pm +++ b/lib/VNWeb/VN/List.pm @@ -7,75 +7,177 @@ use VNWeb::Images::Lib; use VNWeb::ULists::Lib; use VNWeb::TT::Lib 'tagscore_'; -# Returns the tableopts config for this VN list (0) or the VN listing on tags (1). +# Returns the tableopts config for: +# - this VN list ('vn') +# - this VN list with a search query ('vns') +# - the VN listing on tags ('tags') +# - a user's VN list ('ulist') +# The latter has different numeric identifiers, a sad historical artifact. :( sub TABLEOPTS { - my($tags) = @_; - tableopts _pref => $tags ? 'tableopts_vt' : 'tableopts_v', + my $tags = $_[0] eq 'tags'; + my $vns = $_[0] eq 'vns'; + my $vn = $vns || $_[0] eq 'vn'; + my $ulist = $_[0] eq 'ulist'; + die if !$tags && !$vn && !$ulist; + + # Old popularity column: + # sort_id => $ulist ? 14 : 3, + # vis_id => $ulist ? 11 : 0, + tableopts + _pref => $tags ? 'tableopts_vt' : $vn ? 'tableopts_v' : undef, _views => ['rows', 'cards', 'grid'], $tags ? (tagscore => { name => 'Tag score', compat => 'tagscore', sort_id => 0, - sort_sql => 'tvi.rating ?o, v.title', - sort_default => 'desc' + sort_sql => 'tvi.rating ?o, v.sorttitle', + sort_default => 'desc', + sort_num => 1, + }) : (), + $vns ? (qscore => { + name => 'Relevance', + sort_id => 0, + sort_sql => 'sc.score !o, v.sorttitle', + sort_default => 'asc', + sort_num => 1, }) : (), title => { name => 'Title', compat => 'title', - sort_id => 1, - sort_sql => 'v.title', - sort_default => $tags ? undef : 'asc', + sort_id => $ulist ? 0 : 1, + sort_sql => 'v.sorttitle', }, + $ulist ? ( + voted => { + name => 'Vote date', + sort_sql => 'uv.vote_date', + sort_id => 1, + sort_num => 1, + vis_id => 0, + compat => 'voted' + }, + vote => { + name => 'Vote', + sort_sql => 'uv.vote', + sort_id => 2, + sort_num => 1, + vis_id => 1, + compat => 'vote' + }, + label => { + name => 'Labels', + sort_sql => sql('ARRAY(SELECT ul.label FROM unnest(uv.labels) l(id) JOIN ulist_labels ul ON ul.id = l.id WHERE ul.uid = uv.uid AND l.id <> ', \7, ')'), + sort_id => 4, + vis_id => 3, + compat => 'label' + }, + added => { + name => 'Added', + sort_sql => 'uv.added', + sort_id => 5, + sort_num => 1, + vis_id => 4, + compat => 'added' + }, + modified => { + name => 'Modified', + sort_sql => 'uv.lastmod', + sort_id => 6, + sort_num => 1, + vis_id => 5, + compat => 'modified' + }, + started => { + name => 'Start date', + sort_sql => 'uv.started', + sort_id => 7, + sort_num => 1, + vis_id => 6, + compat => 'started' + }, + finished => { + name => 'Finish date', + sort_sql => 'uv.finished', + sort_id => 8, + sort_num => 1, + vis_id => 7, + compat => 'finished' + }, + ) : (), released => { name => 'Release date', compat => 'rel', - sort_id => 2, + sort_id => $ulist ? 9 : 2, sort_sql => 'v.c_released ?o, v.title', + sort_num => 1, + vis_id => $ulist ? 8 : undef, + }, + length => { + name => 'Length', + vis_id => $ulist ? 9 : 4, }, developer => { name => 'Developer', - vis_id => 2, - }, - popularity => { - name => 'Popularity score', - compat => 'pop', - sort_id => 3, - sort_sql => 'v.c_popularity ?o NULLS LAST, v.title', - vis_id => 0, - vis_default => 1, + vis_id => $ulist ? 10 : 2, }, rating => { name => 'Bayesian rating', compat => 'rating', - sort_id => 4, - sort_sql => 'v.c_rating ?o NULLS LAST, v.title', - vis_id => 1, + sort_id => $ulist ? 11 : 4, + sort_sql => 'v.c_rat_rank !o NULLS LAST, v.c_votecount ?o, v.sorttitle', + sort_num => 1, + vis_id => $ulist ? 12 : 1, vis_default => 1, }, average => { name => 'Vote average', - sort_id => 5, - sort_sql => 'v.c_average ?o NULLS LAST, v.title', - vis_id => 3, + sort_id => $ulist ? 12 : 5, + sort_sql => 'v.c_average ?o NULLS LAST, v.c_votecount ?o, v.sorttitle', + sort_num => 1, + vis_id => $ulist ? 13 : 3, }, votes => { name => 'Number of votes', - sort_id => 6, - sort_sql => 'v.c_votecount ?o, v.title', - } + sort_id => $ulist ? 13 : 6, + sort_sql => 'v.c_votecount ?o, v.sorttitle', + sort_num => 1, + sort_default => $tags || $vns ? undef : 'desc', + }, + id => { + name => $ulist ? 'VN entry added' : 'Date added', + sort_id => 10, + sort_sql => 'v.id', + sort_num => 1, + }; } -my $TABLEOPTS = TABLEOPTS 0; +my $TABLEOPTS = TABLEOPTS 'vn'; +my $TABLEOPTS_Q = TABLEOPTS 'vns'; + +sub len_ { + my($v) = @_; + if ($v->{c_lengthnum}) { + vnlength_ $v->{c_length}; + small_ " ($v->{c_lengthnum})"; + } elsif($v->{length}) { + txt_ $VN_LENGTH{$v->{length}}{txt}; + } +} # Also used by VNWeb::TT::TagPage sub listing_ { - my($opt, $list, $count, $tagscore) = @_; + my($opt, $list, $count, $tagscore, $labels) = @_; my sub url { '?'.query_encode %$opt, @_ } - paginate_ \&url, $opt->{p}, [$count, $opt->{s}->results], 't', sub { $opt->{s}->elm_ }; + paginate_ \&url, $opt->{p}, [$count, $opt->{s}->results], 't', $opt->{s}; - div_ class => 'mainbox browse vnbrowse', sub { + my sub votesort { + txt_ ' ('; + sortable_ 'votes', $opt, \&url, 0; + txt_ ')' + } + article_ class => 'browse vnbrowse', sub { table_ class => 'stripe', sub { thead_ sub { tr_ sub { td_ class => 'tc_score', sub { txt_ 'Score'; sortable_ 'tagscore', $opt, \&url } if $tagscore; @@ -85,30 +187,36 @@ sub listing_ { td_ class => 'tc_plat', ''; td_ class => 'tc_lang', ''; td_ class => 'tc_rel', sub { txt_ 'Released'; sortable_ 'released', $opt, \&url }; - td_ class => 'tc_pop', sub { txt_ 'Popularity'; sortable_ 'popularity', $opt, \&url } if $opt->{s}->vis('popularity'); - td_ class => 'tc_rating',sub { txt_ 'Rating'; sortable_ 'rating', $opt, \&url } if $opt->{s}->vis('rating'); - td_ class => 'tc_average',sub{ txt_ 'Average'; sortable_ 'average', $opt, \&url } if $opt->{s}->vis('average'); + td_ class => 'tc_length',sub { txt_ 'Length'; } if $opt->{s}->vis('length'); + td_ class => 'tc_rating', sub { + txt_ 'Rating'; sortable_ 'rating', $opt, \&url; + votesort(); + } if $opt->{s}->vis('rating'); + td_ class => $opt->{s}->vis('rating') ? 'tc_average' : 'tc_rating', sub { + txt_ 'Average'; sortable_ 'average', $opt, \&url; + votesort() if !$opt->{s}->vis('rating'); + } if $opt->{s}->vis('average'); } }; tr_ sub { td_ class => 'tc_score', sub { tagscore_ $_->{tagscore} } if $tagscore; td_ class => 'tc_ulist', sub { ulists_widget_ $_ } if auth; - td_ class => 'tc_title', sub { a_ href => "/$_->{id}", title => $_->{original}||$_->{title}, $_->{title} }; + td_ class => 'tc_title', sub { a_ href => "/$_->{id}", tattr $_ }; td_ class => 'tc_dev', sub { join_ ' & ', sub { - a_ href => "/$_->{id}", title => $_->{original}||$_->{name}, $_->{name}; - }, sort { $a->{name} cmp $b->{name} || $a->{id} <=> $b->{id} } $_->{developers}->@*; + a_ href => "/$_->{id}", tattr $_; + }, $_->{developers}->@*; } if $opt->{s}->vis('developer'); td_ class => 'tc_plat', sub { join_ '', sub { platform_ $_ if $_ ne 'unk' }, sort $_->{platforms}->@* }; - td_ class => 'tc_lang', sub { join_ '', sub { abbr_ class => "icons lang $_", title => $LANGUAGE{$_}, '' }, reverse sort $_->{lang}->@* }; + td_ class => 'tc_lang', sub { join_ '', sub { abbr_ class => "icon-lang-$_", title => $LANGUAGE{$_}{txt}, '' }, reverse sort $_->{lang}->@* }; td_ class => 'tc_rel', sub { rdate_ $_->{c_released} }; - td_ class => 'tc_pop', sprintf '%.2f', ($_->{c_popularity}||0)/100 if $opt->{s}->vis('popularity'); + td_ class => 'tc_length',sub { len_ $_ } if $opt->{s}->vis('length'); td_ class => 'tc_rating',sub { - txt_ sprintf '%.2f', ($_->{c_rating}||0)/100; - b_ class => 'grayedout', sprintf ' (%d)', $_->{c_votecount}; + txt_ $_->{c_rating} ? sprintf '%.2f', $_->{c_rating}/100 : '-'; + small_ sprintf ' (%d)', $_->{c_votecount}; } if $opt->{s}->vis('rating'); td_ class => 'tc_average',sub { - txt_ sprintf '%.2f', ($_->{c_average}||0)/100; - b_ class => 'grayedout', sprintf ' (%d)', $_->{c_votecount} if !$opt->{s}->vis('rating'); + txt_ $_->{c_average} ? sprintf '%.2f', $_->{c_average}/100 : '-'; + small_ sprintf ' (%d)', $_->{c_votecount} if !$opt->{s}->vis('rating'); } if $opt->{s}->vis('average'); } for @$list; } @@ -118,20 +226,22 @@ sub listing_ { my sub infoblock_ { my($canlink) = @_; # grid contains an outer <a>, so may not contain links itself. my sub lnk_ { - my($url, $title, $label) = @_; - a_ href => $url, title => $title, $label if $canlink; - span_ $label if !$canlink; + my($url, @attr) = @_; + a_ href => $url, @attr if $canlink; + span_ @attr if !$canlink; + } + lnk_ "/$_->{id}", tattr $_; + if(!$labels || $opt->{s}->vis('released')) { + br_; + join_ '', sub { platform_ $_ if $_ ne 'unk' }, sort $_->{platforms}->@*; + join_ '', sub { abbr_ class => "icon-lang-$_", title => $LANGUAGE{$_}{txt}, '' }, reverse sort $_->{lang}->@*; + rdate_ $_->{c_released}; } - lnk_ "/$_->{id}", $_->{original}||$_->{title}, $_->{title}; - br_; - join_ '', sub { platform_ $_ if $_ ne 'unk' }, sort $_->{platforms}->@*; - join_ '', sub { abbr_ class => "icons lang $_", title => $LANGUAGE{$_}, '' }, reverse sort $_->{lang}->@*; - rdate_ $_->{c_released}; if($opt->{s}->vis('developer')) { br_; join_ ' & ', sub { - lnk_ "/$_->{id}", $_->{original}||$_->{name}, $_->{name}; - }, sort { $a->{name} cmp $b->{name} || $a->{id} <=> $b->{id} } $_->{developers}->@*; + lnk_ "/$_->{id}", tattr $_; + }, $_->{developers}->@*; } table_ sub { tr_ sub { @@ -139,27 +249,58 @@ sub listing_ { td_ sub { tagscore_ $_->{tagscore} }; } if $tagscore; tr_ sub { - td_ 'Popularity:'; - td_ sprintf '%.2f', ($_->{c_popularity}||0)/100; - } if $opt->{s}->vis('popularity'); + td_ 'Length'; + td_ sub { len_ $_ }; + } if $opt->{s}->vis('length'); + tr_ sub { + td_ $opt->{s}->vis('vote') ? 'Vote:' : 'Voted:'; + td_ sub { + txt_ fmtvote $_->{vote} if $opt->{s}->vis('vote'); + txt_ ' on '.($_->{vote_date} ? fmtdate $_->{vote_date}, 'compact' : '-') if $opt->{s}->vis('voted'); + } + } if $opt->{s}->vis('vote') || $opt->{s}->vis('voted'); + tr_ sub { + td_ 'Labels:'; + td_ sub { + my %labels = map +($_,1), $_->{labels}->@*; + my @l = grep $labels{$_->{id}} && $_->{id} != 7, @$labels; + txt_ @l ? join ', ', map $_->{label}, @l : '-'; + }; + } if $opt->{s}->vis('label'); + tr_ sub { + td_ 'Added on:'; + td_ fmtdate $_->{added}, 'compact'; + } if $opt->{s}->vis('added'); + tr_ sub { + td_ 'Modified on:'; + td_ fmtdate $_->{lastmod}, 'compact'; + } if $opt->{s}->vis('modified'); + tr_ sub { + td_ 'Started:'; + td_ $_->{started}||'-'; + } if $opt->{s}->vis('started'); + tr_ sub { + td_ 'Finished:'; + td_ $_->{finished}||'-'; + } if $opt->{s}->vis('finished'); tr_ sub { td_ 'Rating:'; td_ sub { - txt_ sprintf '%.2f', ($_->{c_rating}||0)/100; - b_ class => 'grayedout', sprintf ' (%d)', $_->{c_votecount}; + txt_ $_->{c_rating} ? sprintf '%.2f', $_->{c_rating}/100 : '-'; + small_ sprintf ' (%d)', $_->{c_votecount}; }; } if $opt->{s}->vis('rating'); tr_ sub { td_ 'Average:'; td_ sub { - txt_ sprintf '%.2f', ($_->{c_average}||0)/100; - b_ class => 'grayedout', sprintf ' (%d)', $_->{c_votecount} if !$opt->{s}->vis('rating'); + txt_ $_->{c_average} ? sprintf '%.2f', $_->{c_average}/100 : ''; + small_ sprintf ' (%d)', $_->{c_votecount} if !$opt->{s}->vis('rating'); }; } if $opt->{s}->vis('average'); } } - div_ class => 'mainbox vncards', sub { + article_ class => 'vncards', sub { my($w,$h) = (90,120); div_ sub { div_ sub { @@ -177,10 +318,10 @@ sub listing_ { } for @$list; } if $opt->{s}->cards; - div_ class => 'mainbox vngrid', sub { + article_ class => 'vngrid', sub { div_ !$_->{image} || image_hidden($_->{image}) ? (class => 'noimage') : (style => 'background-image: url("'.imgurl($_->{image}{id}).'")'), sub { ulists_widget_ $_; - a_ href => "/$_->{id}", title => $_->{original}||$_->{title}, sub { infoblock_ 0 }; + a_ href => "/$_->{id}", title => $_->{title}[3], sub { infoblock_ 0 }; } for @$list; } if $opt->{s}->grid; @@ -189,34 +330,35 @@ sub listing_ { # Enrich some extra fields fields needed for listing_() -# Also used by VNWeb::TT::TagPage +# Also used by TT::TagPage and UList::List sub enrich_listing { - my $opt = shift; + my($widget, $opt, @lst) = @_; - enrich developers => id => vid => sub { - 'SELECT v.id AS vid, p.id, p.name, p.original - FROM vn v, unnest(v.c_developers) vp(id), producers p - WHERE p.id = vp.id AND v.id IN', $_[0], 'ORDER BY p.name, p.id' - }, @_ if $opt->{s}->vis('developer'); + enrich developers => id => vid => sub { sql + 'SELECT v.id AS vid, p.id, p.title + FROM vn v, unnest(v.c_developers) vp(id),', producerst, 'p + WHERE p.id = vp.id AND v.id IN', $_[0], 'ORDER BY p.sorttitle, p.id' + }, @lst if $opt->{s}->vis('developer'); - enrich_image_obj image => @_ if !$opt->{s}->rows; - enrich_ulists_widget @_; + enrich_image_obj image => @lst if !$opt->{s}->rows; + enrich_ulists_widget @lst if $widget; } TUWF::get qr{/v(?:/(?<char>all|[a-z0]))?}, sub { my $opt = tuwf->validate(get => - q => { onerror => undef }, - sq=> { onerror => undef }, + q => { searchquery => 1 }, + sq=> { searchquery => 1 }, p => { upage => 1 }, f => { advsearch_err => 'v' }, - s => { tableopts => $TABLEOPTS }, ch=> { onerror => [], type => 'array', scalar => 1, values => { onerror => undef, enum => ['0', 'a'..'z'] } }, - fil => { required => 0 }, - rfil => { required => 0 }, - cfil => { required => 0 }, + fil => { onerror => '' }, + rfil => { onerror => '' }, + cfil => { onerror => '' }, )->data; - $opt->{q} //= $opt->{sq}; + $opt->{q} = $opt->{sq} if !$opt->{q}; + $opt->{s} = tuwf->validate(get => s => { tableopts => $opt->{q} ? $TABLEOPTS_Q : $TABLEOPTS })->data; + $opt->{s} = $opt->{s}->sort_param(qscore => 'a') if $opt->{q} && tuwf->reqGet('sb'); $opt->{ch} = $opt->{ch}[0]; # compat with old URLs @@ -243,41 +385,63 @@ TUWF::get qr{/v(?:/(?<char>all|[a-z0]))?}, sub { my $where = sql_and 'NOT v.hidden', $opt->{f}->sql_where(), - $opt->{q} ? map sql('v.c_search LIKE', \"%$_%"), normalize_query $opt->{q} : (), - defined($opt->{ch}) && $opt->{ch} ? sql('LOWER(SUBSTR(v.title, 1, 1)) =', \$opt->{ch}) : (), - defined($opt->{ch}) && !$opt->{ch} ? sql('(ASCII(v.title) <', \97, 'OR ASCII(v.title) >', \122, ') AND (ASCII(v.title) <', \65, 'OR ASCII(v.title) >', \90, ')') : (); + defined($opt->{ch}) ? sql 'match_firstchar(v.sorttitle, ', \$opt->{ch}, ')' : (); my $time = time; my($count, $list); db_maytimeout { - $count = tuwf->dbVali('SELECT count(*) FROM vn v WHERE', $where); + $count = tuwf->dbVali('SELECT count(*) FROM', vnt, 'v WHERE', sql_and $where, $opt->{q}->sql_where('v', 'v.id')); $list = $count ? tuwf->dbPagei({results => $opt->{s}->results(), page => $opt->{p}}, ' - SELECT v.id, v.title, v.original, v.c_released, v.c_popularity, v.c_votecount, v.c_rating, v.c_average - , v.image, v.c_platforms::text[] AS platforms, v.c_languages::text[] AS lang - FROM vn v + SELECT v.id, v.title, v.c_released, v.c_votecount, v.c_rating, v.c_average + , v.image, v.c_platforms::text[] AS platforms, v.c_languages::text[] AS lang', + $opt->{s}->vis('length') ? ', v.length, v.c_length, v.c_lengthnum' : (), ' + FROM', vnt, 'v', $opt->{q}->sql_join('v', 'v.id'), ' WHERE', $where, ' ORDER BY', $opt->{s}->sql_order(), ) : []; } || (($count, $list) = (undef, [])); - return tuwf->resRedirect("/$list->[0]{id}") if $count && $count == 1 && $opt->{q} && !defined $opt->{ch}; + my $fullq = join '', $opt->{q}->words->@*; + my $other = length $fullq && $opt->{s}->sorted('qscore') && $opt->{p} == 1 ? tuwf->dbAlli(" + SELECT x.id, i.title + FROM ( + SELECT DISTINCT id + FROM search_cache + WHERE NOT (id BETWEEN 'v1' AND vndbid_max('v')) + AND NOT (id BETWEEN 'r1' AND vndbid_max('r')) + AND label =", \$fullq, ') x, + ', item_info('id', 'null'), 'i + WHERE NOT i.hidden + ORDER BY vndbid_type(x.id) DESC, i.title[1+1] + ') : []; + + return tuwf->resRedirect("/$list->[0]{id}", 'temp') if $count && $count == 1 && $opt->{p} == 1 && $opt->{q} && !defined $opt->{ch} && !@$other; - enrich_listing($opt, $list); + enrich_listing(1, $opt, $list); $time = time - $time; framework_ title => 'Browse visual novels', sub { form_ action => '/v', method => 'get', sub { - div_ class => 'mainbox', sub { + article_ sub { h1_ 'Browse visual novels'; - searchbox_ v => $opt->{q}//''; + searchbox_ v => $opt->{q}; p_ class => 'browseopts', sub { button_ type => 'submit', name => 'ch', value => ($_//''), ($_//'') eq ($opt->{ch}//'') ? (class => 'optselected') : (), !defined $_ ? 'ALL' : $_ ? uc $_ : '#' for (undef, 'a'..'z', 0); }; input_ type => 'hidden', name => 'ch', value => $opt->{ch}//''; - $opt->{f}->elm_; - advsearch_msg_ $count, $time; + $opt->{f}->elm_($count, $time); }; + article_ sub { + h1_ 'Did you mean to search for...'; + ul_ style => 'column-width: 250px', sub { + li_ sub { + strong_ {qw/r Release p Producer c Character s Staff g Tag i Trait/}->{substr $_->{id}, 0, 1}; + txt_ ': '; + a_ href => "/$_->{id}", tattr $_; + } for @$other; + }; + } if @$other; listing_ $opt, $list, $count if $count; }; }; |