diff options
Diffstat (limited to 'lib/VNWeb/Releases')
-rw-r--r-- | lib/VNWeb/Releases/DRM.pm | 120 | ||||
-rw-r--r-- | lib/VNWeb/Releases/Edit.pm | 139 | ||||
-rw-r--r-- | lib/VNWeb/Releases/Elm.pm | 28 | ||||
-rw-r--r-- | lib/VNWeb/Releases/Engines.pm | 4 | ||||
-rw-r--r-- | lib/VNWeb/Releases/Lib.pm | 126 | ||||
-rw-r--r-- | lib/VNWeb/Releases/List.pm | 37 | ||||
-rw-r--r-- | lib/VNWeb/Releases/Page.pm | 197 | ||||
-rw-r--r-- | lib/VNWeb/Releases/VNTab.pm | 37 |
8 files changed, 512 insertions, 176 deletions
diff --git a/lib/VNWeb/Releases/DRM.pm b/lib/VNWeb/Releases/DRM.pm new file mode 100644 index 00000000..7ac7add3 --- /dev/null +++ b/lib/VNWeb/Releases/DRM.pm @@ -0,0 +1,120 @@ +package VNWeb::Releases::DRM; + +use VNWeb::Prelude; +use TUWF 'uri_escape'; + +TUWF::get '/r/drm', sub { + my $opt = tuwf->validate(get => + n => { onerror => '' }, + s => { onerror => '' }, + t => { onerror => undef, enum => [0,1,2] }, + u => { anybool => 1 }, + )->data; + my $where = sql_and + $opt->{s} ? sql 'name ILIKE', \('%'.sql_like($opt->{s}).'%') : (), + defined $opt->{t} ? sql 'state =', \$opt->{t} : (); + + my $lst = tuwf->dbAlli(' + SELECT id, state, name, description, c_ref, ', sql_comma(keys %DRM_PROPERTY), ' + FROM drm + WHERE', $where, $opt->{u} ? () : 'AND c_ref > 0', + 'ORDER BY c_ref DESC + '); + my $missing = $opt->{u} ? 0 : tuwf->dbVali('SELECT COUNT(*) FROM drm WHERE', $where, 'AND c_ref = 0'); + + framework_ title => 'List of DRM implementations', sub { + article_ sub { + h1_ 'List of DRM implementations'; + form_ action => '/r/drm', method => 'get', sub { + fieldset_ class => 'search', sub { + input_ type => 'text', name => 's', id => 's', class => 'text', value => $opt->{s}; + input_ type => 'submit', class => 'submit', value => 'Search!'; + } + }; + my sub opt_ { + my($k,$v,$lbl) = @_; + a_ href => '?'.query_encode(%$opt,$k=>$v), defined $opt->{$k} eq defined $v && (!defined $v || $opt->{$k} == $v) ? (class => 'optselected') : (), $lbl; + } + p_ class => 'browseopts', sub { + a_ href => '?'.query_encode(%$opt,t=>undef), !defined $opt->{t} ? (class => 'optselected') : (), 'All'; + a_ href => '?'.query_encode(%$opt,t=>0), defined $opt->{t} && $opt->{t} == 0 ? (class => 'optselected') : (), 'New'; + a_ href => '?'.query_encode(%$opt,t=>1), defined $opt->{t} && $opt->{t} == 1 ? (class => 'optselected') : (), 'Approved'; + a_ href => '?'.query_encode(%$opt,t=>2), defined $opt->{t} && $opt->{t} == 2 ? (class => 'optselected') : (), 'Deleted'; + }; + my $unused = 0; + section_ class => 'drmlist', sub { + my $d = $_; + h2_ !$d->{c_ref} && !$unused++ ? (id => 'unused') : (), sub { + span_ class => 'strikethrough', $d->{name} if $d->{state} == 2; + txt_ $d->{name} if $d->{state} != 2; + a_ href => '/r?f='.tuwf->compile({advsearch => 'r'})->validate(['drm','=',$d->{name}])->data->query_encode, " ($d->{c_ref})"; + b_ ' (new)' if $d->{state} == 0; + a_ href => "/r/drm/edit/$d->{id}?ref=".uri_escape(query_encode(%$opt)), ' edit' if auth->permDbmod; + }; + my @prop = grep $d->{$_}, keys %DRM_PROPERTY; + p_ sub { + join_ ' ', sub { + abbr_ class => "icon-drm-$_", title => $DRM_PROPERTY{$_}, ''; + txt_ $DRM_PROPERTY{$_}; + }, @prop; + if (!@prop) { + abbr_ class => 'icon-drm-free', title => 'DRM-free', ''; + txt_ 'DRM-free'; + } + }; + div_ sub { lit_ bb_format $d->{description} if $d->{description} }; + } for @$lst; + p_ class => 'center', sub { + txt_ "$missing unused DRM type(s) not shown. "; + a_ href => '?'.query_encode(%$opt,u=>1).'#unused', 'Show all'; + } if $missing; + }; + }; +}; + + +my $FORM = form_compile any => { + id => { uint => 1 }, + state => { uint => 1, range => [0,2] }, + name => { sl => 1, maxlength => 128 }, + description => { default => '', maxlength => 10240 }, + ref => { default => '' }, + map +($_,{anybool=>1}), keys %DRM_PROPERTY +}; + + +sub info_ { + tuwf->dbRowi(' + SELECT id, state, name, description,', sql_comma(keys %DRM_PROPERTY), ' + FROM drm WHERE id =', \shift + ); +} + +TUWF::get qr{/r/drm/edit/(0|$RE{num})}, sub { + return tuwf->resDenied if !auth->permDbmod; + my $d = info_ tuwf->capture(1); + return tuwf->resNotFound if !defined $d->{id}; + $d->{ref} = tuwf->reqGet('ref'); + framework_ title => "Edit DRM: $d->{name}", sub { + div_ widget(DRMEdit => $FORM, $d), ''; + }; +}; + +js_api DRMEdit => $FORM, sub { + my $data = shift; + return tuwf->resDenied if !auth->permDbmod; + my $d = info_ delete $data->{id}; + return tuwf->resNotFound if !defined $d->{id}; + my $ref = delete $data->{ref}; + + return +{ _er => 'Duplicate DRM name' } + if tuwf->dbVali('SELECT 1 FROM drm WHERE id <>', \$d->{id}, 'AND name =', \$d->{name}); + + tuwf->dbExeci('UPDATE drm SET', $data, 'WHERE id =', \$d->{id}); + + my @diff = grep $d->{$_} ne $data->{$_}, qw/state name description/, keys %DRM_PROPERTY; + auth->audit(undef, 'drm edit', join '; ', map "$_: $d->{$_} -> $data->{$_}", @diff) if @diff; + +{ _redir => "/r/drm?$ref" }; +}; + +1; diff --git a/lib/VNWeb/Releases/Edit.pm b/lib/VNWeb/Releases/Edit.pm index 1c44ffb3..b004b7e1 100644 --- a/lib/VNWeb/Releases/Edit.pm +++ b/lib/VNWeb/Releases/Edit.pm @@ -4,36 +4,56 @@ use VNWeb::Prelude; my $FORM = { - id => { required => 0, vndbid => 'r' }, - title => { maxlength => 300 }, - original => { required => 0, default => '', maxlength => 250 }, + id => { default => undef, vndbid => 'r' }, official => { anybool => 1 }, patch => { anybool => 1 }, freeware => { anybool => 1 }, doujin => { anybool => 1 }, - lang => { minlength => 1, sort_keys => 'lang', aoh => { + has_ero => { anybool => 1 }, + titles => { minlength => 1, sort_keys => 'lang', aoh => { lang => { enum => \%LANGUAGE }, mtl => { anybool => 1 }, + title => { default => undef, sl => 1, maxlength => 300 }, + latin => { default => undef, sl => 1, maxlength => 300 }, } }, + # Titles fetched from the VN entry, for auto-filling + vntitles => { _when => 'out', aoh => { + lang => {}, + title => {}, + latin => { default => undef }, + } }, + olang => { enum => \%LANGUAGE, default => 'ja' }, platforms => { aoh => { platform => { enum => \%PLATFORM } } }, media => { aoh => { medium => { enum => \%MEDIUM }, qty => { uint => 1, range => [0,40] }, } }, + drm => { sort_keys => 'name', aoh => { + name => { sl => 1, maxlength => 128 }, + notes => { default => '' }, + description => { default => '', maxlength => 10240 }, + map +($_,{anybool=>1}), keys %DRM_PROPERTY + } }, gtin => { gtin => 1 }, - catalog => { required => 0, default => '', maxlength => 50 }, + catalog => { default => '', sl => 1, maxlength => 50 }, released => { default => 99999999, min => 1, rdate => 1 }, - minage => { required => 0, default => undef, int => 1, enum => \%AGE_RATING }, + minage => { default => undef, int => 1, enum => \%AGE_RATING }, uncensored => { undefbool => 1 }, reso_x => { uint => 1, range => [0,32767] }, reso_y => { uint => 1, range => [0,32767] }, voiced => { uint => 1, enum => \%VOICED }, ani_story => { uint => 1, enum => \%ANIMATED }, ani_ero => { uint => 1, enum => \%ANIMATED }, - website => { required => 0, default => '', weburl => 1 }, - engine => { required => 0, default => '', maxlength => 50 }, - extlinks => validate_extlinks('r'), - notes => { required => 0, default => '', maxlength => 10240 }, + ani_story_sp => { default => undef, uint => 1, range => [0,32767] }, + ani_story_cg => { default => undef, uint => 1, range => [0,32767] }, + ani_cutscene => { default => undef, uint => 1, range => [0,32767] }, + ani_ero_sp => { default => undef, uint => 1, range => [0,32767] }, + ani_ero_cg => { default => undef, uint => 1, range => [0,32767] }, + ani_face => { undefbool => 1 }, + ani_bg => { undefbool => 1 }, + website => { default => '', weburl => 1 }, + engine => { default => '', sl => 1, maxlength => 50 }, + notes => { default => '', maxlength => 10240 }, vn => { sort_keys => 'vid', aoh => { vid => { vndbid => 'v' }, title => { _when => 'out' }, @@ -47,17 +67,14 @@ my $FORM = { } }, hidden => { anybool => 1 }, locked => { anybool => 1 }, - - authmod => { _when => 'out', anybool => 1 }, editsum => { _when => 'in out', editsum => 1 }, + validate_extlinks 'r' }; my $FORM_OUT = form_compile out => $FORM; my $FORM_IN = form_compile in => $FORM; my $FORM_CMP = form_compile cmp => $FORM; -sub to_extlinks { $_[0]{extlinks} = { map +($_, delete $_[0]{$_}), grep /^l_/, keys $_[0]->%* } } - TUWF::get qr{/$RE{rrev}/(?<action>edit|copy)} => sub { my $e = db_entry tuwf->captures('id', 'rev') or return tuwf->resNotFound; @@ -65,46 +82,47 @@ TUWF::get qr{/$RE{rrev}/(?<action>edit|copy)} => sub { return tuwf->resDenied if !can_edit r => $copy ? {} : $e; $e->{editsum} = $copy ? "Copied from $e->{id}.$e->{chrev}" : $e->{chrev} == $e->{maxrev} ? '' : "Reverted to revision $e->{id}.$e->{chrev}"; - $e->{authmod} = auth->permDbmod; - to_extlinks $e; + $e->{titles} = [ sort { $a->{lang} cmp $b->{lang} } $e->{titles}->@* ]; - enrich_merge vid => 'SELECT id AS vid, title FROM vn WHERE id IN', $e->{vn}; - enrich_merge pid => 'SELECT id AS pid, name FROM producers WHERE id IN', $e->{producers}; + $e->{vntitles} = $e->{vn}->@* == 1 ? tuwf->dbAlli('SELECT lang, title, latin FROM vn_titles WHERE id =', \$e->{vn}[0]{vid}) : []; - $e->@{qw/gtin catalog extlinks/} = elm_empty($FORM_OUT)->@{qw/gtin catalog extlinks/} if $copy; + enrich_merge vid => sql('SELECT id AS vid, title[1+1] FROM', vnt, 'v WHERE id IN'), $e->{vn}; + enrich_merge pid => sql('SELECT id AS pid, title[1+1] AS name FROM', producerst, 'p WHERE id IN'), $e->{producers}; + enrich_merge drm => sql('SELECT id AS drm, name FROM drm WHERE id IN'), $e->{drm}; - my $title = ($copy ? 'Copy ' : 'Edit ').$e->{title}; + my @empty_fields = ('gtin', 'catalog', grep /^l_/, keys %$e); + $e->@{@empty_fields} = elm_empty($FORM_OUT)->@{@empty_fields} if $copy; + + my $title = ($copy ? 'Copy ' : 'Edit ').titleprefs_obj($e->{olang}, $e->{titles})->[1]; framework_ title => $title, dbobj => $e, tab => tuwf->capture('action'), sub { editmsg_ r => $e, $title, $copy; - elm_ ReleaseEdit => $FORM_OUT, $copy ? {%$e, id=>undef} : $e; + div_ widget(ReleaseEdit => $FORM_OUT, $copy ? {%$e, id=>undef} : $e), ''; }; }; TUWF::get qr{/$RE{vid}/add}, sub { return tuwf->resDenied if !can_edit r => undef; - my $v = tuwf->dbRowi('SELECT id, title, original FROM vn WHERE id =', \tuwf->capture('id')); + my $v = tuwf->dbRowi('SELECT id, title FROM', vnt, 'v WHERE NOT hidden AND v.id =', \tuwf->capture('id')); return tuwf->resNotFound if !$v->{id}; - my $delrel = tuwf->dbAlli('SELECT r.id, r.title, r.original FROM releases r JOIN releases_vn rv ON rv.id = r.id WHERE r.hidden AND rv.vid =', \$v->{id}, 'ORDER BY id'); - enrich_flatten languages => id => id => 'SELECT id, lang FROM releases_lang WHERE id IN', $delrel; + my $delrel = tuwf->dbAlli('SELECT r.id, r.title FROM', releasest, 'r JOIN releases_vn rv ON rv.id = r.id WHERE r.hidden AND rv.vid =', \$v->{id}, 'ORDER BY id'); + enrich_flatten languages => id => id => 'SELECT id, lang FROM releases_titles WHERE id IN', $delrel; my $e = { elm_empty($FORM_OUT)->%*, - title => $v->{title}, - original => $v->{original}, - vn => [{vid => $v->{id}, title => $v->{title}, rtype => 'complete'}], + vn => [{vid => $v->{id}, title => $v->{title}[1], rtype => 'complete'}], + vntitles => tuwf->dbAlli('SELECT lang, title, latin FROM vn_titles WHERE id =', \$v->{id}), official => 1, }; - $e->{authmod} = auth->permDbmod; - framework_ title => "Add release to $v->{title}", + framework_ title => "Add release to $v->{title}[1]", sub { - editmsg_ r => undef, "Add release to $v->{title}"; + editmsg_ r => undef, "Add release to $v->{title}[1]"; - div_ class => 'mainbox', sub { + article_ sub { h1_ 'Deleted releases'; div_ class => 'warning', sub { p_ q{This visual novel has releases that have been deleted @@ -114,22 +132,22 @@ TUWF::get qr{/$RE{vid}/add}, sub { ul_ sub { li_ sub { txt_ '['.join(',', $_->{languages}->@*)."] $_->{id}:"; - a_ href => "/$_->{id}", title => $_->{original}||$_->{title}, $_->{title}; + a_ href => "/$_->{id}", tattr $_; } for @$delrel; } } } if @$delrel; - elm_ ReleaseEdit => $FORM_OUT, $e; + div_ widget(ReleaseEdit => $FORM_OUT, $e), ''; }; }; -elm_api ReleaseEdit => $FORM_OUT, $FORM_IN, sub { +js_api ReleaseEdit => $FORM_IN, sub { my $data = shift; my $new = !$data->{id}; my $e = $new ? { id => 0 } : db_entry $data->{id} or return tuwf->resNotFound; - return elm_Unauth if !can_edit r => $e; + return tuwf->resDenied if !can_edit r => $e; if(!auth->permDbmod) { $data->{hidden} = $e->{hidden}||0; @@ -139,29 +157,64 @@ elm_api ReleaseEdit => $FORM_OUT, $FORM_IN, sub { if($data->{patch}) { $data->{doujin} = $data->{voiced} = $data->{ani_story} = $data->{ani_ero} = 0; $data->{reso_x} = $data->{reso_y} = 0; + $data->{ani_story_sp} = $data->{ani_story_cg} = $data->{ani_cutscene} = $data->{ani_ero_sp} = $data->{ani_ero_cg} = $data->{ani_face} = $data->{ani_bg} = undef; $data->{engine} = ''; } - - if(!defined $data->{minage} || $data->{minage} != 18) { + if(!$data->{has_ero}) { $data->{uncensored} = undef; $data->{ani_ero} = 0; + $data->{ani_ero_sp} = $data->{ani_ero_cg} = undef; } + ani_compat($data, $e); + + die "No title in main language" if !length [grep $_->{lang} eq $data->{olang}, $data->{titles}->@*]->[0]{title}; $_->{qty} = $MEDIUM{$_->{medium}}{qty} ? $_->{qty}||1 : 0 for $data->{media}->@*; $data->{notes} = bb_subst_links $data->{notes}; die "No VNs selected" if !$data->{vn}->@*; die "Invalid resolution: ($data->{reso_x},$data->{reso_y})" if (!$data->{reso_x} && $data->{reso_y} > 1) || ($data->{reso_x} && !$data->{reso_y}); - to_extlinks $e; - - return elm_Unchanged if !$new && !form_changed $FORM_CMP, $data, $e; + # We need the DRM names for form_changed() + enrich_merge drm => sql('SELECT id AS drm, name FROM drm WHERE id IN'), $e->{drm}; + # And the DRM identifiers to actually save the new form. + enrich_merge name => sql('SELECT name, id AS drm FROM drm WHERE name IN'), $data->{drm}; + for my $d ($data->{drm}->@*) { + $d->{notes} = bb_subst_links $d->{notes}; + $d->{drm} = tuwf->dbVali('INSERT INTO drm', {map +($_,$d->{$_}), 'name', 'description', keys %DRM_PROPERTY}, 'RETURNING id') + if !defined $d->{drm}; + } - $data->{$_} = $data->{extlinks}{$_} for $data->{extlinks}->%*; - delete $data->{extlinks}; + return 'No changes' if !$new && !form_changed $FORM_CMP, $data, $e; my $ch = db_edit r => $e->{id}, $data; - elm_Redirect "/$ch->{nitemid}.$ch->{nrev}"; + +{ _redir => "/$ch->{nitemid}.$ch->{nrev}" }; }; +# Set the old ani_story and ani_ero fields to some sort of value based on the +# new ani_* fields, if they've been changed. +sub ani_compat { + my($r, $old) = @_; + return if !grep +($r->{$_}//'_undef_') ne ($old->{$_}//'_undef_'), + qw{ ani_story_sp ani_story_cg ani_cutscene ani_ero_sp ani_ero_cg ani_face ani_bg }; + + my sub known($) { defined $r->{"ani_$_[0]"} } + my sub hasani($) { $r->{"ani_$_[0]"} && $r->{"ani_$_[0]"} > 1 } + my sub someani($) { hasani $_[0] && ($r->{"ani_$_[0]"} & 512) == 0 } + my sub fullani($) { defined $r->{"ani_$_[0]"} && ($r->{"ani_$_[0]"} & 512) > 0 } + + $r->{ani_story} = + !known 'story_sp' && !known 'story_cg' && !known 'cutscene' ? 0 : + !hasani 'story_sp' && !hasani 'story_cg' && !hasani 'cutscene' ? 1 : + (fullani 'story_sp' || fullani 'story_cg') && !(someani 'story_sp' || someani 'story_cg') ? 4 : 3; + + $r->{ani_ero} = + !known 'ero_sp' && !known 'ero_cg' ? 0 : + !hasani 'ero_sp' && !hasani 'ero_cg' ? 1 : + (fullani 'ero_sp' || fullani 'ero_cg') && !(someani 'ero_sp' || someani 'ero_cg') ? 4 : 3; + + $r->{ani_story} = 2 if $r->{ani_story} < 2 && ($r->{ani_face} || $r->{ani_bg}); +} + + 1; diff --git a/lib/VNWeb/Releases/Elm.pm b/lib/VNWeb/Releases/Elm.pm index 09c9ede2..4abe0b12 100644 --- a/lib/VNWeb/Releases/Elm.pm +++ b/lib/VNWeb/Releases/Elm.pm @@ -26,4 +26,32 @@ elm_api Engines => undef, {}, sub { }); }; + +elm_api DRM => undef, {}, sub { + elm_DRM tuwf->dbAlli(q{ + SELECT name, c_ref AS count FROM drm WHERE c_ref > 0 ORDER BY state = 1+1, c_ref DESC, name + }); +}; + + +js_api Resolutions => {}, sub { + +{ results => [ map +{ id => resolution($_), count => $_->{count} }, tuwf->dbAlli(q{ + SELECT reso_x, reso_y, count(*) AS count FROM releases WHERE NOT hidden AND NOT (reso_x = 0 AND reso_y = 0) + GROUP BY reso_x, reso_y ORDER BY count(*) DESC + })->@* ] }; +}; + + +js_api Engines => {}, sub { + +{ results => tuwf->dbAlli(q{ + SELECT engine AS id, count(*) AS count FROM releases WHERE NOT hidden AND engine <> '' + GROUP BY engine ORDER BY count(*) DESC, engine + }) }; +}; + + +js_api DRM => {}, sub { + +{ results => tuwf->dbAlli('SELECT name AS id, c_ref AS count, state FROM drm ORDER BY state = 1+1, c_ref DESC, name') }; +}; + 1; diff --git a/lib/VNWeb/Releases/Engines.pm b/lib/VNWeb/Releases/Engines.pm index c6e142e2..f5e7e812 100644 --- a/lib/VNWeb/Releases/Engines.pm +++ b/lib/VNWeb/Releases/Engines.pm @@ -14,7 +14,7 @@ TUWF::get qr{/r/engines}, sub { ); framework_ title => 'Engine list', sub { - div_ class => 'mainbox', sub { + article_ sub { h1_ 'Engine list'; p_ sub { lit_ q{ @@ -25,7 +25,7 @@ TUWF::get qr{/r/engines}, sub { }; }; }; - div_ class => 'mainbox browse', sub { + article_ class => 'browse', sub { table_ class => 'stripe', sub { my $c = tuwf->compile({advsearch => 'r'}); tr_ sub { diff --git a/lib/VNWeb/Releases/Lib.pm b/lib/VNWeb/Releases/Lib.pm index 0da2f2a9..6688dedb 100644 --- a/lib/VNWeb/Releases/Lib.pm +++ b/lib/VNWeb/Releases/Lib.pm @@ -3,21 +3,21 @@ package VNWeb::Releases::Lib; use VNWeb::Prelude; use Exporter 'import'; -our @EXPORT = qw/enrich_release_elm releases_by_vn enrich_release release_row_/; +our @EXPORT = qw/enrich_release_elm releases_by_vn enrich_release sort_releases release_row_/; # Enrich a list of releases so that it's suitable as 'Releases' Elm response. # Given objects must have 'id' and 'rtype' fields (appropriate for the VN in context). sub enrich_release_elm { - enrich_merge id => 'SELECT id, title, original, released, reso_x, reso_y FROM releases WHERE id IN', @_; - enrich_flatten lang => id => id => sub { sql('SELECT id, lang FROM releases_lang WHERE id IN', $_, 'ORDER BY lang') }, @_; + enrich_merge id => sql('SELECT id, title[1+1] AS title, title[1+1+1+1] AS alttitle, released, reso_x, reso_y FROM', releasest, 'r WHERE id IN'), @_; + enrich_flatten lang => id => id => sub { sql('SELECT id, lang FROM releases_titles WHERE id IN', $_, 'ORDER BY lang') }, @_; enrich_flatten platforms => id => id => sub { sql('SELECT id, platform FROM releases_platforms WHERE id IN', $_, 'ORDER BY platform') }, @_; } # Return the list of releases associated with a VN in the format suitable as 'Releases' Elm response. sub releases_by_vn { my($id) = @_; - my $l = tuwf->dbAlli('SELECT r.id, rv.rtype FROM releases r JOIN releases_vn rv ON rv.id = r.id WHERE NOT r.hidden AND rv.vid =', \$id, 'ORDER BY r.released, r.title, r.id'); + my $l = tuwf->dbAlli('SELECT r.id, rv.rtype FROM', releasest, 'r JOIN releases_vn rv ON rv.id = r.id WHERE NOT r.hidden AND rv.vid =', \$id, 'ORDER BY r.released, r.sorttitle, r.id'); enrich_release_elm $l; $l } @@ -27,12 +27,31 @@ sub releases_by_vn { # Assumption: Each release already has id, patch, released, gtin and enrich_extlinks(). sub enrich_release { my($r) = @_; - enrich_merge id => 'SELECT id, title, original, notes, minage, official, freeware, doujin, reso_x, reso_y, voiced, ani_story, ani_ero, uncensored FROM releases WHERE id IN', $r; + enrich_merge id => sql( + 'SELECT id, title, olang, notes, minage, official, freeware, has_ero, reso_x, reso_y, voiced, uncensored + , ani_story, ani_ero, ani_story_sp, ani_story_cg, ani_cutscene, ani_ero_sp, ani_ero_cg, ani_face, ani_bg + FROM', releasest, 'r WHERE id IN'), $r; enrich_merge id => sub { sql 'SELECT id, MAX(rtype) AS rtype FROM releases_vn WHERE id IN', $_, 'GROUP BY id' }, grep !$_->{rtype}, ref $r ? @$r : $r; enrich_merge id => sql('SELECT rid as id, status as rlist_status FROM rlists WHERE uid =', \auth->uid, 'AND rid IN'), $r if auth; enrich_flatten platforms => id => id => sub { sql 'SELECT id, platform FROM releases_platforms WHERE id IN', $_, 'ORDER BY id, platform' }, $r; - enrich lang => id => id => sub { 'SELECT id, lang, mtl FROM releases_lang WHERE id IN', $_, 'ORDER BY id, mtl, lang' }, $r; + enrich titles => id => id => sub { 'SELECT id, lang, mtl, title, latin FROM releases_titles WHERE id IN', $_, 'ORDER BY id, mtl, lang' }, $r; enrich media => id => id => sub { 'SELECT id, medium, qty FROM releases_media WHERE id IN', $_, 'ORDER BY id, medium' }, $r; + enrich drm => id => id => sub { 'SELECT r.id, r.drm, r.notes, d.name,', sql_comma(keys %DRM_PROPERTY), 'FROM releases_drm r JOIN drm d ON d.id = r.drm WHERE r.id IN', $_, 'ORDER BY r.id, r.drm' }, $r; +} + + +# Sort an array of releases, assumes the objects come from enrich_release() +# (Not always possible with an SQL ORDER BY due to rtype being context-dependent and platforms coming from other tables) +sub sort_releases { + return [ sort { + $a->{released} <=> $b->{released} || + $b->{rtype} cmp $a->{rtype} || + $b->{official} cmp $a->{official} || + $a->{patch} cmp $b->{patch} || + ($a->{platforms}[0]||'') cmp ($b->{platforms}[0]||'') || + $a->{title}[1] cmp $b->{title}[1] || + idcmp($a->{id}, $b->{id}) + } $_[0]->@* ]; } @@ -41,8 +60,8 @@ sub release_extlinks_ { return if !$r->{extlinks}->@*; if($r->{extlinks}->@* == 1 && $r->{website}) { - a_ href => $r->{extlinks}[0][1], sub { - abbr_ class => 'icons external', title => 'Official website', ''; + a_ href => $r->{extlinks}[0]{url2}, sub { + abbr_ class => 'icon-external', title => 'Official website', ''; }; return } @@ -51,15 +70,15 @@ sub release_extlinks_ { div_ class => 'elm_dd', sub { a_ href => $r->{website}||'#', sub { txt_ scalar $r->{extlinks}->@*; - abbr_ class => 'icons external', title => 'External link', ''; + abbr_ class => 'icon-external', title => 'External link', ''; }; div_ sub { div_ sub { ul_ sub { li_ sub { - a_ href => $_->[1], sub { - span_ $_->[2] if length $_->[2]; - txt_ $_->[0]; + a_ href => $_->{url2}, sub { + span_ $_->{price} if length $_->{price}; + txt_ $_->{label}; } } for $r->{extlinks}->@*; } @@ -72,61 +91,96 @@ sub release_extlinks_ { # Options # id: unique identifier if the same release may be listed on a page twice. -# lang: $lang, whether to display language icons and which language to use for the MTL flag. +# lang: $lang, whether to display language icons and which language to use for the title and MTL flag. # prod: 0/1 whether to display Pub/Dev indication sub release_row_ { my($r, $opt) = @_; - my $mtl = $opt->{lang} - ? [grep $_->{lang} eq $opt->{lang}, $r->{lang}->@*]->[0]{mtl} - : (grep $_->{mtl}, $r->{lang}->@*) == $r->{lang}->@*; + my $lang = $opt->{lang} && (grep $_->{lang} eq $opt->{lang}, $r->{titles}->@*)[0]; + my $mtl = $lang ? $lang->{mtl} : (grep $_->{mtl}, $r->{titles}->@*) == $r->{titles}->@*; + + my $storyani = join "\n", map "$_.", + $r->{ani_story} == 1 ? 'Not animated' : + defined $r->{ani_story_sp} || defined $r->{ani_story_cg} || defined $r->{ani_cutscene} || defined $r->{ani_bg} || defined $r->{ani_face} ? ( + defined $r->{ani_story_sp} ? fmtanimation $r->{ani_story_sp}, 'sprites' : (), + defined $r->{ani_story_cg} ? fmtanimation $r->{ani_story_cg}, 'CGs' : (), + defined $r->{ani_cutscene} ? fmtanimation $r->{ani_cutscene}, 'cutscenes' : (), + defined $r->{ani_bg} ? ($r->{ani_bg} ? 'Animated background effects' : 'No background effects') : (), + defined $r->{ani_face} ? ($r->{ani_face} ? 'Lip and/or eye movement' : 'No facial animations') : (), + ) : $ANIMATED{$r->{ani_story}}{txt}; + + my $eroani = join "\n", map "$_.", + $r->{ani_ero} == 1 ? 'Not animated' : + defined $r->{ani_ero_sp} || defined $r->{ani_ero_cg} ? ( + defined $r->{ani_ero_sp} ? fmtanimation $r->{ani_ero_sp}, 'sprites' : (), + defined $r->{ani_ero_cg} ? fmtanimation $r->{ani_ero_cg}, 'CGs' : (), + ) : $ANIMATED{$r->{ani_ero}}{txt}; my sub icon_ { my($img, $label, $class) = @_; - $class = $class ? " release_icon_$class" : ''; - img_ src => config->{url_static}."/f/$img.svg", class => "release_icons$class", title => $label; + $class = $class ? " icon-rel-$class" : ''; + abbr_ class => "icon-rel-$img$class", title => $label, ''; } my sub icons_ { my($r) = @_; - icon_ 'voiced', $VOICED{$r->{voiced}}{txt}, "voiced$r->{voiced}" if $r->{voiced}; - icon_ 'story_animated', "Story: $ANIMATED{$r->{ani_story}}{txt}", "anim$r->{ani_story}" if $r->{ani_story}; - icon_ 'ero_animated', "Ero: $ANIMATED{$r->{ani_ero}}{txt}", "anim$r->{ani_ero}" if $r->{ani_ero}; - icon_ 'free', 'Freeware' if $r->{freeware}; - icon_ 'nonfree', 'Non-free' if !$r->{freeware}; + icon_ 'notes', bb_format $r->{notes}, text => 1 if $r->{notes}; + icon_ $MEDIUM{ $r->{media}[0]{medium} }{icon}, join ', ', map fmtmedia($_->{medium}, $_->{qty}), $r->{media}->@* if $r->{media}->@*; if($r->{reso_y}) { my $ratio = $r->{reso_x} / $r->{reso_y}; - my $type = $ratio == 4/3 ? '4-3' : $ratio == 16/9 ? '16-9' : 'custom'; + my $type = $ratio == 4/3 ? '43' : $ratio == 16/9 ? '169' : 'custom'; # Ugly workaround: PC-98 has non-square pixels, thus not widescreen - $type = '4-3' if $ratio > 4/3 && grep $_ eq 'p98', $r->{platforms}->@*; - icon_ "resolution_$type", resolution $r; + $type = '43' if $ratio > 4/3 && grep $_ eq 'p98', $r->{platforms}->@*; + icon_ "reso-$type", resolution $r; } - icon_ $MEDIUM{ $r->{media}[0]{medium} }{icon}, join ', ', map fmtmedia($_->{medium}, $_->{qty}), $r->{media}->@* if $r->{media}->@*; - icon_ 'uncensor', 'Uncensored' if $r->{uncensored}; - icon_ 'notes', bb_format $r->{notes}, text => 1 if $r->{notes}; + icon_ 'free', 'Freeware' if $r->{freeware}; + icon_ 'nonfree', 'Non-free' if !$r->{freeware}; + icon_ 'ani-ero', "Erotic scene animation:\n$eroani", "a$r->{ani_ero}" if $r->{ani_ero}; + icon_ 'ani-story', "Story scene animation:\n$storyani", "a$r->{ani_story}" if $r->{ani_story}; + icon_ 'voiced', $VOICED{$r->{voiced}}{txt}, "v$r->{voiced}" if $r->{voiced}; } tr_ $mtl ? (class => 'mtl') : (), sub { - td_ class => 'tc1', sub { rdate_ [grep $_->{lang} eq $opt->{lang}, $opt->{lang}?$r->{lang}->@*:()]->[0]{released}//$r->{released} }; - td_ class => 'tc2', defined $r->{minage} ? minage $r->{minage} : ''; + td_ class => 'tc1', sub { rdate_ $r->{released} }; + td_ class => 'tc2', sub { + span_ class => 'releaseero releaseero_'.(!$r->{has_ero} ? 'no' : $r->{uncensored} ? 'unc' : defined $r->{uncensored} ? 'cen' : 'yes'), + title => !$r->{has_ero} ? 'No erotic scenes' : + $r->{uncensored} ? 'Contains uncensored erotic scenes' + : defined $r->{uncensored} ? 'Contains erotic scenes with optical censoring' : 'Contains erotic scenes', '♥'; + txt_ !$r->{minage} ? 'All' : minage $r->{minage} if defined $r->{minage}; + }; td_ class => 'tc3', sub { platform_ $_ for $r->{platforms}->@*; if(!$opt->{lang}) { - abbr_ class => "icons lang $_->{lang}".($_->{mtl}?' mtl':''), title => $LANGUAGE{$_->{lang}}, '' for $r->{lang}->@*; + abbr_ class => "icon-lang-$_->{lang}".($_->{mtl}?' mtl':''), title => $LANGUAGE{$_->{lang}}{txt}, '' for $r->{titles}->@*; } - abbr_ class => "icons rt$r->{rtype}", title => $r->{rtype}, ''; + abbr_ class => "icon-rt$r->{rtype}", title => $r->{rtype}, ''; }; td_ class => 'tc4', sub { - a_ href => "/$r->{id}", title => $r->{original}||$r->{title}, $r->{title}; + my $title = + $lang && defined $lang->{title} ? titleprefs_obj $lang->{lang}, [$lang] : + $lang ? titleprefs_obj $r->{olang}, [grep $_->{lang} eq $r->{olang}, $r->{titles}->@*] + : $r->{title}; + a_ href => "/$r->{id}", tattr $title; my $note = join ' ', $r->{official} ? () : 'unofficial', $mtl ? 'machine translation' : (), $r->{patch} ? 'patch' : (); - b_ class => 'grayedout', " ($note)" if $note; + small_ " ($note)" if $note; + if ($r->{drm}->@*) { + my($free,$drm); + for my $d ($r->{drm}->@*) { + ${ (grep $d->{$_}, keys %DRM_PROPERTY)[0] ? \$drm : \$free } = 1 + } + my $nfo = join "\n", map $_->{name}.($_->{notes} ? ' ('.bb_format($_->{notes}, text => 1).')' : ''), $r->{drm}->@*; + ($free && $drm ? \&span_ : $drm ? \&b_ : \&small_)->(title => $nfo, $free && !$drm ? ' (drm-free)' : ' (drm)'); + } }; td_ class => 'tc_icons', sub { icons_ $r }; td_ class => 'tc_prod', join ' & ', $r->{publisher} ? 'Pub' : (), $r->{developer} ? 'Dev' : () if $opt->{prod}; td_ class => 'tc5 elm_dd_left', sub { elm_ 'UList.ReleaseEdit', $VNWeb::ULists::Elm::RLIST_STATUS, { rid => $r->{id}, uid => auth->uid, status => $r->{rlist_status}, empty => '--' } if auth; }; - td_ class => 'tc6', sub { release_extlinks_ $r, "$opt->{id}_$r->{id}" }; + td_ class => 'tc6', sub { + release_extlinks_ $r, "$opt->{id}_$r->{id}" if $r->{patch} || $r->{official} || !grep $_->{mtl}, $r->{titles}->@*; + }; } } diff --git a/lib/VNWeb/Releases/List.pm b/lib/VNWeb/Releases/List.pm index a55a7a88..2e8db610 100644 --- a/lib/VNWeb/Releases/List.pm +++ b/lib/VNWeb/Releases/List.pm @@ -11,7 +11,7 @@ sub listing_ { my($opt, $list, $count) = @_; my sub url { '?'.query_encode %$opt, @_ } paginate_ \&url, $opt->{p}, [$count, 50], 't'; - div_ class => 'mainbox browse', sub { + article_ class => 'browse', sub { table_ class => 'stripe releases', sub { thead_ sub { tr_ sub { td_ class => 'tc1', sub { txt_ 'Date'; sortable_ 'released',$opt, \&url; debug_ $list; }; @@ -32,13 +32,15 @@ sub listing_ { TUWF::get qr{/r}, sub { my $opt = tuwf->validate(get => - q => { onerror => undef }, + q => { searchquery => 1 }, p => { upage => 1 }, f => { advsearch_err => 'r' }, - s => { onerror => 'title', enum => [qw/released minage title/] }, + s => { onerror => 'qscore', enum => [qw/qscore released minage title/] }, o => { onerror => 'a', enum => ['a','d'] }, - fil => { required => 0 }, + fil => { onerror => '' }, )->data; + $opt->{s} = 'qscore' if $opt->{q} && tuwf->reqGet('sb'); + $opt->{s} = 'title' if $opt->{s} eq 'qscore' && !$opt->{q}; # URL compatibility with old filters if(!$opt->{f}->{query} && $opt->{fil}) { @@ -50,41 +52,40 @@ TUWF::get qr{/r}, sub { $opt->{f} = advsearch_default 'r' if !$opt->{f}{query} && !defined tuwf->reqGet('f'); - my $where = sql_and 'NOT r.hidden', $opt->{f}->sql_where(), - !$opt->{q} ? () : sql_or - sql('r.c_search LIKE ALL (search_query(', \$opt->{q}, '))'), - $opt->{q} =~ /^\d+$/ && gtintype($opt->{q}) ? sql 'r.gtin =', \$opt->{q} : (), - $opt->{q} =~ /^[a-zA-Z0-9-]+$/ ? sql 'r.catalog =', \$opt->{q} : (); + my $where = sql_and + 'NOT r.hidden', + 'r.official OR EXISTS(SELECT 1 FROM releases_titles rt WHERE rt.id = r.id AND NOT rt.mtl)', + $opt->{f}->sql_where(); my $time = time; my($count, $list); db_maytimeout { - $count = tuwf->dbVali('SELECT count(*) FROM releases r WHERE', $where); + $count = tuwf->dbVali('SELECT count(*) FROM releases r WHERE', sql_and $where, $opt->{q}->sql_where('r', 'r.id')); $list = $count ? tuwf->dbPagei({results => 50, page => $opt->{p}}, ' SELECT r.id, r.patch, r.released, r.gtin, ', sql_extlinks(r => 'r.'), ' - FROM releases r + FROM', releasest, 'r', $opt->{q}->sql_join('r', 'r.id'), ' WHERE', $where, ' ORDER BY', sprintf { - title => 'r.title %s, r.released %1$s', - minage => 'r.minage %s, r.title %1$s, r.released %1$s', - released => 'r.released %s, r.id %1$s', + qscore => '10 - sc.score %s, r.sorttitle %1$s', + title => 'r.sorttitle %s, r.released %1$s', + minage => 'r.minage %s, r.sorttitle %1$s, r.released %1$s', + released => 'r.released %s, r.sorttitle %1$s, r.id %1$s', }->{$opt->{s}}, $opt->{o} eq 'a' ? 'ASC' : 'DESC' ) : []; } || (($count, $list) = (undef, [])); - enrich_extlinks r => $list; + enrich_extlinks r => 0, $list; enrich_release $list; $time = time - $time; framework_ title => 'Browse releases', sub { - div_ class => 'mainbox', sub { + article_ sub { h1_ 'Browse releases'; form_ action => '/r', method => 'get', sub { searchbox_ r => $opt->{q}//''; input_ type => 'hidden', name => 'o', value => $opt->{o}; input_ type => 'hidden', name => 's', value => $opt->{s}; - $opt->{f}->elm_; - advsearch_msg_ $count, $time; + $opt->{f}->elm_($count, $time); }; }; listing_ $opt, $list, $count if $count; diff --git a/lib/VNWeb/Releases/Page.pm b/lib/VNWeb/Releases/Page.pm index 5078376a..47bd6b63 100644 --- a/lib/VNWeb/Releases/Page.pm +++ b/lib/VNWeb/Releases/Page.pm @@ -1,19 +1,23 @@ package VNWeb::Releases::Page; use VNWeb::Prelude; +use TUWF 'uri_escape'; +use VNWeb::Releases::Lib; sub enrich_item { my($r) = @_; - enrich_merge pid => 'SELECT id AS pid, name, original FROM producers WHERE id IN', $r->{producers}; - enrich_merge vid => 'SELECT id AS vid, title, original FROM vn WHERE id IN', $r->{vn}; + enrich_merge pid => sql('SELECT id AS pid, title, sorttitle FROM', producerst, 'p WHERE id IN'), $r->{producers}; + enrich_merge vid => sql('SELECT id AS vid, title, sorttitle FROM', vnt, 'v WHERE id IN'), $r->{vn}; + enrich_merge drm => sql('SELECT id AS drm, name,', sql_join(',', keys %DRM_PROPERTY), 'FROM drm WHERE id IN'), $r->{drm}; - $r->{lang} = [ sort { ($a->{mtl}?1:0) <=> ($b->{mtl}?1:0) || $a->{lang} cmp $b->{lang} } $r->{lang}->@* ]; + $r->{titles} = [ sort { ($b->{lang} eq $r->{olang}) cmp ($a->{lang} eq $r->{olang}) || ($a->{mtl}?1:0) <=> ($b->{mtl}?1:0) || $a->{lang} cmp $b->{lang} } $r->{titles}->@* ]; $r->{platforms} = [ sort map $_->{platform}, $r->{platforms}->@* ]; - $r->{vn} = [ sort { $a->{title} cmp $b->{title} || idcmp($a->{vid}, $b->{vid}) } $r->{vn}->@* ]; - $r->{producers} = [ sort { $a->{name} cmp $b->{name} || idcmp($a->{pid}, $b->{pid}) } $r->{producers}->@* ]; + $r->{vn} = [ sort { $a->{sorttitle} cmp $b->{sorttitle} || idcmp($a->{vid}, $b->{vid}) } $r->{vn}->@* ]; + $r->{producers} = [ sort { $a->{sorttitle} cmp $b->{sorttitle} || idcmp($a->{pid}, $b->{pid}) } $r->{producers}->@* ]; $r->{media} = [ sort { $a->{medium} cmp $b->{medium} || $a->{qty} <=> $b->{qty} } $r->{media}->@* ]; + $r->{drm} = [ sort { !$a->{drm} || !$b->{drm} ? $b->{drm} <=> $a->{drm} : $a->{name} cmp $b->{name} } $r->{drm}->@* ]; $r->{resolution} = resolution $r; } @@ -21,22 +25,28 @@ sub enrich_item { sub _rev_ { my($r) = @_; + # The old ani_* fields are automatically inferred from the new ani_* fields + # for edits made after the fields were introduced. Hide the old fields for + # such revisions to remove some clutter. + my $newani = $r->{chid} > 1110896; revision_ $r, \&enrich_item, [ vn => 'Relations', fmt => sub { - abbr_ class => "icons rt$_->{rtype}", title => $_->{rtype}, ' '; - a_ href => "/$_->{vid}", title => $_->{original}||$_->{title}, $_->{title}; + abbr_ class => "icon-rt$_->{rtype}", title => $_->{rtype}, ' '; + a_ href => "/$_->{vid}", tattr $_; txt_ " ($_->{rtype})" if $_->{rtype} ne 'complete'; } ], [ official => 'Official', fmt => 'bool' ], [ patch => 'Patch', fmt => 'bool' ], [ freeware => 'Freeware', fmt => 'bool' ], + [ has_ero => 'Has ero', fmt => 'bool' ], [ doujin => 'Doujin', fmt => 'bool' ], [ uncensored => 'Uncensored', fmt => 'bool' ], - [ title => 'Title (Romaji)' ], - [ original => 'Original title' ], - [ gtin => 'JAN/EAN/UPC', empty => 0 ], + [ gtin => 'JAN/EAN/UPC/ISBN',empty => 0 ], [ catalog => 'Catalog number' ], - [ lang => 'Languages', fmt => sub { txt_ $LANGUAGE{$_->{lang}}; txt_ ' (machine translation)' if $_->{mtl} } ], + [ titles => 'Languages', txt => sub { + '['.$_->{lang}.($_->{mtl} ? ' machine translation' : '').'] '.($_->{title}//'').(length $_->{latin} ? " / $_->{latin}" : '') + }], + [ olang => 'Main title', fmt => \%LANGUAGE ], [ released => 'Release date', fmt => sub { rdate_ $_ } ], [ minage => 'Age rating', fmt => sub { txt_ minage $_ } ], [ notes => 'Notes' ], @@ -44,19 +54,82 @@ sub _rev_ { [ media => 'Media', fmt => sub { txt_ fmtmedia $_->{medium}, $_->{qty}; } ], [ resolution => 'Resolution' ], [ voiced => 'Voiced', fmt => \%VOICED ], - [ ani_story => 'Story animation', fmt => \%ANIMATED ], - [ ani_ero => 'Ero animation', fmt => \%ANIMATED ], + $newani ? () : + [ ani_story => 'Story animation', fmt => \%ANIMATED ], + [ ani_story_sp => 'Story animation/sprites',fmt => sub { txt_ fmtanimation $_, 'sprites' } ], + [ ani_story_cg => 'Story animation/cg', fmt => sub { txt_ fmtanimation $_, 'CGs' } ], + [ ani_cutscene => 'Cutscene animation', fmt => sub { txt_ fmtanimation $_, 'cutscenes' } ], + $newani ? () : + [ ani_ero => 'Ero animation', fmt => \%ANIMATED ], + [ ani_ero_sp => 'Ero animation/sprites',fmt=> sub { txt_ fmtanimation $_, 'sprites' } ], + [ ani_ero_cg => 'Ero animation/cg', fmt => sub { txt_ fmtanimation $_, 'CGs' } ], + [ ani_face => 'Lip/eye animation', fmt => 'bool' ], + [ ani_bg => 'Background effects', fmt => 'bool' ], [ engine => 'Engine' ], [ producers => 'Producers', fmt => sub { - a_ href => "/$_->{pid}", title => $_->{original}||$_->{name}, $_->{name}; + a_ href => "/$_->{pid}", tattr $_; txt_ ' ('; txt_ join ', ', $_->{developer} ? 'developer' : (), $_->{publisher} ? 'publisher' : (); txt_ ')'; } ], + [ drm => 'DRM', fmt => sub { + a_ href => '/r/drm?s='.uri_escape($_->{name}), $_->{name}; + txt_ " ($_->{notes})" if length $_->{notes}; + } ], revision_extlinks 'r' } +sub _infotable_animation_ { + my($r) = @_; + state @fields = qw|ani_story_sp ani_story_cg ani_cutscene ani_ero_sp ani_ero_cg ani_bg ani_face|; + + return if !$r->{ani_story} && !$r->{ani_ero}; + + my sub txtc { + my($bool, $txt) = @_; + +(sub { $bool ? txt_ $txt : small_ $txt }) + } + + my sub sect { + my($val, $lbl) = @_; + defined $val ? txtc $val > 2, fmtanimation $val, $lbl : (); + } + + my @story = !$r->{ani_story} ? () : + defined $r->{ani_story_sp} || defined $r->{ani_story_cg} || defined $r->{ani_cutscene} || defined $r->{ani_bg} || defined $r->{ani_face} ? ( + defined $r->{ani_story_sp} ? sect $r->{ani_story_sp}, 'sprites' : (), + defined $r->{ani_story_cg} ? sect $r->{ani_story_cg}, 'CGs' : (), + defined $r->{ani_cutscene} ? sect $r->{ani_cutscene}, 'cutscenes' : (), + ) : txtc $r->{ani_story} > 1, $ANIMATED{$r->{ani_story}}{txt}; + + my @ero = !$r->{ani_ero} ? () : + defined $r->{ani_ero_sp} || defined $r->{ani_ero_cg} ? ( + defined $r->{ani_ero_sp} ? sect $r->{ani_ero_sp}, 'sprites' : (), + defined $r->{ani_ero_cg} ? sect $r->{ani_ero_cg}, 'CGs' : (), + ) : txtc $r->{ani_ero} > 1, $ANIMATED{$r->{ani_ero}}{txt}; + + tr_ sub { + td_ 'Animation'; + td_ sub { + dl_ sub { + if(@story) { + dt_ 'Story scenes'; + dd_ sub { join_ \&br_, sub { $_->() }, @story }; + } + if(@ero) { + dt_ 'Erotic scenes'; + dd_ sub { join_ \&br_, sub { $_->() }, @ero }; + } + } if @story || @ero; + join_ \&br_, sub { $_->() }, + defined $r->{ani_bg} ? (txtc $r->{ani_bg}, $r->{ani_bg} ? 'Animated background effects' : 'No background effects') : (), + defined $r->{ani_face} ? (txtc $r->{ani_face}, $r->{ani_face} ? 'Lip and/or eye movement' : 'No facial animations') : (); + }; + }; +} + + sub _infotable_ { my($r) = @_; @@ -65,46 +138,44 @@ sub _infotable_ { td_ class => 'key', 'Relation'; td_ sub { join_ \&br_, sub { - abbr_ class => "icons rt$_->{rtype}", title => $_->{rtype}, ' '; - a_ href => "/$_->{vid}", title => $_->{original}||$_->{title}, $_->{title}; + abbr_ class => "icon-rt$_->{rtype}", title => $_->{rtype}, ' '; + a_ href => "/$_->{vid}", tattr $_; txt_ " ($_->{rtype})" if $_->{rtype} ne 'complete'; }, $r->{vn}->@* } }; - tr_ sub { - td_ 'Title'; - td_ $r->{title}; + tr_ class => 'titles', sub { + td_ $r->{titles}->@* == 1 ? 'Title' : 'Titles'; + td_ sub { + table_ sub { + my($olang) = grep $_->{lang} eq $r->{olang}, $r->{titles}->@*; + tr_ class => 'nostripe title', sub { + td_ style => 'white-space: nowrap', sub { + abbr_ class => "icon-lang-$_->{lang}", title => $LANGUAGE{$_->{lang}}{txt}, ''; + }; + td_ sub { + my $title = $_->{title}//$olang->{title}; + span_ tlang($_->{lang}, $title), $title; + small_ ' (machine translation)' if $_->{mtl}; + my $latin = defined $_->{title} ? $_->{latin} : $olang->{latin}; + if(defined $latin) { + br_; + txt_ $latin; + } + } + } for $r->{titles}->@*; + }; + }; }; tr_ sub { - td_ 'Original title'; - td_ lang_attr($r->{lang}), $r->{original}; - } if $r->{original}; - - tr_ sub { td_ 'Type'; td_ !$r->{official} && $r->{patch} ? 'Unofficial patch' : !$r->{official} ? 'Unofficial' : 'Patch'; } if !$r->{official} || $r->{patch}; tr_ sub { - td_ 'Language'; - td_ sub { - join_ \&br_, sub { - abbr_ class => "icons lang $_->{lang}", title => $LANGUAGE{$_->{lang}}, ' '; - txt_ ' '; - if($_->{mtl}) { - b_ class => 'grayedout', $LANGUAGE{$_->{lang}}; - txt_ ' (machine translation)'; - } else { - txt_ $LANGUAGE{$_->{lang}}; - } - }, $r->{lang}->@*; - } - }; - - tr_ sub { td_ 'Publication'; td_ $r->{freeware} ? 'Freeware' : 'Non-free'; }; @@ -136,14 +207,7 @@ sub _infotable_ { td_ $VOICED{$r->{voiced}}{txt}; } if $r->{voiced}; - tr_ sub { - td_ 'Animation'; - td_ sub { - join_ \&br_, sub { txt_ $_ }, - $r->{ani_story} ? "Story: $ANIMATED{$r->{ani_story}}{txt}" : (), - $r->{ani_ero} ? "Ero scenes: $ANIMATED{$r->{ani_ero}}{txt}" : (); - } - } if $r->{ani_story} || $r->{ani_ero}; + _infotable_animation_ $r; tr_ sub { td_ 'Engine'; @@ -153,6 +217,18 @@ sub _infotable_ { } if length $r->{engine}; tr_ sub { + td_ 'DRM'; + td_ sub { join_ \&br_, sub { + my $d = $_; + my @prop = grep $d->{$_}, keys %DRM_PROPERTY; + abbr_ class => "icon-drm-$_", title => $DRM_PROPERTY{$_}, '' for @prop; + abbr_ class => 'icon-drm-free', title => 'DRM-free', '' if !@prop; + a_ href => '/r/drm?s='.uri_escape($d->{name}), $d->{name}; + lit_ ' ('.bb_format($d->{notes}, inline => 1).')' if length $d->{notes}; + }, $r->{drm}->@* }; + } if $r->{drm}->@*; + + tr_ sub { td_ 'Released'; td_ sub { rdate_ $r->{released} }; }; @@ -163,9 +239,9 @@ sub _infotable_ { } if defined $r->{minage}; tr_ sub { - td_ 'Censoring'; - td_ $r->{uncensored} ? 'No optical censoring (like mosaics)' : 'Includes optical censoring (e.g. mosaics)'; - } if defined $r->{uncensored}; + td_ 'Erotic content'; + td_ $r->{uncensored} ? 'Contains uncensored erotic scenes' : defined $r->{uncensored} ? 'Contains erotic scenes with optical censoring' : 'Contains erotic scenes', + } if $r->{has_ero}; for my $t (qw|developer publisher|) { my @prod = grep $_->{$t}, @{$r->{producers}}; @@ -173,7 +249,7 @@ sub _infotable_ { td_ ucfirst($t).(@prod == 1 ? '' : 's'); td_ sub { join_ \&br_, sub { - a_ href => "/$_->{pid}", title => $_->{original}||$_->{name}, $_->{name}; + a_ href => "/$_->{pid}", tattr $_; }, @prod } } if @prod; @@ -192,7 +268,11 @@ sub _infotable_ { tr_ sub { td_ 'Links'; td_ sub { - join_ ', ', sub { a_ href => $_->[1], $_->[0] }, $r->{extlinks}->@*; + if ($r->{patch} || $r->{official} || !grep $_->{mtl}, $r->{titles}->@*) { + join_ ', ', sub { a_ href => $_->{url2}, $_->{label} }, $r->{extlinks}->@*; + } else { + small_ 'piracy link hidden'; + } } } if $r->{extlinks}->@*; @@ -213,19 +293,20 @@ TUWF::get qr{/$RE{rrev}} => sub { my $r = db_entry tuwf->captures('id','rev'); return tuwf->resNotFound if !$r; + $r->{title} = titleprefs_obj $r->{olang}, $r->{titles}; enrich_item $r; - enrich_extlinks r => $r; + enrich_extlinks r => 0, $r; - framework_ title => $r->{title}, index => !tuwf->capture('rev'), dbobj => $r, hiddenmsg => 1, + framework_ title => $r->{title}[1], index => !tuwf->capture('rev'), dbobj => $r, hiddenmsg => 1, og => { description => bb_format $r->{notes}, text => 1 }, sub { _rev_ $r if tuwf->capture('rev'); - div_ class => 'mainbox release', sub { + article_ class => 'release', sub { itemmsg_ $r; - h1_ sub { txt_ $r->{title}; debug_ $r }; - h2_ class => 'alttitle', lang_attr($r->{lang}), $r->{original} if length $r->{original}; + h1_ tlang($r->{title}[0], $r->{title}[1]), $r->{title}[1]; + h2_ class => 'alttitle', tlang(@{$r->{title}}[2,3]), $r->{title}[3] if $r->{title}[3] && $r->{title}[3] ne $r->{title}[1]; _infotable_ $r; div_ class => 'description', sub { lit_ bb_format $r->{notes} } if $r->{notes}; }; diff --git a/lib/VNWeb/Releases/VNTab.pm b/lib/VNWeb/Releases/VNTab.pm index dabad6dd..33df7207 100644 --- a/lib/VNWeb/Releases/VNTab.pm +++ b/lib/VNWeb/Releases/VNTab.pm @@ -28,26 +28,25 @@ my @rel_cols = ( { # Title id => 'tit', sort_field => 'title', - sort_sql => 'r.title %s, r.released %1$s', + sort_sql => 'r.sorttitle %s, r.released %1$s', column_string => 'Title', - draw => sub { a_ href => "/$_[0]{id}", $_[0]{title} }, + draw => sub { a_ href => "/$_[0]{id}", tattr $_[0] }, }, { # Type id => 'typ', sort_field => 'type', - sort_sql => 'r.patch %s, rv.rtype %1$s, r.released %1$s, r.title %1$s', + sort_sql => 'r.patch %s, rv.rtype %1$s, r.released %1$s, r.sorttitle %1$s', button_string => 'Type', default => 1, - draw => sub { abbr_ class => "icons rt$_[0]{rtype}", title => $_[0]{rtype}, ''; txt_ '(patch)' if $_[0]{patch} }, + draw => sub { abbr_ class => "icon-rt$_[0]{rtype}", title => $_[0]{rtype}, ''; txt_ '(patch)' if $_[0]{patch} }, }, { # Languages id => 'lan', button_string => 'Language', default => 1, - has_data => sub { !!@{$_[0]{lang}} }, - draw => sub { join_ \&br_, sub { abbr_ class => "icons lang $_->{lang}", title => $LANGUAGE{$_->{lang}}, ''; }, $_[0]{lang}->@* }, + draw => sub { join_ \&br_, sub { abbr_ class => "icon-lang-$_->{lang}", title => $LANGUAGE{$_->{lang}}{txt}, ''; }, $_[0]{titles}->@* }, }, { # Publication id => 'pub', sort_field => 'publication', - sort_sql => 'r.freeware %1$s, r.patch %1$s, r.released %1$s, r.title %1$s', + sort_sql => 'r.freeware %1$s, r.patch %1$s, r.released %1$s, r.sorttitle %1$s', column_string => 'Publication', column_width => 70, button_string => 'Publication', @@ -74,7 +73,7 @@ my @rel_cols = ( }, { # Resolution id => 'res', sort_field => 'resolution', - sort_sql => 'r.reso_x %s, r.reso_y %1$s, r.patch %1$s, r.released %1$s, r.title %1$s', + sort_sql => 'r.reso_x %s, r.reso_y %1$s, r.patch %1$s, r.released %1$s, r.sorttitle %1$s', column_string => 'Resolution', button_string => 'Resolution', na_for_patch => 1, @@ -84,7 +83,7 @@ my @rel_cols = ( }, { # Voiced id => 'voi', sort_field => 'voiced', - sort_sql => 'r.voiced %s, r.patch %1$s, r.released %1$s, r.title %1$s', + sort_sql => 'r.voiced %s, r.patch %1$s, r.released %1$s, r.sorttitle %1$s', column_string => 'Voiced', column_width => 70, button_string => 'Voiced', @@ -95,7 +94,7 @@ my @rel_cols = ( }, { # Animation id => 'ani', sort_field => 'ani_ero', - sort_sql => 'r.ani_story %s, r.ani_ero %1$s, r.patch %1$s, r.released %1$s, r.title %1$s', + sort_sql => 'r.ani_story %s, r.ani_ero %1$s, r.patch %1$s, r.released %1$s, r.sorttitle %1$s', column_string => 'Animation', column_width => 110, button_string => 'Animation', @@ -118,7 +117,7 @@ my @rel_cols = ( }, { # Age rating id => 'min', sort_field => 'minage', - sort_sql => 'r.minage %s, r.released %1$s, r.title %1$s', + sort_sql => 'r.minage %s, r.released %1$s, r.sorttitle %1$s', button_string => 'Age rating', default => 1, has_data => sub { defined $_[0]{minage} }, @@ -126,7 +125,7 @@ my @rel_cols = ( }, { # Notes id => 'not', sort_field => 'notes', - sort_sql => 'r.notes %s, r.released %1$s, r.title %1$s', + sort_sql => 'r.notes %s, r.released %1$s, r.sorttitle %1$s', column_string => 'Notes', column_width => 400, button_string => 'Notes', @@ -168,7 +167,7 @@ sub buttons_ { } }; pl 'os', \&platform_, map $_->{platforms}->@*, @$r if $opt->{pla}; - pl 'lang', sub { abbr_ class => "icons lang $_[0]", title => $LANGUAGE{$_[0]}, '' }, map $_->{lang}, map $_->{lang}->@*, @$r if $opt->{lan}; + pl 'lang', sub { abbr_ class => "icon-lang-$_[0]", title => $LANGUAGE{$_[0]}{txt}, '' }, map $_->{lang}, map $_->{titles}->@*, @$r if $opt->{lan}; } @@ -178,7 +177,7 @@ sub listing_ { # Apply language and platform filters my @r = grep + ($opt->{os} eq 'all' || ($_->{platforms} && grep $_ eq $opt->{os}, $_->{platforms}->@*)) && - ($opt->{lang} eq 'all' || ($_->{lang} && grep $_ eq $opt->{lang}, map $_->{lang}, $_->{lang}->@*)), @$r; + ($opt->{lang} eq 'all' || ($_->{titles} && grep $_ eq $opt->{lang}, map $_->{lang}, $_->{titles}->@*)), @$r; # Figure out which columns to display my @col; @@ -187,7 +186,7 @@ sub listing_ { push @col, $c if !@r || !$c->{has_data} || grep $c->{has_data}->($_), @r; # Must have relevant data } - div_ class => 'mainbox releases_compare', sub { + article_ class => 'releases_compare', sub { table_ sub { thead_ sub { tr_ sub { td_ class => 'key', sub { @@ -238,7 +237,7 @@ TUWF::get qr{/$RE{vid}/releases} => sub { my $r = tuwf->dbAlli(' SELECT r.id, rv.rtype, r.patch, r.released, r.gtin - FROM releases r + FROM', releasest, 'r JOIN releases_vn rv ON rv.id = r.id WHERE NOT hidden AND rv.vid =', \$v->{id}, ' ORDER BY', sprintf(+(grep $opt->{s} eq ($_->{sort_field}//''), @rel_cols)[0]{sort_sql}, $opt->{o} eq 'a' ? 'ASC' : 'DESC') @@ -247,9 +246,9 @@ TUWF::get qr{/$RE{vid}/releases} => sub { my sub url { '?'.query_encode %$opt, @_ } - framework_ title => "Releases for $v->{title}", dbobj => $v, tab => 'releases', sub { - div_ class => 'mainbox releases_compare', sub { - h1_ "Releases for $v->{title}"; + framework_ title => "Releases for $v->{title}[1]", dbobj => $v, tab => 'releases', sub { + article_ class => 'releases_compare', sub { + h1_ "Releases for $v->{title}[1]"; if(!@$r) { p_ 'We don\'t have any information about releases of this visual novel yet...'; } else { |