diff options
author | Yorhel <git@yorhel.nl> | 2019-11-27 09:18:21 +0100 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2019-11-27 14:28:18 +0100 |
commit | 4067f0a73a55e898def7320976781a2b8bf6710b (patch) | |
tree | a334579a9e0389337238cf13e19851156c2b9fd2 | |
parent | 9c4835a94263127f780c0b96db89aa25ceefd960 (diff) |
ulist: Add column selection + voted, modified, release date & rating columns
It works pretty well, but Lists.pm is getting *really* ugly now. :(
-rw-r--r-- | data/style.css | 49 | ||||
-rw-r--r-- | elm/ColSelect.elm | 85 | ||||
-rw-r--r-- | elm/ColSelect.js | 5 | ||||
-rw-r--r-- | elm/elm.json | 3 | ||||
-rw-r--r-- | lib/VNWeb/HTML.pm | 7 | ||||
-rw-r--r-- | lib/VNWeb/Prelude.pm | 14 | ||||
-rw-r--r-- | lib/VNWeb/User/Lists.pm | 86 |
7 files changed, 198 insertions, 51 deletions
diff --git a/data/style.css b/data/style.css index 37adbb63..63eb728f 100644 --- a/data/style.css +++ b/data/style.css @@ -263,18 +263,18 @@ div.maintabs { display: flex; justify-content: space-between; position: #maincontent div:nth-child(1).maintabs { margin-top: 0 } div.maintabs.right { justify-content: flex-end } div.maintabs.left { justify-content: flex-start } -div.maintabs ul { margin: 0; padding: 0; list-style-type: none } -div.maintabs li { display: inline-block; margin: 0 0 0 10px } -div.maintabs ul li:nth-child(1) { margin-left: 0!important } -div.maintabs li a { display: inline-block; box-sizing: border-box; height: 21px; padding: 1px 7px 0 7px; border: 1px solid $border$; border-bottom: none; background-color: $tabbg$; color: $grayedout$; } -div.maintabs li.tabselected a, -div.maintabs li a:hover { background: $_blendbg$; color: $maintext$; height: 22px } -div.maintabs.browsetabs li a { color: $maintext$ } -div.maintabs.browsetabs li { margin-left: 5px } +div.maintabs > ul { margin: 0; padding: 0; list-style-type: none } +div.maintabs > ul li { display: inline-block; margin: 0 0 0 10px } +div.maintabs > ul li:nth-child(1) { margin-left: 0!important } +div.maintabs > ul li a { display: inline-block; box-sizing: border-box; height: 21px; padding: 1px 7px 0 7px; border: 1px solid $border$; border-bottom: none; background-color: $tabbg$; color: $grayedout$; } +div.maintabs > ul li.tabselected a, +div.maintabs > ul li a:hover { background: $_blendbg$; color: $maintext$; height: 22px } +div.maintabs.browsetabs > ul li a { color: $maintext$ } +div.maintabs.browsetabs > ul li { margin-left: 5px } div.maintabs.bottom { margin-top: 10px; /* WHY!? */ margin-bottom: -10px } -div.maintabs.bottom li a { padding: 4px 7px 2px 7px; border-bottom: 1px solid $border$; border-top: none } -div.maintabs.bottom li.tabselected a, -div.maintabs.bottom li a:hover { padding-top: 5px; height: 22px; margin-top: -1px } +div.maintabs.bottom > ul li a { padding: 4px 7px 2px 7px; border-bottom: 1px solid $border$; border-top: none } +div.maintabs.bottom > ul li.tabselected a, +div.maintabs.bottom > ul li a:hover { padding-top: 5px; height: 22px; margin-top: -1px } @@ -795,17 +795,22 @@ div.votelist td.tc2 { width: 50px; text-align: right; padding-right: 10px } .ulist .tc1 input { display: none } .ulist .tc1 label:before { content: '▸ ' } .ulist .tc1 input:checked + label:before { content: '▾ ' } -.ulist .tc1 span.blurred { opacity: 0.5 } -.ulist .tc2 b { margin-left: 10px } -.ulist .tc4 { white-space: nowrap; width: 60px; text-align: right; padding-right: 10px } -.ulist .tc4 input { width: 55px; text-align: right } -.ulist .tc5, .ulist .tc6, .ulist .tc7 { white-space: nowrap; width: 100px } - -.ulist .tc6 div, .ulist .tc7 div { height: 1em; padding-bottom: 4px } -.ulist .tc6 div span, .ulist .tc7 div span { position: absolute; z-index: 0 } -.ulist .tc6 div input, .ulist .tc7 div input { position: absolute; z-index: 900; width: 110px; visibility: hidden } -.ulist .tc6 div:hover input, .ulist .tc7 div:hover input, -.ulist .tc6 div input:focus, .ulist .tc7 div input:focus { visibility: visible } +.ulist .tc_title b { margin-left: 10px } +.ulist .tc_vote { white-space: nowrap; width: 60px; text-align: right; padding-right: 10px } +.ulist .tc_vote input { width: 55px; text-align: right } +.ulist .tc_voted, +.ulist .tc_added, +.ulist .tc_modified, +.ulist .tc_started, +.ulist .tc_finished, +.ulist .tc_rdate { white-space: nowrap; width: 100px } +.ulist .tc_rating { white-space: nowrap; width: 80px } + +.ulist .tc_started div, .ulist .tc_finished div { height: 1em; padding-bottom: 4px } +.ulist .tc_started div span, .ulist .tc_finished div span { position: absolute; z-index: 0 } +.ulist .tc_started div input, .ulist .tc_finished div input { position: absolute; z-index: 900; width: 110px; visibility: hidden } +.ulist .tc_started div:hover input, .ulist .tc_finished div:hover input, +.ulist .tc_started div input:focus, .ulist .tc_finished div input:focus { visibility: visible } .ulist .tc_opt { padding: 0 0 5px 70px } .ulist .tc_opt textarea { width: 500px; height: 18px; border: none } diff --git a/elm/ColSelect.elm b/elm/ColSelect.elm new file mode 100644 index 00000000..d8be329b --- /dev/null +++ b/elm/ColSelect.elm @@ -0,0 +1,85 @@ +-- Column selection dropdown for tables. Assumes that the currently selected +-- columns are in the query string as the 'c' parameter, e.g.: +-- +-- ?c=column_id&c=modified&... +-- +-- Accepts a list of columns from Perl, e.g.: +-- +-- [ +-- [ 'column_id', 'Column Label' ], +-- [ 'modified', 'Date modified' ], +-- ... +-- ] +module ColSelect exposing (main) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Browser +import Browser.Navigation exposing (load) +import Set +import Erl -- elm/url can't extract a full list of query parameters and hence can't be used to modify a parameter without removing all others. +import Lib.DropDown as DD +import Lib.Api as Api +import Lib.Html exposing (..) + + +main : Program (String, Columns) Model Msg +main = Browser.element + { init = \e -> (init e, Cmd.none) + , view = view + , update = update + , subscriptions = \model -> DD.sub model.dd + } + + +type alias Columns = List (String, String) + +type alias Model = + { cols : Columns + , url : Erl.Url -- Without the "c" parameter + , sel : Set.Set String + , dd : DD.Config Msg + } + + +init : (String, Columns) -> Model +init (u, c) = + { cols = c + , url = Erl.removeQuery "c" <| Erl.parse u + , sel = Set.fromList <| Erl.getQueryValuesForKey "c" <| Erl.parse u + , dd = DD.init "colselect" Open + } + + +type Msg + = Open Bool + | Toggle String Bool + | Update + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Open b -> ({ model | dd = DD.toggle model.dd b }, Cmd.none) + Toggle s b -> ({ model | sel = if b then Set.insert s model.sel else Set.remove s model.sel }, Cmd.none) + Update -> (model, load <| Erl.toString <| List.foldl (\s u -> Erl.addQuery "c" s u) model.url <| Set.toList model.sel) + + +view : Model -> Html Msg +view model = + let + item (cid, cname) = + let selid = "colselect_" ++ cid + in + li [ class "linkradio" ] + [ inputCheck selid (Set.member cid model.sel) (Toggle cid) + , label [ for selid ] [ text cname ] + ] + in + DD.view model.dd Api.Normal + (text "Select columns") + (\_ -> [ ul [] + <| List.map item model.cols + ++ [ li [ ] [ input [ type_ "button", class "submit", value "update", onClick Update ] [] ] ] + ]) diff --git a/elm/ColSelect.js b/elm/ColSelect.js new file mode 100644 index 00000000..e6812fab --- /dev/null +++ b/elm/ColSelect.js @@ -0,0 +1,5 @@ +var init = Elm.ColSelect.init; +Elm.ColSelect.init = function(opt) { + opt.flags = [ location.href, opt.flags ]; + return init(opt); +}; diff --git a/elm/elm.json b/elm/elm.json index 6867e2b1..3db9993a 100644 --- a/elm/elm.json +++ b/elm/elm.json @@ -6,6 +6,7 @@ "elm-version": "0.19.1", "dependencies": { "direct": { + "RomanErnst/erl": "2.1.1", "elm/browser": "1.0.1", "elm/core": "1.0.2", "elm/file": "1.0.1", @@ -13,13 +14,13 @@ "elm/http": "2.0.0", "elm/json": "1.1.2", "elm/regex": "1.0.0", + "elm/url": "1.0.0", "justinmimbs/date": "3.1.2" }, "indirect": { "elm/bytes": "1.0.3", "elm/parser": "1.1.0", "elm/time": "1.0.0", - "elm/url": "1.0.0", "elm/virtual-dom": "1.0.2" } }, diff --git a/lib/VNWeb/HTML.pm b/lib/VNWeb/HTML.pm index a9ab5f67..11bfd7f8 100644 --- a/lib/VNWeb/HTML.pm +++ b/lib/VNWeb/HTML.pm @@ -667,10 +667,11 @@ sub revision_ { # current page number (1..n), # nextpage (0/1 or, if the full count is known: [$total, $perpage]), # alignment (t/b) +# func sub paginate_ { - my($url, $p, $np, $al) = @_; + my($url, $p, $np, $al, $fun) = @_; my($cnt, $pp) = ref($np) ? @$np : ($p+$np, 1); - return if $p == 1 && $cnt <= $pp; + return if !$fun && $p == 1 && $cnt <= $pp; my sub tab_ { my($page, $label) = @_; @@ -694,6 +695,8 @@ sub paginate_ { $p > 1 and tab_ $p-1, '‹ previous'; }; + $fun->() if $fun; + ul_ sub { my $l = ceil($cnt/$pp)-$p+1; $l > 1 and tab_ $p+1, 'next ›'; diff --git a/lib/VNWeb/Prelude.pm b/lib/VNWeb/Prelude.pm index 10fc3b8d..bf1db2a0 100644 --- a/lib/VNWeb/Prelude.pm +++ b/lib/VNWeb/Prelude.pm @@ -69,6 +69,7 @@ sub import { no strict 'refs'; *{$c.'::RE'} = *RE; *{$c.'::json_api'} = \&json_api; + *{$c.'::in'} = \∈ } @@ -129,4 +130,17 @@ sub json_api { }; } + +# Simple "is this element in the array?" function, using 'eq' to test equality. +# Supports both an @array and \@array. +# Usage: +# +# my $contains_hi = in 'hi', qw/ a b hi c /; # true +# +sub in { + my($q, @a) = @_; + $_ eq $q && return 1 for map ref $_ eq 'ARRAY' ? @$_ : ($_), @a; + 0 +} + 1; diff --git a/lib/VNWeb/User/Lists.pm b/lib/VNWeb/User/Lists.pm index 5b78b93d..2c3a82b3 100644 --- a/lib/VNWeb/User/Lists.pm +++ b/lib/VNWeb/User/Lists.pm @@ -234,7 +234,6 @@ json_api qr{/u/ulist/rstatus.json}, $RSTATUS, sub { -# TODO: Filters to find unlabeled VNs or VNs with/without notes? sub filters_ { my($uid, $own, $labels) = @_; @@ -253,9 +252,10 @@ sub filters_ { my $opt = eval { tuwf->validate(get => p => { upage => 1 }, l => { type => 'array', scalar => 1, required => 0, default => [], values => { int => 1 } }, - s => { required => 0, default => 'title', enum => [qw[ title vote added label started finished ]] }, + s => { required => 0, default => 'title', enum => [qw[ title label vote voted added modified started finished rel rating ]] }, o => { required => 0, default => 'a', enum => ['a', 'd'] }, - )->data } || { p => 1, l => [], s => 'title', o => 'a' }; + c => { type => 'array', scalar => 1, required => 0, default => [], values => { enum => [qw[ vote voted added modified started finished rel rating ]] } }, + )->data } || { p => 1, l => [], s => 'title', o => 'a', c => [] }; # $labels only includes labels we are allowed to see, getting rid of any labels in 'l' that aren't in $labels ensures we only filter on visible labels my %accessible_labels = map +($_->{id}, 1), @filtlabels; @@ -273,6 +273,7 @@ sub filters_ { form_ method => 'get', sub { input_ type => 'hidden', name => 's', value => $opt->{s}; input_ type => 'hidden', name => 'o', value => $opt->{o}; + input_ type => 'hidden', name => 'c', value => $_ for $opt->{c}->@*; p_ class => 'labelfilters', sub { span_ class => 'linkradio', sub { join_ sub { em_ ' / ' }, \&lblfilt_, grep $_->{id} < 10, @filtlabels; @@ -299,7 +300,7 @@ sub filters_ { sub vn_ { - my($uid, $own, $n, $v, $labels) = @_; + my($uid, $own, $opt, $n, $v, $labels) = @_; tr_ mkclass(odd => $n % 2 == 0), id => "ulist_tr_$v->{id}", sub { my %labels = map +($_,1), $v->{labels}->@*; @@ -322,11 +323,12 @@ sub vn_ { } }; }; - td_ class => 'tc2', sub { + td_ class => 'tc_title', sub { a_ href => "/v$v->{id}", title => $v->{original}||$v->{title}, shorten $v->{title}, 70; b_ class => 'grayedout', id => 'ulist_notes_'.$v->{id}, $v->{notes} if $v->{notes} || $own; }; - td_ class => 'tc3', sub { + + td_ class => 'tc_labels', sub { my @l = grep $labels{$_->{id}} && $_->{id} != 7, @$labels; my $txt = @l ? join ', ', map $_->{label}, @l : '-'; if($own) { @@ -335,19 +337,32 @@ sub vn_ { txt_ $txt; } }; - td_ mkclass(tc4 => 1, compact => $own, stealth => $own), sub { + + td_ mkclass(tc_vote => 1, compact => $own, stealth => $own), sub { txt_ fmtvote $v->{vote} if !$own; elm_ 'UList.VoteEdit' => $VNVOTE, { uid => $uid, vid => $v->{id}, vote => fmtvote($v->{vote}) }, fmtvote $v->{vote} if $own; - }; - td_ class => 'tc5', fmtdate $v->{added}, 'compact'; - td_ class => 'tc6', sub { + } if in vote => $opt->{c}; + + td_ class => 'tc_voted', fmtdate $v->{vote_date}, 'compact' if in voted => $opt->{c}; + td_ class => 'tc_added', fmtdate $v->{added}, 'compact' if in added => $opt->{c}; + td_ class => 'tc_modified', fmtdate $v->{lastmod}, 'compact' if in modified => $opt->{c}; + + td_ class => 'tc_started', sub { txt_ $v->{started}||'' if !$own; elm_ 'UList.DateEdit' => $VNDATE, { uid => $uid, vid => $v->{id}, date => $v->{started}||'', start => 1 }, $v->{started}||'' if $own; - }; - td_ class => 'tc7', sub { + } if in started => $opt->{c}; + + td_ class => 'tc_finished', sub { txt_ $v->{finished}||'' if !$own; elm_ 'UList.DateEdit' => $VNDATE, { uid => $uid, vid => $v->{id}, date => $v->{finished}||'', start => 0 }, $v->{finished}||'' if $own; - }; + } if in finished => $opt->{c}; + + td_ class => 'tc_rel', sub { rdate_ $v->{c_released} } if in rel => $opt->{c}; + + td_ class => 'tc_rating', sub { + txt_ sprintf '%.2f', ($v->{c_rating}||0)/10; + b_ class => 'grayedout', sprintf ' (%d)', $v->{c_votecount}; + } if in rating => $opt->{c}; }; tr_ mkclass(hidden => 1, 'collapsed_vid'.$v->{id} => 1, odd => $n % 2 == 0), sub { @@ -378,7 +393,7 @@ sub listing_ { my $count = tuwf->dbVali('SELECT count(*) FROM ulist_vns uv WHERE', $where); my $lst = tuwf->dbPagei({ page => $opt->{p}, results => 50 }, - 'SELECT v.id, v.title, v.original, uv.vote, uv.notes, uv.started, uv.finished + 'SELECT v.id, v.title, v.original, uv.vote, uv.notes, uv.started, uv.finished, v.c_rating, v.c_votecount, v.c_released ,', sql_totime('uv.added'), ' as added ,', sql_totime('uv.lastmod'), ' as lastmod ,', sql_totime('uv.vote_date'), ' as vote_date @@ -387,11 +402,15 @@ sub listing_ { WHERE', $where, ' ORDER BY', { title => 'v.title', + label => sql('ARRAY(SELECT ul.label FROM ulist_vns_labels uvl JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl WHERE uvl.uid = uv.uid AND uvl.vid = uv.vid AND uvl.lbl <> ', \7, ')'), vote => 'uv.vote', + voted => 'uv.vote_date', added => 'uv.added', + modified => 'uv.lastmod', started => 'uv.started', finished => 'uv.finished', - label => sql('ARRAY(SELECT ul.label FROM ulist_vns_labels uvl JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl WHERE uvl.uid = uv.uid AND uvl.vid = uv.vid AND uvl.lbl <> ', \7, ')') + rel => 'v.c_released', + rating => 'v.c_rating', }->{$opt->{s}}, $opt->{o} eq 'd' ? 'DESC' : 'ASC', 'NULLS LAST, v.title' ); @@ -413,7 +432,18 @@ sub listing_ { # TODO: Thumbnail view? # TODO: Add/remove columns (start/finish date; VN rating, etc?) - paginate_ \&url, $opt->{p}, [ $count, 50 ], 't'; + paginate_ \&url, $opt->{p}, [ $count, 50 ], 't', sub { + elm_ ColSelect => undef, [ + [ vote => 'Vote' ], + [ voted => 'Vote date' ], + [ added => 'Added' ], + [ modified => 'Modified' ], + [ started => 'Start date' ], + [ finished => 'Finish date' ], + [ rel => 'Release date' ], + [ rating => 'Rating' ], + ]; + }; div_ class => 'mainbox browse ulist', sub { table_ sub { thead_ sub { tr_ sub { @@ -421,14 +451,18 @@ sub listing_ { input_ type => 'checkbox', class => 'checkall', name => 'collapse_vid', id => 'collapse_vid'; label_ for => 'collapse_vid', sub { txt_ 'Opt' }; }; - td_ class => 'tc2', sub { txt_ 'Title'; sortable_ 'title', $opt, \&url; debug_ $lst }; - td_ class => 'tc3', sub { txt_ 'Labels'; sortable_ 'label', $opt, \&url }; - td_ class => 'tc4', sub { txt_ 'Vote'; sortable_ 'vote', $opt, \&url }; - td_ class => 'tc5', sub { txt_ 'Added'; sortable_ 'added', $opt, \&url }; - td_ class => 'tc6', sub { txt_ 'Start date'; sortable_ 'started', $opt, \&url }; - td_ class => 'tc7', sub { txt_ 'Finish date';sortable_ 'finished', $opt, \&url }; + td_ class => 'tc_title', sub { txt_ 'Title'; sortable_ 'title', $opt, \&url; debug_ $lst }; + td_ class => 'tc_labels', sub { txt_ 'Labels'; sortable_ 'label', $opt, \&url }; + td_ class => 'tc_vote', sub { txt_ 'Vote'; sortable_ 'vote', $opt, \&url } if in vote => $opt->{c}; + td_ class => 'tc_voted', sub { txt_ 'Vote date'; sortable_ 'voted', $opt, \&url } if in voted => $opt->{c}; + td_ class => 'tc_added', sub { txt_ 'Added'; sortable_ 'added', $opt, \&url } if in added => $opt->{c}; + td_ class => 'tc_modified', sub { txt_ 'Modified'; sortable_ 'modified', $opt, \&url } if in modified => $opt->{c}; + td_ class => 'tc_started', sub { txt_ 'Start date'; sortable_ 'started', $opt, \&url } if in started => $opt->{c}; + td_ class => 'tc_finished', sub { txt_ 'Finish date'; sortable_ 'finished', $opt, \&url } if in finished => $opt->{c}; + td_ class => 'tc_rel', sub { txt_ 'Release date';sortable_ 'rel', $opt, \&url } if in rel => $opt->{c}; + td_ class => 'tc_rating', sub { txt_ 'Rating'; sortable_ 'rating', $opt, \&url } if in rating => $opt->{c}; }}; - vn_ $uid, $own, $_, $lst->[$_], $labels for (0..$#$lst); + vn_ $uid, $own, $opt, $_, $lst->[$_], $labels for (0..$#$lst); }; }; paginate_ \&url, $opt->{p}, [ $count, 50 ], 'b'; @@ -475,11 +509,11 @@ TUWF::get qr{/$RE{uid}/ulist}, sub { p_ class => 'center', sub { b_ class => 'standout', style => 'font-size: 30px', '!BETA BETA BETA BETA!'; }; p_ class => 'center', sub { txt_ 'Menu links: '; - a_ href => '?l=1&l=2&l=3&l=4&l=7&l=-1&l=0', 'My Visual Novel list'; + a_ href => '?l=1&l=2&l=3&l=4&l=7&l=-1&l=0&c=vote&c=added&c=started&c=finished', 'My Visual Novel list'; txt_ ' - '; - a_ href => '?l=7', 'My Votes'; # TODO: Show and sort by date cast, hide start/end date columns + a_ href => '?l=7&c=vote&c=voted&s=voted&o=d', 'My Votes'; txt_ ' - '; - a_ href => '?l=5', 'My Wishlist'; # Hide start/end date columns + a_ href => '?l=5&c=added', 'My Wishlist'; }; }; |