package VNDB::Handler::Chars; use strict; use warnings; use TUWF ':html', 'uri_escape'; use Exporter 'import'; use VNDB::Func; use VNDB::Types; our @EXPORT = ('charBrowseTable'); TUWF::register( qr{old/c(?:([1-9]\d*)(?:\.([1-9]\d*))?/(edit|copy)|/new)} => \&edit, qr{c/([a-z0]|all)} => \&list, ); sub edit { my($self, $id, $rev, $copy) = @_; $copy = $rev && $rev eq 'copy' || $copy && $copy eq 'copy'; $rev = undef if defined $rev && $rev !~ /^\d+$/; my $r = $id && $self->dbCharGetRev(id => $id, what => 'extended vns traits', $rev ? (rev => $rev) : ())->[0]; return $self->resNotFound if $id && !$r->{id}; $rev = undef if !$r || $r->{lastrev}; return $self->htmlDenied if !$self->authCan('edit') || $id && (($r->{locked} || $r->{hidden}) && !$self->authCan('dbmod')); my %b4 = !$id ? () : ( (map +($_ => $r->{$_}), qw|name original alias desc image ihid ilock s_bust s_waist s_hip height weight bloodt cup_size age gender main_spoil|), main => $r->{main}||0, bday => $r->{b_month} ? sprintf('%02d-%02d', $r->{b_month}, $r->{b_day}) : '', traits => join(' ', map sprintf('%d-%d', $_->{tid}, $_->{spoil}), sort { $a->{tid} <=> $b->{tid} } @{$r->{traits}}), vns => join(' ', map sprintf('%d-%d-%d-%s', $_->{vid}, $_->{rid}||0, $_->{spoil}, $_->{role}), sort { $a->{vid} <=> $b->{vid} || ($a->{rid}||0) <=> ($b->{rid}||0) } @{$r->{vns}}), ); my $frm; if($self->reqMethod eq 'POST') { return if !$self->authCheckCode; $frm = $self->formValidate( { post => 'name', maxlength => 200 }, { post => 'original', required => 0, maxlength => 200, default => '' }, { post => 'alias', required => 0, maxlength => 500, default => '' }, { post => 'desc', required => 0, maxlength => 5000, default => '' }, { post => 'gender', required => 0, default => 'unknown', enum => [ keys %GENDER ] }, { post => 'image', required => 0, default => 0, template => 'id' }, { post => 'bday', required => 0, default => '', regex => [ qr/^(?:[01]?[0-9])-(?:[0123]?[0-9])$/, 'Birthday must be in MM-DD format.' ] }, { post => 's_bust', required => 0, default => 0, template => 'uint', max => 32767 }, { post => 's_waist', required => 0, default => 0, template => 'uint', max => 32767 }, { post => 's_hip', required => 0, default => 0, template => 'uint', max => 32767 }, { post => 'height', required => 0, default => 0, template => 'uint', max => 32767 }, { post => 'weight', required => 0, default => undef, template => 'uint', max => 32767 }, { post => 'bloodt', required => 0, default => 'unknown', enum => [ keys %BLOOD_TYPE ] }, { post => 'cup_size', required => 0, default => '', enum => [ keys %CUP_SIZE ] }, { post => 'age', required => 0, default => undef, template => 'uint', max => 32767 }, { post => 'main', required => 0, default => 0, template => 'id' }, { post => 'main_spoil', required => 0, default => 0, enum => [ 0..2 ] }, { post => 'traits', required => 0, default => '', regex => [ qr/^(?:[1-9]\d*-[0-2])(?: +[1-9]\d*-[0-2])*$/, 'Incorrect trait format.' ] }, { post => 'vns', required => 0, default => '', regex => [ qr/^(?:[1-9]\d*-\d+-[0-2]-[a-z]+)(?: +[1-9]\d*-\d+-[0-2]-[a-z]+)*$/, 'Incorrect VN format.' ] }, { post => 'editsum', template => 'editsum' }, { post => 'ihid', required => 0 }, { post => 'ilock', required => 0 }, ); $frm->{original} = '' if $frm->{original} eq $frm->{name}; # handle image upload $frm->{image} = _uploadimage($self, $frm); # validate main character if(!$frm->{_err} && $frm->{main}) { my $m = $self->dbCharGet(id => $frm->{main}, what => 'extended')->[0]; push @{$frm->{_err}}, 'Invalid main character. Make sure the ID is correct,' .' that the main character itself is not an instance of an other character,' .' and that this entry is not used as a main character elsewhere.' if !$m || $m->{main} || $r && !$copy && ($m->{id} == $r->{id} || $self->dbCharGet(instance => $r->{id})->[0]); } my(@traits, @vns); if(!$frm->{_err}) { # parse and normalize @vns = sort { $a->[0] <=> $b->[0] || $a->[1] <=> $b->[1] } map [split /-/], split / /, $frm->{vns}; $frm->{vns} = join(' ', map sprintf('%d-%d-%d-%s', @$_), @vns); $frm->{ihid} = $frm->{ihid} ?1:0; $frm->{ilock} = $frm->{ilock}?1:0; $frm->{desc} = $self->bbSubstLinks($frm->{desc}); $frm->{main_spoil} = 0 if !$frm->{main}; @traits = sort { $a->[0] <=> $b->[0] } map /^(\d+)-(\d+)$/&&[$1,$2], split / /, $frm->{traits}; my %traits = @traits ? map +($_->{id}, 1), @{$self->dbTraitGet(results => 500, state => 2, applicable => 1, id => [ map $_->[0], @traits ])} : (); @traits = grep $traits{$_->[0]}, @traits; $frm->{traits} = join(' ', map sprintf('%d-%d', @$_), @traits); # check for changes my $same = $id && !grep +($frm->{$_}//'') ne ($b4{$_}//''), keys %b4; return $self->resRedirect("/c$id", 'post') if !$copy && $same; $frm->{_err} = ["No changes, please don't create an entry that is fully identical to another"] if $copy && $same; } if(!$frm->{_err}) { # modify for dbCharRevisionInsert ($frm->{b_month}, $frm->{b_day}) = delete($frm->{bday}) =~ /^(\d{2})-(\d{2})$/ ? ($1, $2) : (0, 0); $frm->{main} ||= undef; $frm->{traits} = \@traits; $_->[1]||=undef for (@vns); $frm->{vns} = \@vns; my $nrev = $self->dbItemEdit(c => !$copy && $id ? ($r->{id}, $r->{rev}) : (undef, undef), %$frm); return $self->resRedirect("/c$nrev->{itemid}.$nrev->{rev}", 'post'); } } if(!$id) { my $vid = $self->formValidate({ get => 'vid', required => 1, template => 'id'}); $frm->{vns} //= "$vid->{vid}-0-0-primary" if !$vid->{_err}; } $frm->{$_} //= $b4{$_} for keys %b4; $frm->{editsum} //= sprintf 'Reverted to revision c%d.%d', $id, $rev if !$copy && $rev; $frm->{editsum} = sprintf 'New character based on c%d.%d', $id, $r->{rev} if $copy; my $title = !$r ? 'Add new character' : $copy ? "Copy $r->{name}" : "Edit $r->{name}"; $self->htmlHeader(title => $title, noindex => 1); $self->htmlMainTabs('c', $r, $copy ? 'copy' : 'edit') if $r; $self->htmlEditMessage('c', $r, $title, $copy); $self->htmlForm({ frm => $frm, action => $r ? "/old/c$id/".($copy ? 'copy' : 'edit') : '/old/c/new', editsum => 1, upload => 1 }, chare_geninfo => [ 'General info', [ input => name => 'Name (romaji)', short => 'name' ], [ input => name => 'Original name', short => 'original' ], [ static => content => 'The original name of the character, leave blank if it is already in the Latin alphabet.' ], [ text => name => 'Aliases', short => 'alias', rows => 3 ], [ static => content => '(Un)official aliases, separated by a newline.' ], [ text => name => 'Description
English please!', short => 'desc', rows => 6 ], [ select => name => 'Sex', short => 'gender', options => [ map [ $_, $GENDER{$_} ], keys %GENDER ] ], [ input => name => 'Birthday', short => 'bday', width => 100,post => ' MM-DD (e.g. "01-26" for the 26th of January)' ], [ input => name => 'Age', short => 'age', width => 50, post => ' years', allow0 => 1 ], [ input => name => 'Bust', short => 's_bust', width => 50, post => ' cm' ], [ input => name => 'Waist', short => 's_waist',width => 50, post => ' cm' ], [ input => name => 'Hips', short => 's_hip', width => 50, post => ' cm' ], [ input => name => 'Height', short => 'height', width => 50, post => ' cm' ], [ input => name => 'Weight', short => 'weight', width => 50, post => ' kg', allow0 => 1 ], [ select => name => 'Blood type',short => 'bloodt', options => [ map [ $_, $BLOOD_TYPE{$_} ], keys %BLOOD_TYPE ] ], [ select => name => 'Cup size', short => 'cup_size', options => [ map [ $_, $CUP_SIZE{$_} ], keys %CUP_SIZE ] ], [ static => content => '
' ], [ input => name => 'Instance of',short => 'main', width => 50, post => ' ID of the main character - the character of which this is an instance of.' ], [ select => name => 'Spoiler', short => 'main_spoil', options => [ map [$_, fmtspoil $_], 0..2 ] ], ], chare_img => [ 'Image', [ static => nolabel => 1, content => sub { div class => 'img'; p 'No image uploaded yet' if !$frm->{image}; img src => imgurl(ch => $frm->{image}) if $frm->{image}; end; div; h2 'Image ID'; input type => 'text', class => 'text', name => 'image', id => 'image', value => $frm->{image}||''; p 'Use a character image that is already on the server. Set to \'0\' to remove the current image.'; br; br; h2 'Upload new image'; input type => 'file', class => 'text', name => 'img', id => 'img'; p 'Image must be in JPEG or PNG format and at most 1MiB. Images larger than 256x300 will automatically be resized. Image must be safe for work!'; end; }]], chare_traits => [ 'Traits', [ hidden => short => 'traits' ], [ static => nolabel => 1, content => sub { h2 'Current traits'; table; tbody id => 'traits_tbl'; Tr id => 'traits_loading'; td colspan => '3', 'Loading...'; end; end; end; h2 'Add trait'; table; Tr; td class => 'tc_name'; input id => 'trait_input', type => 'text', class => 'text'; end; td colspan => 2, ''; end; end 'table'; }], ], chare_vns => [ 'Visual novels', [ hidden => short => 'vns' ], [ static => nolabel => 1, content => sub { h2 'Selected visual novels'; table; tbody id => 'vns_tbl'; Tr id => 'vns_loading'; td colspan => '4', 'Loading...'; end; end; end; h2 'Add visual novel'; table; Tr; td class => 'tc_vnadd'; input id => 'vns_input', type => 'text', class => 'text'; end; td colspan => 3, ''; end; end; }], ]); $self->htmlFooter; } sub _uploadimage { my($self, $frm) = @_; if($frm->{_err} || !$self->reqPost('img')) { return 0 if !$frm->{image}; push @{$frm->{_err}}, 'No image with that ID' if !-s imgpath(ch => $frm->{image}); return $frm->{image}; } # perform some elementary checks my $imgdata = $self->reqUploadRaw('img'); $frm->{_err} = [ 'Image must be in JPEG or PNG format' ] if $imgdata !~ /^(\xff\xd8|\x89\x50)/; # JPG or PNG headers $frm->{_err} = [ 'Image is too large, only 1MB allowed' ] if length($imgdata) > 1024*1024; return undef if $frm->{_err}; # resize/compress my $im = Image::Magick->new; $im->BlobToImage($imgdata); my($ow, $oh) = ($im->Get('width'), $im->Get('height')); my($nw, $nh) = imgsize($ow, $oh, @{$self->{ch_size}}); $im->Set(background => '#ffffff'); $im->Set(alpha => 'Remove'); if($ow != $nw || $oh != $nh) { $im->GaussianBlur(geometry => '0.5x0.5'); $im->Resize(width => $nw, height => $nh); $im->UnsharpMask(radius => 0, sigma => 0.75, amount => 0.75, threshold => 0.008); } $im->Set(magick => 'JPEG', quality => 90); # Get ID and save my $imgid = $self->dbImageAdd(ch => $nw, $nh); my $fn = imgpath(ch => $imgid); $im->Write($fn); chmod 0666, $fn; return $imgid; } sub list { my($self, $fch) = @_; my $f = $self->formValidate( { get => 'p', required => 0, default => 1, template => 'page' }, { get => 'q', required => 0, default => '' }, { get => 'fil', required => 0, default => '' }, ); return $self->resNotFound if $f->{_err}; my($list, $np) = $self->filFetchDB(char => $f->{fil}, { tagspoil => $self->authPref('spoilers')||0, }, { $fch ne 'all' ? ( char => $fch ) : (), $f->{q} ? ( search => $f->{q} ) : (), results => 50, page => $f->{p}, what => 'vns', }); $self->htmlHeader(title => 'Browse characters'); my $quri = uri_escape($f->{q}); form action => '/c/all', 'accept-charset' => 'UTF-8', method => 'get'; div class => 'mainbox'; h1 'Browse characters'; $self->htmlSearchBox('c', $f->{q}); p class => 'browseopts'; for ('all', 'a'..'z', 0) { a href => "/c/$_?q=$quri;fil=$f->{fil}", $_ eq $fch ? (class => 'optselected') : (), $_ eq 'all' ? 'ALL' : $_ ? uc $_ : '#'; } end; p class => 'filselect'; a id => 'filselect', href => '#c'; lit ' Filters'; end; end; input type => 'hidden', class => 'hidden', name => 'fil', id => 'fil', value => $f->{fil}; end; end 'form'; if(!@$list) { div class => 'mainbox'; h1 'No results'; p 'No characters found that matched your criteria.'; end; } @$list && $self->charBrowseTable($list, $np, $f, "/c/$fch?q=$quri;fil=$f->{fil}"); $self->htmlFooter; } # Also used on Handler::Traits sub charBrowseTable { my($self, $list, $np, $f, $uri) = @_; $self->htmlBrowse( class => 'charb', items => $list, options => $f, nextpage => $np, pageurl => $uri, sorturl => $uri, header => [ [ '' ], [ '' ] ], row => sub { my($s, $n, $l) = @_; Tr; td class => 'tc1'; cssicon "gen $l->{gender}", $GENDER{$l->{gender}} if $l->{gender} ne 'unknown'; end; td class => 'tc2'; a href => "/c$l->{id}", title => $l->{original}||$l->{name}, shorten $l->{name}, 50; b class => 'grayedout'; my $i = 1; my %vns; for (@{$l->{vns}}) { next if $_->{spoil} || $vns{$_->{vid}}++; last if $i++ > 4; txt ', ' if $i > 2; a href => "/v$_->{vid}/chars", title => $_->{vntitle}, shorten $_->{vntitle}, 30; } end; end; end; } ) } 1;