summaryrefslogtreecommitdiff
path: root/elm
diff options
context:
space:
mode:
Diffstat (limited to 'elm')
-rw-r--r--elm/CharEdit.elm81
-rw-r--r--elm/Discussions/Edit.elm34
-rw-r--r--elm/Discussions/PostEdit.elm108
-rw-r--r--elm/Discussions/Reply.elm2
-rw-r--r--elm/Lib/Api.elm3
-rw-r--r--elm/Lib/Autocomplete.elm34
-rw-r--r--elm/Lib/Editsum.elm5
-rw-r--r--elm/Lib/Image.elm183
-rw-r--r--elm/ProducerEdit.elm231
-rw-r--r--elm/Report.elm184
-rw-r--r--elm/Reviews/Comment.elm52
-rw-r--r--elm/Reviews/Edit.elm195
-rw-r--r--elm/Reviews/Vote.elm70
-rw-r--r--elm/UList/ManageLabels.js8
-rw-r--r--elm/UList/SaveDefault.js7
-rw-r--r--elm/UList/VNPage.elm33
-rw-r--r--elm/UList/actiontabs.js17
-rw-r--r--elm/User/Edit.elm12
-rw-r--r--elm/VNEdit.elm621
-rw-r--r--elm/VNEdit.js6
-rw-r--r--elm/iv.js61
-rw-r--r--elm/sethash.js8
22 files changed, 1807 insertions, 148 deletions
diff --git a/elm/CharEdit.elm b/elm/CharEdit.elm
index 0f75a357..837fbb4b 100644
--- a/elm/CharEdit.elm
+++ b/elm/CharEdit.elm
@@ -17,6 +17,7 @@ import Lib.Autocomplete as A
import Lib.Api as Api
import Lib.Editsum as Editsum
import Lib.RDate as RDate
+import Lib.Image as Img
import Gen.Release as GR
import Gen.CharEdit as GCE
import Gen.Types as GT
@@ -65,11 +66,7 @@ type alias Model =
, mainName : String
, mainSearch : A.Model GApi.ApiCharResult
, mainSpoil : Int
- , image : Maybe String
- , imageState : Api.State
- , imageNew : Set.Set String
- , imageSex : Maybe Int
- , imageVio : Maybe Int
+ , image : Img.Image
, traits : List GCE.RecvTraits
, traitSearch : A.Model GApi.ApiTraitResult
, traitSelId : Int
@@ -108,11 +105,7 @@ init d =
, mainName = d.main_name
, mainSearch = A.init ""
, mainSpoil = d.main_spoil
- , image = d.image
- , imageState = Api.Normal
- , imageNew = Set.empty
- , imageSex = d.image_sex
- , imageVio = d.image_vio
+ , image = Img.info d.image_info
, traits = d.traits
, traitSearch = A.init ""
, traitSelId = 0
@@ -148,9 +141,7 @@ encode model =
, cup_size = model.cupSize
, main = if model.mainHas then model.main else Nothing
, main_spoil = model.mainSpoil
- , image = model.image
- , image_sex = model.imageSex
- , image_vio = model.imageVio
+ , image = model.image.id
, traits = List.map (\t -> { tid = t.tid, spoil = t.spoil }) model.traits
, vns = List.map (\v -> { vid = v.vid, rid = v.rid, spoil = v.spoil, role = v.role }) model.vns
}
@@ -188,12 +179,10 @@ type Msg
| MainHas Bool
| MainSearch (A.Msg GApi.ApiCharResult)
| MainSpoil Int
- | ImageSet String
+ | ImageSet String Bool
| ImageSelect
| ImageSelected File
- | ImageLoaded GApi.Response
- | ImageSex Int Bool
- | ImageVio Int Bool
+ | ImageMsg Img.Msg
| TraitDel Int
| TraitSel Int Int
| TraitSpoil Int Int
@@ -240,13 +229,10 @@ update msg model =
Nothing -> ({ model | mainSearch = A.clear nm "", main = Just m1.id, mainName = m1.name }, c)
MainSpoil n -> ({ model | mainSpoil = n }, Cmd.none)
- ImageSet s -> ({ model | image = if s == "" then Nothing else Just s}, Cmd.none)
+ 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)
- ImageSelected f -> ({ model | imageState = Api.Loading }, Api.postImage Api.Ch f ImageLoaded)
- ImageLoaded (GApi.Image i _ _) -> ({ model | image = Just i, imageNew = Set.insert i model.imageNew, imageState = Api.Normal }, Cmd.none)
- ImageLoaded e -> ({ model | imageState = Api.Error e }, Cmd.none)
- ImageSex i _ -> ({ model | imageSex = Just i }, Cmd.none)
- ImageVio i _ -> ({ model | imageVio = Just i }, Cmd.none)
+ ImageSelected f -> let (nm, nc) = Img.upload Api.Ch 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)
TraitDel idx -> ({ model | traits = delidx idx model.traits }, Cmd.none)
TraitSel id spl -> ({ model | traitSelId = id, traitSelSpl = spl }, Cmd.none)
@@ -258,7 +244,7 @@ update msg model =
Just t ->
if not t.applicable || t.state /= 2 || List.any (\l -> l.tid == t.id) model.traits
then ({ model | traitSearch = A.clear nm "" }, c)
- else ({ model | traitSearch = A.clear nm "", traits = model.traits ++ [{ tid = t.id, spoil = t.defaultspoil, name = t.name, group = t.group_name, applicable = t.applicable, new = True }] }, Cmd.none)
+ else ({ model | traitSearch = A.clear nm "", traits = model.traits ++ [{ tid = t.id, spoil = t.defaultspoil, name = t.name, group = t.group_name, applicable = t.applicable, new = True }] }, c)
VnRel idx r -> ({ model | vns = modidx idx (\v -> { v | rid = r }) model.vns }, Cmd.none)
VnRole idx s -> ({ model | vns = modidx idx (\v -> { v | role = s }) model.vns }, Cmd.none)
@@ -275,7 +261,7 @@ update msg model =
if List.any (\v -> v.vid == vn.id) model.vns
then ({ model | vnSearch = A.clear nm "" }, c)
else ({ model | vnSearch = A.clear nm "", vns = model.vns ++ [{ vid = vn.id, title = vn.title, rid = Nothing, spoil = 0, role = "primary" }] }
- , if Dict.member vn.id model.releases then Cmd.none else GR.send { vid = vn.id } (VnRelGet vn.id))
+ , Cmd.batch [c, if Dict.member vn.id model.releases then Cmd.none else GR.send { vid = vn.id } (VnRelGet vn.id)])
VnRelGet vid (GApi.Releases r) -> ({ model | releases = Dict.insert vid r model.releases }, Cmd.none)
VnRelGet _ r -> ({ model | state = Api.Error r }, Cmd.none) -- XXX
@@ -288,6 +274,7 @@ isValid : Model -> Bool
isValid model = not
( (model.name /= "" && model.name == model.original)
|| hasDuplicates (List.map (\v -> (v.vid, Maybe.withDefault 0 v.rid)) model.vns)
+ || not (Img.isValid model.image)
)
@@ -379,48 +366,28 @@ view model =
]
image =
- div [ class "formimage" ]
- [ div [] [
- case model.image of
- Nothing -> text "No image."
- Just id -> img [ src (imageUrl id) ] []
- ]
- , div []
+ table [ class "formimage" ] [ tr []
+ [ td [] [ Img.viewImg model.image ]
+ , td []
[ h2 [] [ text "Image ID" ]
- , inputText "" (Maybe.withDefault "" model.image) ImageSet GCE.valImage
- , Maybe.withDefault (text "") <| Maybe.map (\i -> a [ href <| "/img/"++i ] [ text " (flagging)" ]) model.image
+ , input ([ type_ "text", class "text", tabindex 10, value (Maybe.withDefault "" model.image.id), onInputValidation ImageSet ] ++ GCE.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 []
- , case model.imageState of
- Api.Normal -> text ""
- Api.Loading -> span [ class "spinner" ] []
- Api.Error e -> b [ class "standout" ] [ text <| Api.showResponse e ]
, br [] []
, text "Image must be in JPEG or PNG format and at most 10 MiB. Images larger than 256x300 will automatically be resized."
- , if not (Set.member (Maybe.withDefault "" model.image) model.imageNew) then text "" else div []
- [ br [] []
- , text "Please flag this image: (see the ", a [ href "/d19" ] [ text "image flagging guidelines" ], text " for guidance)"
- , table []
- [ thead [] [ tr [] [ td [] [ text "Sexual" ], td [] [ text "Violence" ] ] ]
- , tr []
- [ td []
- [ label [] [ inputRadio "" (model.imageSex == Just 0) (ImageSex 0), text " Safe" ], br [] []
- , label [] [ inputRadio "" (model.imageSex == Just 1) (ImageSex 1), text " Suggestive" ], br [] []
- , label [] [ inputRadio "" (model.imageSex == Just 2) (ImageSex 2), text " Explicit" ]
- ]
- , td []
- [ label [] [ inputRadio "" (model.imageVio == Just 0) (ImageVio 0), text " Tame" ], br [] []
- , label [] [ inputRadio "" (model.imageVio == Just 1) (ImageVio 1), text " Violent" ], br [] []
- , label [] [ inputRadio "" (model.imageVio == Just 2) (ImageVio 2), text " Brutal" ]
- ]
+ , case Img.viewVote model.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
]
- ]
- ]
]
- ]
+ ] ]
traits =
let
diff --git a/elm/Discussions/Edit.elm b/elm/Discussions/Edit.elm
index f8873fa7..6008cdef 100644
--- a/elm/Discussions/Edit.elm
+++ b/elm/Discussions/Edit.elm
@@ -25,8 +25,7 @@ main = Browser.element
type alias Model =
{ state : Api.State
- , tid : Maybe Int
- , num : Maybe Int
+ , tid : Maybe String
, can_mod : Bool
, can_private : Bool
, locked : Bool
@@ -50,7 +49,6 @@ init d =
, can_mod = d.can_mod
, can_private = d.can_private
, tid = d.tid
- , num = d.num
, locked = d.locked
, hidden = d.hidden
, private = d.private
@@ -73,7 +71,6 @@ searchConfig = { wrap = BoardSearch, id = "boardadd", source = A.boardSource }
encode : Model -> GDE.Send
encode m =
{ tid = m.tid
- , num = m.num
, locked = m.locked
, hidden = m.hidden
, private = m.private
@@ -148,8 +145,6 @@ update msg model =
view : Model -> Html Msg
view model =
let
- thread = model.tid == Nothing || model.num == Just 1
-
board n bd =
li [] <|
[ text "["
@@ -184,7 +179,7 @@ view model =
else text ""
]
- poll () =
+ poll =
[ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ]
, formField "" [ label [] [ inputCheck "" model.pollEnabled PollEnabled, text " Add poll" ] ]
] ++
@@ -211,26 +206,22 @@ view model =
in
form_ Submit (model.state == Api.Loading)
[ div [ class "mainbox" ]
- [ h1 [] [ text <| if model.tid == Nothing then "Create new thread" else "Edit post" ]
+ [ h1 [] [ text <| if model.tid == Nothing then "Create new thread" else "Edit thread" ]
, table [ class "formtable" ] <|
- [ if thread
- then formField "title::Thread title" [ inputText "title" (Maybe.withDefault "" model.title) Title (style "width" "400px" :: required True :: GDE.valTitle) ]
- else formField "Topic" [ a [ href <| "/t" ++ String.fromInt (Maybe.withDefault 0 model.tid) ] [ text (Maybe.withDefault "" model.title) ] ]
- , if thread && model.can_mod
+ [ formField "title::Thread title" [ inputText "title" (Maybe.withDefault "" model.title) Title (style "width" "400px" :: required True :: GDE.valTitle) ]
+ , if model.can_mod
then formField "" [ label [] [ inputCheck "" model.locked Locked, text " Locked" ] ]
else text ""
, if model.can_mod
then formField "" [ label [] [ inputCheck "" model.hidden Hidden, text " Hidden" ] ]
else text ""
- , if thread && model.can_private
+ , if model.can_private
then formField "" [ label [] [ inputCheck "" model.private Private, text " Private" ] ]
else text ""
, if model.tid /= Nothing && model.can_mod
then formField "" [ label [] [ inputCheck "" model.nolastmod Nolastmod, text " Don't update last modification timestamp" ] ]
else text ""
- , if thread
- then formField "boardadd::Boards" (boards ())
- else text ""
+ , formField "boardadd::Boards" (boards ())
, tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ]
, formField "msg::Message"
[ TP.view "msg" model.msg Content 700 ([rows 12, cols 50] ++ GDE.valMsg)
@@ -239,15 +230,10 @@ view model =
]
]
]
- ++ (if thread then poll () else [])
- ++ (if not model.can_mod then [] else
+ ++ poll
+ ++ (if not model.can_mod || model.tid == Nothing then [] else
[ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "DANGER ZONE" ] ]
- , formField ""
- [ inputCheck "" model.delete Delete
- , text <| " Permanently delete this " ++ if thread then "thread and all replies." else "post."
- , text <| if thread then "" else " This causes all replies after this one to be renumbered."
- , text <| " This action can not be reverted, only do this with obvious spam!"
- ]
+ , formField "" [ inputCheck "" model.delete Delete, text " Permanently delete this thread and all replies. This action can not be reverted, only do this with obvious spam!" ]
])
]
, div [ class "mainbox" ]
diff --git a/elm/Discussions/PostEdit.elm b/elm/Discussions/PostEdit.elm
new file mode 100644
index 00000000..0eb787d2
--- /dev/null
+++ b/elm/Discussions/PostEdit.elm
@@ -0,0 +1,108 @@
+module Discussions.PostEdit exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Browser
+import Browser.Navigation exposing (load)
+import Lib.Html exposing (..)
+import Lib.TextPreview as TP
+import Lib.Api as Api
+import Gen.Api as GApi
+import Gen.DiscussionsPostEdit as GPE
+
+
+main : Program GPE.Recv Model Msg
+main = Browser.element
+ { init = \e -> (init e, Cmd.none)
+ , view = view
+ , update = update
+ , subscriptions = always Sub.none
+ }
+
+
+type alias Model =
+ { state : Api.State
+ , id : String
+ , num : Int
+ , can_mod : Bool
+ , hidden : Bool
+ , nolastmod : Bool
+ , delete : Bool
+ , msg : TP.Model
+ }
+
+
+init : GPE.Recv -> Model
+init d =
+ { state = Api.Normal
+ , id = d.id
+ , num = d.num
+ , can_mod = d.can_mod
+ , hidden = d.hidden
+ , nolastmod = False
+ , delete = False
+ , msg = TP.bbcode d.msg
+ }
+
+encode : Model -> GPE.Send
+encode m =
+ { id = m.id
+ , num = m.num
+ , hidden = m.hidden
+ , nolastmod = m.nolastmod
+ , delete = m.delete
+ , msg = m.msg.data
+ }
+
+
+type Msg
+ = Hidden Bool
+ | Nolastmod Bool
+ | Delete Bool
+ | Content TP.Msg
+ | Submit
+ | Submitted GApi.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Hidden b -> ({ model | hidden = b }, Cmd.none)
+ Nolastmod b -> ({ model | nolastmod=b }, Cmd.none)
+ Delete b -> ({ model | delete = b }, Cmd.none)
+ Content m -> let (nm,nc) = TP.update m model.msg in ({ model | msg = nm }, Cmd.map Content nc)
+
+ Submit -> ({ model | state = Api.Loading }, GPE.send (encode model) Submitted)
+ Submitted (GApi.Redirect s) -> (model, load s)
+ Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
+
+
+view : Model -> Html Msg
+view model =
+ form_ Submit (model.state == Api.Loading)
+ [ div [ class "mainbox" ]
+ [ h1 [] [ text "Edit post" ]
+ , table [ class "formtable" ] <|
+ [ formField "Post" [ a [ href <| "/" ++ model.id ++ "." ++ String.fromInt model.num ] [ text <| "#" ++ String.fromInt model.num ++ " on " ++ model.id ] ]
+ , if model.can_mod
+ then formField "" [ label [] [ inputCheck "" model.hidden Hidden, text " Hidden" ] ]
+ else text ""
+ , if model.can_mod
+ then formField "" [ label [] [ inputCheck "" model.nolastmod Nolastmod, text " Don't update last modification timestamp" ] ]
+ else text ""
+ , tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ]
+ , formField "msg::Message"
+ [ TP.view "msg" model.msg Content 700 ([rows 12, cols 50] ++ GPE.valMsg)
+ [ b [ class "standout" ] [ text " (English please!) " ]
+ , a [ href "/d9#3" ] [ text "Formatting" ]
+ ]
+ ]
+ ]
+ ++ (if not model.can_mod then [] else
+ [ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "DANGER ZONE" ] ]
+ , formField "" [ inputCheck "" model.delete Delete, text " Permanently delete this post. This action can not be reverted, only do this with obvious spam!" ]
+ ])
+ ]
+ , div [ class "mainbox" ]
+ [ fieldset [ class "submit" ] [ submitButton "Submit" model.state True ] ]
+ ]
diff --git a/elm/Discussions/Reply.elm b/elm/Discussions/Reply.elm
index a8d25434..3581c91f 100644
--- a/elm/Discussions/Reply.elm
+++ b/elm/Discussions/Reply.elm
@@ -22,7 +22,7 @@ main = Browser.element
type alias Model =
{ state : Api.State
- , tid : Int
+ , tid : String
, old : Bool
, msg : TP.Model
}
diff --git a/elm/Lib/Api.elm b/elm/Lib/Api.elm
index 4af28ea6..fd4a3a7e 100644
--- a/elm/Lib/Api.elm
+++ b/elm/Lib/Api.elm
@@ -45,14 +45,15 @@ showResponse res =
BadCurPass -> "Current password is invalid."
MailChange -> unexp
ImgFormat -> "Unrecognized image format, only JPEG and PNG are accepted."
- Image _ _ _ -> unexp
Releases _ -> unexp
BoardResult _ -> unexp
TagResult _ -> unexp
TraitResult _ -> unexp
VNResult _ -> unexp
ProducerResult _ -> unexp
+ StaffResult _ -> unexp
CharResult _ -> unexp
+ AnimeResult _ -> unexp
ImageResult _ -> unexp
diff --git a/elm/Lib/Autocomplete.elm b/elm/Lib/Autocomplete.elm
index 738f6008..5c5dd33d 100644
--- a/elm/Lib/Autocomplete.elm
+++ b/elm/Lib/Autocomplete.elm
@@ -9,7 +9,9 @@ module Lib.Autocomplete exposing
, traitSource
, vnSource
, producerSource
+ , staffSource
, charSource
+ , animeSource
, init
, clear
, update
@@ -35,7 +37,9 @@ import Gen.Tags as GT
import Gen.Traits as GTR
import Gen.VN as GV
import Gen.Producers as GP
+import Gen.Staff as GS
import Gen.Chars as GC
+import Gen.Anime as GA
type alias Config m a =
@@ -123,7 +127,7 @@ traitSource =
vnSource : SourceConfig m GApi.ApiVNResult
vnSource =
- { source = Endpoint (\s -> GV.send { search = s })
+ { source = Endpoint (\s -> GV.send { search = [s], hidden = False })
<| \x -> case x of
GApi.VNResult e -> Just e
_ -> Nothing
@@ -136,7 +140,7 @@ vnSource =
producerSource : SourceConfig m GApi.ApiProducerResult
producerSource =
- { source = Endpoint (\s -> GP.send { search = s })
+ { source = Endpoint (\s -> GP.send { search = [s], hidden = False })
<| \x -> case x of
GApi.ProducerResult e -> Just e
_ -> Nothing
@@ -147,6 +151,19 @@ producerSource =
}
+staffSource : SourceConfig m GApi.ApiStaffResult
+staffSource =
+ { source = Endpoint (\s -> GS.send { search = s })
+ <| \x -> case x of
+ GApi.StaffResult e -> Just e
+ _ -> Nothing
+ , view = \i ->
+ [ b [ class "grayedout" ] [ text <| "s" ++ String.fromInt i.id ++ ": " ]
+ , text i.name ]
+ , key = \i -> String.fromInt i.aid
+ }
+
+
charSource : SourceConfig m GApi.ApiCharResult
charSource =
{ source = Endpoint (\s -> GC.send { search = s })
@@ -164,6 +181,19 @@ charSource =
}
+animeSource : SourceConfig m GApi.ApiAnimeResult
+animeSource =
+ { source = Endpoint (\s -> GA.send { search = s })
+ <| \x -> case x of
+ GApi.AnimeResult e -> Just e
+ _ -> Nothing
+ , view = \i ->
+ [ b [ class "grayedout" ] [ text <| "a" ++ String.fromInt i.id ++ ": " ]
+ , text i.title ]
+ , key = \i -> String.fromInt i.id
+ }
+
+
type alias Model a =
{ visible : Bool
, value : String
diff --git a/elm/Lib/Editsum.elm b/elm/Lib/Editsum.elm
index 656441e8..20a51872 100644
--- a/elm/Lib/Editsum.elm
+++ b/elm/Lib/Editsum.elm
@@ -59,5 +59,8 @@ view model =
(if model.authmod then lockhid else [])
++
[ TP.view "" model.editsum Editsum 600 [rows 4, cols 50, minlength 2, maxlength 5000, required True]
- [ b [class "title"] [ text "Edit summary", b [class "standout"] [text " (English please!)"] ] ]
+ [ b [class "title"] [ text "Edit summary", b [class "standout"] [ text " (English please!)" ] ]
+ , br [] []
+ , text "Summarize the changes you have made, including links to source(s)."
+ ]
]
diff --git a/elm/Lib/Image.elm b/elm/Lib/Image.elm
new file mode 100644
index 00000000..31bab0b3
--- /dev/null
+++ b/elm/Lib/Image.elm
@@ -0,0 +1,183 @@
+module Lib.Image exposing (..)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Process
+import Task
+import File exposing (File)
+import Lib.Html exposing (..)
+import Lib.Api as Api
+import Lib.Util exposing (imageUrl)
+import Gen.Api as GApi
+import Gen.Image as GI
+import Gen.ImageVote as GIV
+
+
+type State
+ = Normal
+ | Invalid
+ | NotFound
+ | Loading
+ | Error GApi.Response
+
+type alias Image =
+ { id : Maybe String
+ , img : Maybe GApi.ApiImageResult
+ , imgState : State
+ , saveState : Api.State
+ , saveTimer : Bool
+ }
+
+
+info : Maybe GApi.ApiImageResult -> Image
+info img =
+ { id = Maybe.map (\i -> i.id) img
+ , img = img
+ , imgState = Normal
+ , saveState = Api.Normal
+ , saveTimer = False
+ }
+
+
+-- Fetch image info from the ID
+new : Bool -> String -> (Image, Cmd Msg)
+new valid id =
+ ( { id = if id == "" then Nothing else Just id
+ , img = Nothing
+ , imgState = if id == "" then Normal else if valid then Loading else Invalid
+ , saveState = Api.Normal
+ , saveTimer = False
+ }
+ , if valid && id /= "" then GI.send { id = id } Loaded else Cmd.none
+ )
+
+
+-- Upload a new image from a form
+upload : Api.ImageType -> File -> (Image, Cmd Msg)
+upload t f =
+ ( { id = Nothing
+ , img = Nothing
+ , imgState = Loading
+ , saveState = Api.Normal
+ , saveTimer = False
+ }
+ , Api.postImage t f Loaded)
+
+
+type Msg
+ = Loaded GApi.Response
+ | MySex Int Bool
+ | MyVio Int Bool
+ | Save
+ | Saved GApi.Response
+
+
+update : Msg -> Image -> (Image, Cmd Msg)
+update msg model =
+ let
+ save m =
+ if m.saveTimer || Maybe.withDefault True (Maybe.map (\i -> i.token == Nothing || i.my_sexual == Nothing || i.my_violence == Nothing) m.img)
+ then (m, Cmd.none)
+ else ({ m | saveTimer = True }, Task.perform (always Save) (Process.sleep 1000))
+ in
+ case msg of
+ Loaded (GApi.ImageResult [i]) -> ({ model | id = Just i.id, img = Just i, imgState = Normal}, Cmd.none)
+ Loaded (GApi.ImageResult []) -> ({ model | imgState = NotFound}, Cmd.none)
+ Loaded e -> ({ model | imgState = Error e }, Cmd.none)
+
+ MySex v _ -> save { model | img = Maybe.map (\i -> { i | my_sexual = Just v }) model.img }
+ MyVio v _ -> save { model | img = Maybe.map (\i -> { i | my_violence = Just v }) model.img }
+
+ Save ->
+ case Maybe.map (\i -> (i.token, i.my_sexual, i.my_violence)) model.img of
+ Just (Just token, Just sex, Just vio) ->
+ ( { model | saveTimer = False, saveState = Api.Loading }
+ , GIV.send { votes = [{ id = Maybe.withDefault "" model.id, token = token, sexual = sex, violence = vio, overrule = False }] } Saved)
+ _ -> (model, Cmd.none)
+ Saved (GApi.Success) -> ({ model | saveState = Api.Normal}, Cmd.none)
+ Saved e -> ({ model | saveState = Api.Error e }, Cmd.none)
+
+
+
+isValid : Image -> Bool
+isValid img = img.imgState == Normal
+
+
+viewImg : Image -> Html m
+viewImg image =
+ case (image.imgState, image.img) of
+ (Loading, _) -> div [ class "spinner" ] []
+ (NotFound, _) -> b [ class "standout" ] [ text "Image not found." ]
+ (Invalid, _) -> b [ class "standout" ] [ text "Invalid image ID." ]
+ (Error e, _) -> b [ class "standout" ] [ text <| Api.showResponse e ]
+ (_, Nothing) -> text "No image."
+ (_, Just i) ->
+ let
+ maxWidth = toFloat <| if String.startsWith "sf" i.id then 136 else 10000
+ maxHeight = toFloat <| if String.startsWith "sf" i.id then 102 else 10000
+ sWidth = maxWidth / toFloat i.width
+ sHeight = maxHeight / toFloat i.height
+ scale = Basics.min 1 <| if sWidth < sHeight then sWidth else sHeight
+ imgWidth = round <| scale * toFloat i.width
+ imgHeight = round <| scale * toFloat i.height
+ in
+ -- TODO: Onclick iv.js support for screenshot thumbnails
+ label [ class "imghover", style "width" (String.fromInt imgWidth++"px"), style "height" (String.fromInt imgHeight++"px") ]
+ [ div [ class "imghover--visible" ]
+ [ if String.startsWith "sf" i.id
+ then a [ href (imageUrl i.id), attribute "data-iv" <| String.fromInt i.width ++ "x" ++ String.fromInt i.height ++ ":scr" ]
+ [ img [ src <| imageUrl <| String.replace "sf" "st" i.id ] [] ]
+ else img [ src <| imageUrl i.id ] []
+ , a [ class "imghover--overlay", href <| "/img/"++i.id ] <|
+ case (i.sexual_avg, i.violence_avg) of
+ (Just sex, Just vio) ->
+ -- XXX: These thresholds are subject to change, maybe just show the numbers here?
+ [ text <| if sex > 1.3 then "Explicit" else if sex > 0.4 then "Suggestive" else "Tame"
+ , text " / "
+ , text <| if vio > 1.3 then "Brutal" else if vio > 0.4 then "Violent" else "Safe"
+ , text <| " (" ++ String.fromInt i.votecount ++ ")"
+ ]
+ _ -> [ text "Not flagged" ]
+ ]
+ ]
+
+
+viewVote : Image -> Maybe (Html Msg)
+viewVote model =
+ let
+ rad i sex val = input
+ [ type_ "radio"
+ , tabindex 10
+ , required True
+ , onCheck <| (if sex then MySex else MyVio) val
+ , checked <| (if sex then i.my_sexual else i.my_violence) == Just val
+ , name <| "imgvote-" ++ (if sex then "sex" else "vio") ++ "-" ++ Maybe.withDefault "" model.id
+ ] []
+ vote i = table []
+ [ thead [] [ tr []
+ [ td [] [ text "Sexual ", if model.saveState == Api.Loading then span [ class "spinner" ] [] else text "" ]
+ , td [] [ text "Violence" ]
+ ] ]
+ , tfoot [] <|
+ case model.saveState of
+ Api.Error e -> [ tr [] [ td [ colspan 2 ] [ b [ class "standout" ] [ text (Api.showResponse e) ] ] ] ]
+ _ -> []
+ , tr []
+ [ td [ style "white-space" "nowrap" ]
+ [ label [] [ rad i True 0, text " Safe" ], br [] []
+ , label [] [ rad i True 1, text " Suggestive" ], br [] []
+ , label [] [ rad i True 2, text " Explicit" ]
+ ]
+ , td [ style "white-space" "nowrap" ]
+ [ label [] [ rad i False 0, text " Tame" ], br [] []
+ , label [] [ rad i False 1, text " Violent" ], br [] []
+ , label [] [ rad i False 2, text " Brutal" ]
+ ]
+ ]
+ ]
+ in case model.img of
+ Nothing -> Nothing
+ Just i ->
+ if i.token == Nothing then Nothing
+ else Just (vote i)
diff --git a/elm/ProducerEdit.elm b/elm/ProducerEdit.elm
new file mode 100644
index 00000000..0fd78375
--- /dev/null
+++ b/elm/ProducerEdit.elm
@@ -0,0 +1,231 @@
+module ProducerEdit exposing (main)
+
+import Html exposing (..)
+import Html.Events exposing (..)
+import Html.Attributes exposing (..)
+import Browser
+import Browser.Navigation exposing (load)
+import Lib.Util exposing (..)
+import Lib.Html exposing (..)
+import Lib.TextPreview as TP
+import Lib.Autocomplete as A
+import Lib.Api as Api
+import Lib.Editsum as Editsum
+import Gen.Producers as GP
+import Gen.ProducerEdit as GPE
+import Gen.Types as GT
+import Gen.Api as GApi
+
+
+main : Program GPE.Recv Model Msg
+main = Browser.element
+ { init = \e -> (init e, Cmd.none)
+ , view = view
+ , update = update
+ , subscriptions = always Sub.none
+ }
+
+type alias Model =
+ { state : Api.State
+ , editsum : Editsum.Model
+ , ptype : String
+ , name : String
+ , original : String
+ , alias : String
+ , lang : String
+ , website : String
+ , lWikidata : Maybe Int
+ , desc : TP.Model
+ , rel : List GPE.RecvRelations
+ , relSearch : A.Model GApi.ApiProducerResult
+ , id : Maybe Int
+ , dupCheck : Bool
+ , dupProds : List GApi.ApiProducerResult
+ }
+
+
+init : GPE.Recv -> Model
+init d =
+ { state = Api.Normal
+ , editsum = { authmod = d.authmod, editsum = TP.bbcode d.editsum, locked = d.locked, hidden = d.hidden }
+ , ptype = d.ptype
+ , name = d.name
+ , original = d.original
+ , alias = d.alias
+ , lang = d.lang
+ , website = d.website
+ , lWikidata = d.l_wikidata
+ , desc = TP.bbcode d.desc
+ , rel = d.relations
+ , relSearch = A.init ""
+ , id = d.id
+ , dupCheck = False
+ , dupProds = []
+ }
+
+
+encode : Model -> GPE.Send
+encode model =
+ { id = model.id
+ , editsum = model.editsum.editsum.data
+ , hidden = model.editsum.hidden
+ , locked = model.editsum.locked
+ , ptype = model.ptype
+ , name = model.name
+ , original = model.original
+ , alias = model.alias
+ , lang = model.lang
+ , website = model.website
+ , l_wikidata = model.lWikidata
+ , desc = model.desc.data
+ , relations = List.map (\p -> { pid = p.pid, relation = p.relation }) model.rel
+ }
+
+prodConfig : A.Config Msg GApi.ApiProducerResult
+prodConfig = { wrap = RelSearch, id = "relationadd", source = A.producerSource }
+
+type Msg
+ = Editsum Editsum.Msg
+ | Submit
+ | Submitted GApi.Response
+ | PType String
+ | Name String
+ | Original String
+ | Alias String
+ | Lang String
+ | Website String
+ | LWikidata (Maybe Int)
+ | Desc TP.Msg
+ | RelDel Int
+ | RelRel Int String
+ | RelSearch (A.Msg GApi.ApiProducerResult)
+ | DupSubmit
+ | DupResults GApi.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Editsum m -> let (nm,nc) = Editsum.update m model.editsum in ({ model | editsum = nm }, Cmd.map Editsum nc)
+ PType s -> ({ model | ptype = s }, Cmd.none)
+ Name s -> ({ model | name = s, dupProds = [] }, Cmd.none)
+ Original s -> ({ model | original = s, dupProds = [] }, Cmd.none)
+ Alias s -> ({ model | alias = s, dupProds = [] }, Cmd.none)
+ Lang s -> ({ model | lang = s }, Cmd.none)
+ Website s -> ({ model | website = s }, Cmd.none)
+ LWikidata n-> ({ model | lWikidata = n }, Cmd.none)
+ Desc m -> let (nm,nc) = TP.update m model.desc in ({ model | desc = nm }, Cmd.map Desc nc)
+
+ RelDel idx -> ({ model | rel = delidx idx model.rel }, Cmd.none)
+ RelRel idx rel -> ({ model | rel = modidx idx (\p -> { p | relation = rel }) model.rel }, Cmd.none)
+ RelSearch m ->
+ let (nm, c, res) = A.update prodConfig m model.relSearch
+ in case res of
+ Nothing -> ({ model | relSearch = nm }, c)
+ Just p ->
+ if List.any (\l -> l.pid == p.id) model.rel
+ then ({ model | relSearch = A.clear nm "" }, c)
+ else ({ model | relSearch = A.clear nm "", rel = model.rel ++ [{ pid = p.id, name = p.name, original = p.original, relation = "old" }] }, c)
+
+ DupSubmit ->
+ if List.isEmpty model.dupProds
+ then ({ model | state = Api.Loading }, GP.send { hidden = True, search = model.name :: model.original :: String.lines model.alias } DupResults)
+ else ({ model | dupCheck = True, dupProds = [] }, Cmd.none)
+ DupResults (GApi.ProducerResult prods) ->
+ if List.isEmpty prods
+ then ({ model | state = Api.Normal, dupCheck = True, dupProds = [] }, Cmd.none)
+ else ({ model | state = Api.Normal, dupProds = prods }, Cmd.none)
+ DupResults r -> ({ model | state = Api.Error r }, Cmd.none)
+
+ Submit -> ({ model | state = Api.Loading }, GPE.send (encode model) Submitted)
+ Submitted (GApi.Redirect s) -> (model, load s)
+ Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
+
+
+isValid : Model -> Bool
+isValid model = not
+ ( (model.name /= "" && model.name == model.original)
+ || hasDuplicates (List.map (\p -> p.pid) model.rel)
+ )
+
+
+view : Model -> Html Msg
+view model =
+ let
+ titles =
+ [ formField "name::Name (romaji)" [ inputText "name" model.name Name (style "width" "500px" :: GPE.valName) ]
+ , formField "original::Original name"
+ [ inputText "original" model.original Original (style "width" "500px" :: GPE.valOriginal)
+ , if model.name /= "" && model.name == model.original
+ then b [ class "standout" ] [ br [] [], text "Should not be the same as the Name (romaji). Leave blank is the original name is already in the latin alphabet" ]
+ else if model.original /= "" && String.toLower model.name /= String.toLower model.original && not (containsNonLatin model.original)
+ then b [ class "standout" ] [ br [] [], text "Original name does not seem to contain any non-latin characters. Leave this field empty if the name is already in the latin alphabet" ]
+ else text ""
+ ]
+ , formField "alias::Aliases"
+ [ inputTextArea "alias" model.alias Alias (rows 3 :: GPE.valAlias)
+ , br [] []
+ , if hasDuplicates <| String.lines <| String.toLower model.alias
+ then b [ class "standout" ] [ text "List contains duplicate aliases.", br [] [] ]
+ else text ""
+ , text "(Un)official aliases, separated by a newline."
+ ]
+ ]
+
+ geninfo =
+ [ formField "ptype::Type" [ inputSelect "ptype" model.ptype PType [] GT.producerTypes ] ]
+ ++ titles ++
+ [ formField "lang::Primary language" [ inputSelect "lang" model.lang Lang [] GT.languages ]
+ , formField "website::Website" [ inputText "website" model.website Website GPE.valWebsite ]
+ , formField "l_wikidata::Wikidata ID" [ inputWikidata "l_wikidata" model.lWikidata LWikidata ]
+ , formField "desc::Description"
+ [ TP.view "desc" model.desc Desc 600 (style "height" "180px" :: GPE.valDesc) [ b [ class "standout" ] [ text "English please!" ] ] ]
+
+ , tr [ class "newpart" ] [ td [ colspan 2 ] [ text "Database relations" ] ]
+ , formField "Related producers"
+ [ if List.isEmpty model.rel then text ""
+ else table [] <| List.indexedMap (\i p -> tr []
+ [ td [ style "text-align" "right" ] [ b [ class "grayedout" ] [ text <| "p" ++ String.fromInt p.pid ++ ":" ] ]
+ , td [ style "text-align" "right"] [ a [ href <| "/p" ++ String.fromInt p.pid ] [ text p.name ] ]
+ , td []
+ [ text "is an "
+ , inputSelect "" p.relation (RelRel i) [] GT.producerRelations
+ , text " of this producer"
+ ]
+ , td [] [ inputButton "remove" (RelDel i) [] ]
+ ]
+ ) model.rel
+ , A.view prodConfig model.relSearch [placeholder "Add Producer..."]
+ ]
+ ]
+
+ newform () =
+ form_ DupSubmit (model.state == Api.Loading)
+ [ div [ class "mainbox" ] [ h1 [] [ text "Add a new producer" ], table [ class "formtable" ] titles ]
+ , div [ class "mainbox" ]
+ [ if List.isEmpty model.dupProds then text "" else
+ div []
+ [ h1 [] [ text "Possible duplicates" ]
+ , text "The following is a list of producers that match the name(s) you gave. "
+ , text "Please check this list to avoid creating a duplicate producer 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 (\p -> li []
+ [ a [ href <| "/p" ++ String.fromInt p.id ] [ text p.name ]
+ , if p.hidden then b [ class "standout" ] [ text " (deleted)" ] else text ""
+ ]
+ ) model.dupProds
+ ]
+ , fieldset [ class "submit" ] [ submitButton (if List.isEmpty model.dupProds then "Continue" else "Continue anyway") model.state (isValid model) ]
+ ]
+ ]
+
+ fullform () =
+ form_ Submit (model.state == Api.Loading)
+ [ div [ class "mainbox" ] [ h1 [] [ text "Edit producer" ], table [ class "formtable" ] geninfo ]
+ , div [ class "mainbox" ] [ fieldset [ 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 ()
diff --git a/elm/Report.elm b/elm/Report.elm
new file mode 100644
index 00000000..f63a9411
--- /dev/null
+++ b/elm/Report.elm
@@ -0,0 +1,184 @@
+module Report exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Browser
+import Browser.Navigation exposing (load)
+import Lib.Html exposing (..)
+import Lib.Api as Api
+import Lib.Ffi as Ffi
+import Gen.Api as GApi
+import Gen.Report as GR
+
+
+main : Program GR.Send Model Msg
+main = Browser.element
+ { init = \e -> ((Api.Normal, e), Cmd.none)
+ , view = view
+ , update = update
+ , subscriptions = always Sub.none
+ }
+
+type alias Model = (Api.State,GR.Send)
+
+type Msg
+ = Reason String
+ | Message String
+ | Submit
+ | Submitted GApi.Response
+
+
+type alias ReasonLabel =
+ { label : String
+ , vis : String -> Bool -- Given an objectid, returns whether it should be listed
+ , submit : Bool -- Whether it allows submission of the form
+ , msg : String -> List (Html Msg) -- Message to display
+ }
+
+
+vis _ = True
+nomsg _ = []
+objtype s o = String.any (\c -> String.startsWith (String.fromChar c) o) s
+editable = objtype "vrpcs"
+initial = { label = "-- Select --" , vis = vis, submit = False , msg = nomsg }
+
+reasons : List ReasonLabel
+reasons =
+ [ initial
+ , { label = "Spam"
+ , vis = vis
+ , submit = True
+ , msg = nomsg
+ }
+ , { label = "Links to piracy or illegal content"
+ , vis = vis
+ , submit = True
+ , msg = nomsg
+ }
+ , { label = "Off-topic / wrong board"
+ , vis = objtype "tw"
+ , submit = True
+ , msg = nomsg
+ }
+ , { label = "Unwelcome behavior"
+ , vis = objtype "tw"
+ , submit = True
+ , msg = nomsg
+ }
+ , { label = "Unmarked spoilers"
+ , vis = vis
+ , submit = True
+ , msg = \o -> if editable o then [] else
+ [ text "VNDB is an open wiki, it is often easier if you removed the spoilers yourself by "
+ , a [ href ("/" ++ o ++ "/edit") ] [ text " editing the entry" ]
+ , text ". You likely know more about this entry than our moderators, after all. "
+ , br [] []
+ , text "If you're not sure whether something is a spoiler or if you need help with editing, you can also report this issue on the "
+ , a [ href "/t/db" ] [ text "discussion board" ]
+ , text " so that others may be able to help you."
+ ]
+ }
+ , { label = "Incorrect information"
+ , vis = editable
+ , submit = False
+ , msg = \o ->
+ [ text "VNDB is an open wiki, you can correct the information in this database yourself by "
+ , a [ href ("/" ++ o ++ "/edit") ] [ text " editing the entry" ]
+ , text ". You likely know more about this entry than our moderators, after all. "
+ , br [] []
+ , text "If you need help with editing, you can also report this issue on the "
+ , a [ href "/t/db" ] [ text "discussion board" ]
+ , text " so that others may be able to help you."
+ ]
+ }
+ , { label = "Missing information"
+ , vis = editable
+ , submit = False
+ , msg = \o ->
+ [ text "VNDB is an open wiki, you can add any missing information to this database yourself. "
+ , text "You likely know more about this entry than our moderators, after all. "
+ , br [] []
+ , text "If you need help with contributing information, feel free to ask around on the "
+ , a [ href "/t/db" ] [ text "discussion board" ]
+ , text "."
+ ]
+ }
+ , { label = "Not a visual novel"
+ , vis = objtype "v"
+ , submit = False
+ , msg = \_ ->
+ [ text "If you suspect that this entry does not adhere to our "
+ , a [ href "/d2#1" ] [ text "inclusion criteria" ]
+ , text ", please report it in "
+ , a [ href "/t2108" ] [ text "this thread" ]
+ , text ", so that other users have a chance to provide feedback before a moderator makes their final decision."
+ ]
+ }
+ , { label = "Does not belong here"
+ , vis = \o -> editable o && not (objtype "v" o)
+ , submit = True
+ , msg = nomsg
+ }
+ , { label = "Duplicate entry"
+ , vis = editable
+ , submit = True
+ , msg = \_ -> [ text "Please include a link to the entry that this is a duplicate of." ]
+ }
+ , { label = "Other"
+ , vis = vis
+ , submit = True
+ , msg = \o ->
+ if editable o
+ then [ text "Keep in mind that VNDB is an open wiki, you can edit most of the information in this database."
+ , br [] []
+ , text "Reports for issues that do not require a moderator to get involved will most likely be ignored."
+ , br [] []
+ , text "If you need help with contributing to the database, feel free to ask around on the "
+ , a [ href "/t/db" ] [ text "discussion board" ]
+ , text "."
+ ]
+ else []
+ }
+ ]
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg (state,model) =
+ case msg of
+ Reason s -> ((state, { model | reason = s }), Cmd.none)
+ Message s -> ((state, { model | message = s }), Cmd.none)
+ Submit -> ((Api.Loading, model), GR.send model Submitted)
+ Submitted r -> ((Api.Error r, model), Cmd.none)
+
+
+view : Model -> Html Msg
+view (state,model) =
+ let
+ lst = List.filter (\l -> l.vis model.object) reasons
+ cur = List.filter (\l -> l.label == model.reason) lst |> List.head |> Maybe.withDefault initial
+ in
+ form_ Submit (state == Api.Loading)
+ [ div [ class "mainbox" ]
+ [ h1 [] [ text "Submit report" ]
+ , if state == Api.Error GApi.Success
+ then p [] [ text "Your report has been submitted, a moderator will look at it as soon as possible." ]
+ else table [ class "formtable" ] <|
+ [ formField "Subject" [ span [ Ffi.innerHtml model.title ] [] ]
+ , formField ""
+ [ text "Your report will be forwarded to a moderator."
+ , br [] []
+ , text "Keep in mind that not every report will be acted upon, we may decide that the problem you reported is still within acceptable limits."
+ , br [] []
+ , if model.loggedin
+ then text "We generally do not provide feedback on reports, but a moderator may decide to contact you for clarification."
+ else text "We generally do not provide feedback on reports, but you may leave your email address in the message if you wish to be available for clarification."
+ ]
+ , formField "reason::Reason" [ inputSelect "reason" model.reason Reason [style "width" "300px"] <| List.map (\l->(l.label,l.label)) lst ]
+ , formField "" (cur.msg model.object)
+ ] ++ if not cur.submit then [] else
+ [ formField "message::Message" [ inputTextArea "message" model.message Message [] ]
+ , formField "" [ submitButton "Submit" state True ]
+ ]
+ ]
+ ]
diff --git a/elm/Reviews/Comment.elm b/elm/Reviews/Comment.elm
new file mode 100644
index 00000000..fba37168
--- /dev/null
+++ b/elm/Reviews/Comment.elm
@@ -0,0 +1,52 @@
+module Reviews.Comment exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Browser
+import Browser.Navigation exposing (load)
+import Lib.Html exposing (..)
+import Lib.TextPreview as TP
+import Lib.Api as Api
+import Gen.Api as GApi
+import Gen.ReviewsComment as GRC
+
+
+main : Program GRC.Send Model Msg
+main = Browser.element
+ { init = \e -> ((Api.Normal, e.id, TP.bbcode ""), Cmd.none)
+ , view = view
+ , update = update
+ , subscriptions = always Sub.none
+ }
+
+type alias Model = (Api.State, String, TP.Model)
+
+type Msg
+ = Content TP.Msg
+ | Submit
+ | Submitted GApi.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg (state,id,content) =
+ case msg of
+ Content m -> let (nm,nc) = TP.update m content in ((state,id,nm), Cmd.map Content nc)
+ Submit -> ((Api.Loading,id,content), GRC.send { msg = content.data, id = id } Submitted)
+ Submitted (GApi.Redirect s) -> ((state,id,content), load s)
+ Submitted r -> ((Api.Error r,id,content), Cmd.none)
+
+
+view : Model -> Html Msg
+view (state,_,content) =
+ form_ Submit (state == Api.Loading)
+ [ div [ class "mainbox" ]
+ [ fieldset [ class "submit" ]
+ [ TP.view "msg" content Content 600 ([rows 4, cols 50] ++ GRC.valMsg)
+ [ b [] [ text "Comment" ]
+ , b [ class "standout" ] [ text " (English please!) " ]
+ , a [ href "/d9#3" ] [ text "Formatting" ]
+ ]
+ , submitButton "Submit" state True
+ ]
+ ]
+ ]
diff --git a/elm/Reviews/Edit.elm b/elm/Reviews/Edit.elm
new file mode 100644
index 00000000..925de964
--- /dev/null
+++ b/elm/Reviews/Edit.elm
@@ -0,0 +1,195 @@
+module Reviews.Edit exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Browser
+import Browser.Navigation exposing (load)
+import Lib.Html exposing (..)
+import Lib.TextPreview as TP
+import Lib.Api as Api
+import Lib.Util exposing (..)
+import Lib.RDate as RDate
+import Gen.Api as GApi
+import Gen.ReviewsEdit as GRE
+import Gen.ReviewsDelete as GRD
+
+
+main : Program GRE.Recv Model Msg
+main = Browser.element
+ { init = \e -> (init e, Cmd.none)
+ , view = view
+ , update = update
+ , subscriptions = always Sub.none
+ }
+
+
+type alias Model =
+ { state : Api.State
+ , id : Maybe String
+ , vid : Int
+ , vntitle : String
+ , rid : Maybe Int
+ , spoiler : Bool
+ , locked : Bool
+ , isfull : Bool
+ , text : TP.Model
+ , releases : List GRE.RecvReleases
+ , delete : Bool
+ , delState : Api.State
+ , mod : Bool
+ }
+
+
+init : GRE.Recv -> Model
+init d =
+ { state = Api.Normal
+ , id = d.id
+ , vid = d.vid
+ , vntitle = d.vntitle
+ , rid = d.rid
+ , spoiler = d.spoiler
+ , locked = d.locked
+ , isfull = d.isfull
+ , text = TP.bbcode d.text
+ , releases = d.releases
+ , delete = False
+ , delState = Api.Normal
+ , mod = d.mod
+ }
+
+
+encode : Model -> GRE.Send
+encode m =
+ { id = m.id
+ , vid = m.vid
+ , rid = m.rid
+ , spoiler = m.spoiler
+ , locked = m.locked
+ , isfull = m.isfull
+ , text = m.text.data
+ }
+
+
+type Msg
+ = Release (Maybe Int)
+ | Full Bool
+ | Spoiler Bool
+ | Locked Bool
+ | Text TP.Msg
+ | Submit
+ | Submitted GApi.Response
+ | Delete Bool
+ | DoDelete
+ | Deleted GApi.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Release i -> ({ model | rid = i }, Cmd.none)
+ Full b -> ({ model | isfull = b }, Cmd.none)
+ Spoiler b -> ({ model | spoiler = b }, Cmd.none)
+ Locked b -> ({ model | locked = b }, Cmd.none)
+ Text m -> let (nm,nc) = TP.update m model.text in ({ model | text = nm }, Cmd.map Text nc)
+
+ Submit -> ({ model | state = Api.Loading }, GRE.send (encode model) Submitted)
+ Submitted (GApi.Redirect s) -> (model, load s)
+ Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
+
+ Delete b -> ({ model | delete = b }, Cmd.none)
+ DoDelete -> ({ model | delState = Api.Loading }, GRD.send ({ id = Maybe.withDefault "" model.id }) Deleted)
+ Deleted GApi.Success -> (model, load <| "/v" ++ String.fromInt model.vid)
+ Deleted r -> ({ model | delState = Api.Error r }, Cmd.none)
+
+
+showrel r = "[" ++ (RDate.format (RDate.expand r.released)) ++ " " ++ (String.join "," r.lang) ++ "] " ++ r.title ++ " (r" ++ String.fromInt r.id ++ ")"
+
+view : Model -> Html Msg
+view model =
+ let minChars = if model.isfull then 1000 else 200
+ maxChars = if model.isfull then 100000 else 800
+ len = String.length model.text.data
+ in
+ form_ Submit (model.state == Api.Loading)
+ [ div [ class "mainbox" ]
+ [ h1 [] [ text <| if model.id == Nothing then "Submit a review" else "Edit review" ]
+ , p [] [ b [] [ text "Rules" ] ]
+ , ul []
+ [ li [] [ text "Submit only reviews you have written yourself!" ]
+ , li [] [ text "Reviews must be in English." ]
+ , li [] [ text "Try to be as objective as possible." ]
+ , li [] [ text "If you have published the review elsewhere (e.g. a personal blog), feel free to include a link at the end of the review. Formatting tip: ", em [] [ text "[Originally published at <link>]" ] ]
+ , li [] [ text "Your vote (if any) will be displayed alongside the review, even if you have marked your list as private." ]
+ ]
+ , br [] []
+ ]
+ , div [ class "mainbox" ]
+ [ table [ class "formtable" ]
+ [ formField "Subject" [ a [ href <| "/v"++String.fromInt model.vid ] [ text model.vntitle ] ]
+ , formField ""
+ [ inputSelect "" model.rid Release [style "width" "500px" ] <|
+ (Nothing, "No release selected")
+ :: List.map (\r -> (Just r.id, showrel r)) model.releases
+ ++ if model.rid == Nothing || List.any (\r -> Just r.id == model.rid) model.releases then [] else [(model.rid, "Deleted or moved release: r"++Maybe.withDefault "" (Maybe.map String.fromInt model.rid))]
+ , br [] []
+ , text "You do not have to select a release, but indicating which release your review is based on gives more context."
+ ]
+ , tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ]
+ , formField "Review type"
+ [ label [] [ inputRadio "type" (model.isfull == False) (\_ -> Full False), b [] [ text " Mini review" ]
+ , text <| " - Recommendation-style, maximum 800 characters." ]
+ , br [] []
+ , label [] [ inputRadio "type" (model.isfull == True ) (\_ -> Full True ), b [] [ text " Full review" ]
+ , text " - Longer, more detailed." ]
+ , br [] []
+ , b [ class "grayedout" ] [ text "You can always switch between review types later." ]
+ ]
+ , tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ]
+ , formField ""
+ [ label [] [ inputCheck "" model.spoiler Spoiler, text " This review contains spoilers." ]
+ , br [] []
+ , b [ class "grayedout" ] [ text "You do not have to check this option if all spoilers in your review are marked with [spoiler] tags." ]
+ ]
+ , if not model.mod then text "" else
+ formField "" [ label [] [ inputCheck "" model.locked Locked, text " Locked for commenting." ] ]
+ , tr [ class "newpart" ] [ td [ colspan 2 ] [ text "" ] ]
+ , formField "text::Review"
+ [ TP.view "sum" model.text Text 700 ([rows (if model.isfull then 30 else 10), cols 50] ++ GRE.valText)
+ [ a [ href "/d9#3" ] [ text "BBCode formatting supported" ] ]
+ , div [ style "width" "700px", style "text-align" "right" ] <|
+ let num c s = if c then b [ class " standout" ] [ text s ] else text s
+ in
+ [ num (len < minChars) (String.fromInt minChars)
+ , text " / "
+ , b [] [ text (String.fromInt len) ]
+ , text " / "
+ , num (len > maxChars) (if model.isfull then "∞" else String.fromInt maxChars)
+ ]
+ ]
+ ]
+ ]
+ , div [ class "mainbox" ]
+ [ fieldset [ class "submit" ]
+ [ submitButton "Submit" model.state (len <= maxChars && len >= minChars)
+ ]
+ ]
+ , if model.id == Nothing then text "" else
+ div [ class "mainbox" ]
+ [ h1 [] [ text "Delete review" ]
+ , table [ class "formtable" ] [ formField ""
+ [ label [] [ inputCheck "" model.delete Delete, text " Delete this review." ]
+ , if not model.delete then text "" else span []
+ [ br [] []
+ , b [ class "standout" ] [ text "WARNING:" ]
+ , text " Deleting this review is a permanent action and can not be reverted!"
+ , br [] []
+ , br [] []
+ , inputButton "Confirm delete" DoDelete []
+ , case model.delState of
+ Api.Loading -> span [ class "spinner" ] []
+ Api.Error e -> b [ class "standout" ] [ text <| Api.showResponse e ]
+ Api.Normal -> text ""
+ ]
+ ] ]
+ ]
+ ]
diff --git a/elm/Reviews/Vote.elm b/elm/Reviews/Vote.elm
new file mode 100644
index 00000000..490a2b78
--- /dev/null
+++ b/elm/Reviews/Vote.elm
@@ -0,0 +1,70 @@
+module Reviews.Vote exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Browser
+import Lib.Util exposing (..)
+import Lib.Html exposing (..)
+import Lib.Api as Api
+import Gen.Api as GApi
+import Gen.ReviewsVote as GRV
+
+
+main : Program GRV.Recv Model Msg
+main = Browser.element
+ { init = \d -> (init d, Cmd.none)
+ , subscriptions = always Sub.none
+ , view = view
+ , update = update
+ }
+
+type alias Model =
+ { state : Api.State
+ , id : String
+ , my : Maybe Bool
+ , overrule : Bool
+ , mod : Bool
+ }
+
+init : GRV.Recv -> Model
+init d =
+ { state = Api.Normal
+ , id = d.id
+ , my = d.my
+ , overrule = d.overrule
+ , mod = d.mod
+ }
+
+type Msg
+ = Vote Bool
+ | Overrule Bool
+ | Saved GApi.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ let save m = ({ m | state = Api.Loading }, GRV.send { id = m.id, my = m.my, overrule = m.overrule } Saved)
+ in
+ case msg of
+ Vote b -> save { model | my = if model.my == Just b then Nothing else Just b }
+ Overrule b -> let nm = { model | overrule = b } in if isJust model.my then save nm else (nm, Cmd.none)
+
+ Saved GApi.Success -> ({ model | state = Api.Normal }, Cmd.none)
+ Saved e -> ({ model | state = Api.Error e }, Cmd.none)
+
+
+view : Model -> Html Msg
+view model =
+ let but opt lbl = a [ href "#", onClickD (Vote opt), classList [("votebut", True), ("myvote", model.my == Just opt)] ] [ text lbl ]
+ in
+ span []
+ [ case model.state of
+ Api.Loading -> span [ class "spinner" ] []
+ Api.Error e -> b [ class "standout" ] [ text (Api.showResponse e) ]
+ Api.Normal -> text "Was this review helpful? "
+ , but True "yes"
+ , text " / "
+ , but False "no"
+ , if not model.mod then text "" else label [] [ text " / ", inputCheck "" model.overrule Overrule, text " O" ]
+ ]
diff --git a/elm/UList/ManageLabels.js b/elm/UList/ManageLabels.js
index f9f8c68b..3ff2db61 100644
--- a/elm/UList/ManageLabels.js
+++ b/elm/UList/ManageLabels.js
@@ -1,11 +1,3 @@
-document.querySelectorAll('#managelabels').forEach(function(b) {
- b.onclick = function() {
- document.querySelectorAll('.managelabels').forEach(function(e) { e.classList.toggle('hidden') })
- document.querySelectorAll('.savedefault').forEach(function(e) { e.classList.add('hidden') })
- };
- return false;
-});
-
wrap_elm_init('UList.ManageLabels', function(init, opt) {
opt.flags = { uid: pageVars.uid, labels: pageVars.labels };
init(opt);
diff --git a/elm/UList/SaveDefault.js b/elm/UList/SaveDefault.js
deleted file mode 100644
index a253680f..00000000
--- a/elm/UList/SaveDefault.js
+++ /dev/null
@@ -1,7 +0,0 @@
-document.querySelectorAll('#savedefault').forEach(function(b) {
- b.onclick = function() {
- document.querySelectorAll('.savedefault').forEach(function(e) { e.classList.toggle('hidden') })
- document.querySelectorAll('.managelabels').forEach(function(e) { e.classList.add('hidden') })
- };
- return false;
-});
diff --git a/elm/UList/VNPage.elm b/elm/UList/VNPage.elm
index 64c5f99a..b41e6ea1 100644
--- a/elm/UList/VNPage.elm
+++ b/elm/UList/VNPage.elm
@@ -18,26 +18,7 @@ import Gen.UListDel as GDE
import UList.LabelEdit as LE
import UList.VoteEdit as VE
--- We don't have a Gen.* module for this (yet), so define these manually
-type alias RecvLabels =
- { id : Int
- , label : String
- , private : Bool
- }
-
-type alias Recv =
- { uid : Int
- , vid : Int
- , onlist : Bool
- , canvote : Bool
- , vote : Maybe String
- , labels : List RecvLabels
- , selected : List Int
- , notes : String
- }
-
-
-main : Program Recv Model Msg
+main : Program GVN.VNPage Model Msg
main = Browser.element
{ init = \f -> (init f, Cmd.none)
, subscriptions = \model -> Sub.batch [ Sub.map Labels (DD.sub model.labels.dd), Sub.map Vote (DD.sub model.vote.dd) ]
@@ -46,7 +27,7 @@ main = Browser.element
}
type alias Model =
- { flags : Recv
+ { flags : GVN.VNPage
, onlist : Bool
, del : Bool
, state : Api.State -- For adding/deleting; Vote and label edit widgets have their own state
@@ -58,7 +39,7 @@ type alias Model =
, notesVis : Bool
}
-init : Recv -> Model
+init : GVN.VNPage -> Model
init f =
{ flags = f
, onlist = f.onlist
@@ -165,9 +146,11 @@ view model =
, td []
[ a [ href "#", onClickD NotesToggle ] [ text "💬" ]
, span [ class "spinner", classList [("hidden", model.notesState /= Api.Loading)] ] []
- , case model.notesState of
- Api.Error e -> b [ class "standout" ] [ text <| Api.showResponse e ]
- _ -> text ""
+ , case (model.notesState, model.vote.vote /= Nothing && model.flags.canreview, model.flags.review) of
+ (Api.Error e, _, _) -> b [ class "standout" ] [ text <| Api.showResponse e ]
+ (_, False, _) -> text ""
+ (_, True, Nothing) -> a [ href ("/v" ++ String.fromInt model.flags.vid ++ "/addreview") ] [ text " write a review »" ]
+ (_, True, Just w) -> a [ href ("/" ++ w ++ "/edit") ] [ text " edit review »" ]
]
]
else text ""
diff --git a/elm/UList/actiontabs.js b/elm/UList/actiontabs.js
new file mode 100644
index 00000000..0ae2b7f9
--- /dev/null
+++ b/elm/UList/actiontabs.js
@@ -0,0 +1,17 @@
+var buttons = ['managelabels', 'savedefault', 'exportlist'];
+
+buttons.forEach(function(but) {
+ document.querySelectorAll('#'+but).forEach(function(b) {
+ b.onclick = function() {
+ buttons.forEach(function(but2) {
+ document.querySelectorAll('.'+but2).forEach(function(e) {
+ if(but == but2)
+ e.classList.toggle('hidden');
+ else
+ e.classList.add('hidden')
+ })
+ })
+ return false;
+ }
+ })
+})
diff --git a/elm/User/Edit.elm b/elm/User/Edit.elm
index c8ecdddb..d09c77ae 100644
--- a/elm/User/Edit.elm
+++ b/elm/User/Edit.elm
@@ -61,6 +61,7 @@ init d =
type AdminMsg
= PermBoard Bool
+ | PermReview Bool
| PermBoardmod Bool
| PermEdit Bool
| PermImgvote Bool
@@ -73,7 +74,6 @@ type AdminMsg
type PrefMsg
= EMail String
- | ShowNsfw Bool
| MaxSexual Int
| MaxViolence Int
| TraitsSexual Bool
@@ -109,6 +109,7 @@ updateAdmin : AdminMsg -> GUE.SendAdmin -> GUE.SendAdmin
updateAdmin msg model =
case msg of
PermBoard b -> { model | perm_board = b }
+ PermReview b -> { model | perm_review = b }
PermBoardmod b -> { model | perm_boardmod = b }
PermEdit b -> { model | perm_edit = b }
PermImgvote b -> { model | perm_imgvote = b }
@@ -123,7 +124,6 @@ updatePrefs : PrefMsg -> GUE.SendPrefs -> GUE.SendPrefs
updatePrefs msg model =
case msg of
EMail n -> { model | email = n }
- ShowNsfw b -> { model | show_nsfw = b }
MaxSexual n-> { model | max_sexual = n }
MaxViolence n -> { model | max_violence = n }
TraitsSexual b -> { model | traits_sexual = b }
@@ -191,6 +191,7 @@ view model =
, formField "Permissions"
[ text "Fields marked with * indicate permissions assigned to new users by default", br_ 1
, perm opts.perm_boardmod <| label [] [ inputCheck "" m.perm_board (Admin << PermBoard), text " board*", br_ 1 ]
+ , perm opts.perm_boardmod <| label [] [ inputCheck "" m.perm_review (Admin << PermReview), text " review*", br_ 1 ]
, perm False <| label [] [ inputCheck "" m.perm_boardmod (Admin << PermBoardmod), text " boardmod", br_ 1 ]
, perm opts.perm_dbmod <| label [] [ inputCheck "" m.perm_edit (Admin << PermEdit), text " edit*", br_ 1 ]
, perm opts.perm_imgmod <| label [] [ inputCheck "" m.perm_imgvote (Admin << PermImgvote), text " imgvote* (existing votes will stop counting when unset)", br_ 1 ]
@@ -231,11 +232,8 @@ view model =
prefsform m =
[ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "Preferences" ] ]
- , formField "NSFW" [ label [] [ inputCheck "" m.show_nsfw (Prefs << ShowNsfw), text " Show NSFW images by default" ] ]
- , formField ""
- [ b [ class "grayedout" ] [ text "The two options below are only used for character images at the moment, they will eventually replace the above checkbox and apply to all images on the site." ]
- , br [] []
- , inputSelect "" m.max_sexual (Prefs << MaxSexual) [style "width" "400px"]
+ , formField "NSFW"
+ [ inputSelect "" m.max_sexual (Prefs << MaxSexual) [style "width" "400px"]
[ (-1,"Hide all images")
, (0, "Hide sexually suggestive or explicit images")
, (1, "Hide only sexually explicit images")
diff --git a/elm/VNEdit.elm b/elm/VNEdit.elm
new file mode 100644
index 00000000..4fadbf2d
--- /dev/null
+++ b/elm/VNEdit.elm
@@ -0,0 +1,621 @@
+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 Dict
+import Set
+import File exposing (File)
+import File.Select as FSel
+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, Cmd.none)
+ , 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
+ , editsum : Editsum.Model
+ , title : String
+ , original : String
+ , alias : String
+ , desc : TP.Model
+ , 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
+ , staff : List GVE.RecvStaff
+ , staffSearch : 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
+ , scrUplNum : Maybe Int
+ , scrId : Int -- latest used internal id
+ , releases : List GVE.RecvReleases
+ , chars : List GVE.RecvChars
+ , id : Maybe Int
+ , dupCheck : Bool
+ , dupVNs : List GApi.ApiVNResult
+ }
+
+
+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
+ , alias = d.alias
+ , desc = TP.bbcode d.desc
+ , 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
+ , staff = d.staff
+ , staffSearch = A.init ""
+ , seiyuu = d.seiyuu
+ , seiyuuSearch= A.init ""
+ , seiyuuDef = Maybe.withDefault 0 <| List.head <| List.map (\c -> c.id) d.chars
+ , screenshots = List.indexedMap (\n i -> (n, Img.info (Just i.info), i.rid)) d.screenshots
+ , scrUplRel = Nothing
+ , scrUplNum = Nothing
+ , scrId = 100
+ , releases = d.releases
+ , 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
+ , title = model.title
+ , original = model.original
+ , alias = model.alias
+ , desc = model.desc.data
+ , 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
+ , 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 }
+
+staffConfig : A.Config Msg GApi.ApiStaffResult
+staffConfig = { wrap = StaffSearch, id = "staffadd", source = A.staffSource }
+
+seiyuuConfig : A.Config Msg GApi.ApiStaffResult
+seiyuuConfig = { wrap = SeiyuuSearch, id = "seiyuuadd", source = A.staffSource }
+
+type Msg
+ = Editsum Editsum.Msg
+ | Tab Tab
+ | Submit
+ | Submitted GApi.Response
+ | Title String
+ | Original String
+ | Alias String
+ | Desc TP.Msg
+ | Length Int
+ | LWikidata (Maybe Int)
+ | LRenai 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
+ | StaffDel Int
+ | StaffRole Int String
+ | StaffNote Int String
+ | StaffSearch (A.Msg GApi.ApiStaffResult)
+ | SeiyuuDef Int
+ | SeiyuuDel Int
+ | SeiyuuChar Int Int
+ | SeiyuuNote Int String
+ | SeiyuuSearch (A.Msg GApi.ApiStaffResult)
+ | ScrUplRel (Maybe Int)
+ | ScrUplSel
+ | ScrUpl File (List File)
+ | ScrMsg Int Img.Msg
+ | ScrRel Int (Maybe Int)
+ | ScrDel Int
+ | DupSubmit
+ | DupResults GApi.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ 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)
+ 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)
+ Length n -> ({ model | length = n }, Cmd.none)
+ LWikidata n-> ({ model | lWikidata = n }, Cmd.none)
+ LRenai s -> ({ model | lRenai = s }, 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, original = v.original, 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/jpg"] 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)
+
+ 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)
+
+ 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, name = s.name, original = s.original, cid = model.seiyuuDef, note = "" }] }, c)
+
+ ScrUplRel s -> ({ model | scrUplRel = s }, Cmd.none)
+ ScrUplSel -> (model, FSel.files ["image/png", "image/jpg"] 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)
+ 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))
+ 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)
+ 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 GVE.RecvReleases
+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
+
+
+isValid : Model -> Bool
+isValid model = not
+ ( (model.title /= "" && model.title == model.original)
+ || 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)
+ || hasDuplicates (List.map (\s -> (s.aid, s.cid)) model.seiyuu)
+ )
+
+
+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 ""
+ ]
+ , 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 ""
+ ]
+ , formField "alias::Aliases"
+ [ inputTextArea "alias" model.alias Alias (rows 3 :: GVE.valAlias)
+ , br [] []
+ , if hasDuplicates <| String.lines <| String.toLower model.alias
+ then b [ class "standout" ] [ text "List contains duplicate aliases.", br [] [] ]
+ else
+ case relAlias model of
+ Nothing -> text ""
+ Just r -> span []
+ [ b [ class "standout" ] [ text "Release titles should not be added as alias." ]
+ , br [] []
+ , text "Release: "
+ , a [ href <| "/r"++String.fromInt 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."
+ , br [] []
+ , text "Titles that are listed in the releases should not be added here!"
+ ]
+ ]
+
+ geninfo = titles ++
+ [ formField "desc::Description"
+ [ TP.view "desc" model.desc Desc 600 (style "height" "180px" :: GVE.valDesc) [ b [ class "standout" ] [ 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" ]
+
+ , 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 []
+ [ 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" ] [ b [ class "grayedout" ] [ 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 ] ++ 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
+ 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
+ ]
+ ]
+ ] ]
+
+ staff =
+ let
+ head =
+ if List.isEmpty model.staff then [] else [
+ thead [] [ tr []
+ [ td [] []
+ , td [] [ text "Staff" ]
+ , td [] [ text "Role" ]
+ , td [] [ text "Note" ]
+ , td [] []
+ ] ] ]
+ foot =
+ 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 [] [] ]
+ 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." ]
+ ]
+ ] ] ]
+ 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 ] ]
+ , td [] [ inputSelect "" s.role (StaffRole n) [style "width" "150px" ] GT.creditTypes ]
+ , td [] [ inputText "" s.note (StaffNote n) (style "width" "300px" :: GVE.valStaffNote) ]
+ , td [] [ inputButton "remove" (StaffDel n) [] ]
+ ]
+ in table [] <| head ++ [ foot ] ++ List.indexedMap item model.staff
+
+ cast =
+ let
+ chars = List.map (\c -> (c.id, c.name ++ " (c" ++ String.fromInt 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 [] []
+ , b [] [ 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 [] [] ]
+ 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: c" ++ String.fromInt 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) ]
+ , 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 <| "/v" ++ Maybe.withDefault "" (Maybe.map String.fromInt 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
+ 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 |> Maybe.map (Html.map (ScrMsg id)) |> Maybe.withDefault (text "") ]
+ , td []
+ [ b [] [ 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 [ class "standout" ] [ 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!" ]
+ , 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" ++ String.fromInt r ++ "]")]
+ _ -> []
+ ]
+ ])
+
+ add =
+ let free = 10 - List.length model.screenshots
+ in
+ if free <= 0
+ then [ b [] [ 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" ]
+ , 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 [] []
+ , b [] [ 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 <| "/v" ++ Maybe.withDefault "" (Maybe.map String.fromInt 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)
+ [ 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 []
+ [ 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 ""
+ ]
+ ) model.dupVNs
+ ]
+ , fieldset [ 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 []
+ [ 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" ] ]
+ ]
+ ]
+ , 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)
+ ]
+ ]
+ ]
+ in if model.id == Nothing && not model.dupCheck then newform () else fullform ()
diff --git a/elm/VNEdit.js b/elm/VNEdit.js
new file mode 100644
index 00000000..9d07036a
--- /dev/null
+++ b/elm/VNEdit.js
@@ -0,0 +1,6 @@
+wrap_elm_init('VNEdit', function(init, opt) {
+ var app = init(opt);
+ app.ports.ivRefresh.subscribe(function() {
+ setTimeout(ivInit, 10);
+ });
+});
diff --git a/elm/iv.js b/elm/iv.js
index 5892bef8..06bb6f5a 100644
--- a/elm/iv.js
+++ b/elm/iv.js
@@ -1,12 +1,14 @@
//order:8 - After all regular JS, as other files may modify pageVars or modules in the Elm.* namespace.
/* Simple image viewer widget. Usage:
*
- * <a href="full_image.jpg" data-iv="{width}x{height}:{category}">..</a>
+ * <a href="full_image.jpg" data-iv="{width}x{height}:{category}:{flagging}">..</a>
*
* Clicking on the above link will cause the image viewer to open
* full_image.jpg. The {category} part can be empty or absent. If it is not
* empty, next/previous links will show up to point to the other images within
- * the same category.
+ * the same category. The {flagging} part can also be empty or absent,
+ * otherwise it should be a string in the format "svn", where s and v indicate
+ * the sexual/violence scores (0-2) and n the number of votes.
*
* ivInit() should be called when links with "data-iv" attributes are
* dynamically added or removed from the DOM.
@@ -22,8 +24,10 @@ var ivimg;
var ivfull;
var ivnext;
var ivprev;
+var ivhovernext;
+var ivhoverprev;
var ivload;
-var ivclose;
+var ivflag;
var imgw;
var imgh;
@@ -44,24 +48,34 @@ function create_div() {
ivimg = document.createElement('div');
ivparent.appendChild(ivimg);
- ivfull = document.createElement('a');
- ivparent.appendChild(ivfull);
+ var ivlinks = document.createElement('div');
+ ivparent.appendChild(ivlinks);
- ivclose = document.createElement('a');
- ivclose.href = '#';
- ivclose.onclick = ivClose;
- ivclose.textContent = 'close';
- ivparent.appendChild(ivclose);
+ ivfull = document.createElement('a');
+ ivlinks.appendChild(ivfull);
ivprev = document.createElement('a');
ivprev.onclick = show;
ivprev.textContent = '« previous';
- ivparent.appendChild(ivprev);
+ ivlinks.appendChild(ivprev);
ivnext = document.createElement('a');
ivnext.onclick = show;
ivnext.textContent = 'next »';
- ivparent.appendChild(ivnext);
+ ivlinks.appendChild(ivnext);
+
+ ivhoverprev = document.createElement('a');
+ ivhoverprev.onclick = show;
+ ivhoverprev.className = "left-pane";
+ ivimg.appendChild(ivhoverprev);
+
+ ivhovernext = document.createElement('a');
+ ivhovernext.onclick = show;
+ ivhovernext.className = "right-pane";
+ ivimg.appendChild(ivhovernext);
+
+ ivflag = document.createElement('a');
+ ivlinks.appendChild(ivflag);
document.querySelector('body').appendChild(ivparent);
}
@@ -129,25 +143,40 @@ function resize() {
function show(ev) {
var u = this.href;
- var opt = this.getAttribute('data-iv').split(':');
+ var opt = this.getAttribute('data-iv').split(':'); // 0:reso, 1:category, 2:flagging
var idx = this.iv_i;
imgw = Math.floor(opt[0].split('x')[0]);
imgh = Math.floor(opt[0].split('x')[1]);
create_div();
+ var imgs = ivimg.getElementsByTagName("img")
+ if (imgs.length !== 0)
+ ivimg.getElementsByTagName("img")[0].remove()
var img = document.createElement('img');
img.src = u;
ivfull.href = u;
img.onclick = ivClose;
img.onload = function() { ivload.style.display = 'none' };
- ivimg.textContent = '';
ivimg.appendChild(img);
+ var flag = opt[2] ? opt[2].match(/^([0-2])([0-2])([0-9]+)$/) : null;
+ var imgid = u.match(/\/([a-z]{2})\/[0-9]{2}\/([0-9]+)\./);
+ if(flag && imgid) {
+ ivflag.href = '/img/'+imgid[1]+imgid[2];
+ ivflag.textContent = flag[3] == 0 ? 'Not flagged' :
+ (flag[1] == 0 ? 'Safe' : flag[1] == 1 ? 'Suggestive' : 'Explicit') + ' / ' +
+ (flag[2] == 0 ? 'Tame' : flag[2] == 1 ? 'Violent' : 'Brutal' ) + ' (' + flag[3] + ')';
+ ivflag.style.visibility = 'visible';
+ } else
+ ivflag.style.visibility = 'hidden';
+
ivparent.style.display = 'block';
ivload.style.display = 'block';
fixnav(ivprev, opt[1], idx, -1);
fixnav(ivnext, opt[1], idx, 1);
+ fixnav(ivhoverprev, opt[1], idx, -1);
+ fixnav(ivhovernext, opt[1], idx, 1);
resize();
document.addEventListener('click', ivClose);
@@ -167,7 +196,9 @@ window.ivClose = function(ev) {
document.removeEventListener('keydown', keydown);
window.removeEventListener('resize', resize);
ivparent.style.display = 'none';
- ivimg.textContent = '';
+ var imgs = ivimg.getElementsByTagName("img")
+ if (imgs.length !== 0)
+ ivimg.getElementsByTagName("img")[0].remove()
return false;
};
diff --git a/elm/sethash.js b/elm/sethash.js
new file mode 100644
index 00000000..7b054d0b
--- /dev/null
+++ b/elm/sethash.js
@@ -0,0 +1,8 @@
+// Emulate setting a location.hash if none has been set.
+if(pageVars.sethash && location.hash.length <= 1) {
+ var e = document.getElementById(pageVars.sethash);
+ if(e) {
+ e.scrollIntoView();
+ e.classList.add('target');
+ }
+}