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

View File

@ -1,43 +1,58 @@
module Items.FlavorPicker exposing (..)
{-| This module allows the user to pick whatever flavor they want to use.
-}
import Color exposing (Color)
import Element exposing (Element)
import Element.Events
import Html.Attributes
import Layout
import Material.Icons
import Theme exposing (Flavor(..))
import Layout
import Element.Events
-- MODEL
-- UPDATE
-- VIEW
view :
{ height : Int
, flavor : Flavor
, onClick : Flavor -> msg
, themeIcon : Flavor -> Color
, width : Int
} -> Element msg
}
-> Element msg
view data =
let
lightMode = data.flavor == Latte
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
}
)
lightMode =
data.flavor == Latte
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 (..)
import Color exposing (Color)
import Element exposing (Element)
import Element.Background
import Widget.Material.Typography
import Layout
import Theme
import Widget.Material.Typography
-- MODEL
type alias Model = ()
type alias Msg = ()
type alias Model =
()
type alias Msg =
()
-- UPDATE
-- SUBSCRIPTIONS
-- VIEW
view :
{ colorBackground : Color
, width : Int
} -> Element msg
}
-> Element msg
view data =
[ header "Martiplier"
, text "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.header "Martiplier"
, Layout.stdText "Martiplier (short for Matrix Plier) is a unique client. It doesn't let you browse rooms and have chat conversations."
, 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.Background.color (Theme.toElmUiColor data.colorBackground)
@ -34,12 +42,3 @@ view data =
, Element.spacing 20
, 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
of what to expect from the Matrix client.
-}
@ -7,15 +8,18 @@ import Color exposing (Color)
import Element exposing (Element)
import Element.Background
import Element.Border
import Items.FlavorPicker as FlavorPicker
import Layout
import Material.Icons
import Matrix
import Matrix.Settings
import Theme
import Layout
import Items.FlavorPicker as FlavorPicker
import Material.Icons
-- MODEL
type alias Model =
{ accessToken : String
, loginMethod : LoginMethod
@ -23,16 +27,19 @@ type alias Model =
, username : String
}
type Msg
= SetAccessToken String
| SetPassword String
| SetUsername String
| SwitchMethod LoginMethod
type LoginMethod
= AccessToken
| Password
init : Model
init =
{ accessToken = ""
@ -41,8 +48,11 @@ init =
, username = ""
}
-- UPDATE
update : Msg -> Model -> Model
update msg model =
case msg of
@ -58,8 +68,11 @@ update msg model =
SwitchMethod method ->
{ model | loginMethod = method }
-- VIEW
view :
{ colorBackground : Color
, colorMain : Color
@ -73,7 +86,8 @@ view :
, onSubmit : Matrix.Vault -> msg
, toMsg : Msg -> msg
, width : Int
} -> Element msg
}
-> Element msg
view data =
[ viewLoginMethodPicker
{ color = data.colorMain
@ -97,7 +111,7 @@ view data =
, show = False
, text = data.model.accessToken
}
Password ->
Layout.passwordInput
{ color = data.colorTextField
@ -108,17 +122,17 @@ view data =
, text = data.model.password
}
, Layout.containedButton
{ buttonColor = data.colorMain
, clickColor = data.colorText
, icon = always Element.none
, onPress =
data.model
|> toVault
|> Maybe.map data.onSubmit
, text = "LOG IN"
}
|> Element.el
[ Element.centerX ]
{ buttonColor = data.colorMain
, clickColor = data.colorText
, icon = always Element.none
, onPress =
data.model
|> toVault
|> Maybe.map data.onSubmit
, text = "LOG IN"
}
|> Element.el
[ Element.centerX ]
]
|> Element.column
[ Element.Background.color (Theme.toElmUiColor data.colorMenu)
@ -133,7 +147,7 @@ view data =
|> Element.el
[ Element.Background.color (Theme.toElmUiColor data.colorBackground)
, Element.inFront
( FlavorPicker.view
(FlavorPicker.view
{ height = 30
, flavor = data.flavor
, onClick = data.onFlavorPick
@ -147,6 +161,7 @@ view data =
, Element.width (Element.px data.width)
]
toVault : Model -> Maybe Matrix.Vault
toVault model =
case model.loginMethod of
@ -168,6 +183,7 @@ toVault model =
|> Matrix.fromUserId
|> Maybe.map (Matrix.Settings.setPassword model.password)
viewLoginMethodPicker : { color : Color, loginMethod : LoginMethod, toMsg : LoginMethod -> msg } -> Element msg
viewLoginMethodPicker data =
Layout.tab
@ -175,24 +191,24 @@ viewLoginMethodPicker data =
, content = always Element.none
, items =
[ { icon = Layout.iconAsIcon Material.Icons.key
, text = "Access token"
, text = "Access token"
}
, { icon = Layout.iconAsIcon Material.Icons.password
, text = "Password"
}
]
, onSelect =
(\i ->
if i == 0 then
data.toMsg AccessToken
else
data.toMsg Password
)
, selected =
case data.loginMethod of
AccessToken ->
0
Password ->
1
\i ->
if i == 0 then
data.toMsg AccessToken
else
data.toMsg Password
, selected =
case data.loginMethod of
AccessToken ->
0
Password ->
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
( tab
( twoBlocks
, tab, sideIconBar
, iconAsElement, iconAsIcon
, containedButton, outlinedButton, textButton, sideList
, containedButton, outlinedButton, textButton
, 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.
## Screen layout
@docs twoBlocks
## Elements
@docs tab
@docs tab, sideIconBar
## Icons
@docs iconAsElement, iconAsIcon
## Buttons
@docs containedButton, outlinedButton, textButton
@ -32,14 +43,22 @@ beautiful Material design Elm webpage.
@docs textInput, passwordInput
## Text
@docs header, stdText
## Items in a list
@docs itemWithSubtext
## Lists
@docs sideList
## Other elements
@docs loadingIndicator
@ -48,12 +67,18 @@ beautiful Material design Elm webpage.
import Color exposing (Color)
import Element exposing (Element)
import Element.Background
import Element.Events
import Element.Font
import Element.Input
import Html.Attributes
import Material.Icons.Types
import Theme
import Widget
import Widget.Customize as Customize
import Widget.Icon exposing (Icon)
import Widget.Material as Material
import Material.Icons.Types
import Widget.Material.Typography
{-| 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 }
header : String -> Element msg
header =
Element.text
>> Element.el Widget.Material.Typography.h1
>> List.singleton
>> Element.paragraph []
iconAsElement :
{ color : Color
, height : Int
, icon : Material.Icons.Types.Icon msg
, width : Int
} -> Element msg
}
-> Element msg
iconAsElement data =
data.icon
|> iconAsIcon
@ -91,10 +126,12 @@ iconAsElement data =
, Element.width (Element.px data.width)
]
iconAsIcon : Material.Icons.Types.Icon msg -> Widget.Icon.Icon msg
iconAsIcon =
Widget.Icon.elmMaterialIcons Material.Icons.Types.Color
{-| Multiline item
-}
itemWithSubtext :
@ -105,10 +142,10 @@ itemWithSubtext :
, text : String
, title : String
}
-> Widget.Item msg
-> Widget.Item msg
itemWithSubtext data =
Widget.multiLineItem
( { primary = data.color, onPrimary = data.color }
({ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.multiLineItem
)
@ -119,20 +156,22 @@ itemWithSubtext data =
, text = data.text
}
{-| Circular loading bar indicator
-}
loadingIndicator :
{ color : Color
}
-> Element msg
-> Element msg
loadingIndicator data =
Widget.circularProgressIndicator
( { primary = data.color, onPrimary = data.color }
({ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.progressIndicator
)
Nothing
{-| An outlined button representing an important action within a group.
-}
outlinedButton :
@ -150,6 +189,7 @@ outlinedButton data =
)
{ text = data.text, icon = data.icon, onPress = data.onPress }
{-| Show a password field
-}
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 =
let
width px =
Element.width (Element.px px)
in
Widget.itemList
( { primary = data.color, onPrimary = data.color }
({ primary = data.color, onPrimary = data.color }
|> singlePalette
|> Material.sideSheet
)
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.
-}
@ -275,3 +380,48 @@ textInput data =
, label = data.label
, 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 Element exposing (Element)
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 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
@ -28,6 +30,10 @@ main =
-- MODEL
type MatrixAction
= Sync
type alias Model =
{ flavor : Theme.Flavor
, height : Int
@ -35,26 +41,33 @@ type alias Model =
, width : Int
}
type Screen
= ScreenLogin LoginScreen.Model
| ScreenWelcome WelcomeScreen.Model
| ScreenVault WelcomeScreen.Model Int VaultScreen.Model
type Msg
= OnScreenLogin LoginScreen.Msg
= OnMatrix MatrixAction Int Matrix.Vault
| OnReturnHome Vaults
| OnScreenVault Int VaultScreen.Msg
| OnScreenWelcome WelcomeScreen.Msg
| OnSelectVault Int
| OnSelectVault Vaults Int
| OnVaultUpdate Int Matrix.VaultUpdate
| Pass
| ScreenSize { height : Int, width : Int }
| SetFlavor Theme.Flavor
type Screen
= ScreenWelcome WelcomeScreen.Model
| ScreenVault Vaults Int VaultScreen.Model
type alias Vaults =
VaultList.Model
init : () -> ( Model, Cmd Msg )
init () =
( { flavor = Theme.Latte
, height = 480
, screen = ScreenWelcome (WelcomeScreen.init [])
, screen = ScreenWelcome WelcomeScreen.init
, width = 720
}
, Browser.Dom.getViewport
@ -75,33 +88,30 @@ init () =
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnScreenLogin m ->
case model.screen of
ScreenLogin mdl ->
case LoginScreen.update m mdl of
newMdl ->
( { model | screen = ScreenLogin newMdl }
, Cmd.none
)
OnMatrix Sync i vault ->
( model, Matrix.sync (OnVaultUpdate i) vault )
OnReturnHome vaults ->
( { model | screen = ScreenWelcome (WelcomeScreen.fromVaults vaults) }
, Cmd.none
)
_ ->
( model, Cmd.none )
OnScreenVault i m ->
case model.screen of
ScreenVault welcomeMdl j mdl ->
if i == j then
case VaultScreen.update m mdl of
( newMdl, cmd ) ->
newMdl ->
( { model | screen = ScreenVault welcomeMdl i newMdl }
, Cmd.map (OnScreenVault i) cmd
, Cmd.none
)
else
( model, Cmd.none )
_ ->
( model, Cmd.none )
OnScreenWelcome m ->
case model.screen of
ScreenWelcome mdl ->
@ -110,28 +120,41 @@ update msg model =
( { model | screen = ScreenWelcome newMdl }
, 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 )
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 ->
( model, Cmd.none )
@ -139,7 +162,7 @@ update msg model =
( { model | height = height, width = width }
, Cmd.none
)
SetFlavor flavor ->
( { model | flavor = flavor }, Cmd.none )
@ -172,56 +195,72 @@ view model =
|> List.singleton
}
viewScreen : Model -> Element Msg
viewScreen model =
case model.screen of
ScreenLogin mdl ->
LoginScreen.view
{ 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 }
let
colorBackground =
Theme.base model.flavor
ScreenWelcome mdl ->
WelcomeScreen.view
{ 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
}
colorBackground2 =
Theme.mantle model.flavor
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)
]