Приукрасил внешний вид, добавил сверху в диалоге панель с названием чата (именем собеседника)

This commit is contained in:
kirill 2025-05-30 00:02:36 +03:00
parent 7ecdf5ea39
commit 2a18f625c0
6 changed files with 126 additions and 181 deletions

View File

@ -9,7 +9,9 @@ API_ID=2040
API_HASH=b18441a1ff607e10a989891a5462e627
# Конфиг
CURRENT_USER = "user"
DO_NOTIFY = 0 # Linux-only for now
UTC_OFFSET = 0
LANGUAGE = "en" # en, ru
CURRENT_USER = "user" # Name for user session file
DO_NOTIFY = 0 # Linux-only for now
UTC_OFFSET = 0 # UTC timezone offset
LANGUAGE = "en" # en, ru
CHATS_LIMIT = 100 # Chats mount limit
MESSAGES_LIMIT = 50 # Messages mount limit

View File

@ -10,12 +10,12 @@ from src.screens import AuthScreen, ChatScreen
import src.locales
load_dotenv()
API_ID = getenv("API_ID")
API_HASH = getenv("API_HASH")
LANGUAGE = getenv("LANGUAGE")
UTC_OFFSET = getenv("UTC_OFFSET")
env_consts = ["API_ID", "API_HASH", "LANGUAGE", "UTC_OFFSET", "CHATS_LIMIT",
"MESSAGES_LIMIT"]
for name in env_consts:
exec(f"{name} = getenv(\"{name}\")")
if "" in [API_ID, API_HASH, LANGUAGE, UTC_OFFSET]:
if "" in list(map(lambda x: globals()[x], env_consts)):
raise ValueError(
"Недостаточно параметров в .env файле."
"Скопируйте .env.example в .env и заполните свои API-ключи."
@ -23,9 +23,10 @@ if "" in [API_ID, API_HASH, LANGUAGE, UTC_OFFSET]:
"Copy .env.example into .env and fill your API-keys."
)
API_ID = int(API_ID)
API_ID = int(API_ID) # type: ignore
locale = dict(zip(
getattr(src.locales, "codes"), getattr(src.locales, LANGUAGE)
getattr(src.locales, "codes"),
getattr(src.locales, LANGUAGE) # type: ignore
))
class Talc(App):
@ -50,13 +51,15 @@ class Talc(App):
):
super().__init__(driver_class, css_path, watch_css, ansi_color)
self.locale = locale
self.timezone = timezone(timedelta(hours=int(UTC_OFFSET)))
self.timezone = timezone(timedelta(hours=int(UTC_OFFSET))) # type: ignore
self.CHATS_LIMIT = CHATS_LIMIT # type: ignore
self.MESSAGES_LIMIT = MESSAGES_LIMIT # type: ignore
async def on_mount(self) -> None:
self.telegram_client = TelegramClient(
getenv("CURRENT_USER"),
API_ID,
API_HASH
API_HASH # type: ignore
)
await self.telegram_client.connect()

View File

@ -30,8 +30,15 @@ class AuthScreen(Screen):
def compose(self) -> ComposeResult:
with Vertical(id="auth_container"):
yield Label(self.locale["auth_greeting"])
yield Input(placeholder=self.locale["phone_number"], id="phone")
yield Input(placeholder=self.locale["code"], id="code", disabled=True)
yield Input(
placeholder=self.locale["phone_number"],
id="phone"
)
yield Input(
placeholder=self.locale["code"],
id="code",
disabled=True
)
yield Input(
placeholder=self.locale["password"],
id="password",
@ -79,15 +86,17 @@ class ChatScreen(Screen):
self.locale = self.app.locale
async def on_mount(self) -> None:
self.limit = 100
self.limit = int(self.app.CHATS_LIMIT)
#Получение ID пользователя (себя)
# Получение ID пользователя (себя)
self.me_id = await self.telegram_client.get_peer_id("me")
# Получение объекта контейнера чатов
self.chat_container = self\
.query_one("#main_container")\
.query_one("#chats")\
.query_one("#chat_container")
self.switcher = self.screen.query_one(Horizontal)\
.query_one("#dialog_switcher", ContentSwitcher)
print("Первоначальная загрузка виджетов чатов...")
self.mount_chats(
@ -120,7 +129,7 @@ class ChatScreen(Screen):
chats_amount = len(self.chat_container.query(Chat))
if limit > chats_amount:
# Маунт недостоющих, если чатов меньше, чем нужно
# Маунт недостающих, если чатов меньше, чем нужно
for i in range(limit - chats_amount):
chat = Chat(id=f"chat-{i + chats_amount + 1}")
self.chat_container.mount(chat)
@ -178,11 +187,24 @@ class ChatScreen(Screen):
else:
chat.msg = str(dialogs[i].message.message)
if self.switcher.current is not None:
current_dialog = \
self.switcher.query_one(f"#{self.switcher.current}")
if chat.peer_id == int(current_dialog.id[7:]):
chat.add_class("selected_chat")
else:
chat.remove_class("selected_chat")
self.is_chat_update_blocked = False
print("Чаты обновлены")
else:
print("Обновление чатов невозможно: уже выполняется")
if self.switcher.current is not None:
current_dialog = \
self.switcher.query_one(f"#{self.switcher.current}")
await current_dialog.update_dialog()
def notify_send(self, event) -> None:
if not event:
return None
@ -196,9 +218,9 @@ class ChatScreen(Screen):
yield VerticalScroll(id="chat_container")
#TODO: сделать кнопку, чтобы прогрузить больше чатов,
# или ленивую прокрутку
with ContentSwitcher(id="dialog_switcher"):
yield ContentSwitcher(id="dialog_switcher")
# ↑ Внутри него как раз крутятся диалоги
yield Label(
self.locale["start_converse"],
id="start_converse_label"
) #TODO: не показывается надпись, надо будет исправить
#yield Label(
# self.locale["start_converse"],
# id="start_converse_label"
#) #TODO: не показывается надпись, надо будет исправить

View File

@ -74,10 +74,44 @@ ContentSwitcher {
height: 100%;
}
.start_converse_label {
#start_converse_label {
width: 100%;
height: 100%;
text-align: center;
content-align: center middle;
color: $panel;
}
TopBar {
max-height: 3;
width: 100%;
background: $surface;
}
TopBar Horizontal {
align: left middle;
}
.avatar {
border: solid $foreground;
text-align: center;
width: 5;
height: 3;
}
.peername_top_bar {
padding: 1;
}
.odd {
background: $surface;
}
.selected_chat {
background: $foreground;
color: $background
}
.selected_chat .avatar{
border: solid $background;
}

View File

@ -2,24 +2,22 @@
from textual.containers import Horizontal, Vertical, Container, VerticalScroll
from textual.widget import Widget
from textual.reactive import Reactive
from textual.reactive import reactive
from textual.widgets import Input, Button, Label, Static, ContentSwitcher
from textual.app import ComposeResult, RenderResult
from textual.content import Content
from textual.style import Style
from telethon import TelegramClient, events, utils, types
import datetime
from os import getenv
class Chat(Widget):
"""Класс виджета чата для панели чатов"""
username: Reactive[str] = Reactive(" ", recompose=True)
peername: Reactive[str] = Reactive(" ", recompose=True)
msg: Reactive[str] = Reactive(" ", recompose=True)
is_group: Reactive[bool] = Reactive(False, recompose=True)
is_channel: Reactive[bool] = Reactive(False, recompose=True)
peer_id: Reactive[int] = Reactive(0)
username: reactive[str] = reactive(" ", recompose=True)
peername: reactive[str] = reactive(" ", recompose=True)
msg: reactive[str] = reactive(" ", recompose=True)
is_group: reactive[bool] = reactive(False, recompose=True)
is_channel: reactive[bool] = reactive(False)
peer_id: reactive[int] = reactive(0)
def __init__(
self,
@ -39,6 +37,11 @@ class Chat(Widget):
self.switcher = self.screen.query_one(Horizontal)\
.query_one("#dialog_switcher", ContentSwitcher)
if int(self.id[5:]) % 2 != 0:
self.add_class("odd")
else:
self.add_class("even")
def on_click(self) -> None:
# Получение ID диалога и создание DOM-ID на его основе
dialog_id = f"dialog-{str(self.peer_id)}"
@ -60,7 +63,7 @@ class Chat(Widget):
def compose(self) -> ComposeResult:
with Horizontal():
yield Label(f"┌───┐\n{self.peername[:1]:1}\n└───┘")
yield Label(self.peername[:1], classes="avatar")
with Vertical():
yield Label(self.peername, id="peername", markup=False)
if self.is_group:
@ -87,25 +90,19 @@ class Dialog(Widget):
self.is_channel = is_channel
async def on_mount(self) -> None:
self.limit = 10
self.limit = int(self.app.MESSAGES_LIMIT)
if not self.is_channel:
self.msg_input = self.query_one("#msg_input")
self.dialog = self.query_one(Vertical).query_one("#dialog")
self.top_bar = self.query_one(Vertical).query_one(TopBar)
self.switcher = self.screen.query_one(Horizontal)\
.query_one("#dialog_switcher", ContentSwitcher)
self.me = await self.telegram_client.get_me()
await self.update_dialog()
for event in (
events.NewMessage,
events.MessageDeleted,
events.MessageEdited
):
self.telegram_client.on(
event(chats=(self.chat_id))
)(self.update_dialog)
#self.dialog.scroll_down(animate=False, immediate=True)
def mount_messages(self, limit: int) -> None:
@ -126,7 +123,7 @@ class Dialog(Widget):
async def update_dialog(self, event = None) -> None:
print("Запрос обновления сообщений")
if not self.is_msg_update_blocked:
if not self.is_msg_update_blocked and self.switcher.current == self.id:
self.is_msg_update_blocked = True
messages = await self.telegram_client.get_messages(
@ -186,11 +183,15 @@ class Dialog(Widget):
msg.is_me = is_me
msg.username = utils.get_display_name(messages[i].sender)
msg.send_time = messages[i]\
msg.info = messages[i]\
.date\
.astimezone(self.timezone)\
.strftime("%H:%M")
self.top_bar.peername = utils.get_display_name(
await self.telegram_client.get_entity(self.chat_id)
)
self.is_msg_update_blocked = False
print("Сообщения обновлены")
else:
@ -198,6 +199,7 @@ class Dialog(Widget):
def compose(self) -> ComposeResult:
with Vertical():
yield TopBar()
yield VerticalScroll(id="dialog")
if not self.is_channel:
with Horizontal(id="input_place"):
@ -227,27 +229,18 @@ class Dialog(Widget):
class Message(Widget):
"""Класс виджета сообщений для окна диалога"""
message: Reactive[Content] = Reactive("", recompose=True)
is_me: Reactive[bool] = Reactive(False, recompose=True)
username: Reactive[str] = Reactive("", recompose=True)
send_time: Reactive[str] = Reactive("", recompose=True)
message: reactive[Content] = reactive("", recompose=True)
is_me: reactive[bool] = reactive(False, recompose=True)
username: reactive[str] = reactive("", recompose=True)
info: reactive[str] = reactive("", recompose=True)
def __init__(
self,
name=None,
id=None,
classes=None,
disabled=False
) -> None:
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
def on_mount(self) -> None:
pass
def __init__(self, id=None) -> None:
super().__init__(id=id)
def compose(self) -> ComposeResult:
label = Label(self.message, markup=False)
label.border_title = self.username * (not self.is_me)
label.border_subtitle = self.send_time
label.border_subtitle = self.info
with Container():
yield label
@ -256,3 +249,13 @@ class Message(Widget):
self.classes = "is_me_true"
else:
self.classes = "is_me_false"
class TopBar(Widget):
"""Класс виджета верхней панели для окна диалога"""
peername: reactive[str] = reactive(" ", recompose=True)
def compose(self) -> ComposeResult:
with Horizontal():
yield Label(self.peername[:1], classes="avatar")
yield Label(self.peername, classes="peername_top_bar")

View File

@ -1,119 +0,0 @@
/* Основные стили */
Screen {
background: $surface;
color: $text;
}
/* Стили для подсказки */
.help-text {
color: $text-muted;
text-align: center;
padding: 1;
background: $surface;
border: solid $accent;
margin: 1;
}
/* Стили для чатов */
.chat-item {
padding: 1 2;
height: auto;
min-height: 3;
border: solid $accent;
margin: 1 0;
transition: background 500ms;
}
.chat-item:hover {
background: $accent 20%;
}
.chat-item.selected {
background: $accent 30%;
border: solid $accent;
}
.chat-item.focused {
background: $accent 40%;
border: solid $accent;
outline: solid $accent;
}
.chat-item.selected.focused {
background: $accent 50%;
}
#chat_container {
height: 100%;
border: solid $accent;
background: $surface;
}
/* Стили для диалога */
#dialog {
height: 100%;
border: solid $accent;
background: $surface;
}
#input_place {
height: auto;
padding: 1;
background: $surface;
border: solid $accent;
}
/* Стили для сообщений */
.message {
margin: 1 0;
padding: 1;
border: solid $accent;
}
.message.is_me_true {
background: $accent 20%;
margin-left: 20%;
}
.message.is_me_false {
background: $surface;
margin-right: 20%;
}
/* Стили для ASCII-арта */
.ascii-art {
font-family: monospace;
white-space: pre;
margin: 1 0;
padding: 1;
background: $surface;
border: solid $accent;
}
/* Стили для поиска */
#search_input {
margin: 1;
border: solid $accent;
}
/* Стили для кнопок */
Button {
margin: 1;
min-width: 10;
}
Button#load_more {
width: 100%;
margin: 0;
border: none;
background: $accent 20%;
}
Button#load_more:hover {
background: $accent 30%;
}
Button#load_more:disabled {
background: $accent 10%;
color: $text-muted;
}