From 8939317270f5facdd127c1b7640842a02a76e93c Mon Sep 17 00:00:00 2001 From: Yorhel Date: Wed, 1 Jul 2020 13:04:48 +0200 Subject: Char::Edit: Abstract & revamp image voting UI The image voting is now handled separately from form submission and image IDs are validated when changed. This abstraction is hopefully also usable for the VN form. --- elm/CharEdit.elm | 71 +++++++----------------- elm/Lib/Api.elm | 1 - elm/Lib/Image.elm | 162 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 53 deletions(-) create mode 100644 elm/Lib/Image.elm (limited to 'elm') diff --git a/elm/CharEdit.elm b/elm/CharEdit.elm index 0f75a357..75362ef0 100644 --- a/elm/CharEdit.elm +++ b/elm/CharEdit.elm @@ -17,6 +17,7 @@ import Lib.Autocomplete as A import Lib.Api as Api import Lib.Editsum as Editsum import Lib.RDate as RDate +import Lib.Image as Img import Gen.Release as GR import Gen.CharEdit as GCE import Gen.Types as GT @@ -65,11 +66,7 @@ type alias Model = , mainName : String , mainSearch : A.Model GApi.ApiCharResult , mainSpoil : Int - , image : Maybe String - , imageState : Api.State - , imageNew : Set.Set String - , imageSex : Maybe Int - , imageVio : Maybe Int + , image : Img.Image , traits : List GCE.RecvTraits , traitSearch : A.Model GApi.ApiTraitResult , traitSelId : Int @@ -108,11 +105,7 @@ init d = , mainName = d.main_name , mainSearch = A.init "" , mainSpoil = d.main_spoil - , image = d.image - , imageState = Api.Normal - , imageNew = Set.empty - , imageSex = d.image_sex - , imageVio = d.image_vio + , image = Img.info d.image_info , traits = d.traits , traitSearch = A.init "" , traitSelId = 0 @@ -148,9 +141,7 @@ encode model = , cup_size = model.cupSize , main = if model.mainHas then model.main else Nothing , main_spoil = model.mainSpoil - , image = model.image - , image_sex = model.imageSex - , image_vio = model.imageVio + , image = model.image.id , traits = List.map (\t -> { tid = t.tid, spoil = t.spoil }) model.traits , vns = List.map (\v -> { vid = v.vid, rid = v.rid, spoil = v.spoil, role = v.role }) model.vns } @@ -188,12 +179,10 @@ type Msg | MainHas Bool | MainSearch (A.Msg GApi.ApiCharResult) | MainSpoil Int - | ImageSet String + | ImageSet String Bool | ImageSelect | ImageSelected File - | ImageLoaded GApi.Response - | ImageSex Int Bool - | ImageVio Int Bool + | ImageMsg Img.Msg | TraitDel Int | TraitSel Int Int | TraitSpoil Int Int @@ -240,13 +229,10 @@ update msg model = Nothing -> ({ model | mainSearch = A.clear nm "", main = Just m1.id, mainName = m1.name }, c) MainSpoil n -> ({ model | mainSpoil = n }, Cmd.none) - ImageSet s -> ({ model | image = if s == "" then Nothing else Just s}, Cmd.none) + ImageSet s b -> let (nm, nc) = Img.new b s in ({ model | image = nm }, Cmd.map ImageMsg nc) ImageSelect -> (model, FSel.file ["image/png", "image/jpg"] ImageSelected) - ImageSelected f -> ({ model | imageState = Api.Loading }, Api.postImage Api.Ch f ImageLoaded) - ImageLoaded (GApi.Image i _ _) -> ({ model | image = Just i, imageNew = Set.insert i model.imageNew, imageState = Api.Normal }, Cmd.none) - ImageLoaded e -> ({ model | imageState = Api.Error e }, Cmd.none) - ImageSex i _ -> ({ model | imageSex = Just i }, Cmd.none) - ImageVio i _ -> ({ model | imageVio = Just i }, Cmd.none) + ImageSelected f -> let (nm, nc) = Img.upload Api.Ch f in ({ model | image = nm }, Cmd.map ImageMsg nc) + ImageMsg m -> let (nm, nc) = Img.update m model.image in ({ model | image = nm }, Cmd.map ImageMsg nc) TraitDel idx -> ({ model | traits = delidx idx model.traits }, Cmd.none) TraitSel id spl -> ({ model | traitSelId = id, traitSelSpl = spl }, Cmd.none) @@ -288,6 +274,7 @@ isValid : Model -> Bool isValid model = not ( (model.name /= "" && model.name == model.original) || hasDuplicates (List.map (\v -> (v.vid, Maybe.withDefault 0 v.rid)) model.vns) + || not (Img.isValid model.image) ) @@ -380,45 +367,25 @@ view model = image = div [ class "formimage" ] - [ div [] [ - case model.image of - Nothing -> text "No image." - Just id -> img [ src (imageUrl id) ] [] - ] + [ div [] [ Img.viewImg model.image ] , div [] [ h2 [] [ text "Image ID" ] - , inputText "" (Maybe.withDefault "" model.image) ImageSet GCE.valImage - , Maybe.withDefault (text "") <| Maybe.map (\i -> a [ href <| "/img/"++i ] [ text " (flagging)" ]) model.image + , input ([ type_ "text", class "text", tabindex 10, value (Maybe.withDefault "" model.image.id), onInputValidation ImageSet ] ++ GCE.valImage) [] , br [] [] , text "Use an image that already exists on the server or empty to remove the current image." , br_ 2 , h2 [] [ text "Upload new image" ] , inputButton "Browse image" ImageSelect [] - , case model.imageState of - Api.Normal -> text "" - Api.Loading -> span [ class "spinner" ] [] - Api.Error e -> b [ class "standout" ] [ text <| Api.showResponse e ] , br [] [] , text "Image must be in JPEG or PNG format and at most 10 MiB. Images larger than 256x300 will automatically be resized." - , if not (Set.member (Maybe.withDefault "" model.image) model.imageNew) then text "" else div [] - [ br [] [] - , text "Please flag this image: (see the ", a [ href "/d19" ] [ text "image flagging guidelines" ], text " for guidance)" - , table [] - [ thead [] [ tr [] [ td [] [ text "Sexual" ], td [] [ text "Violence" ] ] ] - , tr [] - [ td [] - [ label [] [ inputRadio "" (model.imageSex == Just 0) (ImageSex 0), text " Safe" ], br [] [] - , label [] [ inputRadio "" (model.imageSex == Just 1) (ImageSex 1), text " Suggestive" ], br [] [] - , label [] [ inputRadio "" (model.imageSex == Just 2) (ImageSex 2), text " Explicit" ] - ] - , td [] - [ label [] [ inputRadio "" (model.imageVio == Just 0) (ImageVio 0), text " Tame" ], br [] [] - , label [] [ inputRadio "" (model.imageVio == Just 1) (ImageVio 1), text " Violent" ], br [] [] - , label [] [ inputRadio "" (model.imageVio == Just 2) (ImageVio 2), text " Brutal" ] - ] + , case Img.viewVote model.image of + Nothing -> text "" + Just v -> + div [] + [ br [] [] + , text "Please flag this image: (see the ", a [ href "/d19" ] [ text "image flagging guidelines" ], text " for guidance)" + , Html.map ImageMsg v ] - ] - ] ] ] diff --git a/elm/Lib/Api.elm b/elm/Lib/Api.elm index 3ad3f3aa..e09f1199 100644 --- a/elm/Lib/Api.elm +++ b/elm/Lib/Api.elm @@ -45,7 +45,6 @@ showResponse res = BadCurPass -> "Current password is invalid." MailChange -> unexp ImgFormat -> "Unrecognized image format, only JPEG and PNG are accepted." - Image _ _ _ -> unexp Releases _ -> unexp BoardResult _ -> unexp TagResult _ -> unexp diff --git a/elm/Lib/Image.elm b/elm/Lib/Image.elm new file mode 100644 index 00000000..44ce8240 --- /dev/null +++ b/elm/Lib/Image.elm @@ -0,0 +1,162 @@ +module Lib.Image exposing (..) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Process +import Task +import File exposing (File) +import Lib.Html exposing (..) +import Lib.Api as Api +import Lib.Util exposing (imageUrl) +import Gen.Api as GApi +import Gen.Image as GI +import Gen.ImageVote as GIV + + +type State + = Normal + | Invalid + | NotFound + | Loading + | Error GApi.Response + +type alias Image = + { id : Maybe String + , img : Maybe GApi.ApiImageResult + , imgState : State + , saveState : Api.State + , saveTimer : Bool + } + + +info : Maybe GApi.ApiImageResult -> Image +info img = + { id = Maybe.map (\i -> i.id) img + , img = img + , imgState = Normal + , saveState = Api.Normal + , saveTimer = False + } + + +-- Fetch image info from the ID +new : Bool -> String -> (Image, Cmd Msg) +new valid id = + ( { id = if id == "" then Nothing else Just id + , img = Nothing + , imgState = if id == "" then Normal else if valid then Loading else Invalid + , saveState = Api.Normal + , saveTimer = False + } + , if valid && id /= "" then GI.send { id = id } Loaded else Cmd.none + ) + + +-- Upload a new image from a form +upload : Api.ImageType -> File -> (Image, Cmd Msg) +upload t f = + ( { id = Nothing + , img = Nothing + , imgState = Loading + , saveState = Api.Normal + , saveTimer = False + } + , Api.postImage t f Loaded) + + +type Msg + = Loaded GApi.Response + | MySex Int Bool + | MyVio Int Bool + | Save + | Saved GApi.Response + + +update : Msg -> Image -> (Image, Cmd Msg) +update msg model = + let + save m = + if m.saveTimer || Maybe.withDefault True (Maybe.map (\i -> i.token == Nothing || i.my_sexual == Nothing || i.my_violence == Nothing) m.img) + then (m, Cmd.none) + else ({ m | saveTimer = True }, Task.perform (always Save) (Process.sleep 1000)) + in + case msg of + Loaded (GApi.ImageResult [i]) -> ({ model | id = Just i.id, img = Just i, imgState = Normal}, Cmd.none) + Loaded (GApi.ImageResult []) -> ({ model | imgState = NotFound}, Cmd.none) + Loaded e -> ({ model | imgState = Error e }, Cmd.none) + + MySex v _ -> save { model | img = Maybe.map (\i -> { i | my_sexual = Just v }) model.img } + MyVio v _ -> save { model | img = Maybe.map (\i -> { i | my_violence = Just v }) model.img } + + Save -> + case Maybe.map (\i -> (i.token, i.my_sexual, i.my_violence)) model.img of + Just (Just token, Just sex, Just vio) -> + ( { model | saveTimer = False, saveState = Api.Loading } + , GIV.send { votes = [{ id = Maybe.withDefault "" model.id, token = token, sexual = sex, violence = vio, overrule = False }] } Saved) + _ -> (model, Cmd.none) + Saved (GApi.Success) -> ({ model | saveState = Api.Normal}, Cmd.none) + Saved e -> ({ model | saveState = Api.Error e }, Cmd.none) + + + +isValid : Image -> Bool +isValid img = img.imgState == Normal + + +viewImg : Image -> Html m +viewImg image = + case (image.imgState, image.img) of + (Loading, _) -> div [ class "spinner" ] [] + (NotFound, _) -> b [ class "standout" ] [ text "Image not found." ] + (Invalid, _) -> b [ class "standout" ] [ text "Invalid image ID." ] + (Error e, _) -> b [ class "standout" ] [ text <| Api.showResponse e ] + (_, Nothing) -> text "No image." + (_, Just i) -> + label [ class "imghover", style "width" (String.fromInt i.width++"px"), style "height" (String.fromInt i.height++"px") ] + [ div [ class "imghover--visible" ] + [ img [ src (imageUrl i.id) ] [] + , a [ href <| "/img/"++i.id ] <| + case (i.sexual_avg, i.violence_avg) of + (Just sex, Just vio) -> + -- XXX: These thresholds are subject to change, maybe just show the numbers here? + [ text <| if sex > 1.3 then "Explicit" else if sex > 0.4 then "Suggestive" else "Tame" + , text " / " + , text <| if vio > 1.3 then "Brutal" else if vio > 0.4 then "Violent" else "Safe" + , text <| " (" ++ String.fromInt i.votecount ++ ")" + ] + _ -> [ text "Not flagged" ] + ] + ] + + +viewVote : Image -> Maybe (Html Msg) +viewVote model = + let + vote i = table [] + [ thead [] [ tr [] + [ td [] [ text "Sexual ", if model.saveState == Api.Loading then span [ class "spinner" ] [] else text "" ] + , td [] [ text "Violence" ] + ] ] + , tfoot [] <| + case model.saveState of + Api.Error e -> [ tr [] [ td [ colspan 2 ] [ b [ class "standout" ] [ text (Api.showResponse e) ] ] ] ] + _ -> [] + , tr [] + [ td [] + [ label [] [ inputRadio "" (i.my_sexual == Just 0) (MySex 0), text " Safe" ], br [] [] + , label [] [ inputRadio "" (i.my_sexual == Just 1) (MySex 1), text " Suggestive" ], br [] [] + , label [] [ inputRadio "" (i.my_sexual == Just 2) (MySex 2), text " Explicit" ] + ] + , td [] + [ label [] [ inputRadio "" (i.my_violence == Just 0) (MyVio 0), text " Tame" ], br [] [] + , label [] [ inputRadio "" (i.my_violence == Just 1) (MyVio 1), text " Violent" ], br [] [] + , label [] [ inputRadio "" (i.my_violence == Just 2) (MyVio 2), text " Brutal" ] + ] + ] + ] + in case model.img of + Nothing -> Nothing + Just i -> + if i.token == Nothing then Nothing + else Just (vote i) -- cgit v1.2.3