Cookbook App Demo
This commit is contained in:
0
__init__.py
Normal file
0
__init__.py
Normal file
130
app.py
Normal file
130
app.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import randomname
|
||||||
|
|
||||||
|
from textual import on, work
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.events import Click
|
||||||
|
from textual.screen import Screen
|
||||||
|
from textual.message import Message
|
||||||
|
from textual.css.query import NoMatches
|
||||||
|
from textual.reactive import reactive
|
||||||
|
from textual.containers import (
|
||||||
|
Container,
|
||||||
|
Center,
|
||||||
|
Horizontal,
|
||||||
|
VerticalScroll
|
||||||
|
)
|
||||||
|
from textual.widgets import (
|
||||||
|
Button,
|
||||||
|
Footer,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Rule,
|
||||||
|
Static
|
||||||
|
)
|
||||||
|
|
||||||
|
class CardSelected(Message):
|
||||||
|
def __init__(self, card_id: int) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.card_id = card_id
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeCard(Static):
|
||||||
|
visible_status: bool = reactive(True)
|
||||||
|
|
||||||
|
def __init__(self, recipe_data: dict = {}):
|
||||||
|
super().__init__()
|
||||||
|
self.recipe_name = recipe_data.get('recipe_name', "<no name>")
|
||||||
|
self.card_id = recipe_data.get('card_id', 0)
|
||||||
|
self.border_title = f"{self.recipe_name} - {self.card_id}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.id = f"recipe-{self.card_id}"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Label("Cook Time: 10m", classes="card")
|
||||||
|
yield Rule(line_style="solid", classes="rule-color")
|
||||||
|
yield Label("Description", classes="recipecard-label")
|
||||||
|
yield Static(
|
||||||
|
"This dish is full of nonsense and this is a long text label",
|
||||||
|
classes="card"
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_click(self, event: Click) -> None:
|
||||||
|
self.post_message(CardSelected(self.id))
|
||||||
|
|
||||||
|
def watch_visible_status(self, visible: bool) -> None:
|
||||||
|
if visible:
|
||||||
|
print(f"Card {self.id} is visible!")
|
||||||
|
self.remove_class('invisible')
|
||||||
|
else:
|
||||||
|
print(f"Card {self.id} is invisible!")
|
||||||
|
self.add_class('invisible')
|
||||||
|
|
||||||
|
|
||||||
|
class SearchPageScreen(Screen):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.selected_card_id = None
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Container(id="background-container"):
|
||||||
|
with Center(id="header"):
|
||||||
|
yield Horizontal(
|
||||||
|
Button("Back", id="button-go-back"),
|
||||||
|
Input(placeholder="Search a recipe..", id="search-input"),
|
||||||
|
Button("Search Recipes", id="enter-button"),
|
||||||
|
id="header-components"
|
||||||
|
)
|
||||||
|
with VerticalScroll(id="recipe-list"):
|
||||||
|
for x in range(1, 21):
|
||||||
|
yield RecipeCard(
|
||||||
|
recipe_data={
|
||||||
|
"recipe_name": randomname.generate(sep=" "),
|
||||||
|
"card_id": x
|
||||||
|
}
|
||||||
|
)
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
@on(CardSelected)
|
||||||
|
def card_selected(self, message: CardSelected) -> None:
|
||||||
|
card_id = message.card_id
|
||||||
|
card = self.query_one(f"#{card_id}", RecipeCard)
|
||||||
|
if card_id == self.selected_card_id:
|
||||||
|
card.remove_class("recipe-highlighted")
|
||||||
|
self.selected_card_id = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
old_card = self.query_one(f"#{self.selected_card_id}", RecipeCard)
|
||||||
|
old_card.remove_class("recipe-highlighted")
|
||||||
|
except NoMatches:
|
||||||
|
print(f"Card not fount {self.selected_card_id}")
|
||||||
|
self.selected_card_id = card_id
|
||||||
|
card.add_class('recipe-highlighted')
|
||||||
|
|
||||||
|
@on(Input.Changed)
|
||||||
|
def filter_cards(self, event: Input.Changed) -> None:
|
||||||
|
query = event.value.strip().lower()
|
||||||
|
for card in self.query(RecipeCard):
|
||||||
|
if query in card.recipe_name:
|
||||||
|
card.visible_status = True
|
||||||
|
else:
|
||||||
|
card.visible_status = False
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
|
||||||
|
class CookbookApp(App):
|
||||||
|
CSS_PATH = "cookbook.css"
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.theme = "monokai"
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
self.push_screen(SearchPageScreen())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = CookbookApp()
|
||||||
|
app.run()
|
||||||
|
|
||||||
58
cookbook.css
Normal file
58
cookbook.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#background-container {
|
||||||
|
background: $background;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
width: 100%;
|
||||||
|
background: $warning;
|
||||||
|
border: thick $warning-darken-1;
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-components {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-input {
|
||||||
|
width: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recipe-list {
|
||||||
|
layout: grid;
|
||||||
|
grid-size: 3;
|
||||||
|
grid-gutter: 1;
|
||||||
|
grid-rows: 15;
|
||||||
|
margin: 1 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
RecipeCard {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border: thick $accent;
|
||||||
|
background: $primary-background;
|
||||||
|
margin: 0 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.recipecard-label {
|
||||||
|
text-style: bold italic underline;
|
||||||
|
padding: 0 0 1 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-color {
|
||||||
|
color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 0 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-highlighted {
|
||||||
|
background: $primary-background-lighten-2;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user