diff options
Diffstat (limited to 'elm/Lib')
-rw-r--r-- | elm/Lib/Api.elm | 23 | ||||
-rw-r--r-- | elm/Lib/Autocomplete.elm | 157 | ||||
-rw-r--r-- | elm/Lib/DropDown.elm | 8 | ||||
-rw-r--r-- | elm/Lib/Editsum.elm | 35 | ||||
-rw-r--r-- | elm/Lib/ExtLinks.elm | 130 | ||||
-rw-r--r-- | elm/Lib/Ffi.elm | 2 | ||||
-rw-r--r-- | elm/Lib/Ffi.js | 26 | ||||
-rw-r--r-- | elm/Lib/Html.elm | 41 | ||||
-rw-r--r-- | elm/Lib/Image.elm | 25 | ||||
-rw-r--r-- | elm/Lib/RDate.elm | 62 | ||||
-rw-r--r-- | elm/Lib/TextPreview.elm | 23 | ||||
-rw-r--r-- | elm/Lib/Util.elm | 102 |
12 files changed, 307 insertions, 327 deletions
diff --git a/elm/Lib/Api.elm b/elm/Lib/Api.elm index fd4a3a7e..5b1bf583 100644 --- a/elm/Lib/Api.elm +++ b/elm/Lib/Api.elm @@ -23,29 +23,26 @@ showResponse res = in case res of HTTPError (Http.Timeout) -> "Network timeout, please try again later." HTTPError (Http.NetworkError) -> "Network error, please try again later." + HTTPError (Http.BadStatus 403) -> "Permission denied. Your session may have expired, try reloading the page." + HTTPError (Http.BadStatus 413) -> "File upload too large." + HTTPError (Http.BadStatus 429) -> "Action throttled, please try again later." HTTPError (Http.BadStatus r) -> "Server error " ++ String.fromInt r ++ ", please try again later or report an issue if this persists." HTTPError (Http.BadBody r) -> "Invalid response from the server, please report a bug (debug info: " ++ r ++")." HTTPError (Http.BadUrl _) -> unexp Success -> unexp Redirect _ -> unexp - CSRF -> "Invalid CSRF token, please refresh the page and try again." Invalid -> "Invalid form data, please report a bug." Editsum -> "Invalid edit summary." Unauth -> "You do not have the permission to perform this action." Unchanged -> "No changes" Content _ -> unexp - BadLogin -> "Invalid username or password." - LoginThrottle -> "Action throttled, too many failed login attempts." - InsecurePass -> "Your chosen password is in a database of leaked passwords, please choose another one." - BadEmail -> "Unknown email address." - Bot -> "Invalid answer to the anti-bot question." - Taken -> "Username already taken, please choose a different name." - DoubleEmail -> "Email address already used for another account." - DoubleIP -> "You can only register one account from the same IP within 24 hours." - BadCurPass -> "Current password is invalid." - MailChange -> unexp - ImgFormat -> "Unrecognized image format, only JPEG and PNG are accepted." + ImgFormat -> "Unrecognized image format, only JPEG, PNG and WebP are accepted." + LabelId _ -> unexp + DupNames _ -> "Name or alias already in the database." Releases _ -> unexp + Resolutions _ -> unexp + Engines _ -> unexp + DRM _ -> unexp BoardResult _ -> unexp TagResult _ -> unexp TraitResult _ -> unexp @@ -55,6 +52,8 @@ showResponse res = CharResult _ -> unexp AnimeResult _ -> unexp ImageResult _ -> unexp + UListWidget _ -> unexp + AdvSearchQuery _ -> unexp expectResponse : (Response -> msg) -> Http.Expect msg diff --git a/elm/Lib/Autocomplete.elm b/elm/Lib/Autocomplete.elm index 5c5dd33d..4c465d7c 100644 --- a/elm/Lib/Autocomplete.elm +++ b/elm/Lib/Autocomplete.elm @@ -12,8 +12,12 @@ module Lib.Autocomplete exposing , staffSource , charSource , animeSource + , resolutionSource + , engineSource + , drmSource , init , clear + , refocus , update , view ) @@ -40,6 +44,9 @@ import Gen.Producers as GP import Gen.Staff as GS import Gen.Chars as GC import Gen.Anime as GA +import Gen.Resolutions as GR +import Gen.Engines as GE +import Gen.DRM as GDRM type alias Config m a = @@ -55,6 +62,8 @@ type alias Config m a = type SearchSource m a -- API endpoint to query for completion results + Function to decode results from the API = Endpoint (String -> (GApi.Response -> m) -> Cmd m) (GApi.Response -> Maybe (List a)) + -- API endpoint that returns the full list of possible results + Function to decode results from the API + Function to match results against a query + | LazyList ((GApi.Response -> m) -> Cmd m) (GApi.Response -> Maybe (List a)) (String -> List a -> List a) -- Pure function for instant completion results | Func (String -> List a) @@ -80,20 +89,20 @@ boardSource = , view = (\i -> [ text <| Maybe.withDefault "" (lookup i.btype boardTypes) ] ++ case i.title of - Just title -> [ b [ class "grayedout" ] [ text " > " ], text title ] + Just title -> [ small [] [ text " > " ], text title ] _ -> [] ) - , key = \i -> i.btype ++ String.fromInt i.iid + , key = \i -> Maybe.withDefault i.btype i.iid } -tagtraitStatus i = - case (i.searchable, i.applicable, i.state) of - (_, _, 0) -> b [ class "grayedout" ] [ text " (awaiting approval)" ] - (_, _, 1) -> b [ class "grayedout" ] [ text " (deleted)" ] -- (not returned by the API for now) - (False, False, _) -> b [ class "grayedout" ] [ text " (meta)" ] - (True, False, _) -> b [ class "grayedout" ] [ text " (not applicable)" ] - (False, True, _) -> b [ class "grayedout" ] [ text " (not searchable)" ] +ttStatus i = + case ((i.hidden, i.locked), i.searchable, i.applicable) of + ((True, False), _, _ ) -> small [] [ text " (awaiting approval)" ] + ((True, True ), _, _ ) -> small [] [ text " (deleted)" ] -- (not returned by the API for now) + (_, False, False) -> small [] [ text " (meta)" ] + (_, True, False) -> small [] [ text " (not applicable)" ] + (_, False, True ) -> small [] [ text " (not searchable)" ] _ -> text "" @@ -103,8 +112,8 @@ tagSource = <| \x -> case x of GApi.TagResult e -> Just e _ -> Nothing - , view = \i -> [ text i.name, tagtraitStatus i ] - , key = \i -> String.fromInt i.id + , view = \i -> [ text i.name, ttStatus i ] + , key = \i -> i.id } @@ -117,11 +126,11 @@ traitSource = , view = \i -> [ case i.group_name of Nothing -> text "" - Just g -> b [ class "grayedout" ] [ text <| g ++ " / " ] + Just g -> small [] [ text <| g ++ " / " ] , text i.name - , tagtraitStatus i + , ttStatus i ] - , key = \i -> String.fromInt i.id + , key = \i -> i.id } @@ -132,34 +141,37 @@ vnSource = GApi.VNResult e -> Just e _ -> Nothing , view = \i -> - [ b [ class "grayedout" ] [ text <| "v" ++ String.fromInt i.id ++ ": " ] + [ small [] [ text <| i.id ++ ": " ] , text i.title ] - , key = \i -> String.fromInt i.id + , key = \i -> i.id } producerSource : SourceConfig m GApi.ApiProducerResult producerSource = - { source = Endpoint (\s -> GP.send { search = [s], hidden = False }) + { source = Endpoint (\s -> GP.send { search = [s] }) <| \x -> case x of GApi.ProducerResult e -> Just e _ -> Nothing , view = \i -> - [ b [ class "grayedout" ] [ text <| "p" ++ String.fromInt i.id ++ ": " ] + [ small [] [ text <| i.id ++ ": " ] , text i.name ] - , key = \i -> String.fromInt i.id + , key = \i -> i.id } staffSource : SourceConfig m GApi.ApiStaffResult staffSource = - { source = Endpoint (\s -> GS.send { search = s }) + { 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 ] + [ langIcon i.lang + , small [] [ text <| i.id ++ ": " ] + , text i.title + , if i.alttitle == i.title then text "" else small [] [ text " ", text i.alttitle ] + ] , key = \i -> String.fromInt i.aid } @@ -171,33 +183,73 @@ charSource = GApi.CharResult e -> Just e _ -> Nothing , view = \i -> - [ b [ class "grayedout" ] [ text <| "c" ++ String.fromInt i.id ++ ": " ] - , text i.name + [ small [] [ text <| i.id ++ ": " ] + , text i.title , Maybe.withDefault (text "") <| Maybe.map (\m -> - b [ class "grayedout" ] [ text <| " (instance of c" ++ String.fromInt m.id ++ ": " ++ m.name ] + small [] [ text <| " (instance of " ++ m.id ++ ": " ++ m.title ] ) i.main ] - , key = \i -> String.fromInt i.id + , key = \i -> i.id } -animeSource : SourceConfig m GApi.ApiAnimeResult -animeSource = - { source = Endpoint (\s -> GA.send { search = s }) +animeSource : Bool -> SourceConfig m GApi.ApiAnimeResult +animeSource ref = + { source = Endpoint (\s -> GA.send { search = s, ref = ref }) <| \x -> case x of GApi.AnimeResult e -> Just e _ -> Nothing , view = \i -> - [ b [ class "grayedout" ] [ text <| "a" ++ String.fromInt i.id ++ ": " ] + [ small [] [ text <| "a" ++ String.fromInt i.id ++ ": " ] , text i.title ] , key = \i -> String.fromInt i.id } +resolutionSource : SourceConfig m GApi.ApiResolutions +resolutionSource = + { source = LazyList + (GR.send {}) + (\x -> case x of + GApi.Resolutions e -> Just e + _ -> Nothing) + (\s l -> List.filter (\v -> String.contains (String.toLower s) (String.toLower v.resolution)) l |> List.take 10) + , view = \i -> [ text i.resolution, small [] [ text <| " (" ++ String.fromInt i.count ++ ")" ] ] + , key = \i -> i.resolution + } + + +engineSource : SourceConfig m GApi.ApiEngines +engineSource = + { source = LazyList + (GE.send {}) + (\x -> case x of + GApi.Engines e -> Just e + _ -> Nothing) + (\s l -> List.filter (\v -> String.contains (String.toLower s) (String.toLower v.engine)) l |> List.take 10) + , view = \i -> [ text i.engine, small [] [ text <| " (" ++ String.fromInt i.count ++ ")" ] ] + , key = \i -> i.engine + } + + +drmSource : SourceConfig m GApi.ApiDRM +drmSource = + { source = LazyList + (GDRM.send {}) + (\x -> case x of + GApi.DRM e -> Just e + _ -> Nothing) + (\s l -> List.filter (\v -> String.contains (String.toLower s) (String.toLower v.name)) l |> List.take 10) + , view = \i -> [ text i.name, small [] [ text <| " (" ++ String.fromInt i.count ++ ")" ] ] + , key = \i -> i.name + } + + type alias Model a = { visible : Bool , value : String , results : List a + , all : Maybe (List a) -- Used by LazyList , sel : String , default : String , loading : Bool @@ -210,6 +262,7 @@ init s = { visible = False , value = s , results = [] + , all = Nothing , sel = "" , default = s , loading = False @@ -252,26 +305,26 @@ select cfg offset model = { model | sel = Maybe.withDefault "" <| Maybe.map cfg.source.key <| get nextsel } +-- Blur and focus the input on enter. +refocus : Config m a -> Cmd m +refocus cfg = Dom.blur cfg.id + |> Task.andThen (always (Dom.focus cfg.id)) + |> Task.attempt (always (cfg.wrap Noop)) + + update : Config m a -> Msg a -> Model a -> (Model a, Cmd m, Maybe a) update cfg msg model = let mod m = (m, Cmd.none, Nothing) - -- Ugly hack: blur and focus the input on enter. This does two things: - -- 1. If the user clicked on an entry (resulting in the 'Enter' message), - -- then this will cause the input to be focussed again. This is - -- convenient when adding multiple entries. - refocus = Dom.blur cfg.id - |> Task.andThen (always (Dom.focus cfg.id)) - |> Task.attempt (always (cfg.wrap Noop)) in case msg of Noop -> mod model Blur -> mod { model | visible = False } Focus -> mod { model | loading = False, visible = True } Sel s -> mod { model | sel = s } - Enter r -> (model, refocus, Just r) + Enter r -> (model, refocus cfg, Just r) - Key "Enter" -> (model, refocus, + Key "Enter" -> (model, refocus cfg, case List.filter (\i -> cfg.source.key i == model.sel) model.results |> List.head of Just x -> Just x Nothing -> List.head model.results) @@ -280,26 +333,34 @@ update cfg msg model = Key _ -> mod model Input s -> + let m = { model | value = s, default = "" } + in if String.trim s == "" - then mod { model | value = s, default = "", loading = False, results = [] } - else case cfg.source.source of + then mod { m | loading = False, results = [] } + else case (cfg.source.source) of Endpoint _ _ -> - ( { model | value = s, default = "", loading = True, wait = model.wait + 1 } + ( { m | loading = True, wait = model.wait + 1 } , Task.perform (always <| cfg.wrap <| Search <| model.wait + 1) (Process.sleep 500) , Nothing ) - Func f -> mod { model | value = s, default = "", results = f s } + LazyList e _ f -> + case (model.loading, model.all) of + (_, Just l) -> mod { m | results = f s l } + (True, _) -> mod m + (False, _) -> ({ m | loading = True }, e (cfg.wrap << Results ""), Nothing) + Func f -> mod { m | results = f s } Search i -> if model.value == "" || model.wait /= i then mod model else case cfg.source.source of Endpoint e _ -> (model, e model.value (cfg.wrap << Results model.value), Nothing) + LazyList _ _ _ -> mod model Func _ -> mod model Results s r -> mod <| - if s /= model.value then model -- Discard stale results - else case cfg.source.source of - Endpoint _ d -> { model | loading = False, results = d r |> Maybe.withDefault [] } + case cfg.source.source of + Endpoint _ d -> if s /= model.value then model else { model | loading = False, results = d r |> Maybe.withDefault [] } + LazyList _ d f -> let all = d r in { model | loading = False, all = all, results = Maybe.map (\l -> f model.value l) all |> Maybe.withDefault [] } Func _ -> model @@ -340,7 +401,7 @@ view cfg model attrs = ) in div [ class "elm_dd", class "search", style "width" "300px" ] - [ div [ classList [("hidden", not visible)] ] [ Keyed.node "ul" [] <| msg ++ List.map item model.results ] - , input + [ div [ classList [("hidden", not visible)] ] [ div [] [ Keyed.node "ul" [] <| msg ++ List.map item model.results ] ] + , Html.form [] [ input ] , span [ class "spinner", classList [("hidden", not model.loading)] ] [] ] diff --git a/elm/Lib/DropDown.elm b/elm/Lib/DropDown.elm index 286a61cb..050dcfac 100644 --- a/elm/Lib/DropDown.elm +++ b/elm/Lib/DropDown.elm @@ -1,4 +1,4 @@ -module Lib.DropDown exposing (Config, init, sub, toggle, view) +module Lib.DropDown exposing (Config, init, sub, toggle, view, onClickOutside) import Browser.Events as E import Json.Decode as JD @@ -60,9 +60,9 @@ view conf status lbl cont = ] ++ if conf.hover then [ onMouseEnter (conf.toggle True) ] else [] ) <| case status of - Api.Normal -> [ lbl, span [] [ i [] [ text "▾" ] ] ] + Api.Normal -> [ lbl, span [] [ span [ class "arrow" ] [ text "▾" ] ] ] Api.Loading -> [ lbl, span [] [ span [ class "spinner" ] [] ] ] - Api.Error e -> [ b [ class "standout" ] [ text "error" ], span [] [ i [] [ text "▾" ] ] ] + Api.Error e -> [ b [] [ text "error" ], span [] [ span [ class "arrow" ] [ text "▾" ] ] ] , div [ classList [("hidden", not conf.opened)] ] - <| if conf.opened then cont () else [ text "" ] + [ if conf.opened then div [] (cont ()) else text "" ] ] diff --git a/elm/Lib/Editsum.elm b/elm/Lib/Editsum.elm index 20a51872..7320d66a 100644 --- a/elm/Lib/Editsum.elm +++ b/elm/Lib/Editsum.elm @@ -1,5 +1,5 @@ --- This module provides an the 'Edit summary' box, including the 'hidden' and --- 'locked' moderation checkboxes. +-- This module provides an the 'Edit summary' box, including the entry state +-- option for moderators. module Lib.Editsum exposing (Model, Msg, new, update, view) @@ -11,6 +11,7 @@ import Lib.TextPreview as TP type alias Model = { authmod : Bool + , hasawait : Bool , locked : Bool , hidden : Bool , editsum : TP.Model @@ -18,25 +19,24 @@ type alias Model = type Msg - = Locked Bool - | Hidden Bool + = State Bool Bool Bool | Editsum TP.Msg new : Model new = - { authmod = False - , locked = False - , hidden = False - , editsum = TP.bbcode "" + { authmod = False + , hasawait = False + , locked = False + , hidden = False + , editsum = TP.bbcode "" } update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of - Locked b -> ({ model | locked = b }, Cmd.none) - Hidden b -> ({ model | hidden = b }, Cmd.none) + State hid lock _ -> ({ model | hidden = hid, locked = lock }, Cmd.none) Editsum m -> let (nm,nc) = TP.update m model.editsum in ({ model | editsum = nm }, Cmd.map Editsum nc) @@ -44,14 +44,13 @@ view : Model -> Html Msg view model = let lockhid = - [ label [] - [ inputCheck "" model.hidden Hidden - , text " Deleted" ] - , label [] - [ inputCheck "" model.locked Locked - , text " Locked" ] + [ label [] [ inputRadio "entry_state" (not model.hidden && not model.locked) (State False False), text " Normal " ] + , label [] [ inputRadio "entry_state" (not model.hidden && model.locked) (State False True ), text " Locked " ] + , label [] [ inputRadio "entry_state" ( model.hidden && model.locked) (State True True ), text " Deleted " ] + , if not model.hasawait then text "" else + label [] [ inputRadio "entry_state" ( model.hidden && not model.locked) (State True False), text " Awaiting approval" ] , br [] [] - , if model.hidden + , if model.hidden && model.locked then span [] [ text "Note: edit summary of the last edit should indicate the reason for the deletion.", br [] [] ] else text "" ] @@ -59,7 +58,7 @@ 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!)" ] ] + [ strong [] [ text "Edit summary", b [] [ text " (English please!)" ] ] , br [] [] , text "Summarize the changes you have made, including links to source(s)." ] diff --git a/elm/Lib/ExtLinks.elm b/elm/Lib/ExtLinks.elm deleted file mode 100644 index b37dbb6e..00000000 --- a/elm/Lib/ExtLinks.elm +++ /dev/null @@ -1,130 +0,0 @@ -module Lib.ExtLinks exposing (..) - -import Html exposing (..) -import Html.Attributes exposing (..) -import Html.Events exposing (..) -import Regex -import Lib.Html exposing (..) -import Gen.ReleaseEdit as GRE -import Gen.ExtLinks as GEL - - --- Takes a printf-style string with a single %s or %d formatting code and a parameter to format. --- Supports 0-padding with '%0<num>d' formatting codes, where <num> <= 99. --- Returns (prefix, formatted_param, suffix) --- (This is super ugly and probably better written with elm/parser, but it gets the job done) -splitPrintf : String -> String -> (String, String, String) -splitPrintf s p = - case String.split "%" s of - [ pre, suf ] -> - case String.uncons suf of - Just ('s', suf1) -> (pre, p, suf1) - Just ('d', suf1) -> (pre, p, suf1) - Just ('0', suf1) -> - case String.uncons suf1 of - Just (c2, suf2) -> - case String.uncons suf2 of - Just ('d', suf3) -> (pre, String.padLeft (Char.toCode c2 - 48) '0' p, suf3) - Just (c3, suf3) -> - case String.uncons suf3 of - Just ('d', suf4) -> (pre, String.padLeft (10*(Char.toCode c2 - 48) + Char.toCode c3 - 48) '0' p, suf4) - _ -> (pre, "%", suf) - _ -> (pre, "%", suf) - _ -> (pre, "%", suf) - _ -> (pre, "%", suf) - _ -> (s, "", "") - - -type Rec a - = Unrecognized - | Duplicate - | Add (GEL.Site a, String) -- Site, value - - -type alias Model a = - { links : a - , sites : List (GEL.Site a) - , input : String - , rec : Rec a - , lst : Bool - } - - -type Msg a - = Del (Int -> a -> a) Int - | Input String - | Enter - | Expand - - -new : a -> List (GEL.Site a) -> Model a -new l s = - { links = l - , sites = s - , input = "" - , rec = Unrecognized - , lst = False - } - - -update : Msg a -> Model a -> Model a -update msg model = - let - match s m = (s, List.map (Maybe.withDefault "") m.submatches |> List.filter (\a -> a /= "") |> List.head |> Maybe.withDefault "") - fmtval s v = let (_, val, _) = splitPrintf s.fmt v in val - dup s val = List.filter (\l -> fmtval s l == fmtval s val) (s.links model.links) |> List.isEmpty |> not - find i = - case List.concatMap (\s -> List.map (match s) (Regex.find s.regex i)) model.sites |> List.head of - Nothing -> Unrecognized - Just (s, val) -> if dup s val then Duplicate else Add (s, val) - add s val = { model | input = "", rec = Unrecognized, links = s.add val model.links } - - in case msg of - Del f i -> { model | links = f i model.links } - Input i -> - case find (String.trim i) of - Add (s, val) -> - if s.multi || List.isEmpty (s.links model.links) - then add s val - else { model | input = i, rec = Add (s, val) } - x -> { model | input = i, rec = x } - Enter -> - case model.rec of - Add (s, val) -> add s val - _ -> model - Expand -> { model | lst = not model.lst } - - -view : Model a -> Html (Msg a) -view model = - let msg st s = span [] [ br [] [], b [ class "grayedout" ] [ text ">>> " ], if st then b [ class "standout" ] [ text s ] else text s ] - in - Html.form [ onSubmit Enter ] - [ table [] <| List.concatMap (\s -> - List.indexedMap (\i l -> - let (pre, val, suf) = splitPrintf s.fmt l - in tr [] - [ td [] [ a [ href <| pre ++ val ++ suf, target "_blank" ] [ text s.name ] ] - , td [] [ b [ class "grayedout" ] [ text pre ], text val, b [ class "grayedout" ] [ text suf ] ] - , td [] [ inputButton "remove" (Del s.del i) [] ] - ] - ) (s.links model.links) - ) model.sites - , inputText "" model.input Input [style "width" "500px", placeholder "Add URL..."] - , case (model.input, model.rec) of - ("", _) -> text "" - (_, Unrecognized) -> msg True "Invalid or unrecognized URL." - (_, Duplicate) -> msg True "URL is already listed." - (_, Add (s, _)) -> span [] [ inputButton "Edit" Enter [], msg False <| "URL recognized as: " ++ s.name ] - , div [ style "margin-top" "5px" ] - [ span [ onClickD Expand, style "cursor" "pointer" ] [ text <| if model.lst then "▾ " else "▸ ", text "Recognized sites: " ] - , if model.lst - then table [] <| List.map (\s -> - tr [] - [ td [] [ text s.name ] - , td [] <| List.indexedMap (\i l -> if modBy 2 i == 0 then b [ class "grayedout" ] [ text l ] else text l) s.patt - ] - ) model.sites - else text <| String.join ", " (List.map (\s -> s.name) model.sites) ++ "." - ] - ] diff --git a/elm/Lib/Ffi.elm b/elm/Lib/Ffi.elm index b5601a9b..af8c963a 100644 --- a/elm/Lib/Ffi.elm +++ b/elm/Lib/Ffi.elm @@ -5,7 +5,7 @@ -- This module is a hack to work around the lack of an FFI (Foreign Function -- Interface) in Elm. The functions in this module are stubs, their -- implementations are replaced by the Makefile with calls to --- window.elmFfi_<name> and the actual implementations are in Ffi.js. +-- window.elmFfi_<name> and the actual implementations are in elm-support.js. -- -- Use sparingly, all of this will likely break in future Elm versions. module Lib.Ffi exposing (..) diff --git a/elm/Lib/Ffi.js b/elm/Lib/Ffi.js deleted file mode 100644 index 78d6083a..00000000 --- a/elm/Lib/Ffi.js +++ /dev/null @@ -1,26 +0,0 @@ -window.elmFfi_innerHtml = function(wrap,call) { // \s -> _VirtualDom_property('innerHTML', _Json_wrap(s)) - return function(s) { - return { - $: 'a2', - n: 'innerHTML', - o: wrap(s) - } - } -}; - -window.elmFfi_elemCall = function(wrap,call) { // _Browser_call - return call -}; - -window.elmFfi_fmtFloat = function(wrap,call) { - return function(val) { - return function(prec) { - return val.toLocaleString('en-US', { minimumFractionDigits: prec, maximumFractionDigits: prec }); - } - } -}; - -var urlStatic = document.querySelector('link[rel=stylesheet]').href.replace(/^(https?:\/\/[^/]+)\/.*$/, '$1'); -window.elmFfi_urlStatic = function(wrap,call) { - return urlStatic -}; diff --git a/elm/Lib/Html.elm b/elm/Lib/Html.elm index 2d7d516c..7ec8dacc 100644 --- a/elm/Lib/Html.elm +++ b/elm/Lib/Html.elm @@ -7,6 +7,7 @@ import Json.Decode as JD import List import Lib.Api as Api import Lib.Util exposing (..) +import Lib.Ffi as Ffi import Gen.Types as T @@ -25,6 +26,8 @@ onInputValidation msg = custom "input" <| targetValue (JD.at ["target", "validity", "valid"] JD.bool) +onInvalid : msg -> Attribute msg +onInvalid msg = on "invalid" (JD.succeed msg) -- Multi-<br> (ugly but oh, so, convenient) br_ : Int -> Html m @@ -33,9 +36,9 @@ br_ n = if n == 1 then br [] [] else span [] <| List.repeat n <| br [] [] -- Quick short-hand way of creating a form that can be disabled. -- Usage: --- form_ Submit_msg (state == Disabled) [contents] -form_ : msg -> Bool -> List (Html msg) -> Html msg -form_ sub dis cont = Html.form [ onSubmit sub ] +-- form_ id Submit_msg (state == Disabled) [contents] +form_ : String -> msg -> Bool -> List (Html msg) -> Html msg +form_ s sub dis cont = Html.form [ id s, onSubmit sub ] [ fieldset [disabled dis] cont ] @@ -46,13 +49,13 @@ inputButton val onch attrs = -- Submit button with loading indicator and error message display submitButton : String -> Api.State -> Bool -> Html m -submitButton val state valid = div [] +submitButton val state valid = span [] [ input [ type_ "submit", class "submit", tabindex 10, value val, disabled (state == Api.Loading || not valid) ] [] , case state of - Api.Error r -> p [] [ b [class "standout" ] [ text <| Api.showResponse r ] ] + Api.Error r -> p [] [ b [] [ text <| Api.showResponse r ] ] _ -> if valid then text "" - else p [] [ b [class "standout" ] [ text "The form contains errors, please fix these before submitting. " ] ] + else p [] [ b [] [ text "The form contains errors, please fix these before submitting. " ] ] , if state == Api.Loading then div [ class "spinner" ] [] else text "" @@ -125,10 +128,11 @@ inputTextArea nam val onch attrs = textarea ( , onInput onch , rows 4 , cols 50 + , value val ] ++ attrs ++ (if nam == "" then [] else [ id nam, name nam ]) - ) [ text val ] + ) [] inputCheck : String -> Bool -> (Bool -> m) -> Html m @@ -154,14 +158,14 @@ inputRadio nam val onch = input ( -- Same as an inputText, but formats/parses an integer as Q### -inputWikidata : String -> Maybe Int -> (Maybe Int -> m) -> Html m -inputWikidata nam val onch = +inputWikidata : String -> Maybe Int -> (Maybe Int -> m) -> List (Attribute m) -> Html m +inputWikidata nam val onch attr = inputText nam (case val of Nothing -> "" Just v -> "Q" ++ String.fromInt v) (\v -> onch <| if v == "" then Nothing else String.toInt <| if String.startsWith "Q" v then String.dropLeft 1 v else v) - [ pattern "^Q?[1-9][0-9]{0,8}$" ] + (pattern "^Q?[1-9][0-9]{0,8}$" :: attr) -- Similar to inputCheck and inputRadio with a label, except this is just a link. @@ -187,7 +191,7 @@ formField lbl cont = else let (nlbl, eng) = if String.endsWith "#eng" lbl then (String.dropRight 4 lbl, True) else (lbl, False) - genlbl str = text str :: if eng then [ br [] [], b [ class "standout" ] [ text "English please!" ] ] else [] + genlbl str = text str :: if eng then [ br [] [], b [] [ text "English please!" ] ] else [] in td [ class "label" ] <| case String.split "::" nlbl of @@ -199,10 +203,19 @@ formField lbl cont = langIcon : String -> Html m -langIcon l = abbr [ class "icons lang", class l, title (Maybe.withDefault "" <| lookup l T.languages) ] [ text " " ] +langIcon l = abbr [ class ("icon-lang-"++l), title (Maybe.withDefault "" <| lookup l T.languages) ] [ text " " ] platformIcon : String -> Html m -platformIcon l = abbr [ class "icons", class l, title (Maybe.withDefault "" <| lookup l T.platforms) ] [ text " " ] +platformIcon l = abbr [ class ("icon-plat-"++l), title (Maybe.withDefault "" <| lookup l T.platforms) ] [ text " " ] releaseTypeIcon : String -> Html m -releaseTypeIcon t = abbr [ class ("icons rt"++t), title (Maybe.withDefault "" <| lookup t T.releaseTypes) ] [ text " " ] +releaseTypeIcon t = abbr [ class ("icon-rt"++t), title (Maybe.withDefault "" <| lookup t T.releaseTypes) ] [ text " " ] + +-- Special values: -1 = "add to list", not 1-6 = unknown +-- (Because why use the type system to encode special values?) +ulistIcon : Int -> String -> Html m +ulistIcon n lbl = + let fn = if n == -1 then "add" + else if n >= 1 && n <= 6 then "l" ++ String.fromInt n + else "unknown" + in abbr [ class ("icon-list-"++fn), title lbl ] [] diff --git a/elm/Lib/Image.elm b/elm/Lib/Image.elm index 31bab0b3..14eca441 100644 --- a/elm/Lib/Image.elm +++ b/elm/Lib/Image.elm @@ -108,9 +108,9 @@ 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 ] + (NotFound, _) ->b [] [ text "Image not found." ] + (Invalid, _) -> b [] [ text "Invalid image ID." ] + (Error e, _) -> b [] [ text <| Api.showResponse e ] (_, Nothing) -> text "No image." (_, Just i) -> let @@ -126,16 +126,16 @@ viewImg image = 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 ] [] + then a [ href (imageUrl "" i.id), attribute "data-iv" <| String.fromInt i.width ++ "x" ++ String.fromInt i.height ++ ":scr" ] + [ img [ src <| imageUrl ".t" 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 <| if sex > 1.3 then "Explicit" else if sex > 0.4 then "Suggestive" else "Safe" , text " / " - , text <| if vio > 1.3 then "Brutal" else if vio > 0.4 then "Violent" else "Safe" + , text <| if vio > 1.3 then "Brutal" else if vio > 0.4 then "Violent" else "Tame" , text <| " (" ++ String.fromInt i.votecount ++ ")" ] _ -> [ text "Not flagged" ] @@ -143,14 +143,15 @@ viewImg image = ] -viewVote : Image -> Maybe (Html Msg) -viewVote model = +viewVote : Image -> (Msg -> a) -> a -> Maybe (Html a) +viewVote model wrap msg = let rad i sex val = input [ type_ "radio" , tabindex 10 , required True - , onCheck <| (if sex then MySex else MyVio) val + , onInvalid msg + , onCheck <| \b -> wrap <| (if sex then MySex else MyVio) val b , 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 ] [] @@ -161,7 +162,7 @@ viewVote model = ] ] , tfoot [] <| case model.saveState of - Api.Error e -> [ tr [] [ td [ colspan 2 ] [ b [ class "standout" ] [ text (Api.showResponse e) ] ] ] ] + Api.Error e -> [ tr [] [ td [ colspan 2 ] [ b [] [ text (Api.showResponse e) ] ] ] ] _ -> [] , tr [] [ td [ style "white-space" "nowrap" ] diff --git a/elm/Lib/RDate.elm b/elm/Lib/RDate.elm index 67888114..3eca4cfa 100644 --- a/elm/Lib/RDate.elm +++ b/elm/Lib/RDate.elm @@ -1,8 +1,9 @@ -- Utility module and UI widget for handling release dates. -- --- Release dates are integers with the following format: 0 or yyyymmdd +-- Release dates are integers with the following format: 0, 1 or yyyymmdd -- Special values --- 0 -> unknown +-- 0 -> unknown +-- 1 -> "today" (only used as filter) -- 99999999 -> TBA -- yyyy9999 -> year known, month & day unknown -- yyyymm99 -> year & month known, day unknown @@ -14,6 +15,7 @@ import Html.Events exposing (..) import Date import Lib.Html exposing (..) import Gen.Types as GT +import Gen.Api as GApi type alias RDate = Int @@ -44,18 +46,23 @@ fromDate d = , d = Date.day d } +maxDayInMonth : Int -> Int -> Int +maxDayInMonth y m = Date.fromCalendarDate y (Date.numberToMonth m) 1 |> Date.add Date.Months 1 |> Date.add Date.Days -1 |> Date.day normalize : RDateComp -> RDateComp normalize r = - if r.y == 0 then { y = 0, m = 0, d = 0 } + if r.y == 0 then { y = 0, m = 0, d = clamp 0 1 r.d } else if r.y == 9999 then { y = 9999, m = 99, d = 99 } - else if r.m == 99 then { y = r.y, m = 99, d = 99 } + else if r.m == 0 || r.m == 99 then { y = r.y, m = 99, d = 99 } + else if r.d == 0 then { r | d = 99 } + else if r.d /= 99 && r.d > 28 then { r | d = Basics.min r.d (maxDayInMonth r.y r.m) } -- Make sure the day field is in range else r format : RDateComp -> String format date = case (date.y, date.m, date.d) of + ( 0, 0, 1) -> "today" ( 0, _, _) -> "unknown" (9999, _, _) -> "TBA" ( y, 99, 99) -> String.fromInt y @@ -70,24 +77,45 @@ display today d = in if future then b [ class "future" ] [ text fmt ] else text fmt +monthList : List String +monthList = + [ "Jan" + , "Feb" + , "Mar" + , "Apr" + , "May" + , "Jun" + , "Jul" + , "Aug" + , "Sep" + , "Oct" + , "Nov" + , "Dec" + ] + +monthSelect : List (Int, String) +monthSelect = List.indexedMap (\m s -> (m+1, String.fromInt (m+1) ++ " (" ++ s ++ ")")) monthList + -- Input widget. --- --- BUG: Changing the month or year fields when day 30-31 is selected but no --- longer valid results in an invalid RDate. It also causes the "-day-" option --- to be selected (which is good), so I don't expect that many people will try --- to submit the form without changing it. -view : RDate -> Bool -> (RDate -> msg) -> Html msg -view ro permitUnknown msg = +view : RDate -> Bool -> Bool -> (RDate -> msg) -> Html msg +view ro permitUnknown permitToday msg = let r = expand ro range from to f = List.range from to |> List.map (\n -> (f n |> normalize |> compact, String.fromInt n)) - yl = (if permitUnknown then [(0, "Unknown")] else []) - ++ [(99999999, "TBA")] - ++ List.reverse (range 1980 (GT.curYear + 5) (\n -> {r|y=n})) - ml = ({r|m=99} |> normalize |> compact, "- month -") :: range 1 12 (\n -> {r|m=n}) - maxDay = Date.fromCalendarDate r.y (Date.numberToMonth r.m) 1 |> Date.add Date.Months 1 |> Date.add Date.Days -1 |> Date.day - dl = ({r|d=99} |> normalize |> compact, "- day -") :: range 1 maxDay (\n -> {r|d=n}) + yl = (if permitToday then [(1, "Today" )] else []) + ++ (if permitUnknown then [(0, "Unknown")] else []) + ++ [(99999999, "TBA")] + ++ List.reverse (range 1980 (GT.curYear + 5) (\n -> {r|y=n})) + ml = ({r|m=99} |> normalize |> compact, "- month -") :: List.map (\(m,s) -> (compact (normalize {r|m=m}), s)) monthSelect + dl = ({r|d=99} |> normalize |> compact, "- day -") :: range 1 (maxDayInMonth r.y r.m) (\n -> {r|d=n}) in div [] [ inputSelect "" ro msg [ style "width" "100px" ] yl , if r.y == 0 || r.y == 9999 then text "" else inputSelect "" ro msg [ style "width" "90px" ] ml , if r.m == 0 || r.m == 99 then text "" else inputSelect "" ro msg [ style "width" "90px" ] dl ] + + +-- Handy function for formatting release info as a string +-- (Typically used in selection boxes) +-- (Why is that in this module, you ask? Well, where else do I put it?) +showrel : GApi.ApiReleases -> String +showrel r = "[" ++ (format (expand r.released)) ++ " " ++ (String.join "," r.lang) ++ "] " ++ r.title ++ " (" ++ r.id ++ ")" diff --git a/elm/Lib/TextPreview.elm b/elm/Lib/TextPreview.elm index 9431848a..dc876048 100644 --- a/elm/Lib/TextPreview.elm +++ b/elm/Lib/TextPreview.elm @@ -7,7 +7,6 @@ import Lib.Html exposing (..) import Lib.Ffi as Ffi import Lib.Api as Api import Gen.Api as GApi -import Gen.Markdown as GM import Gen.BBCode as GB @@ -32,16 +31,6 @@ bbcode data = } -markdown : String -> Model -markdown data = - { state = Api.Normal - , data = data - , preview = "" - , display = False - , endpoint = GM.send - , class = "preview docs" - } - type Msg = Edit String @@ -73,18 +62,18 @@ view name model cmdmap width attr header = display = model.display && model.preview /= "" in div [ class "textpreview", style "width" (String.fromInt width ++ "px") ] - [ span [] - [ p [] header - , p [ class "right", classList [("invisible", model.data == "")] ] + [ div [] + [ div [] header + , div [ classList [("invisible", model.data == "")] ] [ case model.state of Api.Loading -> span [ class "spinner" ] [] - Api.Error _ -> b [ class "grayedout" ] [ text "Error loading preview. " ] + Api.Error _ -> small [] [ text "Error loading preview. " ] Api.Normal -> text "" , if display then a [ onClickN (cmdmap TextArea) ] [ text "Edit" ] - else i [] [text "Edit"] + else span [] [text "Edit"] , if display - then i [] [text "Preview"] + then span [] [text "Preview"] else a [ onClickN (cmdmap Preview) ] [ text "Preview" ] ] ] diff --git a/elm/Lib/Util.elm b/elm/Lib/Util.elm index f5954772..edde2e37 100644 --- a/elm/Lib/Util.elm +++ b/elm/Lib/Util.elm @@ -1,9 +1,12 @@ module Lib.Util exposing (..) -import Dict +import Set import Task +import Process import Regex import Lib.Ffi as Ffi +import Gen.Api as GApi +import Gen.Types as GT -- Delete an element from a List delidx : Int -> List a -> List a @@ -28,45 +31,44 @@ hasDuplicates l = step e acc = case acc of Nothing -> Nothing - Just m -> if Dict.member e m then Nothing else Just (Dict.insert e True m) + Just m -> if Set.member e m then Nothing else Just (Set.insert e m) in - case List.foldr step (Just Dict.empty) l of + case List.foldr step (Just Set.empty) l of Nothing -> True Just _ -> False +-- Returns true if list a contains elements also in list b +contains : List comparable -> List comparable -> Bool +contains a b = + let d = Set.fromList b + in List.any (\e -> Set.member e d) a + + -- Haskell's 'lookup' - find an entry in an association list lookup : a -> List (a,b) -> Maybe b lookup n l = List.filter (\(a,_) -> a == n) l |> List.head |> Maybe.map Tuple.second +-- Have to use Process.sleep instead of Task.succeed here, otherwise any +-- subscriptions are not updated. selfCmd : msg -> Cmd msg -selfCmd m = Task.perform (always m) (Task.succeed True) - - --- Based on VNDBUtil::gtintype() -validateGtin : String -> Bool -validateGtin = - let check = String.fromInt - >> String.reverse - >> String.toList - >> List.indexedMap (\i c -> (Char.toCode c - Char.toCode '0') * if modBy 2 i == 0 then 1 else 3) - >> List.sum - inval n = - n < 1000000000 - || (n >= 200000000000 && n < 600000000000) - || (n >= 2000000000000 && n < 3000000000000) - || n >= 9770000000000 - || modBy 10 (check n) /= 0 - in String.filter Char.isDigit >> String.toInt >> Maybe.map (not << inval) >> Maybe.withDefault False - - --- Convert an image ID (e.g. "sf500") into a URL. -imageUrl : String -> String -imageUrl id = +selfCmd m = Task.perform (always m) (Process.sleep 1.0) + + +-- Convert a dir suffix ("" or ".t") and an image ID (e.g. "sf500") into a URL. +imageUrl : String -> String -> String +imageUrl suff id = let num = String.dropLeft 2 id |> String.toInt |> Maybe.withDefault 0 - in Ffi.urlStatic ++ "/" ++ String.left 2 id ++ "/" ++ String.fromInt (modBy 10 (num // 10)) ++ String.fromInt (modBy 10 num) ++ "/" ++ String.fromInt num ++ ".jpg" + in Ffi.urlStatic ++ "/" ++ String.left 2 id ++ suff ++ "/" ++ String.fromInt (modBy 10 (num // 10)) ++ String.fromInt (modBy 10 num) ++ "/" ++ String.fromInt num ++ ".jpg" + +vndbidNum : String -> Int +vndbidNum = String.dropLeft 1 >> String.toInt >> Maybe.withDefault 0 + + +vndbid : Char -> Int -> String +vndbid c n = String.fromChar c ++ String.fromInt n jap_ : Regex.Regex @@ -74,10 +76,54 @@ jap_ = Maybe.withDefault Regex.never (Regex.fromString "[\\u3000-\\u9fff\\uff00- -- Not even close to comprehensive, just excludes a few scripts commonly found on VNDB. nonlatin_ : Regex.Regex -nonlatin_ = Maybe.withDefault Regex.never (Regex.fromString "[\\u3000-\\u9fff\\uff00-\\uff9f\\u0400-\\u04ff\\u1100-\\u11ff\\uac00-\\ud7af]") +nonlatin_ = Maybe.withDefault Regex.never (Regex.fromString "[\\u0400-\\u04ff\\u0600-\\u06ff\\u0e00-\\u0e7f\\u1100-\\u11ff\\u1400-\\u167f\\u3040-\\u3099\\u30a1-\\u30fa\\u3100-\\u9fff\\uac00-\\ud7af\\uff66-\\uffdc\\u{20000}-\\u{323af}]") +-- This regex can't differentiate between Japanese and Chinese, so has a good chance of returning true for Chinese as well. containsJapanese : String -> Bool containsJapanese = Regex.contains jap_ containsNonLatin : String -> Bool containsNonLatin = Regex.contains nonlatin_ + + +-- List of script-languages (i.e. not the generic "Chinese" option), with JA and EN ordered first. +scriptLangs : List (String, String) +scriptLangs = + (List.filter (\(l,_) -> l == "ja") GT.languages) + ++ (List.filter (\(l,_) -> l == "en") GT.languages) + ++ (List.filter (\(l,_) -> l /= "zh" && l /= "en" && l /= "ja") GT.languages) + +-- "Location languages", i.e. generic language without script indicator, again with JA and EN ordered first. +locLangs : List (String, String) +locLangs = + (List.filter (\(l,_) -> l == "ja") GT.languages) + ++ (List.filter (\(l,_) -> l == "en") GT.languages) + ++ (List.filter (\(l,_) -> l /= "zh-Hans" && l /= "zh-Hant" && l /= "en" && l /= "ja") GT.languages) + + +-- Format a release resolution, first argument indicates whether empty string is to be used for "unknown" +resoFmt : Bool -> Int -> Int -> String +resoFmt empty x y = + case (x,y) of + (0,0) -> if empty then "" else "Unknown" + (0,1) -> "Non-standard" + _ -> String.fromInt x ++ "x" ++ String.fromInt y + +-- Inverse of resoFmt +resoParse : Bool -> String -> Maybe (Int, Int) +resoParse empty s = + let t = String.replace "*" "x" s + |> String.replace "×" "x" + |> String.replace " " "" + |> String.replace "\t" "" + |> String.toLower |> String.trim + in + case (t, String.split "x" t) of + ("", _) -> if empty then Just (0,0) else Nothing + ("unknown", _) -> Just (0,0) + ("non-standard", _) -> Just (0,1) + (_, [sx,sy]) -> + case (String.toInt sx, String.toInt sy) of + (Just ix, Just iy) -> if ix < 1 || ix > 32767 || iy < 1 || iy > 32767 then Nothing else Just (ix,iy) + _ -> Nothing + _ -> Nothing |