summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2019-11-27 09:18:21 +0100
committerYorhel <git@yorhel.nl>2019-11-27 14:28:18 +0100
commit4067f0a73a55e898def7320976781a2b8bf6710b (patch)
treea334579a9e0389337238cf13e19851156c2b9fd2
parent9c4835a94263127f780c0b96db89aa25ceefd960 (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.css49
-rw-r--r--elm/ColSelect.elm85
-rw-r--r--elm/ColSelect.js5
-rw-r--r--elm/elm.json3
-rw-r--r--lib/VNWeb/HTML.pm7
-rw-r--r--lib/VNWeb/Prelude.pm14
-rw-r--r--lib/VNWeb/User/Lists.pm86
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'} = \&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';
};
};