diff options
-rw-r--r-- | css/v2.css | 38 | ||||
-rw-r--r-- | elm/Lib/Api.elm | 1 | ||||
-rw-r--r-- | elm/UList/DateEdit.elm | 2 | ||||
-rw-r--r-- | elm/UList/LabelEdit.elm | 2 | ||||
-rw-r--r-- | elm/UList/Widget.elm | 278 | ||||
-rw-r--r-- | lib/VNWeb/AdvSearch.pm | 1 | ||||
-rw-r--r-- | lib/VNWeb/Elm.pm | 22 | ||||
-rw-r--r-- | lib/VNWeb/ULists/Elm.pm | 33 | ||||
-rw-r--r-- | lib/VNWeb/VN/List.pm | 23 | ||||
-rw-r--r-- | static/f/list-add.svg | 5 | ||||
-rw-r--r-- | static/f/list-l1.svg | 5 | ||||
-rw-r--r-- | static/f/list-l2.svg | 5 | ||||
-rw-r--r-- | static/f/list-l3.svg | 6 | ||||
-rw-r--r-- | static/f/list-l4.svg | 5 | ||||
-rw-r--r-- | static/f/list-l5.svg | 5 | ||||
-rw-r--r-- | static/f/list-l7.svg | 6 | ||||
-rw-r--r-- | static/f/list-unknown.svg | 6 |
17 files changed, 412 insertions, 31 deletions
@@ -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> |