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 , 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 "" , 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 , 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) | 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) 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 || 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 = p [] [ text "The main image can't be uploaded or edited in this form, " , text "images can now be added directly to release entries. " , text "We're still discussing what to do with the main VN image." ] 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 ()