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)