diff options
Diffstat (limited to 'elm/AdvSearch/Main.elm')
-rw-r--r-- | elm/AdvSearch/Main.elm | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/elm/AdvSearch/Main.elm b/elm/AdvSearch/Main.elm new file mode 100644 index 00000000..31331692 --- /dev/null +++ b/elm/AdvSearch/Main.elm @@ -0,0 +1,267 @@ +module AdvSearch.Main exposing (main) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Browser +import Set +import Dict +import Task +import Browser.Dom as Dom +import Array as Array +import Json.Encode as JE +import Json.Decode as JD +import Gen.Api as GApi +import Gen.AdvSearchSave as GASS +import Gen.AdvSearchDel as GASD +import Lib.Html exposing (..) +import Lib.Api as Api +import Lib.Ffi as Ffi +import Lib.DropDown as DD +import Lib.Autocomplete as A +import AdvSearch.Lib exposing (..) +import AdvSearch.Fields exposing (..) + + +main : Program Recv Model Msg +main = Browser.element + { init = \e -> (init e, Cmd.none) + , view = view + , update = update + , subscriptions = \m -> Sub.batch [ DD.sub m.saveDd, Sub.map Field (fieldSub m.query) ] + } + +type alias SQuery = { name: String, query: String } +type alias Recv = + { uid : Maybe String + , labels : List { id: Int, label: String } + , defaultSpoil : Int + , saved : List SQuery + , error : Bool + , query : GApi.ApiAdvSearchQuery + } + +type SaveAct = Save | Load | Delete | Default + +type alias Model = + { query : Field + , qtype : QType + , data : Data + , error : Bool + , saved : List SQuery + , saveState : Api.State + , saveDd : DD.Config Msg + , saveAct : SaveAct + , saveName : String + , saveDel : Set.Set String + , loadQuery : Maybe String + } + +type Msg + = Noop + | Field FieldMsg + | SaveToggle Bool + | SaveAct SaveAct + | SaveName String + | SaveSave String + | SaveSaved SQuery GApi.Response + | SaveLoad String + | SaveDelSel String + | SaveDel (Set.Set String) + | SaveDeleted (Set.Set String) GApi.Response + + +-- If the query only contains "quick" selection fields, add the remaining quick fields and sort them. +normalize : QType -> Field -> Data -> (Field, Data) +normalize qtype query odat = + let quickFromId (n,_,_) = Array.get n fields |> Maybe.map (\f -> abs f.quick) |> Maybe.withDefault 0 + present = List.foldl (\f a -> Set.insert (quickFromId f) a) Set.empty + defaults pres = Array.foldl (\f (al,dat,an) -> + if f.qtype == qtype && f.quick > 0 && not (Set.member (abs f.quick) pres) + then let (ndat, nf) = fieldInit an dat + in (nf::al, ndat, an+1) + else (al,dat,an+1) + ) ([],odat,0) fields + cmp a b = compare (quickFromId a) (quickFromId b) + in case query of + (qid, qdd, FMNest qm) -> + let pres = present qm.fields + (nl, ndat, _) = defaults pres + nqm = { qm | fields = List.sortWith cmp (nl++qm.fields) } + in if Set.member 0 pres || List.length nqm.fields > 4 then (query, odat) else ((qid, qdd, FMNest nqm), ndat) + _ -> (query, odat) + + +loadQuery : Data -> GApi.ApiAdvSearchQuery -> (QType, Field, Data) +loadQuery odat arg = + let dat = { objid = 0 + , level = 0 + , parentTypes = Set.empty + , uid = odat.uid + , labels = odat.labels + , defaultSpoil = odat.defaultSpoil + , producers = Dict.union (Dict.fromList <| List.map (\p -> (p.id,p)) <| arg.producers) odat.producers + , staff = Dict.union (Dict.fromList <| List.map (\s -> (s.id,s)) <| arg.staff ) odat.staff + , tags = Dict.union (Dict.fromList <| List.map (\t -> (t.id,t)) <| arg.tags ) odat.tags + , traits = Dict.union (Dict.fromList <| List.map (\t -> (t.id,t)) <| arg.traits ) odat.traits + , anime = Dict.union (Dict.fromList <| List.map (\a -> (a.id,a)) <| arg.anime ) odat.anime + } + qtype = + case arg.qtype of + "v" -> V + "c" -> C + "s" -> S + "p" -> P + _ -> R + + (dat2, query) = JD.decodeValue decodeQuery arg.query |> Result.toMaybe |> Maybe.withDefault (QAnd []) |> fieldFromQuery qtype dat + + -- We always want the top-level query to be a Nest type. + addtoplvl = let (_,m) = fieldCreate -1 (Tuple.mapSecond FMNest (nestInit True qtype qtype [query] dat2)) in m + query2 = case query of + (_,_,FMNest m) -> if m.qtype == qtype then query else addtoplvl + _ -> addtoplvl + dat3 = { dat2 | objid = dat2.objid + 5 } -- +5 for the creation of query2 + + (query3, dat4) = normalize qtype query2 dat3 + in (qtype, query3, dat4) + + +init : Recv -> Model +init arg = + let dat = { objid = 0 + , level = 0 + , parentTypes = Set.empty + , uid = arg.uid + , labels = (0, "Unlabeled") :: List.map (\e -> (e.id, e.label)) arg.labels + , defaultSpoil = arg.defaultSpoil + , producers = Dict.empty + , staff = Dict.empty + , tags = Dict.empty + , traits = Dict.empty + , anime = Dict.empty + } + (qtype, query, ndat) = loadQuery dat arg.query + in { query = query + , qtype = qtype + , data = ndat + , error = arg.error + , saved = arg.saved + , saveState = Api.Normal + , saveDd = DD.init "xsearch_save" SaveToggle + , saveAct = Save + , saveName = "" + , saveDel = Set.empty + , loadQuery = Nothing + } + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Noop -> (model, Cmd.none) + Field m -> + let (ndat, nm, nc) = fieldUpdate model.data m model.query + in ({ model | data = ndat, query = nm, error = False }, Cmd.map Field nc) + SaveToggle b -> + let act = if model.saveAct == Save && not (List.isEmpty model.saved) && fieldToQuery model.data model.query == Nothing then Load else model.saveAct + in ( { model | saveDd = DD.toggle model.saveDd b, saveAct = act, saveDel = Set.empty } + , if b && act == Save then Task.attempt (always Noop) (Dom.focus "xsearch_saveinput") else Cmd.none) + SaveAct n -> ({ model | saveAct = n, saveDel = Set.empty }, Cmd.none) + SaveName n -> ({ model | saveName = n }, Cmd.none) + SaveSave s -> + case Maybe.map encQuery (fieldToQuery model.data model.query) of + Just q -> ({ model | saveState = Api.Loading }, GASS.send { name = s, qtype = showQType model.qtype, query = q } (SaveSaved { name = s, query = q }) ) + Nothing -> (model, Cmd.none) + SaveSaved q GApi.Success -> + let f rep lst = case lst of + (x::xs) -> + if x.name == q.name then q :: f True xs + else if not rep && x.name > q.name then q :: x :: f True xs + else x :: f rep xs + [] -> if rep then [] else [q] + in ({ model | saveState = Api.Normal, saveDd = DD.toggle model.saveDd False, saved = f False model.saved }, Cmd.none) + SaveSaved _ e -> ({ model | saveState = Api.Error e }, Cmd.none) + SaveLoad q -> ({ model | saveState = Api.Loading, saveDd = DD.toggle model.saveDd False, loadQuery = Just q }, Task.attempt (always Noop) (Ffi.elemCall "click" "advsubmit")) + SaveDelSel s -> ({ model | saveDel = (if Set.member s model.saveDel then Set.remove else Set.insert) s model.saveDel }, Cmd.none) + SaveDel d -> ({ model | saveState = Api.Loading }, GASD.send { qtype = showQType model.qtype, name = Set.toList d } (SaveDeleted d)) + SaveDeleted d GApi.Success -> ({ model | saveState = Api.Normal, saveDel = Set.empty, saved = List.filter (\e -> not (Set.member e.name d)) model.saved }, Cmd.none) + SaveDeleted _ e -> ({ model | saveState = Api.Error e }, Cmd.none) + + +saveIcon = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><g fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z\"></path><polyline points=\"17 21 17 13 7 13 7 21\"></polyline><polyline points=\"7 3 7 8 15 8\"></polyline></g></svg>" + +view : Model -> Html Msg +view model = div [ class "xsearch" ] <| + let encQ = Maybe.withDefault "" <| Maybe.map encQuery (fieldToQuery model.data model.query) + in + [ input [ type_ "hidden", id "f", name "f", value (Maybe.withDefault encQ model.loadQuery) ] [] + , input [ type_ "submit", id "advsubmit", class "hidden" ] [] + , if model.data.uid == Nothing then text "" else div [ class "elm_dd_input elm_dd_noarrow elm_dd_rightish short" ] + [ DD.view model.saveDd model.saveState (span [ Ffi.innerHtml saveIcon ] []) <| \() -> + [ div [ class "advheader", style "min-width" "300px" ] + [ div [ class "opts", style "margin-bottom" "5px" ] + [ if model.saveAct == Save then strong [] [ text "Save" ] else a [ href "#", onClickD (SaveAct Save ) ] [ text "Save" ] + , if model.saveAct == Load then strong [] [ text "Load" ] else a [ href "#", onClickD (SaveAct Load ) ] [ text "Load" ] + , if model.saveAct == Delete then strong [] [ text "Delete" ] else a [ href "#", onClickD (SaveAct Delete ) ] [ text "Delete" ] + , if model.saveAct == Default then strong [] [ text "Default"] else a [ href "#", onClickD (SaveAct Default) ] [ text "Default" ] + ] + , h3 [] [ text <| case model.saveAct of + Save -> "Save current filter" + Load -> "Load filter" + Delete -> "Delete saved filter" + Default -> "Default filter" ] + ] + , case (List.filter (\e -> e.name /= "") model.saved, model.saveAct) of + (_, Save) -> + if encQ == "" then text "Nothing to save." else + form_ "" (SaveSave model.saveName) False + [ inputText "xsearch_saveinput" model.saveName SaveName [ required True, maxlength 50, placeholder "Name...", style "width" "290px" ] + , if model.saveName /= "" && List.any (\e -> e.name == model.saveName) model.saved + then text "You already have a filter by that name, click save to overwrite it." + else text "" + , submitButton "Save" model.saveState True + ] + (_, Default) -> + div [] + [ p [ class "center", style "padding" "0px 5px" ] <| + case model.qtype of + V -> [ text "You can set a default filter that will be applied automatically to most listings on the site," + , text " this includes the \"Random visual novel\" button, lists on the homepage, tag pages, etc." + , text " This feature is mainly useful to filter out tags, languages or platforms that you are not interested in seeing." + ] + R -> [ text "You can set a default filter that will be applied automatically to this release browser and the listings on the homepage." + , text " This feature is mainly useful to filter out tags, languages or platforms that you are not interested in seeing." + ] + _ -> [ text "You can set a default filter that will be applied automatically when you open this listing." ] + , br [] [] + , case List.filter (\e -> e.name == "") model.saved of + [d] -> span [] + [ inputButton "Load my default filters" (SaveLoad d.query) [style "width" "100%"] + , br [] [] + , br [] [] + , inputButton "Delete my default filters" (SaveDel (Set.fromList [""])) [style "width" "100%"] + ] + _ -> text "You don't have a default filter set." + , if encQ /= "" then inputButton "Save current filters as default" (SaveSave "") [ style "width" "100%" ] else text "" + ] + ([], _) -> text "You don't have any saved queries." + (l, Load) -> + div [] + [ if encQ == "" || List.any (\e -> encQ == e.query) l + then text "" else text "Unsaved changes will be lost when loading a saved filter." + , ul [] <| List.map (\e -> li [ style "overflow" "hidden", style "text-overflow" "ellipsis" ] [ a [ href "#", onClickD (SaveLoad e.query) ] [ text e.name ] ]) l + ] + (l, Delete) -> + div [] + [ ul [] <| List.map (\e -> li [ style "overflow" "hidden", style "text-overflow" "ellipsis" ] [ linkRadio (Set.member e.name model.saveDel) (always (SaveDelSel e.name)) [ text e.name ] ]) l + , inputButton "Delete selected" (SaveDel model.saveDel) [ disabled (Set.isEmpty model.saveDel) ] + ] + ] + ] + , Html.map Field (fieldView model.data model.query) + , if model.error + then b [] [ text "Error parsing search query. The URL was probably corrupted in some way. " + , text "Please report a bug if you opened this page from VNDB (as opposed to getting here from an external site)." ] + else text "" + ] |