diff --git a/src/Items/FlavorPicker.elm b/src/Items/FlavorPicker.elm new file mode 100644 index 0000000..8096260 --- /dev/null +++ b/src/Items/FlavorPicker.elm @@ -0,0 +1,43 @@ +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 Html.Attributes +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 +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 + } + ) + diff --git a/src/Items/Introduction.elm b/src/Items/Introduction.elm new file mode 100644 index 0000000..fdc487e --- /dev/null +++ b/src/Items/Introduction.elm @@ -0,0 +1,45 @@ +module Items.Introduction exposing (..) + + +import Color exposing (Color) +import Element exposing (Element) +import Element.Background +import Widget.Material.Typography +import Theme + +-- MODEL + +type alias Model = () + +type alias Msg = () + +-- UPDATE + +-- SUBSCRIPTIONS + +-- VIEW + +view : + { colorBackground : Color + , width : Int + } -> 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." + ] + |> Element.column + [ Element.Background.color (Theme.toElmUiColor data.colorBackground) + , Element.padding 30 + , 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 [] diff --git a/src/Items/LoginScreen.elm b/src/Items/LoginScreen.elm new file mode 100644 index 0000000..691be94 --- /dev/null +++ b/src/Items/LoginScreen.elm @@ -0,0 +1,198 @@ +module Items.LoginScreen 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. +-} + +import Color exposing (Color) +import Element exposing (Element) +import Element.Background +import Element.Border +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 + , password : String + , username : String + } + +type Msg + = SetAccessToken String + | SetPassword String + | SetUsername String + | SwitchMethod LoginMethod + +type LoginMethod + = AccessToken + | Password + +init : Model +init = + { accessToken = "" + , loginMethod = AccessToken + , password = "" + , username = "" + } + +-- UPDATE + +update : Msg -> Model -> Model +update msg model = + case msg of + SetAccessToken name -> + { model | accessToken = name } + + SetPassword name -> + { model | password = name } + + SetUsername name -> + { model | username = name } + + SwitchMethod method -> + { model | loginMethod = method } + +-- VIEW + +view : + { colorBackground : Color + , colorMain : Color + , colorMenu : Color + , colorText : Color + , colorTextField : Color + , height : Int + , flavor : Theme.Flavor + , model : Model + , onFlavorPick : Theme.Flavor -> msg + , onSubmit : Matrix.Vault -> msg + , toMsg : Msg -> msg + , width : Int + } -> Element msg +view data = + [ viewLoginMethodPicker + { color = data.colorMain + , loginMethod = data.model.loginMethod + , toMsg = SwitchMethod >> data.toMsg + } + , Layout.textInput + { color = data.colorTextField + , label = "username" + , onChange = SetUsername >> data.toMsg + , placeholder = Just "@alice:example.org" + , text = data.model.username + } + , case data.model.loginMethod of + AccessToken -> + Layout.passwordInput + { color = data.colorTextField + , label = "access-token" + , onChange = SetAccessToken >> data.toMsg + , placeholder = Just "syt_p12smN_aR0KbfXxDr3RVESXNr3I_k5Ay88" + , show = False + , text = data.model.accessToken + } + + Password -> + Layout.passwordInput + { color = data.colorTextField + , label = "password" + , onChange = SetPassword >> data.toMsg + , placeholder = Just "Password" + , show = False + , 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 ] + ] + |> Element.column + [ Element.Background.color (Theme.toElmUiColor data.colorMenu) + , Element.Border.rounded 25 + , Element.centerX + , Element.centerY + , Element.height (Element.px 300) + , Element.padding 30 + , Element.spaceEvenly + , Element.width (Element.px 400) + ] + |> 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 ] + ) + , Element.height (Element.px data.height) + , Element.width (Element.px data.width) + ] + +toVault : Model -> Maybe Matrix.Vault +toVault model = + case model.loginMethod of + AccessToken -> + if model.accessToken == "" then + Nothing + + else + model.username + |> Matrix.fromUserId + |> Maybe.map (Matrix.Settings.setAccessToken model.accessToken) + + Password -> + if model.password == "" then + Nothing + + else + model.username + |> Matrix.fromUserId + |> Maybe.map (Matrix.Settings.setPassword model.password) + +viewLoginMethodPicker : { color : Color, loginMethod : LoginMethod, toMsg : LoginMethod -> msg } -> Element msg +viewLoginMethodPicker data = + Layout.tab + { color = data.color + , content = always Element.none + , items = + [ { icon = Layout.iconAsIcon Material.Icons.key + , 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 + } diff --git a/src/Items/VaultPicker.elm b/src/Items/VaultPicker.elm new file mode 100644 index 0000000..c11a277 --- /dev/null +++ b/src/Items/VaultPicker.elm @@ -0,0 +1,112 @@ +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) ] diff --git a/src/Items/VaultScreen.elm b/src/Items/VaultScreen.elm new file mode 100644 index 0000000..149e542 --- /dev/null +++ b/src/Items/VaultScreen.elm @@ -0,0 +1,170 @@ +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" + } + ) + } diff --git a/src/Items/WelcomeScreen.elm b/src/Items/WelcomeScreen.elm new file mode 100644 index 0000000..e063e03 --- /dev/null +++ b/src/Items/WelcomeScreen.elm @@ -0,0 +1,178 @@ +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) + ] diff --git a/src/Layout.elm b/src/Layout.elm new file mode 100644 index 0000000..cda396a --- /dev/null +++ b/src/Layout.elm @@ -0,0 +1,277 @@ +module Layout exposing + ( tab + , iconAsElement, iconAsIcon + , containedButton, outlinedButton, textButton, sideList + , textInput, passwordInput + , loadingIndicator, itemWithSubtext + ) + +{-| + + +# Layout + +The layout module exposes some boilerplate functions that have produce a +beautiful Material design Elm webpage. + + +## Elements + +@docs tab + +## Icons + +@docs iconAsElement, iconAsIcon + +## Buttons + +@docs containedButton, outlinedButton, textButton + + +## Text fields + +@docs textInput, passwordInput + +## Items in a list + +@docs itemWithSubtext + +## Lists + +@docs sideList + +## Other elements + +@docs loadingIndicator + +-} + +import Color exposing (Color) +import Element exposing (Element) +import Element.Input +import Widget +import Widget.Customize as Customize +import Widget.Icon exposing (Icon) +import Widget.Material as Material +import Material.Icons.Types + + +{-| A contained button representing the most important action of a group. +-} +containedButton : + { buttonColor : Color + , clickColor : Color + , icon : Icon msg + , onPress : Maybe msg + , text : String + } + -> Element msg +containedButton data = + Widget.button + ({ primary = data.buttonColor, onPrimary = data.clickColor } + |> singlePalette + |> Material.containedButton + |> Customize.elementButton [ Element.width Element.fill ] + ) + { text = data.text, icon = data.icon, onPress = data.onPress } + +iconAsElement : + { color : Color + , height : Int + , icon : Material.Icons.Types.Icon msg + , width : Int + } -> Element msg +iconAsElement data = + data.icon + |> iconAsIcon + |> (|>) { size = Basics.min data.height data.width, color = data.color } + |> Element.el [ Element.centerX, Element.centerY ] + |> Element.el + [ Element.height (Element.px data.height) + , 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 : + { color : Color + , leftIcon : Widget.Icon.Icon msg + , onPress : Maybe msg + , rightIcon : Widget.Icon.Icon msg + , text : String + , title : String + } + -> Widget.Item msg +itemWithSubtext data = + Widget.multiLineItem + ( { primary = data.color, onPrimary = data.color } + |> singlePalette + |> Material.multiLineItem + ) + { content = data.rightIcon + , icon = data.leftIcon + , onPress = data.onPress + , title = data.title + , text = data.text + } + +{-| Circular loading bar indicator +-} +loadingIndicator : + { color : Color + } + -> Element msg +loadingIndicator data = + Widget.circularProgressIndicator + ( { primary = data.color, onPrimary = data.color } + |> singlePalette + |> Material.progressIndicator + ) + Nothing + +{-| An outlined button representing an important action within a group. +-} +outlinedButton : + { color : Color + , icon : Icon msg + , onPress : Maybe msg + , text : String + } + -> Element msg +outlinedButton data = + Widget.button + ({ primary = data.color, onPrimary = data.color } + |> singlePalette + |> Material.outlinedButton + ) + { text = data.text, icon = data.icon, onPress = data.onPress } + +{-| Show a password field +-} +passwordInput : + { color : Color + , label : String + , onChange : String -> msg + , placeholder : Maybe String + , show : Bool + , text : String + } + -> Element msg +passwordInput data = + Widget.currentPasswordInputV2 + ({ primary = data.color, onPrimary = data.color } + |> singlePalette + |> Material.passwordInput + |> Customize.elementRow [ Element.width Element.fill ] + ) + { label = data.label + , onChange = data.onChange + , placeholder = + data.placeholder + |> Maybe.map Element.text + |> Maybe.map (Element.Input.placeholder []) + , show = data.show + , text = data.text + } + + +{-| Create a simple palette. +-} +singlePalette : { primary : Color, onPrimary : Color } -> Material.Palette +singlePalette { primary, onPrimary } = + { primary = primary + , secondary = primary + , background = primary + , surface = primary + , error = primary + , on = + { primary = onPrimary + , secondary = onPrimary + , background = onPrimary + , surface = onPrimary + , error = onPrimary + } + } + +sideList : { color : Color, items : List (Widget.Item msg) }-> Element msg +sideList data = + Widget.itemList + ( { primary = data.color, onPrimary = data.color } + |> singlePalette + |> Material.sideSheet + ) + data.items + +{-| A tab selector that always has an item selected. +-} +tab : + { color : Color + , content : Int -> Element msg + , items : List { text : String, icon : Icon msg } + , onSelect : Int -> msg + , selected : Int + } + -> Element msg +tab data = + Widget.tab + ({ primary = data.color, onPrimary = data.color } + |> singlePalette + |> Material.tab + ) + { tabs = + { onSelect = data.onSelect >> Just + , options = data.items + , selected = Just data.selected + } + , content = \_ -> data.content data.selected + } + + +{-| A text button representing an important action within a group. +-} +textButton : + { icon : Icon msg + , onPress : Maybe msg + , text : String + , color : Color + } + -> Element msg +textButton data = + Widget.button + ({ primary = data.color, onPrimary = data.color } + |> singlePalette + |> Material.textButton + ) + { text = data.text, icon = data.icon, onPress = data.onPress } + + +{-| Text input element. +-} +textInput : + { color : Color + , label : String + , onChange : String -> msg + , placeholder : Maybe String + , text : String + } + -> Element msg +textInput data = + Widget.textInput + ({ primary = data.color, onPrimary = data.color } + |> singlePalette + |> Material.textInput + |> Customize.elementRow [ Element.width Element.fill ] + ) + { chips = [] + , text = data.text + , placeholder = + data.placeholder + |> Maybe.map Element.text + |> Maybe.map (Element.Input.placeholder []) + , label = data.label + , onChange = data.onChange + } diff --git a/src/Main.elm b/src/Main.elm new file mode 100644 index 0000000..7548657 --- /dev/null +++ b/src/Main.elm @@ -0,0 +1,227 @@ +module Main exposing (..) + +import Browser +import Browser.Dom +import Browser.Events +import Element exposing (Element) +import Element.Background +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 +main = + Browser.document + { init = init + , subscriptions = subscriptions + , update = update + , view = view + } + + + +-- MODEL + + +type alias Model = + { flavor : Theme.Flavor + , height : Int + , screen : Screen + , width : Int + } + +type Screen + = ScreenLogin LoginScreen.Model + | ScreenWelcome WelcomeScreen.Model + | ScreenVault WelcomeScreen.Model Int VaultScreen.Model + +type Msg + = OnScreenLogin LoginScreen.Msg + | OnScreenVault Int VaultScreen.Msg + | OnScreenWelcome WelcomeScreen.Msg + | OnSelectVault Int + | Pass + | ScreenSize { height : Int, width : Int } + | SetFlavor Theme.Flavor + + +init : () -> ( Model, Cmd Msg ) +init () = + ( { flavor = Theme.Latte + , height = 480 + , screen = ScreenWelcome (WelcomeScreen.init []) + , width = 720 + } + , Browser.Dom.getViewport + |> Task.perform + (\viewport -> + ScreenSize + { height = floor viewport.viewport.height + , width = floor viewport.viewport.width + } + ) + ) + + + +-- UPDATE + + +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 + ) + + _ -> + ( 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 ) -> + ( { model | screen = ScreenVault welcomeMdl i newMdl } + , Cmd.map (OnScreenVault i) cmd + ) + else + ( model, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + OnScreenWelcome m -> + case model.screen of + ScreenWelcome mdl -> + case WelcomeScreen.update m mdl of + newMdl -> + ( { 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 ) + + Pass -> + ( model, Cmd.none ) + + ScreenSize { height, width } -> + ( { model | height = height, width = width } + , Cmd.none + ) + + SetFlavor flavor -> + ( { model | flavor = flavor }, Cmd.none ) + + + +-- SUBSCRIPTIONS + + +subscriptions : Model -> Sub Msg +subscriptions _ = + Sub.batch + [ Browser.Events.onResize (\w h -> ScreenSize { width = w, height = h }) + ] + + + +-- VIEW + + +view : Model -> Browser.Document Msg +view model = + { title = "Matrix Plier" + , body = + model + |> viewScreen + |> Element.layout + [ Element.Background.color (Theme.baseUI model.flavor) + , Element.Font.color (Theme.textUI model.flavor) + ] + |> 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 } + + 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 + } + diff --git a/src/Theme.elm b/src/Theme.elm new file mode 100644 index 0000000..027428d --- /dev/null +++ b/src/Theme.elm @@ -0,0 +1,583 @@ +module Theme exposing (..) + +{-| + + +# Theme + +The Theme helps pick colors from different color schemes. + +-} + +import Catppuccin.Frappe as CF +import Catppuccin.Latte as CL +import Catppuccin.Macchiato as CA +import Catppuccin.Mocha as CO +import Color +import Element +import Element.Background + + +{-| Catppuccin flavor used for display. +-} +type Flavor + = Frappe + | Latte + | Macchiato + | Mocha + + +background : (Flavor -> Color.Color) -> Flavor -> Element.Attribute msg +background toColor = + toColor >> toElmUiColor >> Element.Background.color + + +toElmUiColor : Color.Color -> Element.Color +toElmUiColor = + Color.toRgba >> Element.fromRgb + + +rosewater : Flavor -> Color.Color +rosewater flavor = + case flavor of + Frappe -> + CF.rosewater + + Latte -> + CL.rosewater + + Macchiato -> + CA.rosewater + + Mocha -> + CO.rosewater + + +rosewaterUI : Flavor -> Element.Color +rosewaterUI = + rosewater >> toElmUiColor + + +flamingo : Flavor -> Color.Color +flamingo flavor = + case flavor of + Frappe -> + CF.flamingo + + Latte -> + CL.flamingo + + Macchiato -> + CA.flamingo + + Mocha -> + CO.flamingo + + +flamingoUI : Flavor -> Element.Color +flamingoUI = + flamingo >> toElmUiColor + + +pink : Flavor -> Color.Color +pink flavor = + case flavor of + Frappe -> + CF.pink + + Latte -> + CL.pink + + Macchiato -> + CA.pink + + Mocha -> + CO.pink + + +pinkUI : Flavor -> Element.Color +pinkUI = + pink >> toElmUiColor + + +mauve : Flavor -> Color.Color +mauve flavor = + case flavor of + Frappe -> + CF.mauve + + Latte -> + CL.mauve + + Macchiato -> + CA.mauve + + Mocha -> + CO.mauve + + +mauveUI : Flavor -> Element.Color +mauveUI = + mauve >> toElmUiColor + + +red : Flavor -> Color.Color +red flavor = + case flavor of + Frappe -> + CF.red + + Latte -> + CL.red + + Macchiato -> + CA.red + + Mocha -> + CO.red + + +redUI : Flavor -> Element.Color +redUI = + red >> toElmUiColor + + +maroon : Flavor -> Color.Color +maroon flavor = + case flavor of + Frappe -> + CF.maroon + + Latte -> + CL.maroon + + Macchiato -> + CA.maroon + + Mocha -> + CO.maroon + + +maroonUI : Flavor -> Element.Color +maroonUI = + maroon >> toElmUiColor + + +peach : Flavor -> Color.Color +peach flavor = + case flavor of + Frappe -> + CF.peach + + Latte -> + CL.peach + + Macchiato -> + CA.peach + + Mocha -> + CO.peach + + +peachUI : Flavor -> Element.Color +peachUI = + peach >> toElmUiColor + + +yellow : Flavor -> Color.Color +yellow flavor = + case flavor of + Frappe -> + CF.yellow + + Latte -> + CL.yellow + + Macchiato -> + CA.yellow + + Mocha -> + CO.yellow + + +yellowUI : Flavor -> Element.Color +yellowUI = + yellow >> toElmUiColor + + +green : Flavor -> Color.Color +green flavor = + case flavor of + Frappe -> + CF.green + + Latte -> + CL.green + + Macchiato -> + CA.green + + Mocha -> + CO.green + + +greenUI : Flavor -> Element.Color +greenUI = + green >> toElmUiColor + + +teal : Flavor -> Color.Color +teal flavor = + case flavor of + Frappe -> + CF.teal + + Latte -> + CL.teal + + Macchiato -> + CA.teal + + Mocha -> + CO.teal + + +tealUI : Flavor -> Element.Color +tealUI = + teal >> toElmUiColor + + +sky : Flavor -> Color.Color +sky flavor = + case flavor of + Frappe -> + CF.sky + + Latte -> + CL.sky + + Macchiato -> + CA.sky + + Mocha -> + CO.sky + + +skyUI : Flavor -> Element.Color +skyUI = + sky >> toElmUiColor + + +sapphire : Flavor -> Color.Color +sapphire flavor = + case flavor of + Frappe -> + CF.sapphire + + Latte -> + CL.sapphire + + Macchiato -> + CA.sapphire + + Mocha -> + CO.sapphire + + +sapphireUI : Flavor -> Element.Color +sapphireUI = + sapphire >> toElmUiColor + + +blue : Flavor -> Color.Color +blue flavor = + case flavor of + Frappe -> + CF.blue + + Latte -> + CL.blue + + Macchiato -> + CA.blue + + Mocha -> + CO.blue + + +blueUI : Flavor -> Element.Color +blueUI = + blue >> toElmUiColor + + +lavender : Flavor -> Color.Color +lavender flavor = + case flavor of + Frappe -> + CF.lavender + + Latte -> + CL.lavender + + Macchiato -> + CA.lavender + + Mocha -> + CO.lavender + + +lavenderUI : Flavor -> Element.Color +lavenderUI = + lavender >> toElmUiColor + + +text : Flavor -> Color.Color +text flavor = + case flavor of + Frappe -> + CF.text + + Latte -> + CL.text + + Macchiato -> + CA.text + + Mocha -> + CO.text + + +textUI : Flavor -> Element.Color +textUI = + text >> toElmUiColor + + +subtext1 : Flavor -> Color.Color +subtext1 flavor = + case flavor of + Frappe -> + CF.subtext1 + + Latte -> + CL.subtext1 + + Macchiato -> + CA.subtext1 + + Mocha -> + CO.subtext1 + + +subtext1UI : Flavor -> Element.Color +subtext1UI = + subtext1 >> toElmUiColor + + +subtext0 : Flavor -> Color.Color +subtext0 flavor = + case flavor of + Frappe -> + CF.subtext0 + + Latte -> + CL.subtext0 + + Macchiato -> + CA.subtext0 + + Mocha -> + CO.subtext0 + + +subtext0UI : Flavor -> Element.Color +subtext0UI = + subtext0 >> toElmUiColor + + +overlay2 : Flavor -> Color.Color +overlay2 flavor = + case flavor of + Frappe -> + CF.overlay2 + + Latte -> + CL.overlay2 + + Macchiato -> + CA.overlay2 + + Mocha -> + CO.overlay2 + + +overlay2UI : Flavor -> Element.Color +overlay2UI = + overlay2 >> toElmUiColor + + +overlay1 : Flavor -> Color.Color +overlay1 flavor = + case flavor of + Frappe -> + CF.overlay1 + + Latte -> + CL.overlay1 + + Macchiato -> + CA.overlay1 + + Mocha -> + CO.overlay1 + + +overlay1UI : Flavor -> Element.Color +overlay1UI = + overlay1 >> toElmUiColor + + +overlay0 : Flavor -> Color.Color +overlay0 flavor = + case flavor of + Frappe -> + CF.overlay0 + + Latte -> + CL.overlay0 + + Macchiato -> + CA.overlay0 + + Mocha -> + CO.overlay0 + + +overlay0UI : Flavor -> Element.Color +overlay0UI = + overlay0 >> toElmUiColor + + +surface2 : Flavor -> Color.Color +surface2 flavor = + case flavor of + Frappe -> + CF.surface2 + + Latte -> + CL.surface2 + + Macchiato -> + CA.surface2 + + Mocha -> + CO.surface2 + + +surface2UI : Flavor -> Element.Color +surface2UI = + surface2 >> toElmUiColor + + +surface1 : Flavor -> Color.Color +surface1 flavor = + case flavor of + Frappe -> + CF.surface1 + + Latte -> + CL.surface1 + + Macchiato -> + CA.surface1 + + Mocha -> + CO.surface1 + + +surface1UI : Flavor -> Element.Color +surface1UI = + surface1 >> toElmUiColor + + +surface0 : Flavor -> Color.Color +surface0 flavor = + case flavor of + Frappe -> + CF.surface0 + + Latte -> + CL.surface0 + + Macchiato -> + CA.surface0 + + Mocha -> + CO.surface0 + + +surface0UI : Flavor -> Element.Color +surface0UI = + surface0 >> toElmUiColor + + +base : Flavor -> Color.Color +base flavor = + case flavor of + Frappe -> + CF.base + + Latte -> + CL.base + + Macchiato -> + CA.base + + Mocha -> + CO.base + + +baseUI : Flavor -> Element.Color +baseUI = + base >> toElmUiColor + + +mantle : Flavor -> Color.Color +mantle flavor = + case flavor of + Frappe -> + CF.mantle + + Latte -> + CL.mantle + + Macchiato -> + CA.mantle + + Mocha -> + CO.mantle + + +mantleUI : Flavor -> Element.Color +mantleUI = + mantle >> toElmUiColor + + +crust : Flavor -> Color.Color +crust flavor = + case flavor of + Frappe -> + CF.crust + + Latte -> + CL.crust + + Macchiato -> + CA.crust + + Mocha -> + CO.crust + + +crustUI : Flavor -> Element.Color +crustUI = + crust >> toElmUiColor