summaryrefslogtreecommitdiff
path: root/elm/Lib
diff options
context:
space:
mode:
Diffstat (limited to 'elm/Lib')
-rw-r--r--elm/Lib/Api.elm23
-rw-r--r--elm/Lib/Autocomplete.elm157
-rw-r--r--elm/Lib/DropDown.elm8
-rw-r--r--elm/Lib/Editsum.elm35
-rw-r--r--elm/Lib/ExtLinks.elm130
-rw-r--r--elm/Lib/Ffi.elm2
-rw-r--r--elm/Lib/Ffi.js26
-rw-r--r--elm/Lib/Html.elm41
-rw-r--r--elm/Lib/Image.elm25
-rw-r--r--elm/Lib/RDate.elm62
-rw-r--r--elm/Lib/TextPreview.elm23
-rw-r--r--elm/Lib/Util.elm102
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