added pair screen, home screen, and channels screen
This commit is contained in:
3
desktop_app/api/channel.py
Normal file
3
desktop_app/api/channel.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
class Channel:
|
||||||
|
def __init__(self, name: str, key: str):
|
||||||
|
self.name = name
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from api.node import MeshNode
|
from api.node import MeshNode
|
||||||
from ui.app import Hive
|
from ui.app import mesh
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = Hive()
|
app = mesh()
|
||||||
app.run()
|
app.run()
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.widgets import Header, Footer
|
from textual.widgets import Header, Footer
|
||||||
from ui.screens.pair_screen import PairScreen
|
from ui.screens.pair_screen import PairScreen
|
||||||
|
from api.node import MeshNode
|
||||||
|
|
||||||
|
|
||||||
class Hive(App):
|
class mesh(App):
|
||||||
theme = "rose-pine"
|
CSS_PATH = "assets/global.tcss"
|
||||||
ENABLE_COMMAND_PALETTE = False
|
|
||||||
|
def __init__(self, driver_class = None, css_path = None, watch_css = False, ansi_color = False):
|
||||||
|
super().__init__(driver_class, css_path, watch_css, ansi_color)
|
||||||
|
self.mesh_node: MeshNode = None
|
||||||
|
|
||||||
def on_ready(self):
|
def on_ready(self):
|
||||||
self.push_screen(PairScreen())
|
self.push_screen(PairScreen())
|
||||||
8
desktop_app/ui/assets/channels_banner.txt
Normal file
8
desktop_app/ui/assets/channels_banner.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
_ _
|
||||||
|
| | | |
|
||||||
|
___| |__ __ _ _ __ _ __ ___| |___
|
||||||
|
/ __| '_ \ / _` | '_ \| '_ \ / _ \ / __|
|
||||||
|
| (__| | | | (_| | | | | | | | __/ \__ \
|
||||||
|
\___|_| |_|\__,_|_| |_|_| |_|\___|_|___/
|
||||||
|
|
||||||
|
|
||||||
9
desktop_app/ui/assets/global.tcss
Normal file
9
desktop_app/ui/assets/global.tcss
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.banner {
|
||||||
|
padding: 1;
|
||||||
|
width: 100%;
|
||||||
|
background: $primary 50%;
|
||||||
|
color: $primary-lighten-1;
|
||||||
|
text-align: center;
|
||||||
|
margin: 1;
|
||||||
|
text-style: bold underline;
|
||||||
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
PairScreen {
|
PairScreen {
|
||||||
align: center middle;
|
align: center middle;
|
||||||
}
|
|
||||||
|
|
||||||
EffectLabel {
|
EffectLabel {
|
||||||
min-width: 50;
|
min-width: 50;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#middle {
|
#middle {
|
||||||
border: $success round;
|
border: $success round;
|
||||||
width: 60;
|
width: 45;
|
||||||
height: 28;
|
height: 15;
|
||||||
|
|
||||||
padding: 0 1 1 1;
|
padding: 0 1 1 1;
|
||||||
|
|
||||||
@@ -20,20 +20,8 @@ EffectLabel {
|
|||||||
margin: 0 1;
|
margin: 0 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable {
|
|
||||||
margin-top: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadingIndicator {
|
LoadingIndicator {
|
||||||
height: 1;
|
height: 1;
|
||||||
margin-top: 1;
|
margin-top: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable {
|
|
||||||
height: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
margin: 2 0 0 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
17
desktop_app/ui/screens/main_screen.py
Normal file
17
desktop_app/ui/screens/main_screen.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from textual.screen import Screen
|
||||||
|
from textual.widgets import Header, Footer, ContentSwitcher
|
||||||
|
from ui.widgets.home_sidebar import HomeSidebar
|
||||||
|
from ui.widgets.home_info import HomeInfo
|
||||||
|
from ui.widgets.channels_list import ChannelsList
|
||||||
|
|
||||||
|
|
||||||
|
class MainScreen(Screen):
|
||||||
|
def compose(self):
|
||||||
|
yield Header(show_clock=True)
|
||||||
|
yield HomeSidebar()
|
||||||
|
|
||||||
|
with ContentSwitcher(initial="home-info"):
|
||||||
|
yield HomeInfo(id="home-info")
|
||||||
|
yield ChannelsList(id="channels-list")
|
||||||
|
|
||||||
|
yield Footer()
|
||||||
@@ -1,12 +1,33 @@
|
|||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.containers import VerticalScroll, Vertical
|
from textual.containers import Vertical
|
||||||
from textual.widgets import Static, Button, LoadingIndicator, DataTable
|
from textual.widgets import Static, LoadingIndicator, DataTable
|
||||||
|
from textual import work
|
||||||
|
from ui.screens.main_screen import MainScreen
|
||||||
from textualeffects.widgets import EffectLabel
|
from textualeffects.widgets import EffectLabel
|
||||||
|
|
||||||
|
from api.node import MeshNode
|
||||||
|
|
||||||
|
|
||||||
class PairScreen(Screen):
|
class PairScreen(Screen):
|
||||||
CSS_PATH = "../assets/pair_screen.tcss"
|
CSS_PATH = "../assets/pair_screen.tcss"
|
||||||
|
|
||||||
|
@work
|
||||||
|
async def connect_to_node(self, is_retry = False):
|
||||||
|
if not is_retry:
|
||||||
|
self.notify("This may take a moment...", title="Discovering nearby nodes...")
|
||||||
|
self.app.mesh_node = await MeshNode.discover()
|
||||||
|
|
||||||
|
if self.app.mesh_node == None:
|
||||||
|
self.notify("Check your node is powered on and nearby.\nRetrying...", title="Failed to find a nearby node!", severity="warning")
|
||||||
|
return self.connect_to_node(True)
|
||||||
|
|
||||||
|
self.notify("Hurray! You're on the mesh!", title="Node connected!")
|
||||||
|
self.app.switch_screen(MainScreen())
|
||||||
|
|
||||||
|
|
||||||
|
async def on_compose(self):
|
||||||
|
self.connect_to_node()
|
||||||
|
|
||||||
def compose(self):
|
def compose(self):
|
||||||
|
|
||||||
with Vertical(id="middle") as center_window:
|
with Vertical(id="middle") as center_window:
|
||||||
@@ -15,18 +36,6 @@ class PairScreen(Screen):
|
|||||||
with open("ui/assets/banner.txt", "r") as f:
|
with open("ui/assets/banner.txt", "r") as f:
|
||||||
yield EffectLabel(f.read(), effect="Print")
|
yield EffectLabel(f.read(), effect="Print")
|
||||||
|
|
||||||
yield Static("Make sure your mesh network node is powered and ready to pair. When you're ready, click the \"Pair\" button to connect to the mesh!")
|
yield Static("Attempting to connect to a nearby node. Make sure your mesh network node is powered and ready to pair.")
|
||||||
|
|
||||||
table = DataTable()
|
yield LoadingIndicator()
|
||||||
table.add_columns("[b]Address", "RSSI")
|
|
||||||
yield table
|
|
||||||
|
|
||||||
table.add_row("hi", "[green][/] -34 dBm")
|
|
||||||
table.add_row("hi", "[green][/] -52 dBm")
|
|
||||||
table.add_row("hi", "[yellow][/] -64 dBm")
|
|
||||||
table.add_row("hi", "[red][/] -98 dBm")
|
|
||||||
table.add_row("hi", "[red][/] -101 dBm")
|
|
||||||
|
|
||||||
yield LoadingIndicator()
|
|
||||||
|
|
||||||
yield Button("Select a node to pair", disabled=True, variant="success", id="pair-btn")
|
|
||||||
55
desktop_app/ui/widgets/channels_list.py
Normal file
55
desktop_app/ui/widgets/channels_list.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from textual.containers import VerticalScroll, Vertical, HorizontalGroup
|
||||||
|
from textual.widgets import Static, Button, Rule
|
||||||
|
|
||||||
|
from api.channel import Channel
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelView(Button):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
ChannelView {
|
||||||
|
margin: 0 1;
|
||||||
|
background: $boost;
|
||||||
|
border: $surface-lighten-1 tall;
|
||||||
|
content-align: left middle;
|
||||||
|
text-align: left;
|
||||||
|
width: 30;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel: Channel):
|
||||||
|
super().__init__(" [b]" + channel.name, flat=True)
|
||||||
|
self.channel = channel
|
||||||
|
|
||||||
|
class ChannelsList(Vertical):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
ChannelsList {
|
||||||
|
Rule {
|
||||||
|
color: $surface-lighten-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buttons {
|
||||||
|
margin-bottom: 1;
|
||||||
|
|
||||||
|
Button {
|
||||||
|
margin: 0 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
with VerticalScroll():
|
||||||
|
yield Static("channels", classes="banner")
|
||||||
|
yield ChannelView(Channel("test channel", "AQ=="))
|
||||||
|
yield ChannelView(Channel("test channel", "AQ=="))
|
||||||
|
yield ChannelView(Channel("test channel", "AQ=="))
|
||||||
|
yield ChannelView(Channel("test channel", "AQ=="))
|
||||||
|
yield ChannelView(Channel("test channel", "AQ=="))
|
||||||
|
yield ChannelView(Channel("test channel", "AQ=="))
|
||||||
|
yield ChannelView(Channel("test channel", "AQ=="))
|
||||||
|
|
||||||
|
yield Rule()
|
||||||
|
|
||||||
|
with HorizontalGroup(id="buttons"):
|
||||||
|
yield Button("Create Channel")
|
||||||
|
yield Button("Advertise")
|
||||||
25
desktop_app/ui/widgets/home_info.py
Normal file
25
desktop_app/ui/widgets/home_info.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from textual.containers import Center
|
||||||
|
from textual.widgets import Static
|
||||||
|
from textualeffects.widgets import EffectLabel
|
||||||
|
|
||||||
|
|
||||||
|
class HomeInfo(Center):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
HomeInfo {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 3;
|
||||||
|
|
||||||
|
EffectLabel {
|
||||||
|
min-width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
with open("ui/assets/banner.txt", "r") as f:
|
||||||
|
yield EffectLabel(f.read(), effect="Print")
|
||||||
|
|
||||||
|
yield Static("[cyan][/] [b blink]1[/] new message(s)")
|
||||||
|
yield Static("[cyan][/] [b]2 of 3[/] nodes online")
|
||||||
|
yield Static("[lime][/] [b]SNR:[/] 10.0 dBm [b]| RSSI:[/] -115.0 dBm")
|
||||||
39
desktop_app/ui/widgets/home_sidebar.py
Normal file
39
desktop_app/ui/widgets/home_sidebar.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from textual.containers import Vertical
|
||||||
|
from textual.widgets import Button, ContentSwitcher
|
||||||
|
|
||||||
|
|
||||||
|
class SidebarButton(Button):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
SidebarButton {
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 1;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class HomeSidebar(Vertical):
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
HomeSidebar {
|
||||||
|
width: 11;
|
||||||
|
background: $boost;
|
||||||
|
border-right: $surface-lighten-1 tall;
|
||||||
|
padding: 1;
|
||||||
|
dock: left;
|
||||||
|
margin-top: 1;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed):
|
||||||
|
content_switcher: ContentSwitcher = self.screen.query_one(ContentSwitcher)
|
||||||
|
|
||||||
|
match event.button.id:
|
||||||
|
case "home-btn":
|
||||||
|
content_switcher.current = "home-info"
|
||||||
|
case "channels-btn":
|
||||||
|
content_switcher.current = "channels-list"
|
||||||
|
case "settings-btn":
|
||||||
|
content_switcher.current = "settings"
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
yield SidebarButton("", tooltip="Home", id="home-btn")
|
||||||
|
yield SidebarButton("", tooltip="Channels", id="channels-btn")
|
||||||
|
yield SidebarButton("", tooltip="Settings", id="settings-btn")
|
||||||
Reference in New Issue
Block a user