summaryrefslogtreecommitdiff
path: root/elm3/VNEdit
diff options
context:
space:
mode:
Diffstat (limited to 'elm3/VNEdit')
-rw-r--r--elm3/VNEdit/General.elm155
-rw-r--r--elm3/VNEdit/Main.elm199
-rw-r--r--elm3/VNEdit/New.elm12
-rw-r--r--elm3/VNEdit/Relations.elm90
-rw-r--r--elm3/VNEdit/Screenshots.elm182
-rw-r--r--elm3/VNEdit/Seiyuu.elm105
-rw-r--r--elm3/VNEdit/Staff.elm96
-rw-r--r--elm3/VNEdit/Titles.elm103
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!"
+ ]
+ ]
+ ]