diff options
Diffstat (limited to 'lib/VN3/User/VNList.pm')
-rw-r--r-- | lib/VN3/User/VNList.pm | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/lib/VN3/User/VNList.pm b/lib/VN3/User/VNList.pm new file mode 100644 index 00000000..9b4d34ed --- /dev/null +++ b/lib/VN3/User/VNList.pm @@ -0,0 +1,325 @@ +package VN3::User::VNList; + +use POSIX 'ceil'; +use VN3::Prelude; +use VN3::User::Lib; + + +sub mkurl { + my $opt = shift; + $opt = { %$opt, @_ }; + delete $opt->{t} if $opt->{t} == -1; + delete $opt->{g} if !$opt->{g}; + '?'.join ';', map "$_=$opt->{$_}", sort keys %$opt; +} + + +sub SideBar { + my $opt = shift; + + Div class => 'fixed-size-left-sidebar-xl', sub { + Div class => 'vertical-selector-label', 'Status'; + Div class => 'vertical-selector', sub { + for (-1..$#VNLIST_STATUS) { + A href => mkurl($opt, t => $_, p => 1), mkclass( + 'vertical-selector__item' => 1, + 'vertical-selector__item--active' => $_ == $opt->{t} + ), $_ < 0 ? 'All' : $VNLIST_STATUS[$_]; + } + }; + }; +} + + +sub NextPrev { + my($opt, $count) = @_; + my $numpage = ceil($count/50); + + Div class => 'd-lg-flex jc-between align-items-center', sub { + Div class => 'd-flex align-items-center', ''; + Div class => 'd-block d-lg-none mb-2', ''; + Div class => 'd-flex jc-right align-items-center', sub { + A href => mkurl($opt, p => $opt->{p}-1), mkclass(btn => 1, 'btn--disabled' => $opt->{p} <= 1), '< Prev'; + Div class => 'mx-3 semi-muted', sprintf 'page %d of %d', $opt->{p}, $numpage; + A href => mkurl($opt, p => $opt->{p}+1), mkclass(btn => 1, 'btn--disabled' => $opt->{p} >= $numpage), 'Next >'; + }; + }; +} + + +sub EditDropDown { + my($u, $opt, $item) = @_; + return if $u->{id} != (auth->uid||0); + Div 'data-elm-module' => 'UVNList.Options', + 'data-elm-flags' => JSON::XS->new->encode({uid => $u->{id}, item => $item}), + ''; +} + + +sub VNTable { + my($u, $lst, $opt) = @_; + + my $SortHeader = sub { + my($id, $label) = @_; + my $isasc = $opt->{s} eq $id && $opt->{o} eq 'a'; + A mkclass( + 'table-header' => 1, + 'with-sort-icon' => 1, + 'with-sort-icon--down' => !$isasc, + 'with-sort-icon--up' => $isasc, + 'with-sort-icon--active' => $opt->{s} eq $id, + ), href => mkurl($opt, p => 1, s => $id, o => $isasc ? 'd' : 'a'), $label; + }; + + Table class => 'table table--responsive-single-sm fs-medium vn-list', sub { + Thead sub { + Tr sub { + Th width => '15%', class => 'th--nopad', sub { $SortHeader->(date => 'Date' ) }; + Th width => '40%', class => 'th--nopad', sub { $SortHeader->(title => 'Title') }; + Th width => '10%', class => 'th--nopad', sub { $SortHeader->(vote => 'Vote' ) }; + Th width => '13%', 'Status'; + Th width => '7.33%', ''; + Th width => '7.33%', ''; + Th width => '7.33%', ''; + }; + }; + Tbody sub { + for my $l (@$lst) { + Tr sub { + Td class => 'tabular-nums muted', date_display $l->{date}; + Td sub { + A href => "/v$l->{id}", title => $l->{original}||$l->{title}, $l->{title}; + }; + + if($u->{id} == (auth->uid||0)) { + Td class => 'table-edit-overlay-base', sub { + Div 'data-elm-module' => 'UVNList.Vote', + 'data-elm-flags' => JSON::XS->new->encode({uid => int $u->{id}, vid => int $l->{id}, vote => ''.vote_display $l->{vote}}), + vote_display $l->{vote}; + }; + Td class => 'table-edit-overlay-base', sub { + Div 'data-elm-module' => 'UVNList.Status', + 'data-elm-flags' => JSON::XS->new->encode({uid => int $u->{id}, vid => int $l->{id}, status => int $l->{status}||0}), + $VNLIST_STATUS[$l->{status}||0]; + }; + } else { + Td vote_display $l->{vote}; + Td $VNLIST_STATUS[$l->{status}||0]; + } + + # Release info + Td sub { + A href => 'javascript:;', class => 'vn-list__expand-releases', sub { + Span class => 'expand-arrow mr-2', ''; + Txt sprintf '%d/%d', (scalar grep $_->{status}==2, @{$l->{rel}}), scalar @{$l->{rel}}; + } if @{$l->{rel}}; + }; + + # Notes + Td sub { + # TODO: vn-list__expand-comment--empty for 'add comment' things + A href => 'javascript:;', class => 'vn-list__expand-comment', sub { + Span class => 'expand-arrow mr-2', ''; + Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/heavy/comment.svg'; + } if $l->{notes}; + }; + + Td sub { EditDropDown $u, $opt, $l }; + }; + + # Release info + Tr class => 'vn-list__releases-row d-none', sub { + Td colspan => '6', sub { + Div class => 'vn-list__releases', sub { + Table class => 'table table--responsive-single-sm ml-3', sub { + Tbody sub { + for my $r (@{$l->{rel}}) { + Tr sub { + Td width => '15%', class => 'tabular-nums muted pl-0', date_display $r->{date}; + Td width => '50%', sub { + A href => "/v$r->{rid}", title => $r->{original}||$r->{title}, $r->{title}; + }; + # TODO: Editabe + Td width => '20%', $RLIST_STATUS[$l->{status}]; + Td width => '15%', ''; # TODO: Edit menu + } + } + } + } + } + } + } if @{$l->{rel}}; + + # Notes + Tr class => 'vn-list__comment-row d-none', sub { + Td colspan => '6', sub { + # TODO: Editable + Div class => 'vn-list__comment ml-3', $l->{notes}; + } + } if $l->{notes}; + }; + }; + }; +} + + +sub VNGrid { + my($u, $lst, $opt) = @_; + + Div class => 'vn-grid mb-4', sub { + for my $l (@$lst) { + Div class => 'vn-grid__item', sub { + # TODO: NSFW hiding? What about missing images? + Div class => 'vn-grid__item-bg', style => sprintf("background-image: url('%s')", tuwf->imgurl(cv => $l->{image})), ''; + Div class => 'vn-grid__item-overlay', sub { + A href => 'javascript:;', class => 'vn-grid__item-link', ''; # TODO: Open modal on click + Div class => 'vn-grid__item-top', sub { + EditDropDown $u, $opt, $l; + Div class => 'vn-grid__item-rating', sub { + Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/heavy/comment.svg' if $l->{notes}; + Lit ' '; + Txt vote_display $l->{vote}; + } + }; + Div class => 'vn-grid__item-name', $l->{title}; + } + } + } + } +} + + +sub List { + my($u, $opt) = @_; + + my $lst = tuwf->dbAlli(q{ + SELECT v.id, v.title, v.original, vl.status, vl.notes, vo.vote, v.image, }, + sql_totime('LEAST(vl.added, vo.date)'), q{AS date, + count(*) OVER() AS full_count + FROM vn v + LEFT JOIN votes vo ON vo.vid = v.id AND vo.uid =}, \$u->{id}, q{ + LEFT JOIN vnlists vl ON vl.vid = v.id AND vl.uid =}, \$u->{id}, q{ + WHERE }, sql_and( + 'vo.vid IS NOT NULL OR vl.vid IS NOT NULL', + $opt->{t} >= 1 ? sql('vl.status =', \$opt->{t}) : $opt->{t} == 0 ? 'vl.status = 0 OR vl.status IS NULL' : () + ), + 'ORDER BY', { + title => 'v.title', + date => 'LEAST(vl.added, vo.date)', + vote => 'vo.vote', + }->{$opt->{s}}, + $opt->{o} eq 'a' ? 'ASC' : 'DESC', + 'NULLS LAST', + 'LIMIT', \50, + 'OFFSET', \(($opt->{p}-1)*50) + ); + my $count = @$lst ? $lst->[0]{full_count} : 0; + delete $_->{full_count} for @$lst; + + enrich_list rel => id => vid => sub { sql q{ + SELECT rv.vid, rl.rid, rl.status, r.title, r.original, }, sql_totime('rl.added'), q{ AS date + FROM rlists rl + JOIN releases r ON r.id = rl.rid + JOIN releases_vn rv ON rv.id = r.id + WHERE rl.uid =}, \$u->{id}, q{AND rv.vid IN}, $_[0] + }, $lst; + + Div class => 'col-md', sub { + Div class => 'card card--white card--no-separators mb-5', sub { + Div class => 'card__header', sub { + Div class => 'card__title', 'List'; + Debug $lst; + Div class => 'card__header-buttons', sub { + Div class => 'btn-group', sub { + A href => mkurl($opt, g => 0), mkclass(btn => 1, active => !$opt->{g}, 'js-show-vn-list' => 1), \&ListIcon; + A href => mkurl($opt, g => 1), mkclass(btn => 1, active => $opt->{g}, 'js-show-vn-grid' => 1), \&GridIcon; + }; + }; + }; + + VNTable $u, $lst, $opt unless $opt->{g}; + Div class => 'card__body fs-medium', sub { + VNGrid $u, $lst, $opt if $opt->{g}; + NextPrev $opt, $count; + }; + } + }; +} + + +TUWF::get qr{/$UID_RE/list}, sub { + my $uid = tuwf->capture('id'); + my $u = tuwf->dbRowi(q{ + SELECT u.id, u.username, hd.value AS hide_list + FROM users u + LEFT JOIN users_prefs hd ON hd.uid = u.id AND hd.key = 'hide_list' + WHERE u.id =}, \$uid + ); + return tuwf->resNotFound if !$u->{id} || !show_list $u; + + my $opt = tuwf->validate(get => + t => { vnlist_status => 1, required => 0, default => -1 }, # status + p => { page => 1 }, # page + o => { enum => ['d','a'], required => 0, default => 'a' }, # order (asc/desc) + s => { enum => ['title', 'date', 'vote'], required => 0, default => 'title' }, # sort column + g => { anybool => 1 }, # grid + )->data; + + Framework + title => $u->{username}, + index => 0, + top => sub { + Div class => 'col-md', sub { + Div class => 'detail-page-title', ucfirst $u->{username}; + TopNav list => $u; + } + }, + sub { + Div class => 'row', sub { + SideBar $opt; + List $u, $opt; + }; + }; +}; + + +json_api '/u/setvote', { + uid => { id => 1 }, + vid => { id => 1 }, + vote => { vnvote => 1 } +}, sub { + my $data = shift; + return tuwf->resJSON({Unauth => 1}) if (auth->uid||0) != $data->{uid}; + + tuwf->dbExeci( + 'DELETE FROM votes WHERE', + { vid => $data->{vid}, uid => $data->{uid} } + ) if !$data->{vote}; + + tuwf->dbExeci( + 'INSERT INTO votes', + { vid => $data->{vid}, uid => $data->{uid}, vote => $data->{vote} }, + 'ON CONFLICT (vid, uid) DO UPDATE SET', + { vote => $data->{vote} } + ) if $data->{vote}; + + tuwf->resJSON({Success => 1}) +}; + + +json_api '/u/setvnstatus', { + uid => { id => 1 }, + vid => { id => 1 }, + status => { vnlist_status => 1 } +}, sub { + my $data = shift; + return tuwf->resJSON({Unauth => 1}) if (auth->uid||0) != $data->{uid}; + + tuwf->dbExeci( + 'INSERT INTO vnlists', + { vid => $data->{vid}, uid => $data->{uid}, status => $data->{status} }, + 'ON CONFLICT (vid, uid) DO UPDATE SET', + { status => $data->{status} } + ); + tuwf->resJSON({Success => 1}) +}; |