summaryrefslogtreecommitdiff
path: root/elm/VNEdit.elm
diff options
context:
space:
mode:
Diffstat (limited to 'elm/VNEdit.elm')
-rw-r--r--elm/VNEdit.elm788
1 files changed, 788 insertions, 0 deletions
diff --git a/elm/VNEdit.elm b/elm/VNEdit.elm
new file mode 100644
index 00000000..751cab61
--- /dev/null
+++ b/elm/VNEdit.elm
@@ -0,0 +1,788 @@
+port module VNEdit exposing (main)
+
+import Html exposing (..)
+import Html.Events exposing (..)
+import Html.Keyed as K
+import Html.Attributes exposing (..)
+import Browser
+import Browser.Navigation exposing (load)
+import Browser.Dom as Dom
+import Dict
+import Set
+import Task
+import Date
+import Process
+import File exposing (File)
+import File.Select as FSel
+import Lib.Ffi as Ffi
+import Lib.Util exposing (..)
+import Lib.Html exposing (..)
+import Lib.TextPreview as TP
+import Lib.Autocomplete as A
+import Lib.RDate as RDate
+import Lib.Api as Api
+import Lib.Editsum as Editsum
+import Lib.Image as Img
+import Gen.VN as GV
+import Gen.VNEdit as GVE
+import Gen.Types as GT
+import Gen.Api as GApi
+
+
+main : Program GVE.Recv Model Msg
+main = Browser.element
+ { init = \e -> (init e, Date.today |> Task.perform Today)
+ , view = view
+ , update = update
+ , subscriptions = always Sub.none
+ }
+
+
+port ivRefresh : Bool -> Cmd msg
+
+type Tab
+ = General
+ | Image
+ | Staff
+ | Cast
+ | Screenshots
+ | All
+
+type alias Model =
+ { state : Api.State
+ , tab : Tab
+ , today : Int
+ , invalidDis : Bool
+ , editsum : Editsum.Model
+ , titles : List GVE.RecvTitles
+ , alias : String
+ , description : TP.Model
+ , devStatus : Int
+ , olang : String
+ , length : Int
+ , lWikidata : Maybe Int
+ , lRenai : String
+ , vns : List GVE.RecvRelations
+ , vnSearch : A.Model GApi.ApiVNResult
+ , anime : List GVE.RecvAnime
+ , animeSearch : A.Model GApi.ApiAnimeResult
+ , image : Img.Image
+ , editions : List GVE.RecvEditions
+ , staff : List GVE.RecvStaff
+ -- Search boxes matching the list of editions (n+1), first entry is for the NULL edition.
+ , staffSearch : List (A.Config Msg GApi.ApiStaffResult, A.Model GApi.ApiStaffResult)
+ , seiyuu : List GVE.RecvSeiyuu
+ , seiyuuSearch: A.Model GApi.ApiStaffResult
+ , seiyuuDef : String -- character id for newly added seiyuu
+ , screenshots : List (Int,Img.Image,Maybe String) -- internal id, img, rel
+ , scrQueue : List File
+ , scrUplRel : Maybe String
+ , scrUplNum : Maybe Int
+ , scrId : Int -- latest used internal id
+ , releases : List GVE.RecvReleases
+ , reltitles : List { id: String, title: String }
+ , chars : List GVE.RecvChars
+ , id : Maybe String
+ , dupCheck : Bool
+ , dupVNs : List GApi.ApiVNResult
+ }
+
+
+init : GVE.Recv -> Model
+init d =
+ { state = Api.Normal
+ , tab = General
+ , today = 0
+ , invalidDis = False
+ , editsum = { authmod = d.authmod, editsum = TP.bbcode d.editsum, locked = d.locked, hidden = d.hidden, hasawait = False }
+ , titles = d.titles
+ , alias = d.alias
+ , description = TP.bbcode d.description
+ , devStatus = d.devstatus
+ , olang = d.olang
+ , length = d.length
+ , lWikidata = d.l_wikidata
+ , lRenai = d.l_renai
+ , vns = d.relations
+ , vnSearch = A.init ""
+ , anime = d.anime
+ , animeSearch = A.init ""
+ , image = Img.info d.image_info
+ , editions = d.editions
+ , staff = d.staff
+ , staffSearch = (staffConfig Nothing, A.init "") :: List.map (\e -> (staffConfig (Just e.eid), A.init "")) d.editions
+ , seiyuu = d.seiyuu
+ , seiyuuSearch= A.init ""
+ , seiyuuDef = Maybe.withDefault "" <| List.head <| List.map (\c -> c.id) d.chars
+ , screenshots = List.indexedMap (\n i -> (n, Img.info (Just i.info), i.rid)) d.screenshots
+ , scrQueue = []
+ , scrUplRel = Nothing
+ , scrUplNum = Nothing
+ , scrId = 100
+ , releases = d.releases
+ , reltitles = d.reltitles
+ , chars = d.chars
+ , id = d.id
+ , dupCheck = False
+ , dupVNs = []
+ }
+
+
+encode : Model -> GVE.Send
+encode model =
+ { id = model.id
+ , editsum = model.editsum.editsum.data
+ , hidden = model.editsum.hidden
+ , locked = model.editsum.locked
+ , titles = model.titles
+ , alias = model.alias
+ , devstatus = model.devStatus
+ , description = model.description.data
+ , olang = model.olang
+ , length = model.length
+ , l_wikidata = model.lWikidata
+ , l_renai = model.lRenai
+ , relations = List.map (\v -> { vid = v.vid, relation = v.relation, official = v.official }) model.vns
+ , anime = List.map (\a -> { aid = a.aid }) model.anime
+ , image = model.image.id
+ , editions = model.editions
+ , staff = List.map (\s -> { aid = s.aid, eid = s.eid, note = s.note, role = s.role }) model.staff
+ , seiyuu = List.map (\s -> { aid = s.aid, cid = s.cid, note = s.note }) model.seiyuu
+ , screenshots = List.map (\(_,i,r) -> { scr = Maybe.withDefault "" i.id, rid = r }) model.screenshots
+ }
+
+vnConfig : A.Config Msg GApi.ApiVNResult
+vnConfig = { wrap = VNSearch, id = "relationadd", source = A.vnSource }
+
+animeConfig : A.Config Msg GApi.ApiAnimeResult
+animeConfig = { wrap = AnimeSearch, id = "animeadd", source = A.animeSource False }
+
+staffConfig : Maybe Int -> A.Config Msg GApi.ApiStaffResult
+staffConfig eid =
+ { wrap = (StaffSearch eid)
+ , id = "staffadd-" ++ Maybe.withDefault "" (Maybe.map String.fromInt eid)
+ , source = A.staffSource
+ }
+
+seiyuuConfig : A.Config Msg GApi.ApiStaffResult
+seiyuuConfig = { wrap = SeiyuuSearch, id = "seiyuuadd", source = A.staffSource }
+
+type Msg
+ = Noop
+ | Today Date.Date
+ | Editsum Editsum.Msg
+ | Tab Tab
+ | Invalid Tab
+ | InvalidEnable
+ | Submit
+ | Submitted GApi.Response
+ | Alias String
+ | Desc TP.Msg
+ | DevStatus Int
+ | Length Int
+ | LWikidata (Maybe Int)
+ | LRenai String
+ | TitleAdd String
+ | TitleDel Int
+ | TitleLang Int String
+ | TitleTitle Int String
+ | TitleLatin Int String
+ | TitleOfficial Int Bool
+ | TitleMain Int String
+ | VNDel Int
+ | VNRel Int String
+ | VNOfficial Int Bool
+ | VNSearch (A.Msg GApi.ApiVNResult)
+ | AnimeDel Int
+ | AnimeSearch (A.Msg GApi.ApiAnimeResult)
+ | ImageSet String Bool
+ | ImageSelect
+ | ImageSelected File
+ | ImageMsg Img.Msg
+ | EditionAdd
+ | EditionLang Int (Maybe String)
+ | EditionName Int String
+ | EditionOfficial Int Bool
+ | EditionDel Int Int
+ | StaffDel Int
+ | StaffRole Int String
+ | StaffNote Int String
+ | StaffSearch (Maybe Int) (A.Msg GApi.ApiStaffResult)
+ | SeiyuuDef String
+ | SeiyuuDel Int
+ | SeiyuuChar Int String
+ | SeiyuuNote Int String
+ | SeiyuuSearch (A.Msg GApi.ApiStaffResult)
+ | ScrUplRel (Maybe String)
+ | ScrUplSel
+ | ScrUpl File (List File)
+ | ScrMsg Int Img.Msg
+ | ScrRel Int (Maybe String)
+ | ScrDel Int
+ | DupSubmit
+ | DupResults GApi.Response
+
+
+scrProcessQueue : (Model, Cmd Msg) -> (Model, Cmd Msg)
+scrProcessQueue (model, msg) =
+ case model.scrQueue of
+ (f::fl) ->
+ if List.any (\(_,i,_) -> i.imgState == Img.Loading) model.screenshots
+ then (model, msg)
+ else
+ let (im,ic) = Img.upload Api.Sf f
+ in ( { model | scrQueue = fl, scrId = model.scrId + 1, screenshots = model.screenshots ++ [(model.scrId, im, model.scrUplRel)] }
+ , Cmd.batch [ msg, Cmd.map (ScrMsg model.scrId) ic ] )
+ _ -> (model, msg)
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Noop -> (model, Cmd.none)
+ Today d -> ({ model | today = RDate.fromDate d |> RDate.compact }, Cmd.none)
+ Editsum m -> let (nm,nc) = Editsum.update m model.editsum in ({ model | editsum = nm }, Cmd.map Editsum nc)
+ Tab t -> ({ model | tab = t }, Cmd.none)
+ Invalid t -> if model.invalidDis || model.tab == All || model.tab == t then (model, Cmd.none) else
+ ({ model | tab = t, invalidDis = True }, Task.attempt (always InvalidEnable) (Ffi.elemCall "reportValidity" "mainform" |> Task.andThen (\_ -> Process.sleep 100)))
+ InvalidEnable -> ({ model | invalidDis = False }, Cmd.none)
+ Alias s -> ({ model | alias = s, dupVNs = [] }, Cmd.none)
+ Desc m -> let (nm,nc) = TP.update m model.description in ({ model | description = nm }, Cmd.map Desc nc)
+ DevStatus b-> ({ model | devStatus = b }, Cmd.none)
+ Length n -> ({ model | length = n }, Cmd.none)
+ LWikidata n-> ({ model | lWikidata = n }, Cmd.none)
+ LRenai s -> ({ model | lRenai = s }, Cmd.none)
+
+ TitleAdd s ->
+ ({ model | titles = model.titles ++ [{ lang = s, title = "", latin = Nothing, official = True }], olang = if List.isEmpty model.titles then s else model.olang }
+ , Task.attempt (always Noop) (Dom.focus ("title_" ++ s)))
+ TitleDel i -> ({ model | titles = delidx i model.titles }, Cmd.none)
+ TitleLang i s -> ({ model | titles = modidx i (\e -> { e | lang = s }) model.titles }, Cmd.none)
+ TitleTitle i s -> ({ model | titles = modidx i (\e -> { e | title = s }) model.titles }, Cmd.none)
+ TitleLatin i s -> ({ model | titles = modidx i (\e -> { e | latin = if s == "" then Nothing else Just s }) model.titles }, Cmd.none)
+ TitleOfficial i s -> ({ model | titles = modidx i (\e -> { e | official = s }) model.titles }, Cmd.none)
+ TitleMain i s -> ({ model | olang = s, titles = modidx i (\e -> { e | official = True }) model.titles }, Cmd.none)
+
+ VNDel idx -> ({ model | vns = delidx idx model.vns }, Cmd.none)
+ VNRel idx rel -> ({ model | vns = modidx idx (\v -> { v | relation = rel }) model.vns }, Cmd.none)
+ VNOfficial idx o -> ({ model | vns = modidx idx (\v -> { v | official = o }) model.vns }, Cmd.none)
+ VNSearch m ->
+ let (nm, c, res) = A.update vnConfig m model.vnSearch
+ in case res of
+ Nothing -> ({ model | vnSearch = nm }, c)
+ Just v ->
+ if List.any (\l -> l.vid == v.id) model.vns
+ then ({ model | vnSearch = A.clear nm "" }, c)
+ else ({ model | vnSearch = A.clear nm "", vns = model.vns ++ [{ vid = v.id, title = v.title, relation = "seq", official = True }] }, c)
+
+ AnimeDel i -> ({ model | anime = delidx i model.anime }, Cmd.none)
+ AnimeSearch m ->
+ let (nm, c, res) = A.update animeConfig m model.animeSearch
+ in case res of
+ Nothing -> ({ model | animeSearch = nm }, c)
+ Just a ->
+ if List.any (\l -> l.aid == a.id) model.anime
+ then ({ model | animeSearch = A.clear nm "" }, c)
+ else ({ model | animeSearch = A.clear nm "", anime = model.anime ++ [{ aid = a.id, title = a.title, original = a.original }] }, c)
+
+ 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/jpeg", "image/webp", "image/avif", "image/jxl"] ImageSelected)
+ ImageSelected f -> let (nm, nc) = Img.upload Api.Cv 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)
+
+ EditionAdd ->
+ let f n acc =
+ case acc of
+ Just x -> Just x
+ Nothing -> if not (List.isEmpty (List.filter (\i -> i.eid == n) model.editions)) then Nothing else Just n
+ newid = List.range 0 500 |> List.foldl f Nothing |> Maybe.withDefault 0
+ in ({ model
+ | editions = model.editions ++ [{ eid = newid, lang = Nothing, name = "", official = True }]
+ , staffSearch = model.staffSearch ++ [(staffConfig (Just newid), A.init "")]
+ }, Cmd.none)
+ EditionDel idx eid ->
+ ({ model
+ | editions = delidx idx model.editions
+ , staffSearch = delidx (idx + 1) model.staffSearch
+ , staff = List.filter (\s -> s.eid /= Just eid) model.staff
+ }, Cmd.none)
+ EditionLang idx v -> ({ model | editions = modidx idx (\s -> { s | lang = v }) model.editions }, Cmd.none)
+ EditionName idx v -> ({ model | editions = modidx idx (\s -> { s | name = v }) model.editions }, Cmd.none)
+ EditionOfficial idx v -> ({ model | editions = modidx idx (\s -> { s | official = v }) model.editions }, Cmd.none)
+
+ StaffDel idx -> ({ model | staff = delidx idx model.staff }, Cmd.none)
+ StaffRole idx v -> ({ model | staff = modidx idx (\s -> { s | role = v }) model.staff }, Cmd.none)
+ StaffNote idx v -> ({ model | staff = modidx idx (\s -> { s | note = v }) model.staff }, Cmd.none)
+ StaffSearch eid m ->
+ let idx = List.indexedMap Tuple.pair model.editions
+ |> List.filterMap (\(n,e) -> if Just e.eid == eid then Just (n+1) else Nothing)
+ |> List.head |> Maybe.withDefault 0
+ in case List.drop idx model.staffSearch |> List.head of
+ Nothing -> (model, Cmd.none)
+ Just (sconfig, smodel) ->
+ let (nm, c, res) = A.update sconfig m smodel
+ nnm = if res == Nothing then nm else A.clear nm ""
+ nsearch = modidx idx (\(oc,om) -> (oc,nnm)) model.staffSearch
+ nstaff s = [{ id = s.id, aid = s.aid, eid = eid, title = s.title, alttitle = s.alttitle, role = "staff", note = "" }]
+ in case res of
+ Nothing -> ({ model | staffSearch = nsearch }, c)
+ Just s -> ({ model | staffSearch = nsearch, staff = model.staff ++ nstaff s }, c)
+
+ SeiyuuDef c -> ({ model | seiyuuDef = c }, Cmd.none)
+ SeiyuuDel idx -> ({ model | seiyuu = delidx idx model.seiyuu }, Cmd.none)
+ SeiyuuChar idx v -> ({ model | seiyuu = modidx idx (\s -> { s | cid = v }) model.seiyuu }, Cmd.none)
+ SeiyuuNote idx v -> ({ model | seiyuu = modidx idx (\s -> { s | note = v }) model.seiyuu }, Cmd.none)
+ SeiyuuSearch m ->
+ let (nm, c, res) = A.update seiyuuConfig m model.seiyuuSearch
+ in case res of
+ Nothing -> ({ model | seiyuuSearch = nm }, c)
+ Just s -> ({ model | seiyuuSearch = A.clear nm "", seiyuu = model.seiyuu ++ [{ id = s.id, aid = s.aid, title = s.title, alttitle = s.alttitle, cid = model.seiyuuDef, note = "" }] }, c)
+
+ ScrUplRel s -> ({ model | scrUplRel = s }, Cmd.none)
+ ScrUplSel -> (model, FSel.files ["image/png", "image/jpeg", "image/webp", "image/avif", "image/jxl"] ScrUpl)
+ ScrUpl f1 fl ->
+ if 1 + List.length fl > 10 - List.length model.screenshots
+ then ({ model | scrUplNum = Just (1 + List.length fl) }, Cmd.none)
+ else scrProcessQueue ({ model | scrQueue = (f1::fl), scrUplNum = Nothing }, Cmd.none)
+ ScrMsg id m ->
+ let f (i,s,r) =
+ if i /= id then ((i,s,r), Cmd.none)
+ else let (nm,nc) = Img.update m s in ((i,nm,r), Cmd.map (ScrMsg id) nc)
+ lst = List.map f model.screenshots
+ in scrProcessQueue ({ model | screenshots = List.map Tuple.first lst }, Cmd.batch (ivRefresh True :: List.map Tuple.second lst))
+ ScrRel n s -> ({ model | screenshots = List.map (\(i,img,r) -> if i == n then (i,img,s) else (i,img,r)) model.screenshots }, Cmd.none)
+ ScrDel n -> ({ model | screenshots = List.filter (\(i,_,_) -> i /= n) model.screenshots }, ivRefresh True)
+
+ DupSubmit ->
+ if List.isEmpty model.dupVNs
+ then ({ model | state = Api.Loading }, GV.send { hidden = True, search = (List.concatMap (\e -> [e.title, Maybe.withDefault "" e.latin]) model.titles) ++ String.lines model.alias } DupResults)
+ else ({ model | dupCheck = True, dupVNs = [] }, Cmd.none)
+ DupResults (GApi.VNResult vns) ->
+ if List.isEmpty vns
+ then ({ model | state = Api.Normal, dupCheck = True, dupVNs = [] }, Cmd.none)
+ else ({ model | state = Api.Normal, dupVNs = vns }, Cmd.none)
+ DupResults r -> ({ model | state = Api.Error r }, Cmd.none)
+
+ Submit -> ({ model | state = Api.Loading }, GVE.send (encode model) Submitted)
+ Submitted (GApi.Redirect s) -> (model, load s)
+ Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
+
+
+-- TODO: Fuzzier matching? Exclude stuff like 'x Edition', etc.
+relAlias : Model -> Maybe { id: String, title: String }
+relAlias model =
+ let a = String.toLower model.alias |> String.lines |> List.filter (\l -> l /= "") |> Set.fromList
+ in List.filter (\r -> Set.member (String.toLower r.title) a) model.reltitles |> List.head
+
+
+isValid : Model -> Bool
+isValid model = not
+ ( List.any (\e -> e.title /= "" && Just e.title == e.latin) model.titles
+ || List.isEmpty model.titles
+ || relAlias model /= Nothing
+ || not (Img.isValid model.image)
+ || List.any (\(_,i,r) -> r == Nothing || not (Img.isValid i)) model.screenshots
+ || not (List.isEmpty model.scrQueue)
+ || hasDuplicates (List.map (\e -> (Maybe.withDefault "" e.lang, e.name)) model.editions)
+ || hasDuplicates (List.map (\s -> (s.aid, Maybe.withDefault -1 s.eid, s.role)) model.staff)
+ || hasDuplicates (List.map (\s -> (s.aid, s.cid)) model.seiyuu)
+ )
+
+
+view : Model -> Html Msg
+view model =
+ let
+ title i e = tr []
+ [ td [] [ langIcon e.lang ]
+ , td []
+ [ inputText ("title_"++e.lang) e.title (TitleTitle i) (style "width" "500px" :: onInvalid (Invalid General) :: placeholder "Title (in the original script)" :: GVE.valTitlesTitle)
+ , if not (e.latin /= Nothing || containsNonLatin e.title) then text "" else span []
+ [ br [] []
+ , inputText "" (Maybe.withDefault "" e.latin) (TitleLatin i) (style "width" "500px" :: required True :: onInvalid (Invalid General) :: placeholder "Romanization" :: GVE.valTitlesLatin)
+ , case e.latin of
+ Just s -> if containsNonLatin s then b [] [ br [] [], text "Romanization should only consist of characters in the latin alphabet." ] else text ""
+ Nothing -> text ""
+ ]
+ , if List.length model.titles == 1 then text "" else span []
+ [ br [] []
+ , label [] [ inputRadio "olang" (e.lang == model.olang) (\_ -> TitleMain i e.lang), text " main title (the language the VN was originally written in)" ]
+ ]
+ , if e.lang == model.olang then text "" else span []
+ [ br [] []
+ , label [] [ inputCheck "" e.official (TitleOfficial i), text " official title (from the developer or licensed localization; not from a fan translation)" ]
+ , br [] []
+ , inputButton "remove" (TitleDel i) []
+ ]
+ , br_ 2
+ ]
+ ]
+
+ titles =
+ let lines = List.filter (\e -> e /= "") <| String.lines <| String.toLower model.alias
+ in
+ [ formField "Title(s)"
+ [ table [] <| List.indexedMap title model.titles
+ , inputSelect "" "" TitleAdd [] <| ("", "- Add title -") :: List.filter (\(l,_) -> not (List.any (\e -> e.lang == l) model.titles)) scriptLangs
+ , br_ 2
+ ]
+ , formField "alias::Aliases"
+ [ inputTextArea "alias" model.alias Alias (rows 3 :: onInvalid (Invalid General) :: GVE.valAlias)
+ , br [] []
+ , if hasDuplicates lines
+ then b [] [ text "List contains duplicate aliases.", br [] [] ]
+ else if contains lines <| List.map String.toLower <| List.concatMap (\e -> [e.title, Maybe.withDefault "" e.latin]) model.titles
+ then b [] [ text "Titles listed above should not also be added as alias.", br [] [] ]
+ else
+ case relAlias model of
+ Nothing -> text ""
+ Just r -> span []
+ [ b [] [ text "Release titles should not be added as alias." ]
+ , br [] []
+ , text "Release: "
+ , a [ href <| "/"++r.id ] [ text r.title ]
+ , br [] [], br [] []
+ ]
+ , text "List of additional 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!"
+ ]
+ ]
+
+ geninfo = titles ++
+ [ formField "desc::Description"
+ [ TP.view "desc" model.description Desc 600 (style "height" "180px" :: onInvalid (Invalid General) :: GVE.valDescription) [ b [] [ text "English please!" ] ]
+ , text "Short description of the main story. Please do not include spoilers, and don't forget to list the source in case you didn't write the description yourself."
+ ]
+ , formField "devstatus::Development status"
+ [ inputSelect "devstatus" model.devStatus DevStatus [] GT.devStatus
+ , if model.devStatus == 0
+ && not (List.isEmpty model.releases)
+ && List.isEmpty (List.filter (\r -> r.rtype == "complete" && r.released <= model.today) model.releases)
+ then span []
+ [ br [] []
+ , b [] [ text "Development is marked as finished, but there is no complete release in the database." ]
+ , br [] []
+ , text "Please adjust the development status or ensure there is a completed release."
+ ]
+ else text ""
+ , if model.devStatus /= 0
+ && not (List.isEmpty (List.filter (\r -> r.rtype == "complete" && r.released <= model.today) model.releases))
+ then span []
+ [ br [] []
+ , b [] [ text "Development is not marked as finished, but there is a complete release in the database." ]
+ , br [] []
+ , text "Please adjust the development status or set the release to partial or TBA."
+ ]
+ else text ""
+ ]
+ , formField "length::Length"
+ [ inputSelect "length" model.length Length [] GT.vnLengths
+ , text " (only displayed if there are no length votes)" ]
+ , formField "l_wikidata::Wikidata ID" [ inputWikidata "l_wikidata" model.lWikidata LWikidata [onInvalid (Invalid General)] ]
+ , formField "l_renai::Renai.us link" [ text "http://renai.us/game/", inputText "l_renai" model.lRenai LRenai (onInvalid (Invalid General) :: GVE.valL_Renai), text ".shtml" ]
+
+ , tr [ class "newpart" ] [ td [ colspan 2 ] [ text "Database relations" ] ]
+ , formField "Related VNs"
+ [ if List.isEmpty model.vns then text ""
+ else table [] <| List.indexedMap (\i v -> tr []
+ [ td [ style "text-align" "right" ] [ small [] [ text <| v.vid ++ ":" ] ]
+ , td [ style "text-align" "right"] [ a [ href <| "/" ++ v.vid ] [ text v.title ] ]
+ , td []
+ [ text "is an "
+ , label [] [ inputCheck "" v.official (VNOfficial i), text " official" ]
+ , inputSelect "" v.relation (VNRel i) [] GT.vnRelations
+ , text " of this VN"
+ ]
+ , td [] [ inputButton "remove" (VNDel i) [] ]
+ ]
+ ) model.vns
+ , A.view vnConfig model.vnSearch [placeholder "Add visual novel..."]
+ ]
+ , tr [ class "newpart" ] [ td [ colspan 2 ] [] ]
+ , formField "Related anime"
+ [ if List.isEmpty model.anime then text ""
+ else table [] <| List.indexedMap (\i e -> tr []
+ [ td [ style "text-align" "right" ] [ small [] [ text <| "a" ++ String.fromInt e.aid ++ ":" ] ]
+ , td [] [ a [ href <| "https://anidb.net/anime/" ++ String.fromInt e.aid ] [ text e.title ] ]
+ , td [] [ inputButton "remove" (AnimeDel i) [] ]
+ ]
+ ) model.anime
+ , A.view animeConfig model.animeSearch [placeholder "Add anime..."]
+ ]
+ ]
+
+ image =
+ table [ class "formimage" ] [ tr []
+ [ td [] [ Img.viewImg model.image ]
+ , td []
+ [ h2 [] [ text "Image ID" ]
+ , input ([ type_ "text", class "text", tabindex 10, value (Maybe.withDefault "" model.image.id), onInputValidation ImageSet, onInvalid (Invalid Image) ] ++ GVE.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 []
+ , br [] []
+ , text "Preferably the cover of the CD/DVD/package."
+ , br [] []
+ , text "Supported file types: JPEG, PNG, WebP, AVIF or JXL, at most 10 MiB."
+ , br [] []
+ , text "Images larger than 256x400 are automatically resized."
+ , case Img.viewVote model.image ImageMsg (Invalid 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)"
+ , v
+ ]
+ ]
+ ] ]
+
+ staff =
+ let
+ head lst =
+ if List.isEmpty lst then text "" else
+ thead [] [ tr []
+ [ td [] []
+ , td [] [ text "Staff" ]
+ , td [] [ text "Role" ]
+ , td [] [ text "Note" ]
+ , td [] []
+ ] ]
+ foot searchn lst (sconfig, smodel) =
+ tfoot [] [ tr [] [ td [] [], td [ colspan 4 ]
+ [ text ""
+ , if hasDuplicates (List.map (\(_,s) -> (s.aid, s.role)) lst)
+ then b [] [ text "List contains duplicate staff roles.", br [] [] ]
+ else text ""
+ , A.view sconfig smodel [placeholder "Add staff..."]
+ , if searchn > 0 then text "" else span []
+ [ text "Can't find the person you're looking for? You can "
+ , a [ href "/s/new" ] [ text "create a new entry" ]
+ , text ", but "
+ , a [ href "/s/all" ] [ text "please check for aliasses first." ]
+ , br [] []
+ , text "If one person performed several roles, you can add multiple entries with different major roles."
+ ]
+ ] ] ]
+ item (n,s) = tr []
+ [ td [ style "text-align" "right" ] [ small [] [ text <| s.id ++ ":" ] ]
+ , td [] [ a [ href <| "/" ++ s.id ] [ text s.title ], text <| if s.alttitle == s.title then "" else " " ++ s.alttitle ]
+ , td [] [ inputSelect "" s.role (StaffRole n) [style "width" "150px" ] GT.creditTypes ]
+ , td [] [ inputText "" s.note (StaffNote n) (style "width" "300px" :: onInvalid (Invalid Staff) :: GVE.valStaffNote) ]
+ , td [] [ inputButton "remove" (StaffDel n) [] ]
+ ]
+ edition searchn edi =
+ let eid = Maybe.map (\e -> e.eid) edi
+ lst = List.indexedMap Tuple.pair model.staff |> List.filter (\(_,s) -> s.eid == eid)
+ sch = List.drop searchn model.staffSearch |> List.head
+ in div [style "margin" "0 0 30px 0"]
+ [ Maybe.withDefault (if List.isEmpty model.editions then text "" else h2 [] [ text "Original edition" ])
+ <| Maybe.map (\e -> h2 [] [ text (if e.name == "" then "New edition" else e.name) ]) edi
+ , case edi of
+ Nothing -> text ""
+ Just e ->
+ div [style "margin" "5px 0 0 15px"]
+ [ inputText "" e.name (EditionName (searchn-1)) (placeholder "Edition title" :: style "width" "300px" :: onInvalid (Invalid Staff) :: GVE.valEditionsName)
+ , inputSelect "" e.lang (EditionLang (searchn-1)) [style "width" "150px"]
+ ((Nothing, "Original language") :: List.map (\(i,l) -> (Just i, l)) scriptLangs)
+ , text " ", label [] [ inputCheck "" e.official (EditionOfficial (searchn-1)), text " official" ]
+ , inputButton "remove edition" (EditionDel (searchn-1) e.eid) [style "margin-left" "30px"]
+ ]
+ , table [style "margin" "5px 0 0 15px"]
+ <| head lst
+ :: Maybe.withDefault (text "") (Maybe.map (foot searchn lst) sch)
+ :: List.map item lst
+ ]
+ in edition 0 Nothing
+ :: List.indexedMap (\n e -> edition (n+1) (Just e)) model.editions
+ ++ [ br [] [], inputButton "Add edition" EditionAdd [] ]
+
+
+
+ cast =
+ let
+ chars = List.map (\c -> (c.id, c.title ++ " (" ++ c.id ++ ")")) model.chars
+ head =
+ if List.isEmpty model.seiyuu then [] else [
+ thead [] [ tr []
+ [ td [] [ text "Character" ]
+ , td [] [ text "Cast" ]
+ , td [] [ text "Note" ]
+ , td [] []
+ ] ] ]
+ foot =
+ tfoot [] [ tr [] [ td [ colspan 4 ]
+ [ br [] []
+ , strong [] [ text "Add cast" ]
+ , br [] []
+ , if hasDuplicates (List.map (\s -> (s.aid, s.cid)) model.seiyuu)
+ then b [] [ text "List contains duplicate cast roles.", br [] [] ]
+ else text ""
+ , inputSelect "" model.seiyuuDef SeiyuuDef [] chars
+ , text " voiced by "
+ , div [ style "display" "inline-block" ] [ A.view seiyuuConfig model.seiyuuSearch [] ]
+ , br [] []
+ , text "Can't find the person you're looking for? You can "
+ , a [ href "/s/new" ] [ text "create a new entry" ]
+ , text ", but "
+ , a [ href "/s/all" ] [ text "please check for aliasses first." ]
+ ] ] ]
+ item n s = tr []
+ [ td [] [ inputSelect "" s.cid (SeiyuuChar n) []
+ <| chars ++ if List.any (\c -> c.id == s.cid) model.chars then [] else [(s.cid, "[deleted/moved character: " ++ s.cid ++ "]")] ]
+ , td []
+ [ small [] [ text <| s.id ++ ":" ]
+ , a [ href <| "/" ++ s.id ] [ text s.title ], text <| if s.title == s.alttitle then "" else " " ++ s.alttitle ]
+ , td [] [ inputText "" s.note (SeiyuuNote n) (style "width" "300px" :: onInvalid (Invalid Cast) :: GVE.valSeiyuuNote) ]
+ , td [] [ inputButton "remove" (SeiyuuDel n) [] ]
+ ]
+ in
+ if model.id == Nothing
+ then text <| "Voice actors can be added to this visual novel once it has character entries associated with it. "
+ ++ "To do so, first create this entry without cast, then create the appropriate character entries, and finally come back to this form by editing the visual novel."
+ else if List.isEmpty model.chars && List.isEmpty model.seiyuu
+ then p []
+ [ text "This visual novel does not have any characters associated with it (yet). Please "
+ , a [ href <| "/" ++ Maybe.withDefault "" model.id ++ "/addchar" ] [ text "add the appropriate character entries" ]
+ , text " first and then come back to this form to assign voice actors."
+ ]
+ else table [] <| head ++ [ foot ] ++ List.indexedMap item model.seiyuu
+
+ screenshots =
+ let
+ rellist = List.map (\r -> (Just r.id, RDate.showrel r)) model.releases
+ scr n (id, i, rel) = (String.fromInt id, tr [] <|
+ let getdim img = Maybe.map (\nfo -> (nfo.width, nfo.height)) img |> Maybe.withDefault (0,0)
+ imgdim = getdim i.img
+ relnfo = List.filter (\r -> Just r.id == rel) model.releases |> List.head
+ reldim = relnfo |> Maybe.andThen (\r -> if r.reso_x == 0 then Nothing else Just (r.reso_x, r.reso_y))
+ dimstr (x,y) = String.fromInt x ++ "x" ++ String.fromInt y
+ in
+ [ td [] [ Img.viewImg i ]
+ , td [] [ Img.viewVote i (ScrMsg id) (Invalid Screenshots) |> Maybe.withDefault (text "") ]
+ , td []
+ [ strong [] [ text <| "Screenshot #" ++ String.fromInt (n+1) ]
+ , text " (", a [ href "#", onClickD (ScrDel id) ] [ text "remove" ], text ")"
+ , br [] []
+ , text <| "Image resolution: " ++ dimstr imgdim
+ , br [] []
+ , text <| Maybe.withDefault "" <| Maybe.map (\dim -> "Release resolution: " ++ dimstr dim) reldim
+ , span [] <|
+ if reldim == Just imgdim then [ text " ✔", br [] [] ]
+ else if reldim /= Nothing
+ then [ text " ❌"
+ , br [] []
+ , b [] [ text "WARNING: Resolutions do not match, please take screenshots with the correct resolution and make sure to crop them correctly!" ]
+ ]
+ else if i.img /= Nothing && rel /= Nothing && List.any (\(_,si,sr) -> sr == rel && si.img /= Nothing && imgdim /= getdim si.img) model.screenshots
+ then [ b [] [ text "WARNING: Inconsistent image resolutions for the same release, please take screenshots with the correct resolution and make sure to crop them correctly!" ]
+ , br [] []
+ ]
+ else [ br [] [] ]
+ , br [] []
+ , inputSelect "" rel (ScrRel id) [style "width" "500px"] <| rellist ++
+ case (relnfo, rel) of
+ (_, Nothing) -> [(Nothing, "[No release selected]")]
+ (Nothing, Just r) -> [(Just r, "[Deleted or unlinked release: " ++ r ++ "]")]
+ _ -> []
+ ]
+ ])
+
+ add =
+ let free = 10 - List.length model.screenshots
+ in
+ if not (List.isEmpty model.scrQueue)
+ then [ strong [] [ text "Uploading screenshots" ]
+ , br [] []
+ , text <| (String.fromInt (List.length model.scrQueue)) ++ " remaining... "
+ , span [ class "spinner" ] []
+ ]
+ else if free <= 0
+ then [ strong [] [ text "Enough screenshots" ]
+ , br [] []
+ , text "The limit of 10 screenshots per visual novel has been reached. If you want to add a new screenshot, please remove an existing one first."
+ ]
+ else
+ [ strong [] [ text "Add screenshots" ]
+ , br [] []
+ , text <| String.fromInt free ++ " more screenshot" ++ (if free == 1 then "" else "s") ++ " can be added."
+ , br [] []
+ , inputSelect "" model.scrUplRel ScrUplRel [style "width" "500px"] ((Nothing, "-- select release --") :: rellist)
+ , br [] []
+ , if model.scrUplRel == Nothing then text "" else span []
+ [ inputButton "Select images" ScrUplSel []
+ , case model.scrUplNum of
+ Just num -> text " Too many images selected."
+ Nothing -> text ""
+ , br [] []
+ ]
+ , br [] []
+ , strong [] [ text "Important reminder" ]
+ , ul []
+ [ li [] [ text "Screenshots must be in the native resolution of the game" ]
+ , li [] [ text "Screenshots must not include window borders and should not have copyright markings" ]
+ , li [] [ text "Don't only upload event CGs" ]
+ ]
+ , text "Read the ", a [ href "/d2#6" ] [ text "full guidelines" ], text " for more information."
+ ]
+ in
+ if model.id == Nothing
+ then text <| "Screenshots can be uploaded to this visual novel once it has a release entry associated with it. "
+ ++ "To do so, first create this entry without screenshots, then create the appropriate release entries, and finally come back to this form by editing the visual novel."
+ else if List.isEmpty model.screenshots && List.isEmpty model.releases
+ then p []
+ [ text "This visual novel does not have any releases associated with it (yet). Please "
+ , a [ href <| "/" ++ Maybe.withDefault "" model.id ++ "/add" ] [ text "add the appropriate release entries" ]
+ , text " first and then come back to this form to upload screenshots."
+ ]
+ else
+ table [ class "vnedit_scr" ]
+ [ tfoot [] [ tr [] [ td [] [], td [ colspan 2 ] add ] ]
+ , K.node "tbody" [] <| List.indexedMap scr model.screenshots
+ ]
+
+ newform () =
+ form_ "" DupSubmit (model.state == Api.Loading)
+ [ article [] [ h1 [] [ text "Add a new visual novel" ], table [ class "formtable" ] titles ]
+ , if List.isEmpty model.dupVNs then text "" else
+ article []
+ [ div []
+ [ h1 [] [ text "Possible duplicates" ]
+ , text "The following is a list of visual novels that match the title(s) you gave. "
+ , text "Please check this list to avoid creating a duplicate visual novel entry. "
+ , text "Be especially wary of items that have been deleted! To see why an entry has been deleted, click on its title."
+ , ul [] <| List.map (\v -> li []
+ [ a [ href <| "/" ++ v.id ] [ text v.title ]
+ , if v.hidden then b [] [ text " (deleted)" ] else text ""
+ ]
+ ) model.dupVNs
+ ]
+ ]
+ , article [ class "submit" ] [ submitButton (if List.isEmpty model.dupVNs then "Continue" else "Continue anyway") model.state (isValid model) ]
+ ]
+
+ fullform () =
+ form_ "mainform" Submit (model.state == Api.Loading)
+ [ nav []
+ [ menu []
+ [ li [ classList [("tabselected", model.tab == General )] ] [ a [ href "#", onClickD (Tab General ) ] [ text "General info" ] ]
+ , li [ classList [("tabselected", model.tab == Image )] ] [ a [ href "#", onClickD (Tab Image ) ] [ text "Image" ] ]
+ , li [ classList [("tabselected", model.tab == Staff )] ] [ a [ href "#", onClickD (Tab Staff ) ] [ text "Staff" ] ]
+ , li [ classList [("tabselected", model.tab == Cast )] ] [ a [ href "#", onClickD (Tab Cast ) ] [ text "Cast" ] ]
+ , li [ classList [("tabselected", model.tab == Screenshots)] ] [ a [ href "#", onClickD (Tab Screenshots) ] [ text "Screenshots" ] ]
+ , li [ classList [("tabselected", model.tab == All )] ] [ a [ href "#", onClickD (Tab All ) ] [ text "All items" ] ]
+ ]
+ ]
+ , article [ classList [("hidden", model.tab /= General && model.tab /= All)] ] [ h1 [] [ text "General info" ], table [ class "formtable" ] geninfo ]
+ , article [ classList [("hidden", model.tab /= Image && model.tab /= All)] ] [ h1 [] [ text "Image" ], image ]
+ , article [ classList [("hidden", model.tab /= Staff && model.tab /= All)] ] ( h1 [] [ text "Staff" ] :: staff )
+ , article [ classList [("hidden", model.tab /= Cast && model.tab /= All)] ] [ h1 [] [ text "Cast" ], cast ]
+ , article [ classList [("hidden", model.tab /= Screenshots && model.tab /= All)] ] [ h1 [] [ text "Screenshots" ], screenshots ]
+ , article [ class "submit" ]
+ [ Html.map Editsum (Editsum.view model.editsum)
+ , submitButton "Submit" model.state (isValid model)
+ ]
+ ]
+ in if model.id == Nothing && not model.dupCheck then newform () else fullform ()