diff options
Diffstat (limited to 'elm3/VNEdit')
-rw-r--r-- | elm3/VNEdit/General.elm | 155 | ||||
-rw-r--r-- | elm3/VNEdit/Main.elm | 199 | ||||
-rw-r--r-- | elm3/VNEdit/New.elm | 12 | ||||
-rw-r--r-- | elm3/VNEdit/Relations.elm | 90 | ||||
-rw-r--r-- | elm3/VNEdit/Screenshots.elm | 182 | ||||
-rw-r--r-- | elm3/VNEdit/Seiyuu.elm | 105 | ||||
-rw-r--r-- | elm3/VNEdit/Staff.elm | 96 | ||||
-rw-r--r-- | elm3/VNEdit/Titles.elm | 103 |
8 files changed, 942 insertions, 0 deletions
diff --git a/elm3/VNEdit/General.elm b/elm3/VNEdit/General.elm new file mode 100644 index 00000000..f98bf5c2 --- /dev/null +++ b/elm3/VNEdit/General.elm @@ -0,0 +1,155 @@ +module VNEdit.General exposing (..) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import File exposing (File) +import Json.Decode as JD +import Lib.Html exposing (..) +import Lib.Gen exposing (..) +import Lib.Util exposing (..) +import Lib.Api as Api + + +type alias Model = + { desc : String + , image : Int + , imgState : Api.State + , img_nsfw : Bool + , length : Int + , l_renai : String + , l_wp : String + , anime : String + , animeList : List { aid : Int } + , animeDuplicates : Bool + } + + +init : VNEdit -> Model +init d = + { desc = d.desc + , image = d.image + , imgState = Api.Normal + , img_nsfw = d.img_nsfw + , length = d.length + , l_renai = d.l_renai + , l_wp = d.l_wp + , anime = String.join " " (List.map (.aid >> String.fromInt) d.anime) + , animeList = d.anime + , animeDuplicates = False + } + + +new : Model +new = + { desc = "" + , image = 0 + , imgState = Api.Normal + , img_nsfw = False + , length = 0 + , l_renai = "" + , l_wp = "" + , anime = "" + , animeList = [] + , animeDuplicates = False + } + + +type Msg + = Desc String + | Image String + | ImgNSFW Bool + | Length String + | LWP String + | LRenai String + | Anime String + | ImgUpload (List File) + | ImgDone Api.Response + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Desc s -> ({ model | desc = s }, Cmd.none) + Image s -> ({ model | image = if s == "" then 0 else Maybe.withDefault model.image (String.toInt s) }, Cmd.none) + ImgNSFW b -> ({ model | img_nsfw = b }, Cmd.none) + Length s -> ({ model | length = Maybe.withDefault 0 (String.toInt s) }, Cmd.none) + LWP s -> ({ model | l_wp = s }, Cmd.none) + LRenai s -> ({ model | l_renai = s }, Cmd.none) + + Anime s -> + let lst = List.map (\e -> { aid = Maybe.withDefault 0 (String.toInt e) }) (String.words s) + in ({ model | anime = s, animeList = lst, animeDuplicates = hasDuplicates <| List.map .aid lst }, Cmd.none) + + ImgUpload [i] -> ({ model | imgState = Api.Loading }, Api.postImage Api.Cv i ImgDone) + ImgUpload _ -> (model, Cmd.none) + + ImgDone (Api.Image id _ _) -> ({ model | image = id, imgState = Api.Normal }, Cmd.none) + ImgDone r -> ({ model | image = 0, imgState = Api.Error r }, Cmd.none) + + +view : Model -> (Msg -> a) -> List (Html a) -> Html a +view model wrap titles = card "general" "General info" [] <| + titles ++ List.map (Html.map wrap) + [ cardRow "Description" (Just "English please!") <| formGroup + [ inputTextArea "desc" model.desc Desc [rows 8] + , div [class "form-group__help"] + [ text "Short description of the main story. Please do not include untagged spoilers," + , text " and don't forget to list the source in case you didn't write the description yourself." + , text " Formatting codes are allowed." + ] + ] + , cardRow "Image" Nothing + [ div [class "row"] + [ div [class "col-md col-md--1"] + [ div [style "max-width" "200px", style "margin-bottom" "8px"] + [ dbImg "cv" (if model.imgState == Api.Loading then -1 else model.image) [] Nothing ] + ] + , div [class "col-md col-md--2"] <| formGroups + [ [ label [for "img"] [ text "Upload new image" ] + , input [type_ "file", class "text", name "img", id "img", Api.onFileChange ImgUpload, disabled (model.imgState == Api.Loading) ] [] + , case model.imgState of + Api.Error r -> div [class "invalid-feedback"] [text <| Api.showResponse r] + _ -> text "" + , div [class "form-group__help"] + [ text "Preferably the cover of the CD/DVD/package. Image must be in JPEG or PNG format and at most 5MB. Images larger than 256x400 will automatically be resized." ] + ] + , [ label [for "img_id"] [ text "Image ID" ] + , inputText "img_id" (String.fromInt model.image) Image [pattern "^[0-9]+$", disabled (model.imgState == Api.Loading)] + , div [class "form-group__help"] + [ text "Use a VN image that is already on the server. Set to '0' to remove the current image." ] + ] + , [ label [for "img_nsfw"] [ text "NSFW" ] + , label [class "checkbox"] + [ inputCheck "img_nsfw" model.img_nsfw ImgNSFW + , text " Not safe for work" ] + , div [class "form-group__help"] + [ text "Please check this option if the image contains nudity, gore, or is otherwise not safe in a work-friendly environment." ] + ] + ] + ] + ] + , cardRow "Properties" Nothing <| formGroups + [ [ label [for "length"] [ text "Length" ] + , inputSelect [id "length", name "length", onInput Length] + (String.fromInt model.length) + (List.map (\(a,b) -> (String.fromInt a, b)) vnLengths) + ] + , [ label [] [ text "External links" ] + , p [] [ text "http://en.wikipedia.org/wiki/", inputText "l_wp" model.l_wp LWP [class "form-control--inline", maxlength 100] ] + , p [] [ text "http://renai.us/game/", inputText "l_renai" model.l_renai LRenai [class "form-control--inline", maxlength 100], text ".shtml" ] + ] + -- TODO: Nicer list-editing and search suggestions for anime + , [ label [ for "anime" ] [ text "Anime" ] + , inputText "anime" model.anime Anime [pattern "^[ 0-9]*$"] + , if model.animeDuplicates + then div [class "invalid-feedback"] [ text "There are duplicate anime." ] + else text "" + , div [class "form-group__help"] + [ text "Whitespace separated list of AniDB anime IDs. E.g. \"1015 3348\" will add Shingetsutan Tsukihime and Fate/stay night as related anime." + , br [] [] + , text "Note: It can take a few minutes for the anime titles to appear on the VN page." + ] + ] + ] + ] diff --git a/elm3/VNEdit/Main.elm b/elm3/VNEdit/Main.elm new file mode 100644 index 00000000..bee56211 --- /dev/null +++ b/elm3/VNEdit/Main.elm @@ -0,0 +1,199 @@ +module VNEdit.Main exposing (Model, Msg, main, new, view, update) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Lazy exposing (..) +import Json.Encode as JE +import Browser +import Browser.Navigation exposing (load) +import Lib.Html exposing (..) +import Lib.Gen exposing (..) +import Lib.Api as Api +import Lib.Editsum as Editsum +import VNEdit.Titles as Titles +import VNEdit.General as Gen +import VNEdit.Seiyuu as Seiyuu +import VNEdit.Staff as Staff +import VNEdit.Screenshots as Scr +import VNEdit.Relations as Rel + + +main : Program VNEdit Model Msg +main = Browser.element + { init = \e -> (init e, Cmd.none) + , view = view + , update = update + , subscriptions = always Sub.none + } + + +type alias Model = + { state : Api.State + , new : Bool + , editsum : Editsum.Model + , l_encubed : String + , titles : Titles.Model + , general : Gen.Model + , staff : Staff.Model + , seiyuu : Seiyuu.Model + , relations : Rel.Model + , screenshots : Scr.Model + , id : Maybe Int + , dupVNs : List Api.VN + } + + +init : VNEdit -> Model +init d = + { state = Api.Normal + , new = False + , editsum = { authmod = d.authmod, editsum = d.editsum, locked = d.locked, hidden = d.hidden } + , l_encubed = d.l_encubed + , titles = Titles.init d + , general = Gen.init d + , staff = Staff.init d.staff + , seiyuu = Seiyuu.init d.seiyuu d.chars + , relations = Rel.init d.relations + , screenshots = Scr.init d.screenshots d.releases + , id = d.id + , dupVNs = [] + } + + +new : Model +new = + { state = Api.Normal + , new = True + , editsum = Editsum.new + , l_encubed = "" + , titles = Titles.new + , general = Gen.new + , staff = Staff.init [] + , seiyuu = Seiyuu.init [] [] + , relations = Rel.init [] + , screenshots = Scr.init [] [] + , id = Nothing + , dupVNs = [] + } + + +encode : Model -> VNEditSend +encode model = + { editsum = model.editsum.editsum + , hidden = model.editsum.hidden + , locked = model.editsum.locked + , l_encubed = model.l_encubed + , title = model.titles.title + , original = model.titles.original + , alias = model.titles.alias + , desc = model.general.desc + , image = model.general.image + , img_nsfw = model.general.img_nsfw + , length = model.general.length + , l_renai = model.general.l_renai + , l_wp = model.general.l_wp + , anime = model.general.animeList + , staff = List.map (\e -> { aid = e.aid, role = e.role, note = e.note }) model.staff.staff + , seiyuu = List.map (\e -> { aid = e.aid, cid = e.cid, note = e.note }) model.seiyuu.seiyuu + , screenshots = List.map (\e -> { scr = e.scr, rid = e.rid, nsfw = e.nsfw }) model.screenshots.screenshots + , relations = List.map (\e -> { vid = e.vid, relation = e.relation, official = e.official }) model.relations.relations + } + + +type Msg + = Editsum Editsum.Msg + | Submit + | Submitted Api.Response + | Titles Titles.Msg + | General Gen.Msg + | Staff Staff.Msg + | Seiyuu Seiyuu.Msg + | Relations Rel.Msg + | Screenshots Scr.Msg + | CheckDup + | RecvDup Api.Response + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Editsum m -> ({ model | editsum = Editsum.update m model.editsum }, Cmd.none) + Titles m -> ({ model | titles = Titles.update m model.titles, dupVNs = [] }, Cmd.none) + + Submit -> + let + path = + case model.id of + Just id -> "/v" ++ String.fromInt id ++ "/edit" + Nothing -> "/v/add" + body = vneditSendEncode (encode model) + in ({ model | state = Api.Loading }, Api.post path body Submitted) + + Submitted (Api.Changed id rev) -> (model, load <| "/v" ++ String.fromInt id ++ "." ++ String.fromInt rev) + Submitted r -> ({ model | state = Api.Error r }, Cmd.none) + + General m -> let (nm, c) = Gen.update m model.general in ({ model | general = nm }, Cmd.map General c) + Staff m -> let (nm, c) = Staff.update m model.staff in ({ model | staff = nm }, Cmd.map Staff c) + Seiyuu m -> let (nm, c) = Seiyuu.update m model.seiyuu in ({ model | seiyuu = nm }, Cmd.map Seiyuu c) + Screenshots m -> let (nm, c) = Scr.update m model.screenshots in ({ model | screenshots = nm }, Cmd.map Screenshots c) + Relations m -> let (nm, c) = Rel.update m model.relations in ({ model | relations = nm }, Cmd.map Relations c) + + CheckDup -> + let body = JE.object + [ ("search", JE.list JE.string <| List.filter ((/=)"") <| model.titles.title :: model.titles.original :: model.titles.aliasList) + , ("hidden", JE.bool True) ] + in + if List.isEmpty model.dupVNs + then ({ model | state = Api.Loading }, Api.post "/js/vn.json" body RecvDup) + else ({ model | new = False }, Cmd.none) + + RecvDup (Api.VNResult dup) -> + ({ model | state = Api.Normal, dupVNs = dup, new = not (List.isEmpty dup) }, Cmd.none) + RecvDup r -> ({ model | state = Api.Error r }, Cmd.none) + + + +isValid : Model -> Bool +isValid model = not + ( model.titles.aliasDuplicates + || not (List.isEmpty model.titles.aliasBad) + || model.general.animeDuplicates + || model.staff.duplicates + || model.seiyuu.duplicates + || model.relations.duplicates + ) + + +view : Model -> Html Msg +view model = + if model.new + then form_ CheckDup (model.state == Api.Loading) + [ card "new" "Add a new visual novel" + [ div [class "card__subheading"] + [ text "Carefully read the " + , a [ href "/d2" ] [ text "guidelines" ] + , text " before creating a new visual novel entry, to make sure that the game indeed conforms to our inclusion criteria." + ] + ] <| + List.map (Html.map Titles) <| Titles.view model.titles + , if List.isEmpty model.dupVNs + then text "" + else card "dup" "Possible duplicates" [ div [ class "card__subheading" ] [ text "Please check the list below for possible duplicates." ] ] + [ cardRow "" Nothing <| formGroup [ div [ class "form-group__help" ] [ + ul [] <| List.map (\e -> + li [] [ a [ href <| "/v" ++ String.fromInt e.id, title e.original, target "_black" ] [ text e.title ] + , text <| if e.hidden then " (deleted)" else "" ] + ) model.dupVNs + ] ] ] + , submitButton "Continue" model.state (isValid model) False + ] + + else form_ Submit (model.state == Api.Loading) + [ Gen.view model.general General <| List.map (Html.map Titles) <| Titles.view model.titles + , Html.map Staff <| lazy Staff.view model.staff + , Html.map Seiyuu <| lazy2 Seiyuu.view model.seiyuu model.id + , Html.map Relations <| lazy Rel.view model.relations + , Html.map Screenshots <| lazy2 Scr.view model.screenshots model.id + , Html.map Editsum <| lazy Editsum.view model.editsum + , submitButton "Submit" model.state (isValid model) (model.general.imgState == Api.Loading || Scr.loading model.screenshots) + ] diff --git a/elm3/VNEdit/New.elm b/elm3/VNEdit/New.elm new file mode 100644 index 00000000..eee9ade6 --- /dev/null +++ b/elm3/VNEdit/New.elm @@ -0,0 +1,12 @@ +module VNEdit.New exposing (main) + +import Browser +import VNEdit.Main as Main + +main : Program () Main.Model Main.Msg +main = Browser.element + { init = always (Main.new, Cmd.none) + , view = Main.view + , update = Main.update + , subscriptions = always Sub.none + } diff --git a/elm3/VNEdit/Relations.elm b/elm3/VNEdit/Relations.elm new file mode 100644 index 00000000..673e4ba1 --- /dev/null +++ b/elm3/VNEdit/Relations.elm @@ -0,0 +1,90 @@ +module VNEdit.Relations exposing (Model, Msg, init, update, view) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Lib.Html exposing (..) +import Lib.Gen exposing (VNEditRelations, vnRelations) +import Lib.Api exposing (VN) +import Lib.Util exposing (..) +import Lib.Autocomplete as A + + +type alias Model = + { relations : List VNEditRelations + , search : A.Model VN + , duplicates : Bool + } + + +init : List VNEditRelations -> Model +init l = + { relations = l + , search = A.init + , duplicates = False + } + + +type Msg + = Del Int + | Official Int Bool + | Rel Int String + | Search (A.Msg VN) + + +searchConfig : A.Config Msg VN +searchConfig = { wrap = Search, id = "add-relation", source = A.vnSource } + + +validate : Model -> Model +validate model = { model | duplicates = hasDuplicates <| List.map .vid model.relations } + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Del i -> (validate { model | relations = delidx i model.relations }, Cmd.none) + Official i b -> (validate { model | relations = modidx i (\e -> { e | official = b }) model.relations }, Cmd.none) + Rel i s -> (validate { model | relations = modidx i (\e -> { e | relation = s }) model.relations }, Cmd.none) + Search m -> + let (nm, c, res) = A.update searchConfig m model.search + in case res of + Nothing -> ({ model | search = nm }, c) + Just r -> + let + rel = List.head vnRelations |> Maybe.map Tuple.first |> Maybe.withDefault "" + nrow = { vid = r.id, relation = rel, title = r.title, official = True } + in (validate { model | search = A.clear nm, relations = model.relations ++ [nrow] }, c) + + +view : Model -> Html Msg +view model = + let + entry n e = editListRow "row--ai-center" + [ editListField 1 "text-sm-right single-line" + [ a [href <| "/v" ++ String.fromInt e.vid, title e.title, target "_blank" ] [text e.title ] ] + , editListField 0 "" + [ text "is an " + , label [class "checkbox"] + [ inputCheck "" e.official (Official n) + , text " official" + ] + ] + , editListField 1 "" + [ inputSelect [onInput (Rel n)] e.relation vnRelations ] + , editListField 0 "single-line" [ text " of this VN" ] + , editListField 0 "" [ removeButton (Del n) ] + ] + + in card "relations" "Relations" [] <| + editList (List.indexedMap entry model.relations) + ++ formGroups ( + (if model.duplicates + then [ [ div [ class "invalid-feedback" ] + [ text "The list contains duplicates. Make sure that the same visual novel is not listed multiple times." ] ] ] + else [] + ) ++ + [ label [for "add-relation"] [text "Add relation"] + :: A.view searchConfig model.search [placeholder "Visual Novel...", style "max-width" "400px"] + ] + ) diff --git a/elm3/VNEdit/Screenshots.elm b/elm3/VNEdit/Screenshots.elm new file mode 100644 index 00000000..7505fc15 --- /dev/null +++ b/elm3/VNEdit/Screenshots.elm @@ -0,0 +1,182 @@ +module VNEdit.Screenshots exposing (Model, Msg, loading, init, update, view) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import File exposing (File) +import Lib.Html exposing (..) +import Lib.Util exposing (..) +import Lib.Api as Api +import Lib.Gen exposing (resolutions, VNEditScreenshots, VNEditReleases) +import Lib.Util exposing (lookup, isJust) + + +type alias Model = + { screenshots : List VNEditScreenshots + , releases : List VNEditReleases + , state : List Api.State + , id : Int -- Temporary negative internal screenshot identifier, until the image has been uploaded and the actual ID is known + , rel : Int + , nsfw : Bool + , files : List File + } + + +init : List VNEditScreenshots -> List VNEditReleases -> Model +init scr rels = + { screenshots = scr + , releases = rels + , state = List.map (always Api.Normal) scr + , id = -1 + , rel = Maybe.withDefault 0 <| Maybe.map .id <| List.head rels + , nsfw = False + , files = [] + } + + +loading : Model -> Bool +loading model = List.any (\s -> s /= Api.Normal) model.state + + +type Msg + = Del Int + | SetNSFW Int Bool + | SetRel Int String + | DefNSFW Bool + | DefRel String + | DefFiles (List File) + | Upload + | Done Int Api.Response + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Del i -> ({ model | screenshots = delidx i model.screenshots, state = delidx i model.state }, Cmd.none) + SetNSFW i b -> ({ model | screenshots = modidx i (\e -> { e | nsfw = b }) model.screenshots }, Cmd.none) + SetRel i s -> ({ model | screenshots = modidx i (\e -> { e | rid = Maybe.withDefault e.rid (String.toInt s) }) model.screenshots }, Cmd.none) + DefNSFW b -> ({ model | nsfw = b }, Cmd.none) + DefRel s -> ({ model | rel = Maybe.withDefault 0 (String.toInt s) }, Cmd.none) + DefFiles l -> ({ model | files = l }, Cmd.none) + + Upload -> + let + st = model.state ++ List.map (always Api.Loading) model.files + scr i _ = { scr = model.id - i, rid = model.rel, nsfw = model.nsfw, width = 0, height = 0 } + alst = List.indexedMap scr model.files + lst = model.screenshots ++ alst + nid = model.id - List.length model.files + cmd f i = Api.postImage Api.Sf f (Done i.scr) + cmds = List.map2 cmd model.files alst + in ({ model | screenshots = lst, id = nid, state = st, files = [] }, Cmd.batch cmds) + + Done id r -> + case List.head <| List.filter (\(_,i) -> i.scr == id) <| List.indexedMap (\a b -> (a,b)) model.screenshots of + Nothing -> (model, Cmd.none) + Just (n,_) -> + let + st _ = case r of + Api.Image _ _ _ -> Api.Normal + re -> Api.Error re + scr s = case r of + Api.Image nid width height -> { s | scr = nid, width = width, height = height } + _ -> s + in ({ model | screenshots = modidx n scr model.screenshots, state = modidx n st model.state }, Cmd.none) + + + +view : Model -> Maybe Int -> Html Msg +view model vid = + let + row image remove titl opts after = div [class "screenshot-edit__row"] + [ div [ class "screenshot-edit__screenshot" ] [ image ] + , div [ class "screenshot-edit__fields" ] <| + [ remove + , div [ class "screenshot-edit__title" ] [ text titl ] + , div [ class "screenshot-edit__options" ] opts + ] ++ after + ] + + rm n = div [ class "screenshot-edit__remove" ] [ removeButton (Del n) ] + img n f = dbImg "st" n [class "vn-image-placeholder--wide"] f + + commonRes res = + -- NDS resolution, not in the database + res == "256x384" || isJust (lookup res resolutions) + + resWarn e = + let res = String.fromInt e.width ++ "x" ++ String.fromInt e.height + in case List.filter (\r -> r.id == e.rid) model.releases |> List.head of + Nothing -> text "" -- Shouldn't happen + Just r -> + -- If the release resolution is known and does *not* match the image resolution, warn about that + if r.resolution /= "unknown" && r.resolution /= "nonstandard" && r.resolution /= res + then div [ class "invalid-feedback" ] + [ text <| "Screenshot resolution is not the same as that of the selected release (" ++ r.resolution ++ "). Please make sure take screenshots in that *exact* resolution!" ] + -- Otherwise, if this isn't a non-standard resolution, check for common ones + else if r.resolution == "nonstandard" || commonRes res + then text "" + else div [ class "invalid-feedback" ] + [ text <| "Odd screenshot resolution. Please make sure take screenshots in the correct resolution!" ] + + entry n (s,e) = case s of + Api.Loading -> row (img -1 Nothing) (rm n) "Uploading screenshot" [] [] + Api.Error r -> row + (img 0 Nothing) (rm n) "Upload failed" + [ div [ class "invalid-feedback" ] [ text <| Api.showResponse r ] ] + [] + Api.Normal -> row + (img e.scr <| Just { width = e.width, height = e.height, id = "scr" }) + (rm n) ("Screenshot #" ++ String.fromInt e.scr) + [ span [ class "muted" ] [ text <| String.fromInt e.width ++ "x" ++ String.fromInt e.height ] + , label [ class "checkbox" ] + [ inputCheck "" e.nsfw (SetNSFW n) + , text " Not safe for work" + ] + ] + [ resWarn e + , releaseSelect e.rid (SetRel n) ] + + add = if List.length model.screenshots == 10 then text "" else row + (text "") + (text "") + "Add screenshot" + [ span [ class "muted" ] [ text "Image must be smaller than 5MB and in PNG or JPEG format. No more than 10 screenshots can be uploaded." ] ] + [ releaseSelect model.rel DefRel + , div [ class "screenshot-edit__upload-options" ] + [ div [ class "screenshot-edit__upload-option" ] [ input [ type_ "file", id "addscr", tabindex 10, multiple True, Api.onFileChange DefFiles ] [] ] + , div [ class "screenshot-edit__upload-option" ] + [ label [ class "checkbox screenshot-edit__upload-nsfw-label" ] + [ inputCheck "" model.nsfw DefNSFW + , text " Not safe for work" ] ] + , div [ class "flex-expand" ] [] + , div [ class "screenshot-edit__upload-option" ] + [ button + [ type_ "button", class "btn screenshot-edit__upload-btn", tabindex 10, onClick Upload + , disabled <| List.isEmpty model.files || (List.length model.files + List.length model.screenshots) > 10 + ] [ text "Upload!" ] ] + ] + ] + + releaseSelect rid msg = inputSelect [onInput msg] (String.fromInt rid) + <| List.map (\s -> (String.fromInt s.id, s.display)) model.releases + + norel = + case vid of + Nothing -> [ text "Screenshots can be uploaded after adding releases to this visual novel." ] + Just i -> + [ text "Screenshots can be added after " + , a [ href <| "/v" ++ (String.fromInt i) ++ "/add", target "_blank" ] [ text "adding a release entry" ] + , text "." + ] + + in if List.isEmpty model.releases + then card "screenshots" "Screenshots" [ div [class "card__subheading"] norel ] [] + else card "screenshots" "Screenshots" + [ div [class "card__subheading"] + [ text "Keep in mind that all screenshots must conform to " + , a [href "/d2#6", target "blank"] [ text "strict guidelines" ] + , text ", read those carefully!" + ] + ] + [ div [class "screenshot-edit"] <| List.indexedMap entry (List.map2 (\a b -> (a,b)) model.state model.screenshots) ++ [ add ] ] diff --git a/elm3/VNEdit/Seiyuu.elm b/elm3/VNEdit/Seiyuu.elm new file mode 100644 index 00000000..2f1a0457 --- /dev/null +++ b/elm3/VNEdit/Seiyuu.elm @@ -0,0 +1,105 @@ +module VNEdit.Seiyuu exposing (Model, Msg, init, update, view) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Lib.Util exposing (..) +import Lib.Html exposing (..) +import Lib.Gen exposing (VNEditSeiyuu, VNEditChars) +import Lib.Autocomplete as A +import Lib.Api exposing (Staff) + + +type alias Model = + { chars : List VNEditChars + , seiyuu : List VNEditSeiyuu + , search : A.Model Staff + , duplicates : Bool + } + + +init : List VNEditSeiyuu -> List VNEditChars -> Model +init s c = + { chars = c + , seiyuu = s + , search = A.init + , duplicates = False + } + + +type Msg + = Del Int + | SetNote Int String + | SetChar Int String + | Search (A.Msg Staff) + + +searchConfig : A.Config Msg Staff +searchConfig = { wrap = Search, id = "add-seiyuu", source = A.staffSource } + + +validate : Model -> Model +validate model = { model | duplicates = hasDuplicates <| List.map (\e -> (e.aid,e.cid )) model.seiyuu } + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Del i -> (validate { model | seiyuu = delidx i model.seiyuu }, Cmd.none) + SetNote i s -> (validate { model | seiyuu = modidx i (\e -> { e | note = s }) model.seiyuu }, Cmd.none) + SetChar i s -> (validate { model | seiyuu = modidx i (\e -> { e | cid = Maybe.withDefault e.cid (String.toInt s) }) model.seiyuu }, Cmd.none) + + Search m -> + let (nm, c, res) = A.update searchConfig m model.search + in case res of + Nothing -> ({ model | search = nm }, c) + Just r -> + let + char = List.head model.chars |> Maybe.map .id |> Maybe.withDefault 0 + nrow = { aid = r.aid, cid = char, id = r.id, name = r.name, note = "" } + nmod = { model | search = A.clear nm, seiyuu = model.seiyuu ++ [nrow] } + in (validate nmod, c) + + + +view : Model -> Maybe Int -> Html Msg +view model id = + let + entry n e = editListRow "" + [ editListField 1 "col-form-label single-line" + [ a [href <| "/s" ++ String.fromInt e.id, target "_blank" ] [ text e.name ] ] + , editListField 1 "" + [ inputSelect + [onInput (SetChar n)] + (String.fromInt e.cid) + (List.map (\c -> (String.fromInt c.id, c.name)) model.chars) + ] + , editListField 2 "" [ inputText "" e.note (SetNote n) [placeholder "Note", maxlength 250] ] + , editListField 0 "" [ removeButton (Del n) ] + ] + + nochars = + case id of + Nothing -> [ text "Cast can be added when the visual novel entry has characters linked to it." ] + Just n -> + [ text "Cast can be added after " + , a [ href <| "/c/new?vid=" ++ (String.fromInt n), target "_blank" ] [ text "creating" ] + , text " the appropriate character entries, or after linking " + , a [ href "/c/all" ] [ text "existing characters" ] + , text " to this visual novel entry." + ] + + in if List.isEmpty model.chars + then card "cast" "Cast" [ div [class "card__subheading"] nochars ] [] + else card "cast" "Cast" [] <| + editList (List.indexedMap entry model.seiyuu) + ++ formGroups ( + (if model.duplicates + then [ [ div [ class "invalid-feedback" ] + [ text "The cast list contains duplicates. Make sure that each person is only listed at most once for the same character" ] ] ] + else [] + ) ++ + [ label [for "add-seiyuu"] [text "Add cast"] + :: A.view searchConfig model.search [placeholder "Cast name", style "max-width" "400px"] + ] + ) diff --git a/elm3/VNEdit/Staff.elm b/elm3/VNEdit/Staff.elm new file mode 100644 index 00000000..589475b2 --- /dev/null +++ b/elm3/VNEdit/Staff.elm @@ -0,0 +1,96 @@ +module VNEdit.Staff exposing (Model, Msg, init, update, view) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Lib.Html exposing (..) +import Lib.Autocomplete as A +import Lib.Gen exposing (VNEditStaff, staffRoles) +import Lib.Util exposing (..) +import Lib.Api exposing (Staff) + + +type alias Model = + { staff : List VNEditStaff + , search : A.Model Staff + , duplicates : Bool + } + + +init : List VNEditStaff -> Model +init l = + { staff = l + , search = A.init + , duplicates = False + } + + +type Msg + = Del Int + | SetNote Int String + | SetRole Int String + | Search (A.Msg Staff) + + +searchConfig : A.Config Msg Staff +searchConfig = { wrap = Search, id = "add-staff", source = A.staffSource } + + +validate : Model -> Model +validate model = { model | duplicates = hasDuplicates <| List.map (\e -> (e.aid,e.role)) model.staff } + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Del i -> (validate { model | staff = delidx i model.staff }, Cmd.none) + SetNote i s -> (validate { model | staff = modidx i (\e -> { e | note = s }) model.staff }, Cmd.none) + SetRole i s -> (validate { model | staff = modidx i (\e -> { e | role = s }) model.staff }, Cmd.none) + + Search m -> + let (nm, c, res) = A.update searchConfig m model.search + in case res of + Nothing -> ({ model | search = nm }, c) + Just r -> + let + role = List.head staffRoles |> Maybe.map Tuple.first |> Maybe.withDefault "" + nrow = { aid = r.aid, id = r.id, name = r.name, role = role, note = "" } + in (validate { model | search = A.clear nm, staff = model.staff ++ [nrow] }, c) + + + +view : Model -> Html Msg +view model = + let + entry n e = editListRow "" + [ editListField 1 "col-form-label single-line" + [ a [href <| "/s" ++ String.fromInt e.id, target "_blank" ] [text e.name ] ] + , editListField 1 "" + [ inputSelect [onInput (SetRole n)] e.role staffRoles ] + , editListField 2 "" + [ inputText "" e.note (SetNote n) [placeholder "Note", maxlength 250] ] + , editListField 0 "" [ removeButton (Del n) ] + ] + + in card "staff" "Staff" + [ div [class "card__subheading"] + [ text "For information, check the " + , a [href "/d2#3", target "_blank"] [text "staff editing guidelines"] + , text ". You can " + , a [href "/s/new", target "_blank"] [text "create a new staff entry"] + , text " if it is not in the database yet, but please " + , a [href "/s/all", target "_blank"] [text "check for aliases first"] + , text "." + ] + ] <| + editList (List.indexedMap entry model.staff) + ++ formGroups ( + (if model.duplicates + then [ [ div [ class "invalid-feedback" ] + [ text "The staff list contains duplicates. Make sure that each person is only listed at most once with the same role" ] ] ] + else [] + ) ++ + [ label [for "add-staff"] [text "Add staff"] + :: A.view searchConfig model.search [placeholder "Staff name", style "max-width" "400px"] + ] + ) diff --git a/elm3/VNEdit/Titles.elm b/elm3/VNEdit/Titles.elm new file mode 100644 index 00000000..9dad830d --- /dev/null +++ b/elm3/VNEdit/Titles.elm @@ -0,0 +1,103 @@ +module VNEdit.Titles exposing (..) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Dict +import Lib.Html exposing (..) +import Lib.Gen exposing (..) +import Lib.Util exposing (..) + + +type alias Model = + { title : String + , original : String + , alias : String + , aliasList : List String + , aliasDuplicates : Bool + , aliasBad : List String + , aliasRel : Dict.Dict String Bool + } + + +init : VNEdit -> Model +init d = + { title = d.title + , original = d.original + , alias = d.alias + , aliasList = splitLn d.alias + , aliasDuplicates = False + , aliasBad = [] + , aliasRel = Dict.fromList <| List.map (\e -> (e,True)) <| List.map .title d.releases ++ List.map .original d.releases + } + + +new : Model +new = + { title = "" + , original = "" + , alias = "" + , aliasList = [] + , aliasDuplicates = False + , aliasBad = [] + , aliasRel = Dict.empty + } + + +type Msg + = Title String + | Original String + | Alias String + + +update : Msg -> Model -> Model +update msg model = + case msg of + Title s -> { model | title = s } + Original s -> { model | original = s } + Alias s -> + let + lst = splitLn s + check a = a == model.title || a == model.original || Dict.member a model.aliasRel + in + { model + | alias = s + , aliasList = lst + , aliasDuplicates = hasDuplicates lst + , aliasBad = List.filter check lst + } + + +view : Model -> List (Html Msg) +view model = + [ cardRow "Title" Nothing <| formGroups + [ [ label [for "title"] [text "Title (romaji)"] + , inputText "title" model.title Title [required True, maxlength 250] + ] + , [ label [for "original"] [text "Original"] + , inputText "original" model.original Original [maxlength 250] + , div [class "form-group__help"] [text "The original title of this visual novel, leave blank if it already is in the Latin alphabet."] + ] + ] + , cardRow "Aliases" Nothing <| formGroup + [ inputTextArea "aliases" model.alias Alias + [ rows 4, maxlength 500 + , classList [("is-invalid", model.aliasDuplicates || not (List.isEmpty model.aliasBad))] + ] + , if model.aliasDuplicates + then div [class "invalid-feedback"] + [ text "There are duplicate aliases." ] + else text "" + , if List.isEmpty model.aliasBad + then text "" + else div [class "invalid-feedback"] + [ text + <| "The following aliases are already listed elsewhere and should be removed: " + ++ String.join ", " model.aliasBad + ] + , div [class "form-group__help"] + [ text "List of alternative titles or abbreviations. One line for each alias. Can include both official (japanese/english) titles and unofficial titles used around net." + , br [] [] + , text "Titles that are listed in the releases should not be added here!" + ] + ] + ] |