module AdvSearch.Set exposing (..) import Html exposing (..) import Html.Attributes exposing (..) import Set import Lib.Html exposing (..) import Lib.Util exposing (..) import Gen.Types as GT import Gen.ExtLinks as GEL import AdvSearch.Lib exposing (..) type alias Model a = { sel : Set.Set a , single : Bool , and : Bool , neg : Bool , last : Set.Set a -- Last selection before switching to single mode, if there were multiple items selected } type Msg a = Sel a Bool | Neg Bool | And Bool | Single Bool | Mode -- Toggle between single / multi (or) / multi (and) init : Data -> (Data, Model a) init dat = (dat, { sel = Set.empty, single = True, and = False, neg = False, last = Set.empty }) update : Msg comparable -> Model comparable -> Model comparable update msg model = let singleMode m = { m | sel = if m.single then Set.fromList <| List.take 1 <| Set.toList m.sel else if model.single && not m.single && not (Set.isEmpty model.last) then m.last else m.sel , last = if m.single && not model.single && Set.size m.sel > 1 then m.sel else Set.empty } in case msg of Sel v b -> { model | last = Set.empty, sel = if not b then Set.remove v model.sel else if model.single then Set.fromList [v] else Set.insert v model.sel } Neg b -> { model | neg = b } And b -> { model | and = b } Single b -> singleMode { model | single = b } Mode -> singleMode { model | single = not model.single && model.and, and = not model.single && not model.and } toQuery : (Op -> a -> Query) -> Model a -> Maybe Query toQuery f m = case (m.neg, m.and, Set.toList m.sel) of (_,_,[]) -> Nothing (n,_,[v]) -> Just (f (if n then Ne else Eq) v) (False, False, l) -> Just <| QOr <| List.map (\v -> f Eq v) l (True , False, l) -> Just <| QAnd <| List.map (\v -> f Ne v) l (False, True , l) -> Just <| QAnd <| List.map (\v -> f Eq v) l (True , True , l) -> Just <| QOr <| List.map (\v -> f Ne v) l -- Only recognizes queries generated by setToQuery, doesn't handle alternative query structures. -- Usage: -- setFromQuery (\q -> case q of -- QStr 2 op v -> Just (op, v) -- _ -> Nothing) model fromQuery : (Query -> Maybe (Op,comparable)) -> Data -> Query -> Maybe (Data, Model comparable) fromQuery f dat q = let single qs = f qs |> Maybe.andThen (\(op,v) -> if op /= Ne && op /= Eq then Nothing else Just (dat, { sel = Set.fromList [v], and = False, neg = (op == Ne), single = True, last = Set.empty })) lst and mm xqs = case (mm, xqs) of (Nothing, _) -> Nothing (_, []) -> mm (Just (_,m), x :: xs) -> f x |> Maybe.andThen (\(op,v) -> if (op /= Ne && op /= Eq) || (op == Ne) /= m.neg then Nothing else lst and (Just (dat, {m | and = xor and (op == Ne), single = False, sel = Set.insert v m.sel})) xs) in case q of QAnd (x::xs) -> lst True (single x) xs QOr (x::xs) -> lst False (single x) xs _ -> single q lblPrefix m = text <| (if m.neg then "¬" else "") ++ (if m.single || Set.size m.sel == 1 then "" else if m.and then "∀ " else "∃ ") optsMode m canAnd canSingle = if not canAnd && not canSingle then span [] [] else a [ href "#" , onClickD (if canAnd && canSingle then Mode else if canSingle then Single (not m.single) else And (not m.and)) , title <| if m.single then "Single-selection mode" else if m.and then "Entry must match all selected items" else "Entry must match at least one item" ] [ text <| "Mode:" ++ if m.single then "single" else if m.and then "all" else "any" ] opts m canAnd canSingle = div [ class "opts" ] [ optsMode m canAnd canSingle , linkRadio m.neg Neg [ text "invert" ] ] -- Language type LangField = LangVN | LangVNO | LangRel | LangProd | LangStaff type alias LangModel = (LangField, Model String) langInit field dat = init dat |> Tuple.mapSecond (\m -> (field,m)) langUpdate msg (field, model) = (field, update msg model) langView (field, model) = let tprefix = if field == LangVNO then "O " else "L " label = if field == LangVNO then "Orig language" else "Language" msg = case field of LangVN -> "Language(s) in which the visual novel is available." LangVNO -> "Language the visual novel has been originally written in." LangRel -> "Language(s) in which the release is available." LangProd -> "Primary language of the producer." LangStaff -> "Primary language of the staff." canAnd = case field of LangVN -> True LangVNO -> False LangRel -> True LangProd -> False LangStaff -> False lst = case field of LangVN -> scriptLangs LangVNO -> scriptLangs LangRel -> scriptLangs LangProd -> locLangs LangStaff -> locLangs in ( case Set.toList model.sel of [] -> small [] [ text label ] [v] -> span [ class "nowrap" ] [ text tprefix, lblPrefix model, langIcon v, text <| Maybe.withDefault "" (lookup v GT.languages) ] l -> span [ class "nowrap" ] <| text tprefix :: lblPrefix model :: List.intersperse (text "") (List.map langIcon l) , \() -> [ div [ class "advheader" ] [ h3 [] [ text msg ] , opts model canAnd True ] , ul [ style "columns" "2"] <| List.map (\(l,t) -> li [] [ linkRadio (Set.member l model.sel) (Sel l) [ langIcon l, text t ] ]) lst ] ) langFromQuery field dat qs = Maybe.map (\(d,m) -> (d,(field,m))) <| fromQuery (\q -> case (field, q) of (LangVNO, QStr 3 op v) -> Just (op, v) (LangVNO, _) -> Nothing (_, QStr 2 op v) -> Just (op, v) _ -> Nothing) dat qs langToQuery (field, model) = toQuery (QStr (if field == LangVNO then 3 else 2)) model -- Platform platformView unk model = let lst = if unk then ("", "Unknown") :: GT.platforms else GT.platforms fmt p t = [ if p == "" then text "" else platformIcon p, text t ] in ( case Set.toList model.sel of [] -> small [] [ text "Platform" ] [v] -> span [ class "nowrap" ] <| lblPrefix model :: fmt v (Maybe.withDefault "" (lookup v lst)) l -> span [ class "nowrap" ] <| lblPrefix model :: List.intersperse (text "") (List.map platformIcon l) , \() -> [ div [ class "advheader" ] [ h3 [] [ text "Platforms for which the visual novel is available." ] , opts model True True ] , ul [ style "columns" "2"] <| List.map (\(p,t) -> li [classList [("separator", p == "web")]] [ linkRadio (Set.member p model.sel) (Sel p) (fmt p t) ] ) lst ] ) platformFromQuery = fromQuery (\q -> case q of QStr 4 op v -> Just (op, v) _ -> Nothing) -- Length lengthView model = ( case Set.toList model.sel of [] -> small [] [ text "Length" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| Maybe.withDefault "" (lookup v GT.vnLengths) ] l -> span [] [ lblPrefix model, text <| "Length (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "Length (estimated play time)" ] , opts model False True ] , ul [] <| List.map (\(l,t) -> li [] [ linkRadio (Set.member l model.sel) (Sel l) [ text t ] ]) GT.vnLengths ] ) lengthFromQuery = fromQuery (\q -> case q of QInt 5 op v -> Just (op, v) _ -> Nothing) -- Development status devStatusView model = ( case Set.toList model.sel of [] -> small [] [ text "Status" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| Maybe.withDefault "" (lookup v GT.devStatus) ] l -> span [] [ lblPrefix model, text <| "Dev Status (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "Development status" ] , opts model False True ] , ul [] <| List.map (\(l,t) -> li [] [ linkRadio (Set.member l model.sel) (Sel l) [ text t ] ]) GT.devStatus ] ) devStatusFromQuery = fromQuery (\q -> case q of QInt 66 op v -> Just (op, v) _ -> Nothing) -- Character role roleView model = ( case Set.toList model.sel of [] -> small [] [ text "Role" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| Maybe.withDefault "" (lookup v GT.charRoles) ] l -> span [] [ lblPrefix model, text <| "Role (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "Role" ] , opts model True True ] , ul [] <| List.map (\(l,t) -> li [] [ linkRadio (Set.member l model.sel) (Sel l) [ text t ] ]) GT.charRoles ] ) roleFromQuery = fromQuery (\q -> case q of QStr 2 op v -> Just (op, v) _ -> Nothing) -- Blood type bloodView model = ( case Set.toList model.sel of [] -> small [] [ text "Blood type" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| "Blood type " ++ Maybe.withDefault "" (lookup v GT.bloodTypes) ] l -> span [] [ lblPrefix model, text <| "Blood type (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "Blood type" ] , opts model False True ] , ul [] <| List.map (\(l,t) -> li [] [ linkRadio (Set.member l model.sel) (Sel l) [ text t ] ]) GT.bloodTypes ] ) bloodFromQuery = fromQuery (\q -> case q of QStr 3 op v -> Just (op, v) _ -> Nothing) -- Character sex type alias SexModel = (Bool, Model String) type SexMsg = SexSpoil | SexSel (Msg String) sexInit spoil dat = init dat |> Tuple.mapSecond (\m -> (spoil,m)) sexFromQuery spoil dat qf = Maybe.map (Tuple.mapSecond (\m -> (spoil,m))) <| fromQuery (\q -> case (spoil, q) of (False, QStr 4 op v) -> Just (op, v) (True, QStr 5 op v) -> Just (op, v) _ -> Nothing) dat qf sexUpdate msg (spoil,model) = case msg of SexSpoil -> (not spoil, model) SexSel m -> (spoil, update m model) sexView (spoil,model) = ( case Set.toList model.sel of [] -> small [] [ text "Sex" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| "Sex: " ++ Maybe.withDefault "" (lookup v GT.genders) ] l -> span [] [ lblPrefix model, text <| "Sex (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader", style "width" "280px" ] [ h3 [] [ text "Sex" ] , div [ class "opts" ] [ Html.map SexSel (optsMode model False True) , a [ href "#", onClickD SexSpoil ] [ text <| if spoil then "spoilers" else "no spoilers" ] , linkRadio model.neg (SexSel << Neg) [ text "invert" ] ] ] , ul [] <| List.map (\(l,t) -> li [] [ linkRadio (Set.member l model.sel) (SexSel << Sel l) [ text t ] ]) GT.genders ] ) -- Staff gender genderView model = ( case Set.toList model.sel of [] -> small [] [ text "Gender" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| Maybe.withDefault "" (lookup v GT.genders) ] l -> span [] [ lblPrefix model, text <| "Gender (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "Gender" ] , opts model False True ] , ul [] <| List.map (\(k,l) -> li [] [ if k == "b" then text "" else linkRadio (Set.member k model.sel) (Sel k) [ text l ] ]) GT.genders ] ) genderFromQuery = fromQuery (\q -> case q of QStr 4 op v -> Just (op, v) _ -> Nothing) -- Release medium mediumView model = ( case Set.toList model.sel of [] -> small [] [ text "Medium" ] [v] -> span [ class "nowrap" ] [ lblPrefix model , text <| if v == "" then "Medium: Unknown" else Maybe.withDefault "" <| List.head <| List.filterMap (\(k,l,_) -> if v == k then Just l else Nothing) GT.media ] l -> span [] [ lblPrefix model, text <| "Media (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "Medium" ] , opts model True True ] , ul [] <| List.map (\(k,l,_) -> li [] [ linkRadio (Set.member k model.sel) (Sel k) [ text l ] ]) (("", "Unknown", True) :: GT.media) ] ) mediumFromQuery = fromQuery (\q -> case q of QStr 11 op v -> Just (op, v) _ -> Nothing) -- Release voiced voicedView model = ( case Set.toList model.sel of [] -> small [] [ text "Voiced" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| Maybe.withDefault "" (lookup v GT.voiced) ] l -> span [] [ lblPrefix model, text <| "Voiced (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "Voiced" ] , opts model False True ] , ul [] <| List.map (\(k,l) -> li [] [ linkRadio (Set.member k model.sel) (Sel k) [ text l ] ]) GT.voiced ] ) voicedFromQuery = fromQuery (\q -> case q of QInt 12 op v -> Just (op, v) _ -> Nothing) -- Release animation animatedView story model = let lbl = (if story then "Story" else "Ero") ++ " animation" in ( case Set.toList model.sel of [] -> small [] [ text lbl ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| (if story then "S " else "E ") ++ Maybe.withDefault "" (lookup v GT.animated) ] l -> span [ class "nowrap" ] [ lblPrefix model, text <| lbl ++ " (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text lbl ] , opts model False True ] , ul [] <| List.map (\(k,l) -> li [] [ linkRadio (Set.member k model.sel) (Sel k) [ text l ] ]) GT.animated ] ) animatedFromQuery story = fromQuery (\q -> case q of QInt 13 op v -> if not story then Just (op, v) else Nothing QInt 14 op v -> if story then Just (op, v) else Nothing _ -> Nothing) -- Release type rtypeView model = ( case Set.toList model.sel of [] -> small [] [ text "Type" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| Maybe.withDefault "" (lookup v GT.releaseTypes) ] l -> span [ class "nowrap" ] [ lblPrefix model, text <| "Types (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "Release type" ] , opts model False True ] , ul [] <| List.map (\(k,l) -> li [] [ linkRadio (Set.member k model.sel) (Sel k) [ text l ] ]) GT.releaseTypes ] ) rtypeFromQuery = fromQuery (\q -> case q of QStr 16 op v -> Just (op, v) _ -> Nothing) -- Labels -- TODO: Do something with labels from other users - if only to display them correctly. labelView dat model = ( case Set.toList model.sel of [] -> small [] [ text "Labels" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| Maybe.withDefault "" (lookup v dat.labels) ] l -> span [ class "nowrap" ] [ lblPrefix model, text <| "Labels (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "VN labels" ] , opts model True True ] , ul [] <| List.map (\(k,l) -> li [] [ linkRadio (Set.member k model.sel) (Sel k) [ text l ] ]) dat.labels ] ) labelFromQuery dat q = fromQuery (\qs -> case qs of QTuple 12 op uid l -> if Just (vndbid 'u' uid) == dat.uid then Just (op, l) else Nothing _ -> Nothing) dat q -- Staff role sroleView model = let lst = ("seiyuu","Voice actor") :: GT.creditTypes in ( case Set.toList model.sel of [] -> small [] [ text "Role" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| Maybe.withDefault "" <| lookup v lst ] l -> span [ class "nowrap" ] [ lblPrefix model, text <| "Roles (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "Role" ] , opts model True True ] , ul [] <| List.map (\(k,l) -> li [] [ linkRadio (Set.member k model.sel) (Sel k) [ text l ] ]) lst ] ) sroleFromQuery = fromQuery (\q -> case q of QStr 5 op v -> Just (op, v) _ -> Nothing) -- Release list status rlistView model = ( case Set.toList model.sel of [] -> small [] [ text "List status" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| Maybe.withDefault "" <| lookup v GT.rlistStatus ] l -> span [ class "nowrap" ] [ lblPrefix model, text <| "List (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "List status" ] , opts model False True ] , ul [] <| List.map (\(k,l) -> li [] [ linkRadio (Set.member k model.sel) (Sel k) [ text l ] ]) GT.rlistStatus ] ) rlistFromQuery = fromQuery (\q -> case q of QInt 18 op v -> Just (op, v) _ -> Nothing) -- Producer type ptypeView model = ( case Set.toList model.sel of [] -> small [] [ text "Type" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| Maybe.withDefault "" (lookup v GT.producerTypes) ] l -> span [ class "nowrap" ] [ lblPrefix model, text <| "Types (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "Producer type" ] , opts model False True ] , ul [] <| List.map (\(k,l) -> li [] [ linkRadio (Set.member k model.sel) (Sel k) [ text l ] ]) GT.producerTypes ] ) ptypeFromQuery = fromQuery (\q -> case q of QStr 4 op v -> Just (op, v) _ -> Nothing) -- Extlinks (releases only, for now) extlinkView links model = let lst = List.map (\l -> (l.advid, l.name)) links in ( case Set.toList model.sel of [] -> small [] [ text "External links" ] [v] -> span [ class "nowrap" ] [ lblPrefix model, text <| Maybe.withDefault "" (lookup v lst) ] l -> span [ class "nowrap" ] [ lblPrefix model, text <| "Links (" ++ String.fromInt (List.length l) ++ ")" ] , \() -> [ div [ class "advheader" ] [ h3 [] [ text "External links" ] , opts model True True ] , ul [ style "columns" "2" ] <| List.map (\(k,l) -> li [] [ linkRadio (Set.member k model.sel) (Sel k) [ text l ] ]) lst ] ) extlinkFromQuery num = fromQuery (\q -> case q of QStr n op v -> if n == num then Just (op, v) else Nothing _ -> Nothing)