Full-screen dog loader overlay: dim background, panel, input block
This commit is contained in:
@@ -9,11 +9,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from textual import work
|
from rich.align import Align
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.text import Text
|
||||||
|
from textual import on, work
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
from textual.containers import Horizontal, Vertical
|
from textual.containers import Horizontal, Vertical
|
||||||
from textual.css.query import NoMatches
|
from textual.css.query import NoMatches
|
||||||
|
from textual.events import InputEvent
|
||||||
from textual.reactive import reactive
|
from textual.reactive import reactive
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.timer import Timer
|
from textual.timer import Timer
|
||||||
@@ -53,19 +57,28 @@ def _mask_token(value: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
class DogLoader(Static):
|
class DogLoader(Static):
|
||||||
"""ASCII-«собачка» — анимация ожидания обновления статистики.
|
"""Полноэкранный индикатор загрузки: бегущая ASCII-собачка.
|
||||||
|
|
||||||
Кадры сменяются по таймеру (виляет хвостом). Показывается как
|
overlay поверх всего дашборда с полупрозрачным затемнением; собака
|
||||||
центрированный overlay поверх дашборда на время загрузки данных.
|
«бежит» (переставляет лапы, виляет хвостом), под ней — дорожка из 🐾.
|
||||||
|
Показывается на время обновления статистики и блокирует ввод.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
FRAMES = (
|
BODY = (
|
||||||
" /^ ^\\\n ( o.o ) ~\n \\ - / ~~~\n |_|",
|
" __",
|
||||||
" /^ ^\\\n ( o.o ) ~~~\n \\ - / ~~~~\n |_|",
|
" ____/ \\___",
|
||||||
" /^ ^\\\n ( o.o ) ~~\n \\ - / ~\n |_|",
|
" / oo \\____",
|
||||||
|
" | __ \\____",
|
||||||
|
" \\____/ \\____________/",
|
||||||
|
)
|
||||||
|
LEGS = (
|
||||||
|
(" || || \\ ~", " || || \\_/"),
|
||||||
|
(" // \\\\ \\ ~~", " // \\\\ \\_/"),
|
||||||
|
(" || || \\ ~", " || || \\_/"),
|
||||||
|
(" \\\\ // \\ ~~", " \\\\ // \\_/"),
|
||||||
)
|
)
|
||||||
CAPTION = "Обновление статистики…"
|
CAPTION = "Обновление статистики…"
|
||||||
INTERVAL = 0.15
|
INTERVAL = 0.12
|
||||||
|
|
||||||
frame = reactive(0)
|
frame = reactive(0)
|
||||||
|
|
||||||
@@ -90,11 +103,36 @@ class DogLoader(Static):
|
|||||||
self._timer = None
|
self._timer = None
|
||||||
|
|
||||||
def _next_frame(self) -> None:
|
def _next_frame(self) -> None:
|
||||||
self.frame = (self.frame + 1) % len(self.FRAMES)
|
self.frame = (self.frame + 1) % len(self.LEGS)
|
||||||
|
|
||||||
|
def _art(self) -> str:
|
||||||
|
index = self.frame % len(self.LEGS)
|
||||||
|
legs1, legs2 = self.LEGS[index]
|
||||||
|
return "\n".join([*self.BODY, legs1, legs2])
|
||||||
|
|
||||||
|
def _paws(self) -> str:
|
||||||
|
index = self.frame % len(self.LEGS)
|
||||||
|
return " ".join("🐾" if (i + index) % 2 == 0 else " " for i in range(8))
|
||||||
|
|
||||||
def render(self) -> Any:
|
def render(self) -> Any:
|
||||||
art = self.FRAMES[self.frame % len(self.FRAMES)]
|
theme = self.app.current_theme
|
||||||
return f"{art}\n\n{self.CAPTION}"
|
body = Text(self._art(), style=f"bold {theme.accent}", justify="center")
|
||||||
|
paws = Text(self._paws(), style=str(theme.secondary), justify="center")
|
||||||
|
caption = Text(self.CAPTION, style=f"bold {theme.foreground}", justify="center")
|
||||||
|
content = Text("\n").join([body, Text(), paws, Text(), caption])
|
||||||
|
return Panel(
|
||||||
|
Align.center(content, vertical="middle"),
|
||||||
|
title="[b]🐕 selectel-ml-tui[/b]",
|
||||||
|
border_style=str(theme.primary),
|
||||||
|
padding=(1, 4),
|
||||||
|
)
|
||||||
|
|
||||||
|
@on(InputEvent)
|
||||||
|
def _block_input(self, event: InputEvent) -> None:
|
||||||
|
"""Перехватить ввод, пока показывается индикатор загрузки."""
|
||||||
|
if self.has_class("visible"):
|
||||||
|
event.stop()
|
||||||
|
event.prevent_default()
|
||||||
|
|
||||||
|
|
||||||
class ConsumptionBars(Static):
|
class ConsumptionBars(Static):
|
||||||
@@ -160,7 +198,6 @@ class DashboardScreen(Screen[Any]):
|
|||||||
yield Static("По моделям", id="models-title")
|
yield Static("По моделям", id="models-title")
|
||||||
yield DataTable(id="models-table")
|
yield DataTable(id="models-table")
|
||||||
yield DogLoader(id="dog-loader")
|
yield DogLoader(id="dog-loader")
|
||||||
yield Static("", id="progress-line")
|
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
@@ -178,20 +215,28 @@ class DashboardScreen(Screen[Any]):
|
|||||||
self.refreshing = False
|
self.refreshing = False
|
||||||
|
|
||||||
def watch_refreshing(self, refreshing: bool) -> None:
|
def watch_refreshing(self, refreshing: bool) -> None:
|
||||||
"""Показать/скрыть собачку и строку прогресса при обновлении."""
|
"""Показать/скрыть полноэкранную собачку при обновлении."""
|
||||||
try:
|
try:
|
||||||
loader = self.query_one("#dog-loader", DogLoader)
|
loader = self.query_one("#dog-loader", DogLoader)
|
||||||
line = self.query_one("#progress-line", Static)
|
|
||||||
except NoMatches: # watcher может сработать до монтирования детей
|
except NoMatches: # watcher может сработать до монтирования детей
|
||||||
return
|
return
|
||||||
if refreshing:
|
if refreshing:
|
||||||
loader.add_class("visible")
|
loader.add_class("visible")
|
||||||
loader.start()
|
loader.start()
|
||||||
line.update("[b]🐕 Собачка обновляет статистику…[/]")
|
self.set_focus(None)
|
||||||
else:
|
else:
|
||||||
loader.remove_class("visible")
|
loader.remove_class("visible")
|
||||||
loader.stop()
|
loader.stop()
|
||||||
line.update("")
|
try:
|
||||||
|
self.query_one("#models-table", DataTable).focus()
|
||||||
|
except NoMatches: # pragma: no cover - на всякий случай
|
||||||
|
pass
|
||||||
|
|
||||||
|
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
|
||||||
|
"""Заблокировать горячие клавиши на время обновления."""
|
||||||
|
if self.refreshing:
|
||||||
|
return False
|
||||||
|
return super().check_action(action, parameters)
|
||||||
|
|
||||||
def watch_data(self, data: DashboardData) -> None:
|
def watch_data(self, data: DashboardData) -> None:
|
||||||
status = "offline" if data.offline else "online"
|
status = "offline" if data.offline else "online"
|
||||||
@@ -617,23 +662,16 @@ class SelectelMLTUI(App[Any]):
|
|||||||
}
|
}
|
||||||
#dog-loader {
|
#dog-loader {
|
||||||
display: none;
|
display: none;
|
||||||
layer: overlay;
|
dock: top;
|
||||||
width: auto;
|
layer: _loading;
|
||||||
height: auto;
|
width: 100%;
|
||||||
padding: 1 4;
|
height: 100%;
|
||||||
border: round $accent;
|
background: $background 80%;
|
||||||
background: $surface;
|
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
#dog-loader.visible {
|
#dog-loader.visible {
|
||||||
display: block;
|
display: block;
|
||||||
align: center middle;
|
|
||||||
}
|
|
||||||
#progress-line {
|
|
||||||
dock: bottom;
|
|
||||||
height: 1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user