Compare commits

...

3 Commits

Author SHA1 Message Date
Bram 8181ef2dfa Add initial log viewer 2024-11-05 10:28:10 +01:00
Bram 7711ce8c0d elm-format 2024-11-03 21:09:37 +01:00
Bram 85790d4e7b Refactor spike
This commit makes it easier to transfer information across screens
2024-11-03 20:58:15 +01:00
14 changed files with 983 additions and 639 deletions

View File

@ -14,6 +14,7 @@
"icidasset/elm-material-icons": "11.0.0", "icidasset/elm-material-icons": "11.0.0",
"kudzu-forest/elm-constant-time-queue": "1.4.0", "kudzu-forest/elm-constant-time-queue": "1.4.0",
"mdgriffith/elm-ui": "1.1.8", "mdgriffith/elm-ui": "1.1.8",
"micahhahn/elm-safe-recursion": "2.0.0",
"noordstar/elm-iddict": "1.0.1", "noordstar/elm-iddict": "1.0.1",
"noordstar/elm-matrix-sdk-beta": "3.6.0", "noordstar/elm-matrix-sdk-beta": "3.6.0",
"noordstar/elm-palette": "1.0.0" "noordstar/elm-palette": "1.0.0"
@ -31,7 +32,6 @@
"elm/virtual-dom": "1.0.3", "elm/virtual-dom": "1.0.3",
"elm-community/intdict": "3.1.0", "elm-community/intdict": "3.1.0",
"fredcy/elm-parseint": "2.0.1", "fredcy/elm-parseint": "2.0.1",
"micahhahn/elm-safe-recursion": "2.0.0",
"miniBill/elm-fast-dict": "1.2.1", "miniBill/elm-fast-dict": "1.2.1",
"noahzgordon/elm-color-extra": "1.0.2", "noahzgordon/elm-color-extra": "1.0.2",
"turboMaCk/queue": "1.1.0" "turboMaCk/queue": "1.1.0"

View File

@ -1,43 +1,58 @@
module Items.FlavorPicker exposing (..) module Items.FlavorPicker exposing (..)
{-| This module allows the user to pick whatever flavor they want to use. {-| This module allows the user to pick whatever flavor they want to use.
-} -}
import Color exposing (Color) import Color exposing (Color)
import Element exposing (Element) import Element exposing (Element)
import Element.Events
import Html.Attributes import Html.Attributes
import Layout
import Material.Icons import Material.Icons
import Theme exposing (Flavor(..)) import Theme exposing (Flavor(..))
import Layout
import Element.Events
-- MODEL -- MODEL
-- UPDATE -- UPDATE
-- VIEW -- VIEW
view : view :
{ height : Int { height : Int
, flavor : Flavor , flavor : Flavor
, onClick : Flavor -> msg , onClick : Flavor -> msg
, themeIcon : Flavor -> Color , themeIcon : Flavor -> Color
, width : Int , width : Int
} -> Element msg }
-> Element msg
view data = view data =
let let
lightMode = data.flavor == Latte lightMode =
icon = if lightMode then Material.Icons.dark_mode else Material.Icons.light_mode data.flavor == Latte
newFlavor = if lightMode then Frappe else Latte
in
Element.el
[ Element.Events.onClick (data.onClick newFlavor)
, Element.htmlAttribute (Html.Attributes.style "cursor" "pointer")
]
( Layout.iconAsElement
{ color = data.themeIcon data.flavor
, height = data.height
, icon = icon
, width = data.width
}
)
icon =
if lightMode then
Material.Icons.dark_mode
else
Material.Icons.light_mode
newFlavor =
if lightMode then
Frappe
else
Latte
in
Element.el
[ Element.Events.onClick (data.onClick newFlavor)
, Element.htmlAttribute (Html.Attributes.style "cursor" "pointer")
]
(Layout.iconAsElement
{ color = data.themeIcon data.flavor
, height = data.height
, icon = icon
, width = data.width
}
)

View File

@ -1,32 +1,40 @@
module Items.Introduction exposing (..) module Items.Introduction exposing (..)
import Color exposing (Color) import Color exposing (Color)
import Element exposing (Element) import Element exposing (Element)
import Element.Background import Element.Background
import Widget.Material.Typography import Layout
import Theme import Theme
import Widget.Material.Typography
-- MODEL -- MODEL
type alias Model = ()
type alias Msg = () type alias Model =
()
type alias Msg =
()
-- UPDATE -- UPDATE
-- SUBSCRIPTIONS -- SUBSCRIPTIONS
-- VIEW -- VIEW
view : view :
{ colorBackground : Color { colorBackground : Color
, width : Int , width : Int
} -> Element msg }
-> Element msg
view data = view data =
[ header "Martiplier" [ Layout.header "Martiplier"
, text "Martiplier (short for Matrix Plier) is a unique client. It doesn't let you browse rooms and have chat conversations." , Layout.stdText "Martiplier (short for Matrix Plier) is a unique client. It doesn't let you browse rooms and have chat conversations."
, text "Instead, it offers you a more debug-like display of a user account. This helps when trying to do bulk operations, or to discover information about a client." , Layout.stdText "Instead, it offers you a more debug-like display of a user account. This helps when trying to do bulk operations, or to discover information about a client."
] ]
|> Element.column |> Element.column
[ Element.Background.color (Theme.toElmUiColor data.colorBackground) [ Element.Background.color (Theme.toElmUiColor data.colorBackground)
@ -34,12 +42,3 @@ view data =
, Element.spacing 20 , Element.spacing 20
, Element.width (Element.px data.width) , Element.width (Element.px data.width)
] ]
header : String -> Element msg
header =
Element.text >> Element.el Widget.Material.Typography.h1
>> List.singleton >> Element.paragraph []
text : String -> Element msg
text =
Element.text >> List.singleton >> Element.paragraph []

113
src/Items/ItemPicker.elm Normal file
View File

@ -0,0 +1,113 @@
module Items.ItemPicker exposing (..)
import Color exposing (Color)
import Element exposing (Element)
import Iddict exposing (Iddict)
import Layout
import Material.Icons
-- MODEL
type alias Model a =
{ hover : Maybe Int
, items : Iddict a
}
type Msg
= OnHover Int
| OnHoverOut Int
init : Iddict a -> Model a
init iddict =
{ hover = Nothing, items = iddict }
-- UPDATE
update : Msg -> Model a -> Model a
update msg model =
case msg of
OnHover i ->
{ model | hover = Just i }
OnHoverOut i ->
if model.hover == Just i then
{ model | hover = Nothing }
else
model
updateContent : (Iddict a -> Iddict a) -> Model a -> Model a
updateContent f model =
{ model | items = f model.items }
-- VIEW
{-| Extract the original data set out of the item picker.
-}
extract : Model a -> Iddict a
extract =
.items
{-| Display the item picker. Note that the item should not be wider than 360px.
-}
view :
{ colorMenu : Color
, colorText : Color
, height : Int
, model : Model a
, toText : Int -> a -> String
, toTitle : Int -> a -> String
, onAddNew : Maybe msg
, onClick : Int -> msg
, width : Int
}
-> Element msg
view data =
Layout.sideList
{ color = data.colorMenu
, items =
data.model.items
|> Iddict.toList
|> List.map
(\( iid, item ) ->
Layout.itemWithSubtext
{ color = data.colorText
, leftIcon = always Element.none
, onPress = Just (data.onClick iid)
, rightIcon = Layout.iconAsIcon Material.Icons.launch
, text = data.toText iid item
, title = data.toTitle iid item
}
)
|> (\items ->
case data.onAddNew of
Nothing ->
items
Just onAddNew ->
List.append items
[ Layout.itemWithSubtext
{ color = data.colorText
, leftIcon = Layout.iconAsIcon Material.Icons.add_circle
, onPress = Just onAddNew
, rightIcon = always Element.none
, text = "Click here"
, title = "Add new"
}
]
)
, width = data.width
}

49
src/Items/LogViewer.elm Normal file
View File

@ -0,0 +1,49 @@
module Items.LogViewer exposing (..)
import Element exposing (Element)
import Element.Font
-- MODEL
-- UPDATE
-- VIEW
viewRecent :
{ height : Int
, logs : List { channel : String, content : String }
, width : Int
}
-> Element msg
viewRecent data =
let
channelWidth =
90
contentWidth =
data.width - channelWidth
in
Element.table
[]
{ data = data.logs
, columns =
[ { header = Element.el [ Element.Font.bold ] (Element.text "Channel")
, width = Element.px channelWidth
, view = .channel >> Element.text
}
, { header = Element.el [ Element.Font.bold ] (Element.text "Content")
, width = Element.px contentWidth
, view = .content >> String.replace "\n" " " >> stripText (contentWidth // 10) >> Element.text
}
]
}
stripText : Int -> String -> String
stripText n text =
if String.length text < n then
text
else
String.left (n - 3) text ++ "..."

View File

@ -1,4 +1,5 @@
module Items.LoginScreen exposing (..) module Items.LoginView exposing (..)
{-| The Login screen allows the user to log in, as well as view a short display {-| The Login screen allows the user to log in, as well as view a short display
of what to expect from the Matrix client. of what to expect from the Matrix client.
-} -}
@ -7,15 +8,18 @@ import Color exposing (Color)
import Element exposing (Element) import Element exposing (Element)
import Element.Background import Element.Background
import Element.Border import Element.Border
import Items.FlavorPicker as FlavorPicker
import Layout
import Material.Icons
import Matrix import Matrix
import Matrix.Settings import Matrix.Settings
import Theme import Theme
import Layout
import Items.FlavorPicker as FlavorPicker
import Material.Icons
-- MODEL -- MODEL
type alias Model = type alias Model =
{ accessToken : String { accessToken : String
, loginMethod : LoginMethod , loginMethod : LoginMethod
@ -23,16 +27,19 @@ type alias Model =
, username : String , username : String
} }
type Msg type Msg
= SetAccessToken String = SetAccessToken String
| SetPassword String | SetPassword String
| SetUsername String | SetUsername String
| SwitchMethod LoginMethod | SwitchMethod LoginMethod
type LoginMethod type LoginMethod
= AccessToken = AccessToken
| Password | Password
init : Model init : Model
init = init =
{ accessToken = "" { accessToken = ""
@ -41,8 +48,11 @@ init =
, username = "" , username = ""
} }
-- UPDATE -- UPDATE
update : Msg -> Model -> Model update : Msg -> Model -> Model
update msg model = update msg model =
case msg of case msg of
@ -58,8 +68,11 @@ update msg model =
SwitchMethod method -> SwitchMethod method ->
{ model | loginMethod = method } { model | loginMethod = method }
-- VIEW -- VIEW
view : view :
{ colorBackground : Color { colorBackground : Color
, colorMain : Color , colorMain : Color
@ -73,7 +86,8 @@ view :
, onSubmit : Matrix.Vault -> msg , onSubmit : Matrix.Vault -> msg
, toMsg : Msg -> msg , toMsg : Msg -> msg
, width : Int , width : Int
} -> Element msg }
-> Element msg
view data = view data =
[ viewLoginMethodPicker [ viewLoginMethodPicker
{ color = data.colorMain { color = data.colorMain
@ -97,7 +111,7 @@ view data =
, show = False , show = False
, text = data.model.accessToken , text = data.model.accessToken
} }
Password -> Password ->
Layout.passwordInput Layout.passwordInput
{ color = data.colorTextField { color = data.colorTextField
@ -108,17 +122,17 @@ view data =
, text = data.model.password , text = data.model.password
} }
, Layout.containedButton , Layout.containedButton
{ buttonColor = data.colorMain { buttonColor = data.colorMain
, clickColor = data.colorText , clickColor = data.colorText
, icon = always Element.none , icon = always Element.none
, onPress = , onPress =
data.model data.model
|> toVault |> toVault
|> Maybe.map data.onSubmit |> Maybe.map data.onSubmit
, text = "LOG IN" , text = "LOG IN"
} }
|> Element.el |> Element.el
[ Element.centerX ] [ Element.centerX ]
] ]
|> Element.column |> Element.column
[ Element.Background.color (Theme.toElmUiColor data.colorMenu) [ Element.Background.color (Theme.toElmUiColor data.colorMenu)
@ -133,7 +147,7 @@ view data =
|> Element.el |> Element.el
[ Element.Background.color (Theme.toElmUiColor data.colorBackground) [ Element.Background.color (Theme.toElmUiColor data.colorBackground)
, Element.inFront , Element.inFront
( FlavorPicker.view (FlavorPicker.view
{ height = 30 { height = 30
, flavor = data.flavor , flavor = data.flavor
, onClick = data.onFlavorPick , onClick = data.onFlavorPick
@ -147,6 +161,7 @@ view data =
, Element.width (Element.px data.width) , Element.width (Element.px data.width)
] ]
toVault : Model -> Maybe Matrix.Vault toVault : Model -> Maybe Matrix.Vault
toVault model = toVault model =
case model.loginMethod of case model.loginMethod of
@ -168,6 +183,7 @@ toVault model =
|> Matrix.fromUserId |> Matrix.fromUserId
|> Maybe.map (Matrix.Settings.setPassword model.password) |> Maybe.map (Matrix.Settings.setPassword model.password)
viewLoginMethodPicker : { color : Color, loginMethod : LoginMethod, toMsg : LoginMethod -> msg } -> Element msg viewLoginMethodPicker : { color : Color, loginMethod : LoginMethod, toMsg : LoginMethod -> msg } -> Element msg
viewLoginMethodPicker data = viewLoginMethodPicker data =
Layout.tab Layout.tab
@ -175,24 +191,24 @@ viewLoginMethodPicker data =
, content = always Element.none , content = always Element.none
, items = , items =
[ { icon = Layout.iconAsIcon Material.Icons.key [ { icon = Layout.iconAsIcon Material.Icons.key
, text = "Access token" , text = "Access token"
} }
, { icon = Layout.iconAsIcon Material.Icons.password , { icon = Layout.iconAsIcon Material.Icons.password
, text = "Password" , text = "Password"
} }
] ]
, onSelect = , onSelect =
(\i -> \i ->
if i == 0 then if i == 0 then
data.toMsg AccessToken data.toMsg AccessToken
else
data.toMsg Password else
) data.toMsg Password
, selected = , selected =
case data.loginMethod of case data.loginMethod of
AccessToken -> AccessToken ->
0 0
Password -> Password ->
1 1
} }

83
src/Items/VaultList.elm Normal file
View File

@ -0,0 +1,83 @@
module Items.VaultList exposing (..)
{-|
# Vault list
The vault list contains a list of stored vaults that can be picked from.
-}
import Iddict exposing (Iddict)
import Matrix
-- MODEL
type alias Model =
Iddict VaultBlock
type Msg
= AddVault { name : String, vault : Matrix.Vault }
| OnVaultUpdate Int Matrix.VaultUpdate
type alias VaultBlock =
{ logs : List { channel : String, content : String }
, name : String
, vault : Matrix.Vault
}
init : Model
init =
Iddict.empty
-- UPDATE
addVault : { name : String, vault : Matrix.Vault } -> Model -> Model
addVault =
AddVault >> update
insertVaultUpdate : Int -> Matrix.VaultUpdate -> Model -> Model
insertVaultUpdate i vu =
update (OnVaultUpdate i vu)
update : Msg -> Model -> Model
update msg model =
case msg of
AddVault { name, vault } ->
Iddict.insert { name = name, logs = [], vault = vault } model
|> Tuple.second
OnVaultUpdate i vu ->
Iddict.update i
(Maybe.map
(\block ->
{ block
| logs =
List.append (Matrix.logs vu) block.logs
|> List.take 500
, vault = Matrix.update vu block.vault
}
)
)
model
-- VIEW
getVault : Int -> Model -> Maybe Matrix.Vault
getVault i model =
Iddict.get i model |> Maybe.map .vault

View File

@ -1,112 +0,0 @@
module Items.VaultPicker exposing (..)
import Color exposing (Color)
import Element exposing (Element)
import Iddict exposing (Iddict)
import Material.Icons
import Matrix
import Layout
-- MODEL
type alias Model =
{ hover : Maybe Int, vaults : Iddict NamedVault }
type alias NamedVault = { name : String, vault : Matrix.Vault }
type Msg
= AddVault String Matrix.Vault
| OnHover Int
| OnHoverOut Int
| OnRemoveVault Int
init : List NamedVault -> Model
init items =
{ hover = Nothing
, vaults =
items
|> List.indexedMap Tuple.pair
|> Iddict.fromList
}
-- UPDATE
addVault : NamedVault -> Model -> Model
addVault data =
AddVault data.name data.vault |> update
update : Msg -> Model -> Model
update msg model =
case msg of
AddVault name vault ->
{ model
| vaults =
model.vaults
|> Iddict.insert { name = name, vault = vault }
|> Tuple.second
}
OnHover i ->
{ model | hover = Just i }
OnHoverOut i ->
case model.hover of
Just h ->
if i == h then
{ model | hover = Nothing }
else
model
Nothing ->
model
OnRemoveVault i ->
{ model | vaults = Iddict.remove i model.vaults }
-- VIEW
getVault : Int -> Model -> Maybe Matrix.Vault
getVault i model =
Iddict.get i model.vaults |> Maybe.map .vault
view :
{ colorItem : Color
, colorList : Color
, model : Model
, onAddVault : msg
, onSelectVault : Int -> msg
, width : Int
} -> Element msg
view data =
Layout.sideList
{ color = data.colorList
, items =
data.model.vaults
|> Iddict.toList
|> List.map
(\(vid, { name, vault }) ->
Layout.itemWithSubtext
{ color = data.colorItem
, leftIcon = always Element.none
, onPress = Just (data.onSelectVault vid)
, rightIcon = Layout.iconAsIcon Material.Icons.launch
, text = name
, title = "Vault #" ++ String.fromInt vid
}
)
|> (\items ->
[ Layout.itemWithSubtext
{ color = data.colorItem
, leftIcon = Layout.iconAsIcon Material.Icons.add_circle
, onPress = Just data.onAddVault
, rightIcon = always Element.none
, text = "Click here"
, title = "Add new"
}
]
|> List.append items
)
}
|> Element.el
[ Element.width (Element.px data.width) ]

View File

@ -1,170 +0,0 @@
module Items.VaultScreen exposing (..)
import Color exposing (Color)
import Element exposing (Element)
import Layout
import Matrix
import Matrix.Room
import Queue exposing (Queue)
-- MODEL
type alias Model =
{ recentLogs : Queue { channel : String, content : String }
, screen : Screen
}
type Msg
= OnSwitchScreen Screen
| OnVaultUpdate Matrix.VaultUpdate
type Screen
= LandingScreen
| Room String
init : Model
init =
{ recentLogs = Queue.empty
, screen = LandingScreen
}
maxItemsInRecentLogsQueue : Int
maxItemsInRecentLogsQueue = 100
-- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnSwitchScreen screen ->
( { model | screen = screen }, Cmd.none )
OnVaultUpdate vu ->
( { model
| recentLogs =
model.recentLogs
|> addItemsToQueue (Matrix.logs vu)
|> stripQueueToSize maxItemsInRecentLogsQueue
}
, Cmd.none
)
addItemsToQueue : List a -> Queue a -> Queue a
addItemsToQueue items queue =
List.foldl Queue.enqueue queue items
stripQueueToSize : Int -> Queue a -> Queue a
stripQueueToSize i queue =
if Queue.length queue > i then
stripQueueToSize i (Queue.dequeue queue)
else
queue
-- VIEW
view :
{ colorSelectedRoom : Color
, colorText : Color
, height : Int
, model : Model
, onVaultUpdate : Matrix.VaultUpdate -> msg
, vault : Matrix.Vault
, toMsg : Msg -> msg
, width : Int
} -> Element msg
view data =
viewScreen
{ colorSelectedRoom = data.colorSelectedRoom
, colorText = data.colorText
, height = data.height
, model = data.model
, onVaultUpdate = data.onVaultUpdate
, vault = data.vault
, screen = data.model.screen
, toMsg = data.toMsg
, width = data.width
}
viewScreen :
{ colorSelectedRoom : Color
, colorText : Color
, height : Int
, model : Model
, onVaultUpdate : Matrix.VaultUpdate -> msg
, vault : Matrix.Vault
, screen : Screen
, toMsg : Msg -> msg
, width : Int
} -> Element msg
viewScreen data =
case data.screen of
LandingScreen ->
Element.wrappedRow
[ Element.height (Element.px data.height)
, Element.width (Element.px data.width)
]
[ viewRoomList
{ colorSelected = data.colorSelectedRoom
, colorText = data.colorText
, rooms = Matrix.rooms data.vault
, toMsg = data.toMsg
}
]
Room roomId ->
case Matrix.fromRoomId roomId data.vault of
Just room ->
viewRoom
{ height = data.height
, model = room
, toMsg = data.toMsg
, width = data.width
}
Nothing ->
viewScreen { data | screen = LandingScreen }
viewRoom :
{ height : Int
, model : Matrix.Room.Room
, toMsg : Msg -> msg
, width : Int
} -> Element msg
viewRoom data =
[ Matrix.Room.name data.model
|> Maybe.withDefault "Nameless room"
|> Element.text
]
|> Element.column []
viewRoomList :
{ colorSelected : Color
, colorText : Color
, rooms : List Matrix.Room.Room
, toMsg : Msg -> msg
} -> Element msg
viewRoomList data =
Layout.sideList
{ color = data.colorSelected
, items =
data.rooms
|> List.map
(\room ->
Layout.itemWithSubtext
{ color = data.colorText
, leftIcon = always Element.none -- TODO: Add room image
, onPress =
Matrix.Room.roomId room
|> Room
|> OnSwitchScreen
|> data.toMsg
|> Just
, rightIcon = always Element.none -- TODO: Choose icon?
, text = Matrix.Room.roomId room
, title =
Matrix.Room.name room
|> Maybe.withDefault "Nameless room"
}
)
}

View File

@ -1,178 +0,0 @@
module Items.WelcomeScreen exposing (..)
import Color exposing (Color)
import Element exposing (Element)
import Element.Background
import Items.FlavorPicker as FlavorPicker
import Items.Introduction as Intro
import Items.LoginScreen as Login
import Items.VaultPicker as VaultPicker
import Matrix
import Theme
-- MODEL
type alias Model =
{ loginView : Maybe Login.Model
, vaultView : VaultPicker.Model
}
type Msg
= OnAddVault
| OnLoginView Login.Msg
| OnSubmitVault { name : String, vault : Matrix.Vault }
| OnVaultView VaultPicker.Msg
init : List { name : String, vault : Matrix.Vault } -> Model
init vaults =
{ loginView = Nothing
, vaultView = VaultPicker.init vaults
}
-- UPDATE
update : Msg -> Model -> Model
update msg model =
case msg of
OnAddVault ->
case model.loginView of
Just _ ->
model
Nothing ->
{ model | loginView = Just Login.init }
OnLoginView m ->
{ model
| loginView = Maybe.map (Login.update m) model.loginView
}
OnSubmitVault vault ->
{ model
| loginView = Nothing
, vaultView = VaultPicker.addVault vault model.vaultView
}
OnVaultView m ->
{ model
| vaultView = VaultPicker.update m model.vaultView
}
-- VIEW
getVault : Int -> Model -> Maybe Matrix.Vault
getVault i model =
VaultPicker.getVault i model.vaultView
view :
{ colorBackground : Color
, colorBackground2 : Color
, colorMain : Color
, colorMenu : Color
, colorText : Color
, colorTextField : Color
, height : Int
, flavor : Theme.Flavor
, model : Model
, onFlavorPick : Theme.Flavor -> msg
, onSelectVault : Int -> msg
, toMsg : Msg -> msg
, width : Int
} -> Element msg
view data =
let
goesVertical = 2 * data.width <= 3 * data.height
direction = if goesVertical then Element.column else Element.row
width = if goesVertical then data.width else data.width // 2
height = if goesVertical then data.height // 2 else data.height
in
case data.model.loginView of
Just m ->
direction
[ Element.height (Element.px data.height)
, Element.width (Element.px data.width)
]
[ viewIntroduction
{ colorBackground = data.colorBackground
, colorItem = data.colorText
, colorList = data.colorMenu
, height = height
, model = data.model.vaultView
, onSelectVault = data.onSelectVault
, toMsg = data.toMsg
, width = width
}
, Login.view
{ colorBackground = data.colorBackground2
, colorMain = data.colorMain
, colorMenu = data.colorMenu
, colorText = data.colorText
, colorTextField = data.colorTextField
, height = height
, flavor = data.flavor
, model = m
, onFlavorPick = data.onFlavorPick
, onSubmit = \vault -> OnSubmitVault { name = "New Vault", vault = vault } |> data.toMsg
, toMsg = OnLoginView >> data.toMsg
, width = width
}
]
Nothing ->
viewIntroduction
{ colorBackground = data.colorBackground
, colorItem = data.colorText
, colorList = data.colorMenu
, height = data.height
, model = data.model.vaultView
, onSelectVault = data.onSelectVault
, toMsg = data.toMsg
, width = data.width
}
|> Element.el
[ Element.Background.color (Theme.toElmUiColor data.colorBackground)
, Element.inFront
( FlavorPicker.view
{ height = 30
, flavor = data.flavor
, onClick = data.onFlavorPick
, themeIcon = always data.colorText
, width = 30
}
|> Element.el [ Element.alignRight ]
|> Element.el [ Element.padding 10, Element.width Element.fill ]
)
]
viewIntroduction :
{ colorBackground : Color
, colorItem : Color
, colorList : Color
, height : Int
, model : VaultPicker.Model
, onSelectVault : Int -> msg
, toMsg : Msg -> msg
, width : Int
} -> Element msg
viewIntroduction data =
[ Intro.view
{ colorBackground = data.colorBackground
, width = data.width
}
, VaultPicker.view
{ colorItem = data.colorItem
, colorList = data.colorList
, onAddVault = data.toMsg OnAddVault
, onSelectVault = data.onSelectVault
, model = data.model
, width = Basics.min 360 (4 * data.width // 5)
}
|> Element.el
[ Element.centerX
]
]
|> Element.column
[ Element.height (Element.px data.height)
, Element.scrollbarY
, Element.width (Element.px data.width)
]

View File

@ -1,9 +1,13 @@
module Layout exposing module Layout exposing
( tab ( twoBlocks
, tab, sideIconBar
, iconAsElement, iconAsIcon , iconAsElement, iconAsIcon
, containedButton, outlinedButton, textButton, sideList , containedButton, outlinedButton, textButton
, textInput, passwordInput , textInput, passwordInput
, loadingIndicator, itemWithSubtext , header, stdText
, itemWithSubtext
, sideList
, loadingIndicator
) )
{-| {-|
@ -15,14 +19,21 @@ The layout module exposes some boilerplate functions that have produce a
beautiful Material design Elm webpage. beautiful Material design Elm webpage.
## Screen layout
@docs twoBlocks
## Elements ## Elements
@docs tab @docs tab, sideIconBar
## Icons ## Icons
@docs iconAsElement, iconAsIcon @docs iconAsElement, iconAsIcon
## Buttons ## Buttons
@docs containedButton, outlinedButton, textButton @docs containedButton, outlinedButton, textButton
@ -32,14 +43,22 @@ beautiful Material design Elm webpage.
@docs textInput, passwordInput @docs textInput, passwordInput
## Text
@docs header, stdText
## Items in a list ## Items in a list
@docs itemWithSubtext @docs itemWithSubtext
## Lists ## Lists
@docs sideList @docs sideList
## Other elements ## Other elements
@docs loadingIndicator @docs loadingIndicator
@ -48,12 +67,18 @@ beautiful Material design Elm webpage.
import Color exposing (Color) import Color exposing (Color)
import Element exposing (Element) import Element exposing (Element)
import Element.Background
import Element.Events
import Element.Font
import Element.Input import Element.Input
import Html.Attributes
import Material.Icons.Types
import Theme
import Widget import Widget
import Widget.Customize as Customize import Widget.Customize as Customize
import Widget.Icon exposing (Icon) import Widget.Icon exposing (Icon)
import Widget.Material as Material import Widget.Material as Material
import Material.Icons.Types import Widget.Material.Typography
{-| A contained button representing the most important action of a group. {-| A contained button representing the most important action of a group.
@ -75,12 +100,22 @@ containedButton data =
) )
{ text = data.text, icon = data.icon, onPress = data.onPress } { text = data.text, icon = data.icon, onPress = data.onPress }
header : String -> Element msg
header =
Element.text
>> Element.el Widget.Material.Typography.h1
>> List.singleton
>> Element.paragraph []
iconAsElement : iconAsElement :
{ color : Color { color : Color
, height : Int , height : Int
, icon : Material.Icons.Types.Icon msg , icon : Material.Icons.Types.Icon msg
, width : Int , width : Int
} -> Element msg }
-> Element msg
iconAsElement data = iconAsElement data =
data.icon data.icon
|> iconAsIcon |> iconAsIcon
@ -91,10 +126,12 @@ iconAsElement data =
, Element.width (Element.px data.width) , Element.width (Element.px data.width)
] ]
iconAsIcon : Material.Icons.Types.Icon msg -> Widget.Icon.Icon msg iconAsIcon : Material.Icons.Types.Icon msg -> Widget.Icon.Icon msg
iconAsIcon = iconAsIcon =
Widget.Icon.elmMaterialIcons Material.Icons.Types.Color Widget.Icon.elmMaterialIcons Material.Icons.Types.Color
{-| Multiline item {-| Multiline item
-} -}
itemWithSubtext : itemWithSubtext :
@ -105,10 +142,10 @@ itemWithSubtext :
, text : String , text : String
, title : String , title : String
} }
-> Widget.Item msg -> Widget.Item msg
itemWithSubtext data = itemWithSubtext data =
Widget.multiLineItem Widget.multiLineItem
( { primary = data.color, onPrimary = data.color } ({ primary = data.color, onPrimary = data.color }
|> singlePalette |> singlePalette
|> Material.multiLineItem |> Material.multiLineItem
) )
@ -119,20 +156,22 @@ itemWithSubtext data =
, text = data.text , text = data.text
} }
{-| Circular loading bar indicator {-| Circular loading bar indicator
-} -}
loadingIndicator : loadingIndicator :
{ color : Color { color : Color
} }
-> Element msg -> Element msg
loadingIndicator data = loadingIndicator data =
Widget.circularProgressIndicator Widget.circularProgressIndicator
( { primary = data.color, onPrimary = data.color } ({ primary = data.color, onPrimary = data.color }
|> singlePalette |> singlePalette
|> Material.progressIndicator |> Material.progressIndicator
) )
Nothing Nothing
{-| An outlined button representing an important action within a group. {-| An outlined button representing an important action within a group.
-} -}
outlinedButton : outlinedButton :
@ -150,6 +189,7 @@ outlinedButton data =
) )
{ text = data.text, icon = data.icon, onPress = data.onPress } { text = data.text, icon = data.icon, onPress = data.onPress }
{-| Show a password field {-| Show a password field
-} -}
passwordInput : passwordInput :
@ -197,14 +237,79 @@ singlePalette { primary, onPrimary } =
} }
} }
sideList : { color : Color, items : List (Widget.Item msg) }-> Element msg
sideIconBar :
{ colorBackground : Color
, colorText : Color
, height : Int
, items : List { icon : Widget.Icon.Icon msg, onPress : msg, text : String }
, width : Int
}
-> Element msg
sideIconBar data =
let
buttonHeight =
round (toFloat data.width * 1.618)
fontSize =
data.width // 6
iconSize =
data.width * 3 // 5
in
data.items
|> List.map
(\item ->
[ item.icon { size = iconSize, color = data.colorText }
|> Element.el [ Element.centerX ]
, Element.paragraph [] [ Element.text item.text ]
]
|> Element.column
[ Element.centerX
, Element.centerY
, Element.Font.bold
, Element.Font.center
, Element.Font.size fontSize
, Element.htmlAttribute (Html.Attributes.style "cursor" "pointer")
]
|> Element.el
[ Element.centerY
, Element.Events.onClick item.onPress
, Element.height (Element.px data.width)
, Element.width (Element.px data.width)
]
|> Element.el
[ Element.height (Element.px buttonHeight)
]
)
|> Element.column
[ Element.Background.color (Theme.toElmUiColor data.colorBackground)
, Element.height (Element.px data.height)
, Element.scrollbarY
, Element.width (Element.px data.width)
]
sideList : { color : Color, items : List (Widget.Item msg), width : Int } -> Element msg
sideList data = sideList data =
let
width px =
Element.width (Element.px px)
in
Widget.itemList Widget.itemList
( { primary = data.color, onPrimary = data.color } ({ primary = data.color, onPrimary = data.color }
|> singlePalette |> singlePalette
|> Material.sideSheet |> Material.sideSheet
) )
data.items data.items
|> Element.el [ Element.centerX, width (Basics.min 360 data.width) ]
|> Element.el [ width data.width ]
stdText : String -> Element msg
stdText =
Element.text >> List.singleton >> Element.paragraph []
{-| A tab selector that always has an item selected. {-| A tab selector that always has an item selected.
-} -}
@ -275,3 +380,48 @@ textInput data =
, label = data.label , label = data.label
, onChange = data.onChange , onChange = data.onChange
} }
{-| Two blocks either next to each other or below each other, depending on the
screen shape.
-}
twoBlocks :
{ height : Int
, el1 : { height : Int, width : Int } -> Element msg
, el2 : { height : Int, width : Int } -> Element msg
, width : Int
}
-> Element msg
twoBlocks data =
let
goesVertical =
2 * data.width <= 3 * data.height
direction =
if goesVertical then
Element.column
else
Element.row
width =
if goesVertical then
data.width
else
data.width // 2
height =
if goesVertical then
data.height // 2
else
data.height
in
direction
[ Element.height (Element.px data.height)
, Element.width (Element.px data.width)
]
[ data.el1 { height = height, width = width }
, data.el2 { height = height, width = width }
]

View File

@ -5,13 +5,15 @@ import Browser.Dom
import Browser.Events import Browser.Events
import Element exposing (Element) import Element exposing (Element)
import Element.Background import Element.Background
import Element.Font
import Iddict
import Items.VaultList as VaultList
import Matrix
import Recursion
import Screen.Vault as VaultScreen
import Screen.Welcome as WelcomeScreen
import Task import Task
import Theme import Theme
import Items.LoginScreen as LoginScreen
import Items.VaultScreen as VaultScreen
import Items.WelcomeScreen as WelcomeScreen
import Element.Font
import Items.VaultScreen as VaultScreen
main : Program () Model Msg main : Program () Model Msg
@ -28,6 +30,10 @@ main =
-- MODEL -- MODEL
type MatrixAction
= Sync
type alias Model = type alias Model =
{ flavor : Theme.Flavor { flavor : Theme.Flavor
, height : Int , height : Int
@ -35,26 +41,33 @@ type alias Model =
, width : Int , width : Int
} }
type Screen
= ScreenLogin LoginScreen.Model
| ScreenWelcome WelcomeScreen.Model
| ScreenVault WelcomeScreen.Model Int VaultScreen.Model
type Msg type Msg
= OnScreenLogin LoginScreen.Msg = OnMatrix MatrixAction Int Matrix.Vault
| OnReturnHome Vaults
| OnScreenVault Int VaultScreen.Msg | OnScreenVault Int VaultScreen.Msg
| OnScreenWelcome WelcomeScreen.Msg | OnScreenWelcome WelcomeScreen.Msg
| OnSelectVault Int | OnSelectVault Vaults Int
| OnVaultUpdate Int Matrix.VaultUpdate
| Pass | Pass
| ScreenSize { height : Int, width : Int } | ScreenSize { height : Int, width : Int }
| SetFlavor Theme.Flavor | SetFlavor Theme.Flavor
type Screen
= ScreenWelcome WelcomeScreen.Model
| ScreenVault Vaults Int VaultScreen.Model
type alias Vaults =
VaultList.Model
init : () -> ( Model, Cmd Msg ) init : () -> ( Model, Cmd Msg )
init () = init () =
( { flavor = Theme.Latte ( { flavor = Theme.Latte
, height = 480 , height = 480
, screen = ScreenWelcome (WelcomeScreen.init []) , screen = ScreenWelcome WelcomeScreen.init
, width = 720 , width = 720
} }
, Browser.Dom.getViewport , Browser.Dom.getViewport
@ -75,33 +88,30 @@ init () =
update : Msg -> Model -> ( Model, Cmd Msg ) update : Msg -> Model -> ( Model, Cmd Msg )
update msg model = update msg model =
case msg of case msg of
OnScreenLogin m -> OnMatrix Sync i vault ->
case model.screen of ( model, Matrix.sync (OnVaultUpdate i) vault )
ScreenLogin mdl ->
case LoginScreen.update m mdl of OnReturnHome vaults ->
newMdl -> ( { model | screen = ScreenWelcome (WelcomeScreen.fromVaults vaults) }
( { model | screen = ScreenLogin newMdl } , Cmd.none
, Cmd.none )
)
_ ->
( model, Cmd.none )
OnScreenVault i m -> OnScreenVault i m ->
case model.screen of case model.screen of
ScreenVault welcomeMdl j mdl -> ScreenVault welcomeMdl j mdl ->
if i == j then if i == j then
case VaultScreen.update m mdl of case VaultScreen.update m mdl of
( newMdl, cmd ) -> newMdl ->
( { model | screen = ScreenVault welcomeMdl i newMdl } ( { model | screen = ScreenVault welcomeMdl i newMdl }
, Cmd.map (OnScreenVault i) cmd , Cmd.none
) )
else else
( model, Cmd.none ) ( model, Cmd.none )
_ -> _ ->
( model, Cmd.none ) ( model, Cmd.none )
OnScreenWelcome m -> OnScreenWelcome m ->
case model.screen of case model.screen of
ScreenWelcome mdl -> ScreenWelcome mdl ->
@ -110,28 +120,41 @@ update msg model =
( { model | screen = ScreenWelcome newMdl } ( { model | screen = ScreenWelcome newMdl }
, Cmd.none , Cmd.none
) )
_ ->
( model, Cmd.none )
OnSelectVault i ->
case model.screen of
ScreenVault welcomeMdl j _ ->
if i == j then
( model, Cmd.none )
else
( { model | screen = ScreenVault welcomeMdl i VaultScreen.init }
, Cmd.none
)
ScreenWelcome mdl ->
( { model | screen = ScreenVault mdl i VaultScreen.init }
, Cmd.none
)
_ -> _ ->
( model, Cmd.none ) ( model, Cmd.none )
OnSelectVault vaults i ->
( { model
| screen =
case model.screen of
ScreenWelcome _ ->
ScreenVault vaults i VaultScreen.init
ScreenVault _ j old ->
if i == j then
ScreenVault vaults j old
else
ScreenVault vaults i VaultScreen.init
}
, Cmd.none
)
OnVaultUpdate i vu ->
( { model
| screen =
case model.screen of
ScreenWelcome mdl ->
WelcomeScreen.updateVault i vu mdl
|> ScreenWelcome
ScreenVault vaults j mdl ->
ScreenVault (VaultList.insertVaultUpdate i vu vaults) j mdl
}
, Cmd.none
)
Pass -> Pass ->
( model, Cmd.none ) ( model, Cmd.none )
@ -139,7 +162,7 @@ update msg model =
( { model | height = height, width = width } ( { model | height = height, width = width }
, Cmd.none , Cmd.none
) )
SetFlavor flavor -> SetFlavor flavor ->
( { model | flavor = flavor }, Cmd.none ) ( { model | flavor = flavor }, Cmd.none )
@ -172,56 +195,72 @@ view model =
|> List.singleton |> List.singleton
} }
viewScreen : Model -> Element Msg viewScreen : Model -> Element Msg
viewScreen model = viewScreen model =
case model.screen of let
ScreenLogin mdl -> colorBackground =
LoginScreen.view Theme.base model.flavor
{ colorBackground = Theme.base model.flavor
, colorMain = Theme.mauve model.flavor
, colorMenu = Theme.mantle model.flavor
, colorText = Theme.text model.flavor
, colorTextField = Theme.surface0 model.flavor
, flavor = model.flavor
, height = model.height
, model = mdl
, onFlavorPick = SetFlavor
, onSubmit = always Pass
, toMsg = OnScreenLogin
, width = model.width
}
ScreenVault welcomeMdl i mdl ->
case WelcomeScreen.getVault i welcomeMdl of
Just vault ->
VaultScreen.view
{ colorSelectedRoom = Theme.mantle model.flavor
, colorText = Theme.text model.flavor
, height = model.height
, model = mdl
, onVaultUpdate = always Pass
, toMsg = OnScreenVault i
, vault = vault
, width = model.width
}
Nothing ->
viewScreen { model | screen = ScreenWelcome welcomeMdl }
ScreenWelcome mdl -> colorBackground2 =
WelcomeScreen.view Theme.mantle model.flavor
{ colorBackground = Theme.base model.flavor
, colorBackground2 = Theme.mantle model.flavor
, colorMain = Theme.mauve model.flavor
, colorMenu = Theme.surface0 model.flavor
, colorText = Theme.text model.flavor
, colorTextField = Theme.surface1 model.flavor
, flavor = model.flavor
, height = model.height
, model = mdl
, onFlavorPick = SetFlavor
, onSelectVault = OnSelectVault
, toMsg = OnScreenWelcome
, width = model.width
}
colorMain =
Theme.mauve model.flavor
colorSurface0 =
Theme.surface0 model.flavor
colorSurface1 =
Theme.surface1 model.flavor
colorText =
Theme.text model.flavor
in
Recursion.runRecursion
(\screen ->
case screen of
ScreenVault vaults i mdl ->
case Iddict.get i vaults of
Just block ->
VaultScreen.view
{ colorBackground = colorBackground
, colorBackground2 = colorBackground2
, colorMain = colorMain
, colorText = colorText
, height = model.height
, logs = block.logs
, model = mdl
, onReturnToMenu = OnReturnHome vaults
, onSync = OnMatrix Sync i block.vault
, onVaultUpdate = OnVaultUpdate i
, toMsg = OnScreenVault i
, vault = block.vault
, width = model.width
}
|> Recursion.base
Nothing ->
WelcomeScreen.fromVaults vaults
|> ScreenWelcome
|> Recursion.recurse
ScreenWelcome mdl ->
WelcomeScreen.view
{ colorBackground = colorBackground
, colorBackground2 = colorBackground2
, colorMain = colorMain
, colorMenu = colorSurface0
, colorText = colorText
, colorTextField = colorSurface1
, flavor = model.flavor
, height = model.height
, model = mdl
, onFlavorPick = SetFlavor
, onSelectVault = OnSelectVault
, toMsg = OnScreenWelcome
, width = model.width
}
|> Recursion.base
)
model.screen

161
src/Screen/Vault.elm Normal file
View File

@ -0,0 +1,161 @@
module Screen.Vault exposing (..)
import Color exposing (Color)
import Element exposing (Element)
import Items.LogViewer as LogViewer
import Layout
import Material.Icons
import Matrix
-- MODEL
type alias Model =
Screen
type Msg
= GoToScreen Screen
type Screen
= Home
| Logs
init : Model
init =
Home
-- UPDATE
update : Msg -> Model -> Model
update msg _ =
case msg of
GoToScreen screen ->
screen
-- VIEW
view :
{ colorBackground : Color
, colorBackground2 : Color
, colorMain : Color
, colorText : Color
, height : Int
, logs : List { channel : String, content : String }
, model : Model
, onReturnToMenu : msg
, onSync : msg
, onVaultUpdate : Matrix.VaultUpdate -> msg
, toMsg : Msg -> msg
, vault : Matrix.Vault
, width : Int
}
-> Element msg
view data =
Element.row
[ Element.height (Element.px data.height)
, Element.width (Element.px data.width)
]
[ Layout.sideIconBar
{ colorBackground = data.colorBackground2
, colorText = data.colorText
, height = data.height
, items =
[ { icon = Layout.iconAsIcon Material.Icons.arrow_back
, onPress = data.onReturnToMenu
, text = "Return to menu"
}
, { icon = Layout.iconAsIcon Material.Icons.home
, onPress = data.toMsg (GoToScreen Home)
, text = "Home"
}
, { icon = Layout.iconAsIcon Material.Icons.inbox
, onPress = data.toMsg (GoToScreen Logs)
, text = "Logs"
}
]
, width = 100
}
, viewContent
{ colorMain = data.colorMain
, colorText = data.colorText
, height = data.height
, logs = data.logs
, model = data.model
, onSync = data.onSync
, vault = data.vault
, width = data.width - 100
}
]
viewContent :
{ colorMain : Color
, colorText : Color
, height : Int
, logs : List { channel : String, content : String }
, model : Model
, onSync : msg
, vault : Matrix.Vault
, width : Int
}
-> Element msg
viewContent data =
let
paddingSize =
30
in
Element.el [ Element.padding paddingSize ]
(case data.model of
Logs ->
LogViewer.viewRecent
{ height = data.height - 2 * paddingSize
, logs = data.logs
, width = data.width - 2 * paddingSize
}
Home ->
viewStartMenu
{ colorMain = data.colorMain
, colorText = data.colorText
, height = data.height - 2 * paddingSize
, onSync = data.onSync
, width = data.width - 2 * paddingSize
}
)
viewStartMenu :
{ colorMain : Color
, colorText : Color
, height : Int
, onSync : msg
, width : Int
}
-> Element msg
viewStartMenu data =
Element.column
[ Element.height (Element.px data.height)
, Element.spacing 5
, Element.width (Element.px data.width)
]
[ Layout.header "Start Menu"
, Layout.stdText "The elm-matrix-sdk vault uses the /sync endpoint to get the latest updates. Make sure to run this function to get the latest information."
, Layout.containedButton
{ buttonColor = data.colorMain
, clickColor = data.colorText
, icon = always Element.none
, onPress = Just data.onSync
, text = "SYNC"
}
]

179
src/Screen/Welcome.elm Normal file
View File

@ -0,0 +1,179 @@
module Screen.Welcome exposing (..)
import Color exposing (Color)
import Element exposing (Element)
import Items.Introduction as Introduction
import Items.ItemPicker as ItemPicker
import Items.LoginView as LoginView
import Items.VaultList as VaultList
import Layout
import Matrix
import Matrix.Settings
import Theme
-- MODEL
type alias Model =
{ login : Maybe LoginView.Model
, vaults : ItemPicker.Model VaultList.VaultBlock
}
type Msg
= OnAddNew
| OnLogin LoginView.Msg
| OnSubmitVault { name : String, vault : Matrix.Vault }
| OnVaultUpdate Int Matrix.VaultUpdate
| OnVaults ItemPicker.Msg
init : Model
init =
{ login = Nothing -- Just LoginView.init
, vaults = ItemPicker.init VaultList.init
}
fromVaults : VaultList.Model -> Model
fromVaults items =
{ login = Nothing
, vaults = ItemPicker.init items
}
-- UPDATE
update : Msg -> Model -> Model
update msg model =
case msg of
OnAddNew ->
{ model | login = Just LoginView.init }
OnLogin m ->
{ model | login = Maybe.map (LoginView.update m) model.login }
OnSubmitVault block ->
{ login = Nothing
, vaults = ItemPicker.updateContent (VaultList.addVault block) model.vaults
}
OnVaultUpdate i vu ->
{ model | vaults = ItemPicker.updateContent (VaultList.insertVaultUpdate i vu) model.vaults }
OnVaults m ->
{ model | vaults = ItemPicker.update m model.vaults }
updateVault : Int -> Matrix.VaultUpdate -> Model -> Model
updateVault i vu =
update (OnVaultUpdate i vu)
-- VIEW
view :
{ colorBackground : Color
, colorBackground2 : Color
, colorMain : Color
, colorMenu : Color
, colorText : Color
, colorTextField : Color
, flavor : Theme.Flavor
, height : Int
, model : Model
, onFlavorPick : Theme.Flavor -> msg
, onSelectVault : VaultList.Model -> Int -> msg
, toMsg : Msg -> msg
, width : Int
}
-> Element msg
view data =
let
onSelectVault =
data.onSelectVault (ItemPicker.extract data.model.vaults)
in
case data.model.login of
Just login ->
Layout.twoBlocks
{ height = data.height
, el1 =
\{ height, width } ->
viewIntroduction
{ colorBackground = data.colorBackground
, colorMenu = data.colorMenu
, colorText = data.colorText
, height = height
, model = data.model.vaults
, onAddNew = Nothing
, onSelectVault = onSelectVault
, width = width
}
, el2 =
\{ height, width } ->
LoginView.view
{ colorBackground = data.colorBackground2
, colorMain = data.colorMain
, colorMenu = data.colorMenu
, colorText = data.colorText
, colorTextField = data.colorTextField
, height = height
, flavor = data.flavor
, model = login
, onFlavorPick = data.onFlavorPick
, onSubmit = \vault -> OnSubmitVault { name = "New Vault", vault = vault } |> data.toMsg
, toMsg = OnLogin >> data.toMsg
, width = width
}
, width = data.width
}
Nothing ->
viewIntroduction
{ colorBackground = data.colorBackground
, colorMenu = data.colorMenu
, colorText = data.colorText
, height = data.height
, model = data.model.vaults
, onAddNew = Just (data.toMsg OnAddNew)
, onSelectVault = onSelectVault
, width = data.width
}
viewIntroduction :
{ colorBackground : Color
, colorMenu : Color
, colorText : Color
, height : Int
, model : ItemPicker.Model VaultList.VaultBlock
, onAddNew : Maybe msg
, onSelectVault : Int -> msg
, width : Int
}
-> Element msg
viewIntroduction data =
[ Introduction.view { colorBackground = data.colorBackground, width = data.width }
, ItemPicker.view
{ colorMenu = data.colorMenu
, colorText = data.colorText
, height = data.height
, model = data.model
, toText = \_ vb -> Matrix.Settings.getDeviceName vb.vault
, toTitle = \vid vb -> vb.name ++ " #" ++ String.fromInt (vid + 1)
, onAddNew = data.onAddNew
, onClick = data.onSelectVault
, width = data.width
}
]
|> Element.column
[ Element.height (Element.px data.height)
, Element.scrollbarY
, Element.width (Element.px data.width)
]