diff options
Diffstat (limited to 'elm/VNEdit.elm')
-rw-r--r-- | elm/VNEdit.elm | 465 |
1 files changed, 316 insertions, 149 deletions
diff --git a/elm/VNEdit.elm b/elm/VNEdit.elm index 4fadbf2d..751cab61 100644 --- a/elm/VNEdit.elm +++ b/elm/VNEdit.elm @@ -6,10 +6,15 @@ 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 @@ -26,7 +31,7 @@ import Gen.Api as GApi main : Program GVE.Recv Model Msg main = Browser.element - { init = \e -> (init e, Cmd.none) + { init = \e -> (init e, Date.today |> Task.perform Today) , view = view , update = update , subscriptions = always Sub.none @@ -46,11 +51,14 @@ type Tab type alias Model = { state : Api.State , tab : Tab + , today : Int + , invalidDis : Bool , editsum : Editsum.Model - , title : String - , original : String + , titles : List GVE.RecvTitles , alias : String - , desc : TP.Model + , description : TP.Model + , devStatus : Int + , olang : String , length : Int , lWikidata : Maybe Int , lRenai : String @@ -59,18 +67,22 @@ type alias Model = , anime : List GVE.RecvAnime , animeSearch : A.Model GApi.ApiAnimeResult , image : Img.Image + , editions : List GVE.RecvEditions , staff : List GVE.RecvStaff - , staffSearch : A.Model GApi.ApiStaffResult + -- 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 : Int -- character id for newly added seiyuu - , screenshots : List (Int,Img.Image,Maybe Int) -- internal id, img, rel - , scrUplRel : Maybe Int + , 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 Int + , id : Maybe String , dupCheck : Bool , dupVNs : List GApi.ApiVNResult } @@ -80,11 +92,14 @@ init : GVE.Recv -> Model init d = { state = Api.Normal , tab = General - , editsum = { authmod = d.authmod, editsum = TP.bbcode d.editsum, locked = d.locked, hidden = d.hidden } - , title = d.title - , original = d.original + , 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 - , desc = TP.bbcode d.desc + , description = TP.bbcode d.description + , devStatus = d.devstatus + , olang = d.olang , length = d.length , lWikidata = d.l_wikidata , lRenai = d.l_renai @@ -93,16 +108,19 @@ init d = , anime = d.anime , animeSearch = A.init "" , image = Img.info d.image_info + , editions = d.editions , staff = d.staff - , staffSearch = A.init "" + , 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 0 <| List.head <| List.map (\c -> c.id) d.chars + , 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 @@ -116,18 +134,20 @@ encode model = , editsum = model.editsum.editsum.data , hidden = model.editsum.hidden , locked = model.editsum.locked - , title = model.title - , original = model.original + , titles = model.titles , alias = model.alias - , desc = model.desc.data + , 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 - , staff = List.map (\s -> { aid = s.aid, role = s.role, note = s.note }) model.staff - , seiyuu = List.map (\s -> { aid = s.aid, cid = s.cid, note = s.note }) model.seiyuu + , 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 } @@ -135,26 +155,40 @@ 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 } +animeConfig = { wrap = AnimeSearch, id = "animeadd", source = A.animeSource False } -staffConfig : A.Config Msg GApi.ApiStaffResult -staffConfig = { wrap = StaffSearch, id = "staffadd", source = A.staffSource } +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 - = Editsum Editsum.Msg + = Noop + | Today Date.Date + | Editsum Editsum.Msg | Tab Tab + | Invalid Tab + | InvalidEnable | Submit | Submitted GApi.Response - | Title String - | Original String | 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 @@ -165,38 +199,70 @@ type Msg | 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 (A.Msg GApi.ApiStaffResult) - | SeiyuuDef Int + | StaffSearch (Maybe Int) (A.Msg GApi.ApiStaffResult) + | SeiyuuDef String | SeiyuuDel Int - | SeiyuuChar Int Int + | SeiyuuChar Int String | SeiyuuNote Int String | SeiyuuSearch (A.Msg GApi.ApiStaffResult) - | ScrUplRel (Maybe Int) + | ScrUplRel (Maybe String) | ScrUplSel | ScrUpl File (List File) | ScrMsg Int Img.Msg - | ScrRel Int (Maybe Int) + | 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) - Title s -> ({ model | title = s, dupVNs = [] }, Cmd.none) - Original s -> ({ model | original = s, dupVNs = [] }, 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.desc in ({ model | desc = nm }, Cmd.map Desc nc) + 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) @@ -207,7 +273,7 @@ update msg model = 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, original = v.original, relation = "seq", official = True }] }, 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 -> @@ -220,18 +286,47 @@ update msg model = 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/jpg"] ImageSelected) + 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 m -> - let (nm, c, res) = A.update staffConfig m model.staffSearch - in case res of - Nothing -> ({ model | staffSearch = nm }, c) - Just s -> ({ model | staffSearch = A.clear nm "", staff = model.staff ++ [{ id = s.id, aid = s.aid, name = s.name, original = s.original, role = "staff", note = "" }] }, c) + 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) @@ -241,33 +336,26 @@ update msg model = 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, name = s.name, original = s.original, cid = model.seiyuuDef, note = "" }] }, 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/jpg"] ScrUpl) + 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 - let imgs = List.map (Img.upload Api.Sf) (f1::fl) - in ( { model - | scrId = model.scrId + 100 - , scrUplNum = Nothing - , screenshots = model.screenshots ++ List.indexedMap (\n (i,_) -> (model.scrId+n,i,model.scrUplRel)) imgs - } - , List.indexedMap (\n (_,c) -> Cmd.map (ScrMsg (model.scrId+n)) c) imgs |> Cmd.batch) + 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 ({ model | screenshots = List.map Tuple.first lst }, Cmd.batch (ivRefresh True :: List.map Tuple.second lst)) + 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 = model.title :: model.original :: String.lines model.alias } DupResults) + 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 @@ -281,19 +369,22 @@ update msg model = -- TODO: Fuzzier matching? Exclude stuff like 'x Edition', etc. -relAlias : Model -> Maybe GVE.RecvReleases +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 || Set.member (String.toLower r.original) a) model.releases |> List.head + in List.filter (\r -> Set.member (String.toLower r.title) a) model.reltitles |> List.head isValid : Model -> Bool isValid model = not - ( (model.title /= "" && model.title == model.original) + ( 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 - || hasDuplicates (List.map (\s -> (s.aid, s.role)) model.staff) + || 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) ) @@ -301,37 +392,57 @@ isValid model = not view : Model -> Html Msg view model = let - titles = - [ formField "title::Title (romaji)" - [ inputText "title" model.title Title (style "width" "500px" :: GVE.valTitle) - , if containsNonLatin model.title - then b [ class "standout" ] [ br [] [], text "This title field should only contain latin-alphabet characters, please put the \"actual\" title in the field below and the romanization above." ] - else text "" + 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 ] - , formField "original::Original title" - [ inputText "original" model.original Original (style "width" "500px" :: GVE.valOriginal) - , if model.title /= "" && model.title == model.original - then b [ class "standout" ] [ br [] [], text "Should not be the same as the Title (romaji). Leave blank is the original title is already in the latin alphabet" ] - else if model.original /= "" && String.toLower model.title /= String.toLower model.original && not (containsNonLatin model.original) - then b [ class "standout" ] [ br [] [], text "Original title does not seem to contain any non-latin characters. Leave this field empty if the title is already in the latin alphabet" ] - else text "" + ] + + 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 :: GVE.valAlias) + [ inputTextArea "alias" model.alias Alias (rows 3 :: onInvalid (Invalid General) :: GVE.valAlias) , br [] [] - , if hasDuplicates <| String.lines <| String.toLower model.alias - then b [ class "standout" ] [ text "List contains duplicate aliases.", 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 [ class "standout" ] [ text "Release titles should not be added as alias." ] + [ b [] [ text "Release titles should not be added as alias." ] , br [] [] , text "Release: " - , a [ href <| "/r"++String.fromInt r.id ] [ text r.title ] + , a [ href <| "/"++r.id ] [ text r.title ] , br [] [], br [] [] ] - , 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." + , 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!" ] @@ -339,19 +450,43 @@ view model = geninfo = titles ++ [ formField "desc::Description" - [ TP.view "desc" model.desc Desc 600 (style "height" "180px" :: GVE.valDesc) [ b [ class "standout" ] [ text "English please!" ] ] + [ 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 "length::Length" [ inputSelect "length" model.length Length [] GT.vnLengths ] - , formField "l_wikidata::Wikidata ID" [ inputWikidata "l_wikidata" model.lWikidata LWikidata ] - , formField "l_renai::Renai.us link" [ text "http://renai.us/game/", inputText "l_renai" model.lRenai LRenai [], text ".shtml" ] + , 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" ] [ b [ class "grayedout" ] [ text <| "v" ++ String.fromInt v.vid ++ ":" ] ] - , td [ style "text-align" "right"] [ a [ href <| "/v" ++ String.fromInt v.vid ] [ text v.title ] ] + [ 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" ] @@ -367,7 +502,7 @@ view model = , formField "Related anime" [ if List.isEmpty model.anime then text "" else table [] <| List.indexedMap (\i e -> tr [] - [ td [ style "text-align" "right" ] [ b [ class "grayedout" ] [ text <| "a" ++ String.fromInt e.aid ++ ":" ] ] + [ 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) [] ] ] @@ -381,66 +516,94 @@ view model = [ 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 ] ++ GVE.valImage) [] + , 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. Image must be in JPEG or PNG format and at most 10 MiB. Images larger than 256x400 will automatically be resized." - , case Img.viewVote model.image of + , 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)" - , Html.map ImageMsg v + , v ] ] ] ] staff = let - head = - if List.isEmpty model.staff then [] else [ + head lst = + if List.isEmpty lst then text "" else thead [] [ tr [] [ td [] [] , td [] [ text "Staff" ] , td [] [ text "Role" ] , td [] [ text "Note" ] , td [] [] - ] ] ] - foot = + ] ] + foot searchn lst (sconfig, smodel) = tfoot [] [ tr [] [ td [] [], td [ colspan 4 ] - [ br [] [] - , if hasDuplicates (List.map (\s -> (s.aid, s.role)) model.staff) - then b [ class "standout" ] [ text "List contains duplicate staff roles.", br [] [] ] + [ text "" + , if hasDuplicates (List.map (\(_,s) -> (s.aid, s.role)) lst) + then b [] [ text "List contains duplicate staff roles.", br [] [] ] else text "" - , A.view staffConfig model.staffSearch [placeholder "Add staff..."] - , 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_ 2 - , text "Some guidelines:" - , ul [] - [ li [] [ text "Please add major staff only, i.e. people who had a significant and noticable impact on the work." ] - , li [] [ text "If one person performed several roles, you can add multiple entries with different major roles." ] + , 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" ] [ b [ class "grayedout" ] [ text <| "s" ++ String.fromInt s.id ++ ":" ] ] - , td [] [ a [ href <| "/s" ++ String.fromInt s.id ] [ text s.name ] ] + 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" :: GVE.valStaffNote) ] + , td [] [ inputText "" s.note (StaffNote n) (style "width" "300px" :: onInvalid (Invalid Staff) :: GVE.valStaffNote) ] , td [] [ inputButton "remove" (StaffDel n) [] ] ] - in table [] <| head ++ [ foot ] ++ List.indexedMap item model.staff + 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.name ++ " (c" ++ String.fromInt c.id ++ ")")) model.chars + chars = List.map (\c -> (c.id, c.title ++ " (" ++ c.id ++ ")")) model.chars head = if List.isEmpty model.seiyuu then [] else [ thead [] [ tr [] @@ -452,10 +615,10 @@ view model = foot = tfoot [] [ tr [] [ td [ colspan 4 ] [ br [] [] - , b [] [ text "Add cast" ] + , strong [] [ text "Add cast" ] , br [] [] , if hasDuplicates (List.map (\s -> (s.aid, s.cid)) model.seiyuu) - then b [ class "standout" ] [ text "List contains duplicate cast roles.", br [] [] ] + then b [] [ text "List contains duplicate cast roles.", br [] [] ] else text "" , inputSelect "" model.seiyuuDef SeiyuuDef [] chars , text " voiced by " @@ -468,11 +631,11 @@ view model = ] ] ] 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: c" ++ String.fromInt s.cid ++ "]")] ] + <| chars ++ if List.any (\c -> c.id == s.cid) model.chars then [] else [(s.cid, "[deleted/moved character: " ++ s.cid ++ "]")] ] , td [] - [ b [ class "grayedout" ] [ text <| "s" ++ String.fromInt s.id ++ ":" ] - , a [ href <| "/s" ++ String.fromInt s.id ] [ text s.name ] ] - , td [] [ inputText "" s.note (SeiyuuNote n) (style "width" "300px" :: GVE.valSeiyuuNote) ] + [ 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 @@ -482,15 +645,14 @@ view model = 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 <| "/v" ++ Maybe.withDefault "" (Maybe.map String.fromInt model.id) ++ "/addchar" ] [ text "add the appropriate character entries" ] + , 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 - showrel r = "[" ++ (RDate.format (RDate.expand r.released)) ++ " " ++ (String.join "," r.lang) ++ "] " ++ r.title ++ " (r" ++ String.fromInt r.id ++ ")" - rellist = List.map (\r -> (Just r.id, showrel r)) model.releases + 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 @@ -499,9 +661,9 @@ view model = dimstr (x,y) = String.fromInt x ++ "x" ++ String.fromInt y in [ td [] [ Img.viewImg i ] - , td [] [ Img.viewVote i |> Maybe.map (Html.map (ScrMsg id)) |> Maybe.withDefault (text "") ] + , td [] [ Img.viewVote i (ScrMsg id) (Invalid Screenshots) |> Maybe.withDefault (text "") ] , td [] - [ b [] [ text <| "Screenshot #" ++ String.fromInt (n+1) ] + [ strong [] [ text <| "Screenshot #" ++ String.fromInt (n+1) ] , text " (", a [ href "#", onClickD (ScrDel id) ] [ text "remove" ], text ")" , br [] [] , text <| "Image resolution: " ++ dimstr imgdim @@ -512,10 +674,10 @@ view model = else if reldim /= Nothing then [ text " ❌" , br [] [] - , b [ class "standout" ] [ text "WARNING: Resolutions do not match, please take screenshots with the correct resolution and make sure to crop them correctly!" ] + , 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 [ class "standout" ] [ text "WARNING: Inconsistent image resolutions for the same release, please take screenshots with the correct resolution and make sure to crop them correctly!" ] + 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 [] [] ] @@ -523,7 +685,7 @@ view model = , 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" ++ String.fromInt r ++ "]")] + (Nothing, Just r) -> [(Just r, "[Deleted or unlinked release: " ++ r ++ "]")] _ -> [] ] ]) @@ -531,13 +693,19 @@ view model = add = let free = 10 - List.length model.screenshots in - if free <= 0 - then [ b [] [ text "Enough screenshots" ] + 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 - [ b [] [ text "Add screenshots" ] + [ strong [] [ text "Add screenshots" ] , br [] [] , text <| String.fromInt free ++ " more screenshot" ++ (if free == 1 then "" else "s") ++ " can be added." , br [] [] @@ -551,7 +719,7 @@ view model = , br [] [] ] , br [] [] - , b [] [ text "Important reminder" ] + , 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" ] @@ -566,7 +734,7 @@ view model = 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 <| "/v" ++ Maybe.withDefault "" (Maybe.map String.fromInt model.id) ++ "/add" ] [ text "add the appropriate release entries" ] + , 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 @@ -576,29 +744,29 @@ view model = ] newform () = - form_ DupSubmit (model.state == Api.Loading) - [ div [ class "mainbox" ] [ h1 [] [ text "Add a new visual novel" ], table [ class "formtable" ] titles ] - , div [ class "mainbox" ] - [ if List.isEmpty model.dupVNs then text "" else - div [] + 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" ++ String.fromInt v.id ] [ text v.title ] - , if v.hidden then b [ class "standout" ] [ text " (deleted)" ] else text "" + [ a [ href <| "/" ++ v.id ] [ text v.title ] + , if v.hidden then b [] [ text " (deleted)" ] else text "" ] ) model.dupVNs ] - , fieldset [ class "submit" ] [ submitButton (if List.isEmpty model.dupVNs then "Continue" else "Continue anyway") model.state (isValid model) ] ] + , article [ class "submit" ] [ submitButton (if List.isEmpty model.dupVNs then "Continue" else "Continue anyway") model.state (isValid model) ] ] fullform () = - form_ Submit (model.state == Api.Loading) - [ div [ class "maintabs left" ] - [ ul [] + 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" ] ] @@ -607,15 +775,14 @@ view model = , li [ classList [("tabselected", model.tab == All )] ] [ a [ href "#", onClickD (Tab All ) ] [ text "All items" ] ] ] ] - , div [ class "mainbox", classList [("hidden", model.tab /= General && model.tab /= All)] ] [ h1 [] [ text "General info" ], table [ class "formtable" ] geninfo ] - , div [ class "mainbox", classList [("hidden", model.tab /= Image && model.tab /= All)] ] [ h1 [] [ text "Image" ], image ] - , div [ class "mainbox", classList [("hidden", model.tab /= Staff && model.tab /= All)] ] [ h1 [] [ text "Staff" ], staff ] - , div [ class "mainbox", classList [("hidden", model.tab /= Cast && model.tab /= All)] ] [ h1 [] [ text "Cast" ], cast ] - , div [ class "mainbox", classList [("hidden", model.tab /= Screenshots && model.tab /= All)] ] [ h1 [] [ text "Screenshots" ], screenshots ] - , div [ class "mainbox" ] [ fieldset [ class "submit" ] - [ Html.map Editsum (Editsum.view model.editsum) - , submitButton "Submit" model.state (isValid model) - ] + , 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 () |