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

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 API_HASH=b18441a1ff607e10a989891a5462e627
# Конфиг # Конфиг
CURRENT_USER = "user" CURRENT_USER = "user" # Name for user session file
DO_NOTIFY = 0 # Linux-only for now DO_NOTIFY = 0 # Linux-only for now
UTC_OFFSET = 0 UTC_OFFSET = 0 # UTC timezone offset
LANGUAGE = "en" # en, ru 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 import src.locales
load_dotenv() load_dotenv()
API_ID = getenv("API_ID") env_consts = ["API_ID", "API_HASH", "LANGUAGE", "UTC_OFFSET", "CHATS_LIMIT",
API_HASH = getenv("API_HASH") "MESSAGES_LIMIT"]
LANGUAGE = getenv("LANGUAGE") for name in env_consts:
UTC_OFFSET = getenv("UTC_OFFSET") 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( raise ValueError(
"Недостаточно параметров в .env файле." "Недостаточно параметров в .env файле."
"Скопируйте .env.example в .env и заполните свои API-ключи." "Скопируйте .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." "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( locale = dict(zip(
getattr(src.locales, "codes"), getattr(src.locales, LANGUAGE) getattr(src.locales, "codes"),
getattr(src.locales, LANGUAGE) # type: ignore
)) ))
class Talc(App): class Talc(App):
@ -50,13 +51,15 @@ class Talc(App):
): ):
super().__init__(driver_class, css_path, watch_css, ansi_color) super().__init__(driver_class, css_path, watch_css, ansi_color)
self.locale = locale 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: async def on_mount(self) -> None:
self.telegram_client = TelegramClient( self.telegram_client = TelegramClient(
getenv("CURRENT_USER"), getenv("CURRENT_USER"),
API_ID, API_ID,
API_HASH API_HASH # type: ignore
) )
await self.telegram_client.connect() await self.telegram_client.connect()

View File

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

View File

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