diff --git a/assets/1F993.png b/assets/1F993.png new file mode 100644 index 0000000..290f365 Binary files /dev/null and b/assets/1F993.png differ diff --git a/assets/Atkinson-Hyperlegible-Bold-102.ttf b/assets/Atkinson-Hyperlegible-Bold-102.ttf new file mode 100644 index 0000000..14b7196 Binary files /dev/null and b/assets/Atkinson-Hyperlegible-Bold-102.ttf differ diff --git a/assets/Atkinson-Hyperlegible-BoldItalic-102.ttf b/assets/Atkinson-Hyperlegible-BoldItalic-102.ttf new file mode 100644 index 0000000..4532705 Binary files /dev/null and b/assets/Atkinson-Hyperlegible-BoldItalic-102.ttf differ diff --git a/assets/Atkinson-Hyperlegible-Font-License-2020-1104.pdf b/assets/Atkinson-Hyperlegible-Font-License-2020-1104.pdf new file mode 100644 index 0000000..afe27dc Binary files /dev/null and b/assets/Atkinson-Hyperlegible-Font-License-2020-1104.pdf differ diff --git a/assets/Atkinson-Hyperlegible-Italic-102.ttf b/assets/Atkinson-Hyperlegible-Italic-102.ttf new file mode 100644 index 0000000..89e5ce4 Binary files /dev/null and b/assets/Atkinson-Hyperlegible-Italic-102.ttf differ diff --git a/assets/Atkinson-Hyperlegible-Regular-102.ttf b/assets/Atkinson-Hyperlegible-Regular-102.ttf new file mode 100644 index 0000000..c4fa6fb Binary files /dev/null and b/assets/Atkinson-Hyperlegible-Regular-102.ttf differ diff --git a/assets/OpenMoji-color-glyf_colr_1.ttf b/assets/OpenMoji-color-glyf_colr_1.ttf new file mode 100644 index 0000000..86cf85b Binary files /dev/null and b/assets/OpenMoji-color-glyf_colr_1.ttf differ diff --git a/assets/remixicon.ttf b/assets/remixicon.ttf new file mode 100644 index 0000000..22ce6de Binary files /dev/null and b/assets/remixicon.ttf differ diff --git a/src/Zenith/GUI.hs b/src/Zenith/GUI.hs index 89c993d..f5d5f33 100644 --- a/src/Zenith/GUI.hs +++ b/src/Zenith/GUI.hs @@ -11,6 +11,7 @@ import Database.Persist import Lens.Micro ((&), (+~), (.~), (?~), (^.), set) import Lens.Micro.TH import Monomer +import qualified Monomer.Lens as L import TextShow import ZcashHaskell.Types ( ZcashNet(..) @@ -19,12 +20,25 @@ import ZcashHaskell.Types ) import Zenith.Core import Zenith.DB -import Zenith.Types +import Zenith.GUI.Theme +import Zenith.Types hiding (ZcashAddress) +import Zenith.Utils (displayAmount, showAddress) data AppModel = AppModel - { _network :: !ZcashNet + { _configuration :: !Config + , _network :: !ZcashNet , _wallets :: ![Entity ZcashWallet] + , _selWallet :: !Int + , _accounts :: ![Entity ZcashAccount] + , _selAcc :: !Int + , _addresses :: ![Entity WalletAddress] + , _selAddr :: !Int + , _transactions :: ![Entity UserTx] + , _setTx :: !Int , _msg :: !(Maybe T.Text) + , _zebraOn :: !Bool + , _balance :: !Integer + , _unconfBalance :: !(Maybe Integer) } deriving (Eq, Show) makeLenses ''AppModel @@ -33,21 +47,133 @@ data AppEvent = AppInit | ShowMsg !T.Text | CloseMsg + | WalletClicked + | AccountClicked deriving (Eq, Show) +remixArrowRightWideLine :: T.Text +remixArrowRightWideLine = toGlyph 0xF496 + +remixHourglassFill :: T.Text +remixHourglassFill = toGlyph 0xF338 + +remixIcon :: T.Text -> WidgetNode s e +remixIcon i = label i `styleBasic` [textFont "Remix", textMiddle] + buildUI :: WidgetEnv AppModel AppEvent -> AppModel -> WidgetNode AppModel AppEvent buildUI wenv model = widgetTree where + btnColor = rgbHex "#1818B2" + btnHiLite = rgbHex "#207DE8" + currentWallet = + if null (model ^. wallets) + then Nothing + else Just ((model ^. wallets) !! (model ^. selWallet)) + currentAccount = + if null (model ^. accounts) + then Nothing + else Just ((model ^. accounts) !! (model ^. selAcc)) widgetTree = - zstack - [ vstack - [ label "Hello World" - , spacer - , hstack [button "Try the overlay" $ ShowMsg "It works!"] - ] `styleBasic` - [padding 10] - , msgOverlay `nodeVisible` isJust (model ^. msg) + zstack [mainWindow, msgOverlay `nodeVisible` isJust (model ^. msg)] + mainWindow = + vstack + [ windowHeader + , spacer + , balanceBox + , filler + , mainPane + , filler + , windowFooter + ] + windowHeader = + hstack + [ box_ [onClick WalletClicked, alignMiddle] walletButton `styleBasic` + [cursorHand, height 25, padding 3] `styleHover` + [bgColor btnHiLite] + , box_ [onClick AccountClicked, alignMiddle] accountButton `styleBasic` + [cursorHand, height 25, padding 3] `styleHover` + [bgColor btnHiLite] + , filler + , remixIcon remixErrorWarningFill + , label "Testnet" `nodeVisible` (model ^. network == TestNet) + ] `styleBasic` + [bgColor btnColor] + walletButton = + hstack + [ label "Wallet: " `styleBasic` [textFont "Bold", textColor white] + , label (maybe "None" (zcashWalletName . entityVal) currentWallet) `styleBasic` + [textFont "Regular", textColor white] + , remixIcon remixArrowRightWideLine `styleBasic` [textColor white] + ] + accountButton = + hstack + [ label "Account: " `styleBasic` [textFont "Bold", textColor white] + , label (maybe "None" (zcashAccountName . entityVal) currentAccount) `styleBasic` + [textFont "Regular", textColor white] + , remixIcon remixArrowRightWideLine `styleBasic` [textColor white] + ] + mainPane = box_ [alignMiddle] $ hstack [addressBox, txBox] + balanceBox = + box_ + [alignMiddle] + (vstack + [ animFadeIn + (label (displayAmount (model ^. network) $ model ^. balance) `styleBasic` + [textSize 20]) + , hstack + [ filler + , remixIcon remixHourglassFill `styleBasic` [textSize 8] + , label + (maybe "0" (displayAmount (model ^. network)) $ + model ^. unconfBalance) `styleBasic` + [textSize 8] `nodeVisible` + isJust (model ^. unconfBalance) + , filler + ] + ]) + addressBox = + box_ + [alignMiddle] + (vstack + [ label "Addresses:" `styleBasic` [textFont "Regular"] + , vscroll (vstack (zipWith addrRow [0 ..] (model ^. addresses))) `nodeKey` + "addrScroll" + ]) `styleBasic` + [border 1 whiteSmoke] + addrRow :: Int -> Entity WalletAddress -> WidgetNode AppModel AppEvent + addrRow idx wAddr = + box_ + [onClick $ ShowMsg ("You clicked address " <> showt idx)] + (label + (walletAddressName (entityVal wAddr) <> + ": " <> showAddress (walletAddressUAddress $ entityVal wAddr))) + txBox = + box_ + [alignMiddle] + (vstack + [ label "Transactions:" `styleBasic` [textFont "Regular"] + , label "2024-04-05 0.003 ZEC" `styleBasic` [textFont "Regular"] + ]) `styleBasic` + [border 1 whiteSmoke] + windowFooter = + hstack + [ label + ("Last block sync: " <> + maybe "N/A" (showt . zcashWalletLastSync . entityVal) currentWallet) `styleBasic` + [padding 3, textSize 8] + , filler + , image_ "./assets/1F993.png" [fitHeight] `styleBasic` + [height 24, width 24] `nodeVisible` + (model ^. zebraOn) + , label + ("Connected on " <> + c_zebraHost (model ^. configuration) <> + ":" <> showt (c_zebraPort $ model ^. configuration)) `styleBasic` + [padding 3, textSize 8] `nodeVisible` + (model ^. zebraOn) + , label "Disconnected" `styleBasic` [padding 3, textSize 8] `nodeVisible` + not (model ^. zebraOn) ] msgOverlay = alert CloseMsg $ @@ -63,6 +189,8 @@ handleEvent wenv node model evt = case evt of AppInit -> [] ShowMsg t -> [Model $ model & msg ?~ t] + WalletClicked -> [Model $ model & msg ?~ "You clicked Wallet!"] + AccountClicked -> [Model $ model & msg ?~ "You clicked Account!"] CloseMsg -> [Model $ model & msg .~ Nothing] runZenithGUI :: Config -> IO () @@ -82,18 +210,65 @@ runZenithGUI config = do Right chainInfo -> do initDb dbFilePath walList <- getWallets pool $ zgb_net chainInfo - let model = AppModel (zgb_net chainInfo) walList Nothing + accList <- + if not (null walList) + then runNoLoggingT $ getAccounts pool $ entityKey $ head walList + else return [] + addrList <- + if not (null accList) + then runNoLoggingT $ getAddresses pool $ entityKey $ head accList + else return [] + txList <- + if not (null addrList) + then getUserTx pool $ entityKey $ head addrList + else return [] + let model = + AppModel + config + (zgb_net chainInfo) + walList + 0 + accList + 0 + addrList + 0 + txList + 0 + Nothing + True + 314259000 + (Just 300000) startApp model handleEvent buildUI params - Left e -> - print $ - "No Zebra node available on port " <> - show port <> ". Check your configuration." + Left e -> do + initDb dbFilePath + let model = + AppModel + config + TestNet + [] + 0 + [] + 0 + [] + 0 + [] + 0 + (Just $ + "Couldn't connect to Zebra on " <> + host <> ":" <> showt port <> ". Check your configuration.") + False + 314259000 + (Just 30000) + startApp model handleEvent buildUI params where params = [ appWindowTitle "Zenith - Zcash Full Node Wallet" - , appTheme darkTheme - , appFontDef "Regular" "./assets/DejaVuSansMono.ttf" - , appFontDef "Bold" "./assets/DejaVuSansMono-Bold.ttf" - , appFontDef "Italic" "./assets/DejaVuSansMono-Oblique.ttf" + , appTheme zenithTheme + , appFontDef "Regular" "./assets/Atkinson-Hyperlegible-Regular-102.ttf" --"./assets/DejaVuSansMono.ttf" + , appFontDef "Bold" "./assets/Atkinson-Hyperlegible-Bold-102.ttf" + , appFontDef "Italic" "./assets/Atkinson-Hyperlegible-Italic-102.ttf" + , appFontDef "Remix" "./assets/remixicon.ttf" + , appDisableAutoScale True + , appScaleFactor 2.0 , appInitEvent AppInit ] diff --git a/src/Zenith/GUI/Theme.hs b/src/Zenith/GUI/Theme.hs new file mode 100644 index 0000000..50fc4be --- /dev/null +++ b/src/Zenith/GUI/Theme.hs @@ -0,0 +1,183 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Zenith.GUI.Theme + ( zenithTheme + ) where + +import Lens.Micro ((&), (+~), (.~), (?~), (^.), at, set) +import Monomer +import Monomer.Core.Themes.BaseTheme +import Monomer.Core.Themes.SampleThemes +import Monomer.Graphics (rgbHex, transparent) +import Monomer.Graphics.ColorTable +import qualified Monomer.Lens as L + +zenithTheme :: Theme +zenithTheme = + baseTheme zenithThemeColors & L.basic . L.labelStyle . L.text ?~ + TextStyle + Nothing + (Just . FontSize $ 10) + Nothing + Nothing + (Just whiteSmoke) + Nothing + Nothing + Nothing + Nothing + Nothing + Nothing & + L.hover . + L.labelStyle . L.text ?~ + TextStyle + Nothing + (Just . FontSize $ 10) + Nothing + Nothing + (Just whiteSmoke) + Nothing + Nothing + Nothing + Nothing + Nothing + Nothing + +zenithThemeColors :: BaseThemeColors +zenithThemeColors = + BaseThemeColors + { clearColor = gray01 + , sectionColor = gray01 + , btnFocusBorder = blue09 + , btnBgBasic = gray07b + , btnBgHover = gray08 + , btnBgFocus = gray07c + , btnBgActive = gray06 + , btnBgDisabled = gray05 + , btnText = gray02 + , btnTextDisabled = gray01 + , btnMainFocusBorder = blue08 + , btnMainBgBasic = blue05b + , btnMainBgHover = blue06 + , btnMainBgFocus = blue05c + , btnMainBgActive = blue05 + , btnMainBgDisabled = blue04 + , btnMainText = white + , btnMainTextDisabled = gray08 + , dialogBg = gray01 + , dialogBorder = gray01 + , dialogText = white + , dialogTitleText = white + , emptyOverlay = gray05 & L.a .~ 0.8 + , shadow = gray00 & L.a .~ 0.33 + , externalLinkBasic = blue07 + , externalLinkHover = blue08 + , externalLinkFocus = blue07 + , externalLinkActive = blue06 + , externalLinkDisabled = gray06 + , iconBg = gray08 + , iconFg = gray01 + , inputIconFg = black + , inputBorder = gray02 + , inputFocusBorder = blue08 + , inputBgBasic = gray04 + , inputBgHover = gray06 + , inputBgFocus = gray05 + , inputBgActive = gray03 + , inputBgDisabled = gray07 + , inputFgBasic = gray06 + , inputFgHover = blue08 + , inputFgFocus = blue08 + , inputFgActive = blue07 + , inputFgDisabled = gray07 + , inputSndBasic = gray05 + , inputSndHover = gray06 + , inputSndFocus = gray05 + , inputSndActive = gray05 + , inputSndDisabled = gray03 + , inputHlBasic = gray07 + , inputHlHover = blue08 + , inputHlFocus = blue08 + , inputHlActive = blue08 + , inputHlDisabled = gray08 + , inputSelBasic = gray06 + , inputSelFocus = blue06 + , inputText = white + , inputTextDisabled = gray02 + , labelText = white + , scrollBarBasic = gray01 & L.a .~ 0.2 + , scrollThumbBasic = gray07 & L.a .~ 0.6 + , scrollBarHover = gray01 & L.a .~ 0.4 + , scrollThumbHover = gray07 & L.a .~ 0.8 + , slMainBg = gray00 + , slNormalBgBasic = transparent + , slNormalBgHover = gray05 + , slNormalText = white + , slNormalFocusBorder = blue08 + , slSelectedBgBasic = gray04 + , slSelectedBgHover = gray05 + , slSelectedText = white + , slSelectedFocusBorder = blue08 + , tooltipBorder = gray05 + , tooltipBg = rgbHex "#1D212B" + , tooltipText = white + } + +--black = rgbHex "#000000" +{-white = rgbHex "#FFFFFF"-} +blue01 = rgbHex "#002159" + +blue02 = rgbHex "#01337D" + +blue03 = rgbHex "#03449E" + +blue04 = rgbHex "#0552B5" + +blue05 = rgbHex "#0967D2" + +blue05b = rgbHex "#0F6BD7" + +blue05c = rgbHex "#1673DE" + +blue06 = rgbHex "#2186EB" + +blue06b = rgbHex "#2489EE" + +blue06c = rgbHex "#2B8FF6" + +blue07 = rgbHex "#47A3F3" + +blue07b = rgbHex "#50A6F6" + +blue07c = rgbHex "#57ACFC" + +blue08 = rgbHex "#7CC4FA" + +blue09 = rgbHex "#BAE3FF" + +blue10 = rgbHex "#E6F6FF" + +gray00 = rgbHex "#111111" + +gray01 = rgbHex "#2E2E2E" + +gray02 = rgbHex "#393939" + +gray03 = rgbHex "#515151" + +gray04 = rgbHex "#626262" + +gray05 = rgbHex "#7E7E7E" + +gray06 = rgbHex "#9E9E9E" + +gray07 = rgbHex "#B1B1B1" + +gray07b = rgbHex "#B4B4B4" + +gray07c = rgbHex "#BBBBBB" + +gray08 = rgbHex "#CFCFCF" + +gray09 = rgbHex "#E1E1E1" + +gray10 = rgbHex "#F7F7F7" diff --git a/src/Zenith/Utils.hs b/src/Zenith/Utils.hs index 96ca8dd..0b7821e 100644 --- a/src/Zenith/Utils.hs +++ b/src/Zenith/Utils.hs @@ -12,6 +12,7 @@ import System.Process (createProcess_, shell) import Text.Regex.Posix import ZcashHaskell.Orchard (encodeUnifiedAddress, isValidUnifiedAddress) import ZcashHaskell.Sapling (isValidShieldedAddress) +import ZcashHaskell.Types (ZcashNet(..)) import Zenith.Types ( AddressGroup(..) , UnifiedAddressDB(..) @@ -39,6 +40,12 @@ displayTaz s | abs s < 100000000 = show (fromIntegral s / 100000) ++ " mTAZ " | otherwise = show (fromIntegral s / 100000000) ++ " TAZ " +displayAmount :: ZcashNet -> Integer -> T.Text +displayAmount n a = + if n == MainNet + then T.pack $ displayZec a + else T.pack $ displayTaz a + -- | Helper function to display abbreviated Unified Address showAddress :: UnifiedAddressDB -> T.Text showAddress u = T.take 20 t <> "..." diff --git a/zenith.cabal b/zenith.cabal index bdad228..6a99f95 100644 --- a/zenith.cabal +++ b/zenith.cabal @@ -28,6 +28,7 @@ library exposed-modules: Zenith.CLI Zenith.GUI + Zenith.GUI.Theme Zenith.Core Zenith.DB Zenith.Types