on master help sign in
Edit screen
commit971e20e8b1398f9d9dc94e9fb9f6e08b5d9d270e authoriko <ilyakooo0@gmail.com> dateThu 11 Aug 2022, 07:32 (4 years ago) parentac71122
12 files changed+546−27
backend/BW/Logic.purs+5−1 backend/Bridge.purs+16−2 bridge.yaml+4−2 frontend/Bridge.elm+24−7 frontend/Main.elm+44−5 frontend/Pages/Cipher.elm+9−5 frontend/Pages/EditCipher.elm+367 frontend/Pages/Login.elm+3 frontend/Pages/MasterPassword.elm+2−1 frontend/Pages/Navigation.elm+10−4 frontend/Utils.elm+58 style.css+4
backend/BW/Logic.purs+5−1
@@ -241,7 +241,11 @@ decodeCipher cipher = do
241241 , username
242242 }
243243 n -> liftEffect $ throwError $ error $ "Unsupported cipher type: " <> show n
244− pure $ Bridge.Sub_LoadCipher { cipherType, name, id: cipher.id }
244+ pure
245+ $ Bridge.Sub_LoadCipher
246+ { cipher: Bridge.FullCipher { cipher: cipherType, name }
247+ , id: cipher.id
248+ }
245249
246250decryptNullable ::
247251 forall x r.
backend/Bridge.purs+16−2
@@ -465,11 +465,25 @@ derive instance genericCmd :: Generic Cmd _
465465derive instance eqCmd :: Eq Cmd
466466derive instance ordCmd :: Ord Cmd
467467
468+newtype FullCipher =
469+ FullCipher {
470+ cipher :: Cipher
471+ , name :: String
472+ }
473+
474+derive instance newtypeFullCipher :: Newtype FullCipher _
475+instance encodeJsonFullCipher :: EncodeJson FullCipher where
476+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
477+instance decodeJsonFullCipher :: DecodeJson FullCipher where
478+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
479+derive instance genericFullCipher :: Generic FullCipher _
480+derive instance eqFullCipher :: Eq FullCipher
481+derive instance ordFullCipher :: Ord FullCipher
482+
468483newtype Sub_LoadCipher =
469484 Sub_LoadCipher {
470− cipherType :: Cipher
485+ cipher :: FullCipher
471486 , id :: String
472− , name :: String
473487 }
474488
475489derive instance newtypeSub_LoadCipher :: Newtype Sub_LoadCipher _
bridge.yaml+4−2
@@ -16,8 +16,7 @@ Sub:
1616- Reset
1717- LoadCipher:
1818 id: String
19− name: String
20− cipherType: Cipher
19+ cipher: FullCipher
2120- RecieveEmail: String
2221
2322Cmd:
@@ -34,6 +33,9 @@ Cmd:
3433- Copy: String
3534- Open: String
3635
36+FullCipher:
37+ name: String
38+ cipher: Cipher
3739
3840Cipher:
3941- LoginCipher:
frontend/Bridge.elm+24−7
@@ -543,25 +543,42 @@ jsonEncCmd val =
543543
544544
545545
546+type alias FullCipher =
547+ { cipher: Cipher
548+ , name: String
549+ }
550+
551+jsonDecFullCipher : Json.Decode.Decoder ( FullCipher )
552+jsonDecFullCipher =
553+ Json.Decode.succeed (\pcipher pname -> {cipher = pcipher, name = pname})
554+ |> required "cipher" (jsonDecCipher)
555+ |> required "name" (Json.Decode.string)
556+
557+jsonEncFullCipher : FullCipher -> Value
558+jsonEncFullCipher val =
559+ Json.Encode.object
560+ [ ("cipher", jsonEncCipher val.cipher)
561+ , ("name", Json.Encode.string val.name)
562+ ]
563+
564+
565+
546566type alias Sub_LoadCipher =
547− { cipherType: Cipher
567+ { cipher: FullCipher
548568 , id: String
549− , name: String
550569 }
551570
552571jsonDecSub_LoadCipher : Json.Decode.Decoder ( Sub_LoadCipher )
553572jsonDecSub_LoadCipher =
554− Json.Decode.succeed (\pcipherType pid pname -> {cipherType = pcipherType, id = pid, name = pname})
555− |> required "cipherType" (jsonDecCipher)
573+ Json.Decode.succeed (\pcipher pid -> {cipher = pcipher, id = pid})
574+ |> required "cipher" (jsonDecFullCipher)
556575 |> required "id" (Json.Decode.string)
557− |> required "name" (Json.Decode.string)
558576
559577jsonEncSub_LoadCipher : Sub_LoadCipher -> Value
560578jsonEncSub_LoadCipher val =
561579 Json.Encode.object
562− [ ("cipherType", jsonEncCipher val.cipherType)
580+ [ ("cipher", jsonEncFullCipher val.cipher)
563581 , ("id", Json.Encode.string val.id)
564− , ("name", Json.Encode.string val.name)
565582 ]
566583
567584
frontend/Main.elm+44−5
@@ -10,6 +10,7 @@ import Notification
1010import Pages.Captcha as Captcha
1111import Pages.Cipher as Cipher
1212import Pages.Ciphers as Ciphers
13+import Pages.EditCipher as EditCipher exposing (Msg(..))
1314import Pages.Loader exposing (loader)
1415import Pages.Login as Login
1516import Pages.MasterPassword as MasterPassword
@@ -25,7 +26,8 @@ notificationLingerSeconds =
2526
2627
2728type Msg
28− = RecieveMessage String
29+ = Noop
30+ | RecieveMessage String
2931 | ShowLoginPage
3032 | ShowError String
3133 | CloseNotification
@@ -47,9 +49,11 @@ type Msg
4749 | Copy String
4850 | Open String
4951 | CaptchaMsg Captcha.Msg
52+ | EditCipherMsg EditCipher.Msg
5053 | ShowCaptcha Captcha.HCaptchSiteKey
5154 | ClearNotification { currentTime : Time.Posix }
5255 | UpdateLastNotificatioTime Time.Posix
56+ | EditCipher Bridge.FullCipher
5357
5458
5559type alias CipherId =
@@ -73,6 +77,7 @@ type PageModel
7377 = LoginModel Login.Model
7478 | LoadingPage
7579 | CiphersModel Ciphers.Model
80+ | EditCipherModel EditCipher.Model
7681 | MasterPasswordModel MasterPassword.Model
7782 | CipherModel Cipher.Model
7883 | CaptchaModel Captcha.Model
@@ -87,6 +92,10 @@ showPage :
8792 , topButton : Maybe (Navigation.TopButton Msg)
8893 }
8994showPage email page =
95+ let
96+ simpleBackButton =
97+ BackButton { action = PopView, icon = Nothing }
98+ in
9099 case page of
91100 LoginModel model ->
92101 let
@@ -115,7 +124,7 @@ showPage email page =
115124 LoadingPage ->
116125 { title = []
117126 , body = [ loader ]
118− , topButton = Just (BackButton PopView)
127+ , topButton = Just simpleBackButton
119128 }
120129
121130 MasterPasswordModel model ->
@@ -135,7 +144,7 @@ showPage email page =
135144 in
136145 { title = p.title model
137146 , body = p.view model
138− , topButton = Just (BackButton PopView)
147+ , topButton = Just simpleBackButton
139148 }
140149
141150 CaptchaModel model ->
@@ -145,7 +154,17 @@ showPage email page =
145154 in
146155 { title = p.title model
147156 , body = p.view model
148− , topButton = Just (BackButton PopView)
157+ , topButton = Just simpleBackButton
158+ }
159+
160+ EditCipherModel model ->
161+ let
162+ p =
163+ EditCipher.page editCipherCallbacks EditCipherMsg
164+ in
165+ { title = p.title model
166+ , body = p.view model
167+ , topButton = Just (BackButton { action = PopView, icon = Just "close" })
149168 }
150169
151170
@@ -351,6 +370,14 @@ update msg model =
351370 _ ->
352371 ( model, Cmd.none )
353372
373+ EditCipherMsg imsg ->
374+ case currentPage of
375+ EditCipherModel page ->
376+ (EditCipher.page editCipherCallbacks EditCipherMsg).update imsg page |> processPage EditCipherModel
377+
378+ _ ->
379+ ( model, Cmd.none )
380+
354381 ShowCipherPage cipher ->
355382 (Cipher.page cipherCallbacks CipherMsg).init cipher
356383 |> Tuple.mapFirst (\pageModel -> appendPageStack <| CipherModel pageModel)
@@ -396,6 +423,13 @@ update msg model =
396423 UpdateLastNotificatioTime time ->
397424 ( { model | lastNotificationTime = time }, Cmd.none )
398425
426+ Noop ->
427+ ( model, Cmd.none )
428+
429+ EditCipher cipher ->
430+ (EditCipher.page editCipherCallbacks EditCipherMsg).init cipher
431+ |> Tuple.mapFirst (\pageModel -> keepStackWith <| EditCipherModel pageModel)
432+
399433
400434loginCallbacks : Login.Callbacks Msg
401435loginCallbacks =
@@ -409,7 +443,12 @@ ciphersCallbacks =
409443
410444cipherCallbacks : Cipher.Callbacks Msg
411445cipherCallbacks =
412− { copy = Copy, open = Open }
446+ { copy = Copy, open = Open, edit = EditCipher }
447+
448+
449+editCipherCallbacks : EditCipher.Callbacks Msg
450+editCipherCallbacks =
451+ { save = always Noop }
413452
414453
415454captchaCallbacks : Captcha.Callbacks
frontend/Pages/Cipher.elm+9−5
@@ -23,7 +23,7 @@ type Msg
2323
2424
2525type alias Callbacks msg =
26− { copy : String -> msg, open : String -> msg }
26+ { copy : String -> msg, open : String -> msg, edit : Bridge.FullCipher -> msg }
2727
2828
2929page : Callbacks emsg -> Page Bridge.Sub_LoadCipher Model Msg emsg
@@ -32,7 +32,11 @@ page callbacks liftMsg =
3232 , view = \model -> view model |> List.map (Html.map liftMsg)
3333 , update = \msg model -> update callbacks liftMsg msg model
3434 , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
35− , title = \{ cipher } -> [ text cipher.name, span [ Attr.class "u-float-right" ] [ iconButton "edit" (callbacks.copy "") ] ]
35+ , title =
36+ \{ cipher } ->
37+ [ text cipher.cipher.name
38+ , span [ Attr.class "u-float-right" ] [ iconButton "edit" (callbacks.edit cipher.cipher) ]
39+ ]
3640 }
3741
3842
@@ -79,7 +83,7 @@ row { name, value, nameIcon, icons } =
7983
8084view : Model -> List (Html Msg)
8185view { passwordHidden, cipher, cvvHidden } =
82− (case cipher.cipherType of
86+ (case cipher.cipher.cipher of
8387 Bridge.LoginCipher { username, password, uris } ->
8488 maybeList username
8589 (\x ->
@@ -109,7 +113,7 @@ view { passwordHidden, cipher, cvvHidden } =
109113 )
110114
111115 Bridge.NoteCipher contents ->
112− [ row { name = "Note", value = contents, nameIcon = "edit", icons = [] }
116+ [ row { name = "Note", value = contents, nameIcon = "switcher-environments", icons = [] }
113117 ]
114118
115119 Bridge.CardCipher { number, code, cardholderName, expMonth, expYear } ->
@@ -179,7 +183,7 @@ view { passwordHidden, cipher, cvvHidden } =
179183 ++ maybeList ssn
180184 (\x ->
181185 row
182− { name = "Socia security number"
186+ { name = "Social security number"
183187 , value = x
184188 , nameIcon = ""
185189 , icons = [ ( "copy", Copy x ) ]
frontend/Pages/EditCipher.elmnew file+367
@@ -0,0 +1,367 @@
1+module Pages.EditCipher exposing (..)
2+
3+import Bridge
4+import Html exposing (..)
5+import Html.Attributes as Attr
6+import Html.Events as Ev
7+import Page exposing (..)
8+import Utils exposing (..)
9+
10+
11+type alias Model =
12+ { fullCipher : Bridge.FullCipher
13+ }
14+
15+
16+type Msg
17+ = EditCipher Bridge.FullCipher
18+
19+
20+type alias Callbacks msg =
21+ { save : Bridge.FullCipher -> msg }
22+
23+
24+page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
25+page callbacks liftMsg =
26+ { init = \cipher -> Tuple.mapSecond (Cmd.map liftMsg) (init cipher)
27+ , view = \model -> view model |> List.map (Html.map liftMsg)
28+ , update = \msg model -> update callbacks liftMsg msg model
29+ , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
30+ , title =
31+ \{ fullCipher } ->
32+ [ text fullCipher.name
33+ , span [ Attr.class "u-float-right" ] [ iconButton "task-outstanding" (callbacks.save fullCipher) ]
34+ ]
35+ }
36+
37+
38+init : Bridge.FullCipher -> ( Model, Cmd Msg )
39+init cipher =
40+ ( { fullCipher = cipher
41+ }
42+ , Cmd.none
43+ )
44+
45+
46+update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
47+update _ _ msg model =
48+ case msg of
49+ EditCipher fullCipher ->
50+ ( Ok { model | fullCipher = fullCipher }, Cmd.none )
51+
52+
53+subscriptions : Model -> Sub Msg
54+subscriptions _ =
55+ Sub.none
56+
57+
58+heading : String -> String -> Html msg
59+heading icons name =
60+ span [ Attr.class "u-text--muted" ] [ i [ Attr.class ("p-icon--" ++ icons) ] [], text (" " ++ name) ]
61+
62+
63+row : { name : String, nameIcon : String, attrs : List (Html.Attribute msg) } -> Html msg
64+row { name, nameIcon, attrs } =
65+ p []
66+ [ heading nameIcon name
67+ , br [] []
68+ , input attrs []
69+ ]
70+
71+
72+view : Model -> List (Html Msg)
73+view { fullCipher } =
74+ case fullCipher.cipher of
75+ Bridge.LoginCipher cipher ->
76+ let
77+ { username, password, uris } =
78+ cipher
79+
80+ edit c =
81+ { fullCipher | cipher = Bridge.LoginCipher c } |> EditCipher
82+ in
83+ [ row
84+ { name = "Username"
85+ , nameIcon = "user"
86+ , attrs =
87+ [ Attr.type_ "text"
88+ , Attr.value (Maybe.withDefault "" username)
89+ , Ev.onInput (\x -> edit { cipher | username = Just x })
90+ , Attr.attribute "autocomplete" "username"
91+ ]
92+ }
93+ , row
94+ { name = "Password"
95+ , nameIcon = "security"
96+ , attrs =
97+ [ Attr.type_ "text"
98+ , Attr.value (Maybe.withDefault "" password)
99+ , Ev.onInput (\x -> edit { cipher | password = Just x })
100+ , Attr.attribute "autocomplete" "new-password"
101+ ]
102+ }
103+ ]
104+ ++ (uris
105+ |> List.indexedMap
106+ (\i uri ->
107+ row
108+ { name = "URL"
109+ , nameIcon = "get-link"
110+ , attrs =
111+ [ Attr.type_ "url"
112+ , Attr.value uri
113+ , Ev.onInput (\x -> edit { cipher | uris = replace i x uris })
114+ ]
115+ }
116+ )
117+ )
118+ ++ [ row
119+ { name = "URL"
120+ , nameIcon = "get-link"
121+ , attrs =
122+ [ Attr.type_ "url"
123+ , Ev.onInput (\x -> edit { cipher | uris = uris ++ [ x ] })
124+ ]
125+ }
126+ ]
127+
128+ Bridge.NoteCipher contents ->
129+ [ p []
130+ [ heading "switcher-environments" "Note"
131+ , br [] []
132+ , textarea
133+ [ Attr.spellcheck True
134+ , Attr.style "height" "70vh"
135+ , Attr.value contents
136+ , Ev.onInput (\x -> EditCipher { fullCipher | cipher = Bridge.NoteCipher x })
137+ ]
138+ []
139+ ]
140+ ]
141+
142+ Bridge.CardCipher cipher ->
143+ let
144+ { number, code, cardholderName, expMonth, expYear } =
145+ cipher
146+
147+ edit c =
148+ { fullCipher | cipher = Bridge.CardCipher c } |> EditCipher
149+ in
150+ [ row
151+ { name = "Card number"
152+ , nameIcon = "containers"
153+ , attrs =
154+ [ Attr.attribute "autocomplete" "cc-number"
155+ , Attr.type_ "text"
156+ , Attr.value (Maybe.withDefault "" number)
157+ , Ev.onInput (\x -> edit { cipher | number = Just x })
158+ ]
159+ }
160+ , p []
161+ [ heading "timed-out" "Expiration date"
162+ , br [] []
163+ , input
164+ [ Attr.attribute "autocomplete" "cc-exp-month"
165+ , Attr.type_ "text"
166+ , Attr.value (Maybe.withDefault "" expMonth)
167+ , Ev.onInput (\x -> edit { cipher | expMonth = Just x })
168+ , Attr.style "width" "20%"
169+ , Attr.class "u-align--right"
170+ ]
171+ []
172+ , text " / "
173+ , input
174+ [ Attr.attribute "autocomplete" "cc-exp-year"
175+ , Attr.type_ "text"
176+ , Attr.value (Maybe.withDefault "" expYear)
177+ , Ev.onInput (\x -> edit { cipher | expYear = Just x })
178+ , Attr.style "width" "20%"
179+ ]
180+ []
181+ ]
182+ , row
183+ { name = "CVV"
184+ , nameIcon = "security"
185+ , attrs =
186+ [ Attr.attribute "autocomplete" "cc-csc"
187+ , Attr.type_ "text"
188+ , Attr.value (Maybe.withDefault "" code)
189+ , Ev.onInput (\x -> edit { cipher | code = Just x })
190+ ]
191+ }
192+ , row
193+ { name = "Cardholder name"
194+ , nameIcon = "user"
195+ , attrs =
196+ [ Attr.attribute "autocomplete" "cc-name"
197+ , Attr.type_ "text"
198+ , Attr.value (Maybe.withDefault "" cardholderName)
199+ , Ev.onInput (\x -> edit { cipher | cardholderName = Just x })
200+ ]
201+ }
202+ ]
203+
204+ Bridge.IdentityCipher cipher ->
205+ let
206+ { address1, address2, address3, company, email, firstName, lastName, licenseNumber, middleName, passportNumber, phone, ssn, username } =
207+ cipher
208+
209+ edit c =
210+ { fullCipher | cipher = Bridge.IdentityCipher c } |> EditCipher
211+ in
212+ [ row
213+ { name = "First name"
214+ , nameIcon = ""
215+ , attrs =
216+ [ Attr.attribute "autocomplete" "given-name"
217+ , Attr.type_ "text"
218+ , Attr.value (Maybe.withDefault "" firstName)
219+ , Ev.onInput (\x -> edit { cipher | firstName = Just x })
220+ ]
221+ }
222+ , row
223+ { name = "Middle name"
224+ , nameIcon = ""
225+ , attrs =
226+ [ Attr.attribute "autocomplete" "additional-name"
227+ , Attr.type_ "text"
228+ , Attr.value (Maybe.withDefault "" middleName)
229+ , Ev.onInput (\x -> edit { cipher | middleName = Just x })
230+ ]
231+ }
232+ , row
233+ { name = "Last name"
234+ , nameIcon = ""
235+ , attrs =
236+ [ Attr.attribute "autocomplete" "family-name"
237+ , Attr.type_ "text"
238+ , Attr.value (Maybe.withDefault "" lastName)
239+ , Ev.onInput (\x -> edit { cipher | lastName = Just x })
240+ ]
241+ }
242+ , row
243+ { name = "Username"
244+ , nameIcon = ""
245+ , attrs =
246+ [ Attr.attribute "autocomplete" "username"
247+ , Attr.type_ "text"
248+ , Attr.value (Maybe.withDefault "" username)
249+ , Ev.onInput (\x -> edit { cipher | username = Just x })
250+ ]
251+ }
252+ , row
253+ { name = "Company"
254+ , nameIcon = ""
255+ , attrs =
256+ [ Attr.attribute "autocomplete" "organization"
257+ , Attr.type_ "text"
258+ , Attr.value (Maybe.withDefault "" company)
259+ , Ev.onInput (\x -> edit { cipher | company = Just x })
260+ ]
261+ }
262+ , row
263+ { name = "Social security number"
264+ , nameIcon = ""
265+ , attrs =
266+ [ Attr.attribute "autocomplete" "off"
267+ , Attr.type_ "number"
268+ , Attr.value (Maybe.withDefault "" ssn)
269+ , Ev.onInput (\x -> edit { cipher | ssn = Just x })
270+ ]
271+ }
272+ , row
273+ { name = "Passport number"
274+ , nameIcon = ""
275+ , attrs =
276+ [ Attr.attribute "autocomplete" "off"
277+ , Attr.type_ "number"
278+ , Attr.value (Maybe.withDefault "" passportNumber)
279+ , Ev.onInput (\x -> edit { cipher | passportNumber = Just x })
280+ ]
281+ }
282+ , row
283+ { name = "License Number"
284+ , nameIcon = ""
285+ , attrs =
286+ [ Attr.attribute "autocomplete" "off"
287+ , Attr.type_ "number"
288+ , Attr.value (Maybe.withDefault "" licenseNumber)
289+ , Ev.onInput (\x -> edit { cipher | licenseNumber = Just x })
290+ ]
291+ }
292+ , row
293+ { name = "Email"
294+ , nameIcon = ""
295+ , attrs =
296+ [ Attr.attribute "autocomplete" "email"
297+ , Attr.type_ "email"
298+ , Attr.value (Maybe.withDefault "" email)
299+ , Ev.onInput (\x -> edit { cipher | email = Just x })
300+ ]
301+ }
302+ , row
303+ { name = "Phone number"
304+ , nameIcon = ""
305+ , attrs =
306+ [ Attr.attribute "autocomplete" "tel"
307+ , Attr.type_ "tel"
308+ , Attr.value (Maybe.withDefault "" phone)
309+ , Ev.onInput (\x -> edit { cipher | phone = Just x })
310+ ]
311+ }
312+ , row
313+ { name = "Address line 1"
314+ , nameIcon = ""
315+ , attrs =
316+ [ Attr.attribute "autocomplete" "address-line1"
317+ , Attr.type_ "text"
318+ , Attr.value (Maybe.withDefault "" address1)
319+ , Ev.onInput (\x -> edit { cipher | address1 = Just x })
320+ ]
321+ }
322+ , row
323+ { name = "Address line 2"
324+ , nameIcon = ""
325+ , attrs =
326+ [ Attr.attribute "autocomplete" "address-line2"
327+ , Attr.type_ "text"
328+ , Attr.value (Maybe.withDefault "" address2)
329+ , Ev.onInput (\x -> edit { cipher | address2 = Just x })
330+ ]
331+ }
332+ , row
333+ { name = "Address line 3"
334+ , nameIcon = ""
335+ , attrs =
336+ [ Attr.attribute "autocomplete" "address-line3"
337+ , Attr.type_ "text"
338+ , Attr.value (Maybe.withDefault "" address3)
339+ , Ev.onInput (\x -> edit { cipher | address3 = Just x })
340+ ]
341+ }
342+ ]
343+
344+
345+hiddenPassword : String
346+hiddenPassword =
347+ String.repeat 12 "●"
348+
349+
350+hiddenCvv : String
351+hiddenCvv =
352+ String.repeat 3 "●"
353+
354+
355+iconButton : String -> msg -> Html msg
356+iconButton name msg =
357+ button [ Attr.class "p-button is-inline is-dense has-icon", Ev.onClick msg ]
358+ [ i [ Attr.class ("p-icon--" ++ name) ] [] ]
359+
360+
361+hiddenButtonIcon : Bool -> String
362+hiddenButtonIcon hidden =
363+ if hidden then
364+ "show"
365+
366+ else
367+ "hide"
frontend/Pages/Login.elm+3
@@ -76,6 +76,7 @@ view model =
7676 , Attr.value model.server
7777 , Ev.onInput UpdateServer
7878 , Attr.attribute "autocomplete" "url"
79+ , Attr.required True
7980 ]
8081 []
8182 , label [] [ text "Email" ]
@@ -84,6 +85,7 @@ view model =
8485 , Attr.value model.email
8586 , Ev.onInput UpdateEmail
8687 , Attr.attribute "autocomplete" "email"
88+ , Attr.required True
8789 ]
8890 []
8991 , label [] [ text "Password" ]
@@ -92,6 +94,7 @@ view model =
9294 , Attr.value model.password
9395 , Ev.onInput UpdatePassword
9496 , Attr.attribute "autocomplete" "current-password"
97+ , Attr.required True
9598 ]
9699 []
97100 ]
frontend/Pages/MasterPassword.elm+2−1
@@ -83,7 +83,8 @@ view model =
8383 [ Attr.type_ "password"
8484 , Attr.value model.password
8585 , Ev.onInput UpdatePassword
86− , Attr.attribute "autocomplete" "current-password"
86+ , Attr.attribute "autocomplete" "new-password"
87+ , Attr.required True
8788 ]
8889 []
8990 , alignRight [ button [ Attr.type_ "submit" ] [ text "Log in" ] ]
frontend/Pages/Navigation.elm+10−4
@@ -18,7 +18,7 @@ import Utils exposing (..)
1818
1919
2020type TopButton msg
21− = BackButton msg
21+ = BackButton { action : msg, icon : Maybe String }
2222 | MenuButton (MenuConfig msg)
2323
2424
@@ -46,10 +46,16 @@ navigation { topButton, title } =
4646 [ Attr.class "p-button--base is-inline u-no-margin", Attr.style "visibility" "hidden" ]
4747 []
4848
49− Just (BackButton msg) ->
49+ Just (BackButton { action, icon }) ->
5050 button
51− [ Attr.class "p-button--base is-inline u-no-margin", Ev.onClick msg ]
52− [ i [ Attr.class "p-icon--chevron-up ninety-counter" ] [] ]
51+ [ Attr.class "p-button--base is-inline u-no-margin", Ev.onClick action ]
52+ [ case icon of
53+ Nothing ->
54+ i [ Attr.class "p-icon--chevron-up ninety-counter" ] []
55+
56+ Just iconName ->
57+ i [ Attr.class ("p-icon--" ++ iconName) ] []
58+ ]
5359
5460 Just (MenuButton cfg) ->
5561 button
frontend/Utils.elm+58
@@ -86,3 +86,61 @@ tailEmpty x =
8686unwords : List String -> String
8787unwords =
8888 String.join " "
89+
90+
91+zip : List a -> List b -> List ( a, b )
92+zip =
93+ List.map2 (\a b -> ( a, b ))
94+
95+
96+mapIndex : (a -> a) -> Int -> List a -> List a
97+mapIndex f i xs =
98+ case ( i, xs ) of
99+ ( _, [] ) ->
100+ []
101+
102+ ( 0, x :: xx ) ->
103+ f x :: xx
104+
105+ ( j, x :: xx ) ->
106+ x :: mapIndex f (j - 1) xx
107+
108+
109+replace : Int -> a -> List a -> List a
110+replace i a xs =
111+ case ( i, xs ) of
112+ ( 0, _ :: xx ) ->
113+ a :: xx
114+
115+ ( j, x :: xx ) ->
116+ x :: replace (j - 1) a xx
117+
118+ ( _, [] ) ->
119+ []
120+
121+
122+index : Int -> List a -> Maybe a
123+index i xs =
124+ case ( i, xs ) of
125+ ( 0, x :: _ ) ->
126+ Just x
127+
128+ ( j, _ :: xx ) ->
129+ index (j - 1) xx
130+
131+ ( _, [] ) ->
132+ Nothing
133+
134+
135+spanList : (a -> Bool) -> List a -> ( List a, List a )
136+spanList f aa =
137+ case aa of
138+ [] ->
139+ ( aa, aa )
140+
141+ a :: rest ->
142+ if f a then
143+ spanList f rest |> Tuple.mapFirst ((::) a)
144+
145+ else
146+ ( [], aa )
style.css+4
@@ -91,3 +91,7 @@ p {
9191.no-scroll-bar::-webkit-scrollbar {
9292 display: none;
9393}
94+
95+textarea {
96+ resize: none;
97+}