summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--css/v2.css38
-rw-r--r--elm/Lib/Api.elm1
-rw-r--r--elm/UList/DateEdit.elm2
-rw-r--r--elm/UList/LabelEdit.elm2
-rw-r--r--elm/UList/Widget.elm278
-rw-r--r--lib/VNWeb/AdvSearch.pm1
-rw-r--r--lib/VNWeb/Elm.pm22
-rw-r--r--lib/VNWeb/ULists/Elm.pm33
-rw-r--r--lib/VNWeb/VN/List.pm23
-rw-r--r--static/f/list-add.svg5
-rw-r--r--static/f/list-l1.svg5
-rw-r--r--static/f/list-l2.svg5
-rw-r--r--static/f/list-l3.svg6
-rw-r--r--static/f/list-l4.svg5
-rw-r--r--static/f/list-l5.svg5
-rw-r--r--static/f/list-l7.svg6
-rw-r--r--static/f/list-unknown.svg6
17 files changed, 412 insertions, 31 deletions
diff --git a/css/v2.css b/css/v2.css
index e54c248f..c8143748 100644
--- a/css/v2.css
+++ b/css/v2.css
@@ -631,8 +631,7 @@ div#vntags { margin: 0 30px 0 30px; border-top: 1px solid $bo
.vnbrowse .tc_score { padding-left: 30px; width: 70px }
.vnbrowse .tc_title { padding-left: 30px }
.vnbrowse .tc_score + td { padding-left: 0 }
-.vnbrowse .tc_ulist { text-align: right; width: 8px; white-space: nowrap }
-.vnbrowse .tc_ulist abbr { display: inline-block; width: 20px; }
+.vnbrowse .tc_ulist { width: 10px }
.vnbrowse .tc_plat { text-align: right; padding: 0; }
.vnbrowse .tc_lang { padding: 0; }
.vnbrowse .tc_pop { text-align: right; padding-right: 10px }
@@ -911,19 +910,23 @@ div.votelist td.tc2 { width: 50px; text-align: right; padding-right: 10px }
.ulist .tc_opt .tco3 { white-space: nowrap; width: 60px; text-align: right; padding-bottom: 0 }
-/***** User VN list browser ******/
-
-#expandall, .collapse_but { cursor: pointer }
-.browse.rlist .tc1 { width: 16px; padding-bottom: 0 }
-.browse.rlist .tc2 { width: 16px; padding-bottom: 0 }
-.browse.rlist .tc3 { width: 60px }
-.browse.rlist .tc3_5 b { margin-left: 10px }
-.browse.rlist .tc4 { width: 60px; text-align: right; padding-top: 0; padding-bottom: 0 }
-.browse.rlist .tc6 { width: 100px }
-.browse.rlist .tc7 { width: 90px }
-.browse.rlist .tc8 { width: 70px }
-.browse.rlist tfoot select { width: 200px }
-.browse.rlist .relhid .tc6 { padding-left: 15px; width: auto }
+/***** ulist-widget (elm/Ulist/Widget) *****/
+
+.ulist-widget-icon { cursor: pointer }
+.ulist-widget {
+ z-index: 100; position: fixed; height: 100%; width: 100%; top: 0; right: 0; display: flex; align-items: center; justify-content: center; text-align: left; white-space: normal; background: $boxbg; overflow: auto;
+ > div {
+ background: $blendbg; width: 600px; min-height: 300px; border: 2px solid $border; padding: 10px;
+ > div.spinner { position: absolute; top: unquote('calc(50% - 8px)'); right: unquote('calc(50% - 8px)'); }
+ }
+ table tr { background: none!important }
+ table td:first-child { width: 100px }
+ textarea, select { margin: 0 -1px; width: 100% }
+ .date span:not(.spinner) { display: none }
+ .tco1 { white-space: nowrap; width: 100px }
+ .tco2 { white-space: nowrap; width: 100px }
+ .tco3 { width: 60px; text-align: right; padding-bottom: 0 }
+}
/***** User notifications *****/
@@ -1268,6 +1271,11 @@ div.imagebrowse { padding: 0; display: flex; flex-wrap: wrap }
/****** Icons *******/
.platicon { width: 16px; height: 16px; margin: -1px 2px -1px 0; border: 0; padding: 0; object-fit: contain }
+/* XXX: Not a fan of this filtering solution. Also, these don't work on light skins */
+.liststatus_icon { width: 15px; height: 15px; margin: -1px 0; object-fit: contain; filter: invert(100%); opacity: 0.9 }
+.liststatus_icon.add { opacity: 0.4 }
+.liststatus_icon.l2 { opacity: 1; filter: invert(48%) sepia(23%) saturate(3672%) hue-rotate(86deg) brightness(103%) contrast(116%) }
+.liststatus_icon.blacklist { opacity: 1; filter: invert(10%) sepia(96%) saturate(5309%) hue-rotate(359deg) brightness(96%) contrast(113%) }
.icons {
background: url(/g/icons.png?#{$icons-version}) no-repeat;
diff --git a/elm/Lib/Api.elm b/elm/Lib/Api.elm
index fec77dbd..2ba63fa1 100644
--- a/elm/Lib/Api.elm
+++ b/elm/Lib/Api.elm
@@ -58,6 +58,7 @@ showResponse res =
CharResult _ -> unexp
AnimeResult _ -> unexp
ImageResult _ -> unexp
+ UListWidget _ -> unexp
AdvSearchQuery _ -> unexp
diff --git a/elm/UList/DateEdit.elm b/elm/UList/DateEdit.elm
index d20dbba7..36534f21 100644
--- a/elm/UList/DateEdit.elm
+++ b/elm/UList/DateEdit.elm
@@ -1,4 +1,4 @@
-module UList.DateEdit exposing (main)
+module UList.DateEdit exposing (main,init,view,update,Model,Msg)
import Html exposing (..)
import Html.Attributes exposing (..)
diff --git a/elm/UList/LabelEdit.elm b/elm/UList/LabelEdit.elm
index d2877cf0..f6a60d3b 100644
--- a/elm/UList/LabelEdit.elm
+++ b/elm/UList/LabelEdit.elm
@@ -1,3 +1,5 @@
+-- TODO: Would be nice to have a "create new label" option in this model, to make custom labels more discoverable.
+
port module UList.LabelEdit exposing (main, init, update, view, isPublic, Model, Msg)
import Html exposing (..)
diff --git a/elm/UList/Widget.elm b/elm/UList/Widget.elm
new file mode 100644
index 00000000..84c085c4
--- /dev/null
+++ b/elm/UList/Widget.elm
@@ -0,0 +1,278 @@
+-- TODO: Integrate this with UList.VNPage and have this replace UList.Opt.
+-- XXX: Only one widget can be instantiated per VN on a single page.
+module UList.Widget exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Browser
+import Task
+import Process
+import Set
+import Date
+import Dict exposing (Dict)
+import Lib.Util exposing (..)
+import Lib.Html exposing (..)
+import Lib.Ffi as Ffi
+import Lib.Api as Api
+import Lib.RDate as RDate
+import Lib.DropDown as DD
+import Gen.Api as GApi
+import Gen.UListWidget as UW
+import Gen.UListVNNotes as GVN
+import Gen.UListDel as GDE
+import UList.LabelEdit as LE
+import UList.VoteEdit as VE
+import UList.DateEdit as DE
+import UList.ReleaseEdit as RE
+
+
+main : Program UW.Recv Model Msg
+main = Browser.element
+ { init = \f -> (init f, Date.today |> Task.perform Today)
+ , subscriptions = \m -> if not m.open then Sub.none else Sub.batch <|
+ [ DD.onClickOutside "ulist-widget-box" (Open False)
+ , Sub.map Label (DD.sub m.labels.dd)
+ , Sub.map Vote (DD.sub m.vote.dd)
+ ] ++ List.map (\r -> Sub.map (Rel r.rid) (DD.sub r.dd)) m.rels
+ , view = view
+ , update = update
+ }
+
+type alias Model =
+ { uid : String
+ , vid : String
+ , loadState : Api.State
+ , today : Date.Date
+ , title : Maybe String -- Nothing is used here to indicate that we haven't loaded the full data yet.
+ , open : Bool
+ , onlist : Bool
+ , del : Bool
+ , labels : LE.Model
+ , vote : VE.Model
+ , canvote : Bool
+ , canreview : Bool
+ , review : Maybe String
+ , notes : String
+ , notesRev : Int
+ , notesSaved : String
+ , notesState : Api.State
+ , started : DE.Model
+ , finished : DE.Model
+ , rels : List RE.Model
+ , relNfo : Dict String GApi.ApiReleases
+ , relOptions : List (String, String)
+ }
+
+init : UW.Recv -> Model
+init f =
+ { uid = f.uid
+ , vid = f.vid
+ , loadState = Api.Normal
+ , today = Date.fromOrdinalDate 2100 1
+ , title = Maybe.map (\full -> full.title) f.full
+ , open = False
+ , onlist = f.labels /= Nothing
+ , del = False
+ -- TODO: LabelEdit and VoteEdit create an internal vid-based ID, so this widget can't be used on VN pages or UList listings. Need to fix that.
+ , labels = LE.init
+ { uid = f.uid
+ , vid = f.vid
+ , selected = List.map (\l -> l.id) (Maybe.withDefault [] f.labels)
+ , labels = Maybe.withDefault
+ (List.map (\l -> {id = l.id, label = l.label, private = True}) (Maybe.withDefault [] f.labels))
+ (Maybe.map (\full -> full.labels) f.full)
+ }
+ , vote = VE.init { uid = f.uid, vid = f.vid, vote = Maybe.andThen (\full -> full.vote) f.full }
+ , canvote = Maybe.map (\full -> full.canvote ) f.full |> Maybe.withDefault False
+ , canreview = Maybe.map (\full -> full.canreview ) f.full |> Maybe.withDefault False
+ , review = Maybe.andThen (\full -> full.review) f.full
+ , notes = Maybe.map (\full -> full.notes ) f.full |> Maybe.withDefault ""
+ , notesRev = 0
+ , notesSaved = Maybe.map (\full -> full.notes ) f.full |> Maybe.withDefault ""
+ , notesState = Api.Normal
+ , started = let m = DE.init { uid = f.uid, vid = f.vid, date = Maybe.map (\full -> full.started ) f.full |> Maybe.withDefault "", start = True } in { m | visible = True }
+ , finished = let m = DE.init { uid = f.uid, vid = f.vid, date = Maybe.map (\full -> full.finished) f.full |> Maybe.withDefault "", start = False } in { m | visible = True }
+ , rels = List.map (\st -> RE.init ("widget-" ++ f.vid) { uid = f.uid, rid = st.id, status = Just st.status, empty = "" }) <| Maybe.withDefault [] <| Maybe.map (\full -> full.rlist) f.full
+ , relNfo = Dict.fromList <| List.map (\r -> (r.id, r)) <| Maybe.withDefault [] <| Maybe.map (\full -> full.releases) f.full
+ , relOptions = Maybe.withDefault [] <| Maybe.map (\full -> List.map (\r -> (r.id, showrel r)) full.releases) f.full
+ }
+
+
+type Msg
+ = Today Date.Date
+ | Open Bool
+ | Loaded GApi.Response
+ | Label LE.Msg
+ | Vote VE.Msg
+ | Notes String
+ | NotesSave Int
+ | NotesSaved Int GApi.Response
+ | Started DE.Msg
+ | Finished DE.Msg
+ | Del Bool
+ | Delete
+ | Deleted GApi.Response
+ | Rel String RE.Msg
+ | RelAdd String
+
+
+setOnList : Model -> Model
+setOnList model =
+ { model | onlist = model.onlist
+ || model.vote.ovote /= Nothing
+ || not (Set.isEmpty model.labels.sel)
+ || model.notes /= ""
+ || model.started.val /= ""
+ || model.finished.val /= ""
+ || not (List.isEmpty model.rels)
+ }
+
+
+isPublic : Model -> Bool
+isPublic model =
+ LE.isPublic model.labels
+ || (isJust model.vote.vote && List.any (\l -> l.id == 7 && not l.private) model.labels.labels)
+
+
+showrel : GApi.ApiReleases -> String
+showrel r = "[" ++ (RDate.format (RDate.expand r.released)) ++ " " ++ (String.join "," r.lang) ++ "] " ++ r.title ++ " (" ++ r.id ++ ")"
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Today d -> ({ model | today = d }, Cmd.none)
+ Open b ->
+ if b && model.title == Nothing
+ then ({ model | open = b, loadState = Api.Loading }, UW.send { uid = model.uid, vid = model.vid } Loaded)
+ else ({ model | open = b }, Cmd.none)
+
+ Loaded (GApi.UListWidget w) -> let m = init w in ({ m | open = True }, Cmd.none)
+ Loaded e -> ({ model | loadState = Api.Error e }, Cmd.none)
+
+ Label m -> let (nm, nc) = LE.update m model.labels in (setOnList { model | labels = nm }, Cmd.map Label nc)
+ Vote m -> let (nm, nc) = VE.update m model.vote in (setOnList { model | vote = nm }, Cmd.map Vote nc)
+ Started m -> let (nm, nc) = DE.update m model.started in (setOnList { model | started = nm }, Cmd.map Started nc)
+ Finished m -> let (nm, nc) = DE.update m model.finished in (setOnList { model | finished = nm }, Cmd.map Finished nc)
+
+ Notes s ->
+ ( { model | notes = s, notesRev = model.notesRev + 1 }
+ , Task.perform (\_ -> NotesSave (model.notesRev+1)) <| Process.sleep 1000)
+ NotesSave rev ->
+ if rev /= model.notesRev || model.notes == model.notesSaved
+ then (model, Cmd.none)
+ else ( { model | notesState = Api.Loading }
+ , GVN.send { uid = model.uid, vid = model.vid, notes = model.notes } (NotesSaved rev))
+ NotesSaved rev GApi.Success ->
+ if model.notesRev /= rev
+ then (model, Cmd.none)
+ else (setOnList {model | notesSaved = model.notes, notesState = Api.Normal }, Cmd.none)
+ NotesSaved _ e -> ({ model | notesState = Api.Error e }, Cmd.none)
+
+ Del b -> ({ model | del = b }, Cmd.none)
+ Delete -> ({ model | loadState = Api.Loading }, GDE.send { uid = model.uid, vid = model.vid } Deleted)
+ Deleted GApi.Success -> (init { uid = model.uid, vid = model.vid, labels = Nothing, full = Nothing }, Cmd.none)
+ Deleted e -> ({ model | loadState = Api.Error e }, Cmd.none)
+
+ Rel rid m ->
+ case List.filterMap (\r -> if r.rid == rid then Just (RE.update m r) else Nothing) model.rels |> List.head of
+ Nothing -> (model, Cmd.none)
+ Just (rm, rc) ->
+ let
+ nr = if rm.state == Api.Normal && rm.status == Nothing
+ then List.filter (\r -> r.rid /= rid) model.rels
+ else List.map (\r -> if r.rid == rid then rm else r) model.rels
+ in ({ model | rels = nr }, Cmd.map (Rel rid) rc)
+ RelAdd rid ->
+ ( setOnList { model | rels = model.rels ++ (if rid == "" then [] else [RE.init model.vid { rid = rid, uid = model.uid, status = Just 2, empty = "" }]) }
+ , Task.perform (always <| Rel rid <| RE.Set (Just 2) True) <| Task.succeed True)
+
+
+view : Model -> Html Msg
+view model =
+ let
+ icon () =
+ let fn = if not model.onlist then "add"
+ else List.range 1 5
+ |> List.filter (\n -> Set.member n model.labels.tsel)
+ |> List.maximum
+ |> Maybe.map (\n -> "l" ++ String.fromInt n)
+ |> Maybe.withDefault "unknown"
+ lbl = if not model.onlist then "Add to list"
+ else String.join ", " <| List.filterMap (\l -> if Set.member l.id model.labels.tsel && l.id /= 7 then Just l.label else Nothing) model.labels.labels
+ in img [ src (Ffi.urlStatic ++ "/f/list-" ++ fn ++ ".svg"), class ("ulist-widget-icon liststatus_icon "++fn), title lbl, onClickN (Open True) ] []
+
+ rel r =
+ case Dict.get r.rid model.relNfo of
+ Nothing -> text ""
+ Just nfo -> relnfo r nfo
+
+ relnfo r nfo =
+ tr []
+ [ td [ class "tco1" ] [ Html.map (Rel r.rid) (RE.view r) ]
+ , td [ class "tco2" ] [ RDate.display model.today nfo.released ]
+ , td [ class "tco3" ]
+ <| List.map platformIcon nfo.platforms
+ ++ List.map langIcon nfo.lang
+ ++ [ releaseTypeIcon nfo.rtype ]
+ , td [ class "tco4" ] [ a [ href ("/"++nfo.id), title nfo.original ] [ text nfo.title ] ]
+ ]
+
+ box () =
+ [ h2 [] [ text (Maybe.withDefault "" model.title) ]
+ , div [ style "text-align" "right", style "margin" "3px 0" ] <|
+ case (model.del, model.onlist) of
+ ( _, False) -> [ b [ class "grayedout" ] [ text "not on your list" ] ]
+ (True, _) ->
+ [ a [ onClickD Delete ] [ text "Yes, delete" ]
+ , text " | "
+ , a [ onClickD (Del False) ] [ text "Cancel" ]
+ ]
+ (False, True) ->
+ [ span [ classList [("hidden", not (isPublic model))], title "This visual novel is on your public list" ] [ text "👁 " ]
+ , text "On your list | "
+ , a [ onClickD (Del True) ] [ text "Remove from list" ]
+ ]
+ , table []
+ [ tr [] [ td [] [ text "Labels" ], td [] [ Html.map Label (LE.view model.labels "- select label -") ] ]
+ , if not model.canvote then text "" else
+ tr []
+ [ td [] [ text "Vote" ]
+ , td []
+ [ div [ style "width" "80px", style "display" "inline-block" ] [ Html.map Vote (VE.view model.vote "- vote -") ]
+ , case (model.vote.vote /= Nothing && model.canreview, model.review) of
+ (False, _) -> text ""
+ (True, Nothing) -> a [ href ("/" ++ model.vid ++ "/addreview") ] [ text " write a review »" ]
+ (True, Just w) -> a [ href ("/" ++ w ++ "/edit") ] [ text " edit review »" ]
+ ]
+ ]
+ , tr [] [ td [] [ text "Start date" ], td [ class "date" ] [ Html.map Started (DE.view model.started ) ] ]
+ , tr [] [ td [] [ text "Finish date" ], td [ class "date" ] [ Html.map Finished (DE.view model.finished) ] ]
+ , tr []
+ [ td [] [ text "Notes ", span [ class "spinner", classList [("hidden", model.notesState /= Api.Loading)] ] [] ]
+ , td [] <|
+ [ textarea ([ rows 2, cols 40, onInput Notes, onBlur (NotesSave model.notesRev)] ++ GVN.valNotes) [ text model.notes ]
+ ] ++ case model.notesState of
+ Api.Error e -> [ br [] [], b [ class "standout" ] [ text <| Api.showResponse e ] ]
+ _ -> []
+ ]
+ ]
+ , if List.isEmpty model.relOptions then text "" else h2 [] [ text "Releases" ]
+ , table [] <|
+ (if List.isEmpty model.relOptions then text "" else tfoot [] [ tr []
+ [ td [] []
+ , td [ colspan 3 ]
+ [ inputSelect "" "" RelAdd [] <| ("", "-- add release --") :: List.filter (\(rid,_) -> not <| List.any (\r -> r.rid == rid) model.rels) model.relOptions ]
+ ] ]
+ ) :: List.map rel model.rels
+ ]
+ in
+ if model.open
+ then div [ class "ulist-widget elm_dd_input" ]
+ [ div [ id "ulist-widget-box" ] <|
+ case model.loadState of
+ Api.Loading -> [ div [ class "spinner" ] [] ]
+ Api.Error e -> [ b [ class "standout" ] [ text <| Api.showResponse e ] ]
+ Api.Normal -> box () ]
+ else icon ()
diff --git a/lib/VNWeb/AdvSearch.pm b/lib/VNWeb/AdvSearch.pm
index 6c8474cf..7e610fd6 100644
--- a/lib/VNWeb/AdvSearch.pm
+++ b/lib/VNWeb/AdvSearch.pm
@@ -751,6 +751,7 @@ sub elm_search_query {
sub elm_ {
my($self) = @_;
+ # TODO: labels can be lazily loaded to reduce page weight
state $schema ||= tuwf->compile({ type => 'hash', keys => {
uid => { vndbid => 'u', required => 0 },
labels => { aoh => { id => { uint => 1 }, label => {} } },
diff --git a/lib/VNWeb/Elm.pm b/lib/VNWeb/Elm.pm
index 68b244c7..1c02a5fe 100644
--- a/lib/VNWeb/Elm.pm
+++ b/lib/VNWeb/Elm.pm
@@ -168,7 +168,27 @@ $apis{AdvSearchQuery} = [ { type => 'hash', keys => { # Response to 'AdvSearchLo
tags => $apis{TagResult}[0],
traits => $apis{TraitResult}[0],
anime => $apis{AnimeResult}[0],
-} } ],
+} } ];
+$apis{UListWidget} = [ { type => 'hash', keys => { # Initialization for UList.Widget and response to UListWidget
+ uid => { vndbid => 'u' },
+ vid => { vndbid => 'v' },
+ # Only includes selected labels, null if the VN is not on the list at all.
+ labels => { required => 0, aoh => { id => { int => 1 }, label => {required => 0, default => ''} } },
+ # Can be set to null to lazily load the extra data as needed
+ full => { required => 0, type => 'hash', keys => {
+ title => {},
+ labels => { aoh => { id => { int => 1 }, label => {}, private => { anybool => 1 } } },
+ canvote => { anybool => 1 },
+ canreview => { anybool => 1 },
+ vote => { vnvote => 1 },
+ review => { required => 0, vndbid => 'w' },
+ notes => { required => 0, default => '' },
+ started => { required => 0, default => '' },
+ finished => { required => 0, default => '' },
+ releases => $apis{Releases}[0],
+ rlist => { aoh => { id => { vndbid => 'r' }, status => { uint => 1 } } },
+ } },
+} } ];
# Compile %apis into a %schema and generate the elm_Response() functions
diff --git a/lib/VNWeb/ULists/Elm.pm b/lib/VNWeb/ULists/Elm.pm
index e1a61737..0d9eeb06 100644
--- a/lib/VNWeb/ULists/Elm.pm
+++ b/lib/VNWeb/ULists/Elm.pm
@@ -2,6 +2,7 @@ package VNWeb::ULists::Elm;
use VNWeb::Prelude;
use VNWeb::ULists::Lib;
+use VNWeb::Releases::Lib 'releases_by_vn';
# Should be called after any change to the ulist_* tables.
@@ -235,6 +236,38 @@ elm_api UListRStatus => undef, $RLIST_STATUS, sub {
+our $WIDGET = form_compile out => $VNWeb::Elm::apis{UListWidget}[0]{keys};
+
+elm_api UListWidget => $WIDGET, { uid => { vndbid => 'u' }, vid => { vndbid => 'v' } }, sub {
+ my($data) = @_;
+ return elm_Unauth if !ulists_own $data->{uid};
+ my $v = tuwf->dbRowi('SELECT title, c_released FROM vn WHERE id =', \$data->{vid});
+ return elm_Invalid if !defined $v->{title};
+ my $lst = tuwf->dbRowi('SELECT vid, vote, notes, started, finished FROM ulist_vns WHERE uid =', \$data->{uid}, 'AND vid =', \$data->{vid});
+ my $review = tuwf->dbVali('SELECT id FROM reviews WHERE uid =', \$data->{uid}, 'AND vid =', \$data->{vid});
+ my $canvote = sprintf('%08d', $v->{c_released}||0) < strftime '%Y%m%d', gmtime;
+ elm_UListWidget {
+ uid => $data->{uid},
+ vid => $data->{vid},
+ labels => !$lst->{vid} ? undef : tuwf->dbAlli('SELECT lbl AS id, \'\' AS label FROM ulist_vns_labels WHERE uid =', \$data->{uid}, 'AND vid =', \$data->{vid}),
+ full => {
+ title => $v->{title},
+ labels => tuwf->dbAlli('SELECT id, label, private FROM ulist_labels WHERE uid =', \$data->{uid}, 'ORDER BY CASE WHEN id < 10 THEN id ELSE 10 END, label'),
+ canvote => $lst->{vote} || $canvote || 0,
+ canreview => $review || ($canvote && can_edit(w => {})) || 0,
+ vote => fmtvote($lst->{vote}),
+ review => $review,
+ notes => $lst->{notes}||'',
+ started => $lst->{started}||'',
+ finished => $lst->{finished}||'',
+ releases => releases_by_vn($data->{vid}),
+ rlist => tuwf->dbAlli('SELECT rid AS id, status FROM rlists WHERE uid =', \$data->{uid}, 'AND rid IN(SELECT id FROM releases_vn WHERE vid =', \$data->{vid}, ')'),
+ },
+ };
+};
+
+
+
our %SAVED_OPTS = (
# Labels
diff --git a/lib/VNWeb/VN/List.pm b/lib/VNWeb/VN/List.pm
index 670c8358..0c28a1c4 100644
--- a/lib/VNWeb/VN/List.pm
+++ b/lib/VNWeb/VN/List.pm
@@ -97,9 +97,12 @@ sub listing_ {
}, sort { $a->{name} cmp $b->{name} || $a->{id} <=> $b->{id} } $_->{developers}->@*;
} if $opt->{s}->vis('developer');
td_ class => 'tc_ulist', sub {
- b_ class => $_->{userlist_obtained} == $_->{userlist_all} ? 'done' : 'todo', sprintf '%d/%d', $_->{userlist_obtained}, $_->{userlist_all} if $_->{userlist_all};
- abbr_ title => join(', ', $_->{vnlist_labels}->@*), scalar $_->{vnlist_labels}->@* if $_->{vnlist_labels} && $_->{vnlist_labels}->@*;
- abbr_ title => 'No labels', ' ' if $_->{vnlist_labels} && !$_->{vnlist_labels}->@*;
+ elm_ 'UList.Widget', $VNWeb::ULists::Elm::WIDGET, {
+ uid => auth->uid,
+ vid => $_->{id},
+ labels => $_->{on_vnlist} ? $_->{vnlist_labels} : undef,
+ full => undef,
+ } if auth;
};
td_ class => 'tc_plat', sub { join_ '', sub { platform_ $_ if $_ ne 'unk' }, sort $_->{platforms}->@* };
td_ class => 'tc_lang', sub { join_ '', sub { abbr_ class => "icons lang $_", title => $LANGUAGE{$_}, '' }, reverse sort $_->{lang}->@* };
@@ -202,18 +205,10 @@ sub enrich_listing {
enrich_image_obj image => @_ if !$opt->{s}->rows;
- enrich_merge id => sub { sql '
- SELECT irv.vid AS id
- , COUNT(*) AS userlist_all
- , SUM(CASE WHEN irl.status = 1+1 THEN 1 ELSE 0 END) AS userlist_obtained
- FROM rlists irl
- JOIN releases_vn irv ON irv.id = irl.rid
- WHERE irl.uid =', \auth->uid, 'AND irv.vid IN', $_, '
- GROUP BY irv.vid
- ' }, @_ if auth;
+ enrich_merge id => sql('SELECT vid AS id, true AS on_vnlist FROM ulist_vns WHERE uid =', \auth->uid, 'AND vid IN'), @_ if auth;
- enrich_flatten vnlist_labels => id => vid => sub { sql '
- SELECT uvl.vid, ul.label
+ enrich vnlist_labels => id => vid => sub { sql '
+ SELECT uvl.vid, ul.id, ul.label
FROM ulist_vns_labels uvl
JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl
WHERE uvl.uid =', \auth->uid, 'AND uvl.vid IN', $_[0], '
diff --git a/static/f/list-add.svg b/static/f/list-add.svg
new file mode 100644
index 00000000..96b9f281
--- /dev/null
+++ b/static/f/list-add.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 426.67 426.67" xmlns="http://www.w3.org/2000/svg">
+<polygon points="234.67 192 234.67 106.67 192 106.67 192 192 106.67 192 106.67 234.67 192 234.67 192 320 234.67 320 234.67 234.67 320 234.67 320 192"/>
+<path d="m213.33 0c-117.82 0-213.33 95.513-213.33 213.33s95.513 213.33 213.33 213.33 213.33-95.513 213.33-213.33-95.512-213.33-213.33-213.33zm0 388.05c-96.495 0-174.72-78.225-174.72-174.72s78.225-174.72 174.72-174.72c96.446 0.117 174.6 78.273 174.72 174.72 0 96.496-78.224 174.72-174.72 174.72z"/>
+</svg>
diff --git a/static/f/list-l1.svg b/static/f/list-l1.svg
new file mode 100644
index 00000000..7ca55f17
--- /dev/null
+++ b/static/f/list-l1.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 426.67 426.67" xmlns="http://www.w3.org/2000/svg">
+<path d="m213.33 0c-117.33 0-213.33 96-213.33 213.33s96 213.33 213.33 213.33 213.33-96 213.33-213.33-95.999-213.33-213.33-213.33zm0 388.27c-96 0-174.93-78.933-174.93-174.93s78.933-174.93 174.93-174.93 174.93 78.933 174.93 174.93-78.933 174.93-174.93 174.93z"/>
+<path d="m149.33 87.467v251.73l187.73-125.87-187.73-125.87zm42.667 74.666 76.8 51.2-76.8 49.067v-100.27z"/>
+</svg>
diff --git a/static/f/list-l2.svg b/static/f/list-l2.svg
new file mode 100644
index 00000000..65ca67f9
--- /dev/null
+++ b/static/f/list-l2.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 426.67 426.67" xmlns="http://www.w3.org/2000/svg">
+<polygon points="293.33 135.04 190.08 240.21 137.17 187.09 108.8 215.47 192.21 298.67 326.19 168.75"/>
+<path d="m213.33 0c-117.82 0-213.33 95.513-213.33 213.33s95.513 213.33 213.33 213.33 213.33-95.513 213.33-213.33-95.512-213.33-213.33-213.33zm0 388.05c-96.495 0-174.72-78.225-174.72-174.72s78.225-174.72 174.72-174.72c96.446 0.117 174.6 78.273 174.72 174.72 0 96.496-78.224 174.72-174.72 174.72z"/>
+</svg>
diff --git a/static/f/list-l3.svg b/static/f/list-l3.svg
new file mode 100644
index 00000000..dd1ec71a
--- /dev/null
+++ b/static/f/list-l3.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 426.67 426.67" xmlns="http://www.w3.org/2000/svg">
+<path d="m213.33 0c-117.33 0-213.33 96-213.33 213.33s96 213.33 213.33 213.33 213.33-96 213.33-213.33-95.999-213.33-213.33-213.33zm0 388.27c-96 0-174.93-78.933-174.93-174.93s78.933-174.93 174.93-174.93 174.93 78.933 174.93 174.93-78.933 174.93-174.93 174.93z"/>
+<rect x="149.33" y="128" width="42.667" height="170.67"/>
+<rect x="234.67" y="128" width="42.667" height="170.67"/>
+</svg>
diff --git a/static/f/list-l4.svg b/static/f/list-l4.svg
new file mode 100644
index 00000000..29e8bbce
--- /dev/null
+++ b/static/f/list-l4.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 426.67 426.67" xmlns="http://www.w3.org/2000/svg">
+<path d="m213.33 0c-117.82 0-213.33 95.513-213.33 213.33s95.513 213.33 213.33 213.33 213.33-95.513 213.33-213.33-95.512-213.33-213.33-213.33zm0 388.05c-96.495 0-174.72-78.225-174.72-174.72s78.225-174.72 174.72-174.72c96.446 0.117 174.6 78.273 174.72 174.72 0 96.496-78.224 174.72-174.72 174.72z"/>
+<path d="M256,128H128v170.667h170.667V128H256z M256,256h-85.333v-85.333H256V256z"/>
+</svg>
diff --git a/static/f/list-l5.svg b/static/f/list-l5.svg
new file mode 100644
index 00000000..7c3dd3e9
--- /dev/null
+++ b/static/f/list-l5.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 512.43 512.43" xmlns="http://www.w3.org/2000/svg">
+<circle cx="256" cy="289.39" r="42.667"/>
+<path d="m512.43 199.15-171.09-34.773-85.334-152.32-85.333 152.32-170.67 34.56 118.19 128-20.48 173.44 158.29-66.773 158.51 66.773-20.267-173.23 118.19-128zm-242.13 201.6-14.293-6.614-14.08 5.973-101.12 42.667 13.013-110.72 1.92-16.427-11.307-12.16-74.453-81.706 107.73-21.333 16.427-3.627 8.107-14.08 53.76-96.213 53.973 96.213 8.107 14.507 16.213 3.2 107.73 21.333-74.453 80.853-11.307 12.16 2.133 17.28 13.013 111.36-101.12-42.666z"/>
+</svg>
diff --git a/static/f/list-l7.svg b/static/f/list-l7.svg
new file mode 100644
index 00000000..9af189f7
--- /dev/null
+++ b/static/f/list-l7.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 505.81 505.81" xmlns="http://www.w3.org/2000/svg">
+<path d="m390.17 60.981c-40.151 0.287-77.221 21.574-97.707 56.107-20.486-34.532-57.556-55.819-97.707-56.107-14.644 0.017-29.142 2.916-42.667 8.533l28.8 29.227c4.566-0.927 9.208-1.427 13.867-1.493 27.505 0.211 52.871 14.879 66.773 38.613l30.933 50.987 30.933-50.987c13.903-23.734 39.268-38.402 66.773-38.613 44.929 1.164 80.434 38.482 79.36 83.413 0 34.987-29.867 85.333-69.333 137.39l25.813 25.813c42.667-55.04 79.787-115.84 79.787-163.2 1.071-64.959-50.669-118.51-115.62-119.68z"/>
+<path d="m366.27 359.65c-24.533 28.373-50.133 55.467-73.813 78.293-76.8-74.453-177.07-193.07-177.07-257.28 7e-3 -20.439 7.35-40.197 20.693-55.68-7.602-9.841-17.906-17.254-29.653-21.333-17.729 21.744-27.378 48.958-27.307 77.013 0 114.99 213.33 306.99 213.33 306.99s39.893-36.053 85.333-86.4c7.041-7.68-9.172-44.373-11.519-41.6z"/>
+<rect transform="matrix(.7071 -.7071 .7071 .7071 -105.69 250.66)" x="228.4" y="-78.936" width="42.667" height="663.68"/>
+</svg>
diff --git a/static/f/list-unknown.svg b/static/f/list-unknown.svg
new file mode 100644
index 00000000..182992ac
--- /dev/null
+++ b/static/f/list-unknown.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="0 0 426.67 426.67" xmlns="http://www.w3.org/2000/svg">
+<rect x="192" y="298.67" width="42.667" height="42.667"/>
+<path d="m213.33 0c-117.82 0-213.33 95.513-213.33 213.33s95.513 213.33 213.33 213.33 213.33-95.513 213.33-213.33-95.512-213.33-213.33-213.33zm0 388.05c-96.495 0-174.72-78.225-174.72-174.72s78.225-174.72 174.72-174.72c96.446 0.117 174.6 78.273 174.72 174.72 0 96.496-78.224 174.72-174.72 174.72z"/>
+<path d="m296.32 150.4c-10.974-45.833-57.025-74.091-102.86-63.117-38.533 9.226-65.646 43.762-65.462 83.384h42.667c2.003-23.564 22.729-41.043 46.293-39.04s41.043 22.729 39.04 46.293c-4.358 21.204-23.38 36.169-45.013 35.413-10.486 0-18.987 8.501-18.987 18.987v45.013h42.667v-24.32c45.12-11.635 72.565-57.312 61.653-102.61z"/>
+</svg>