diff --git a/src/app.py b/src/app.py index beb2714..bf7632b 100644 --- a/src/app.py +++ b/src/app.py @@ -38,7 +38,7 @@ class Talc(App): (Keys.ControlF, "notify(\"Нажата кнопка папок\")", locale["folders"]), (Keys.Tab, "notify(\"Нажат таб\")", locale["switch_focus"]), (Keys.Enter, "notify(\"Нажат энтер\")", locale["enter"]), - (Keys.Escape, "notify(\"Нажат эскейп\")", locale["back"]), + (Keys.Escape, "show_start_screen()", locale["back"]), (_character_to_key("/"), "notify(\"Нажат слэш\")", locale["search"]) ] @@ -51,9 +51,11 @@ class Talc(App): ): super().__init__(driver_class, css_path, watch_css, ansi_color) self.locale = locale + self.title = self.locale["talc"] self.timezone = timezone(timedelta(hours=int(UTC_OFFSET))) # type: ignore self.CHATS_LIMIT = CHATS_LIMIT # type: ignore self.MESSAGES_LIMIT = MESSAGES_LIMIT # type: ignore + self.msg_buffer = None async def on_mount(self) -> None: self.telegram_client = TelegramClient( @@ -63,8 +65,8 @@ class Talc(App): ) await self.telegram_client.connect() - chat_screen = ChatScreen(telegram_client=self.telegram_client) - self.install_screen(chat_screen, name="chats") + self.chat_screen = ChatScreen(telegram_client=self.telegram_client) + self.install_screen(self.chat_screen, name="chats") if not await self.telegram_client.is_user_authorized(): auth_screen = AuthScreen(telegram_client=self.telegram_client) @@ -75,6 +77,9 @@ class Talc(App): self.scroll_sensitivity_y = 1.0 + def show_start_screen(self) -> None: + self.chat_screen.switcher.current = "start_label" + async def on_exit_app(self): await self.telegram_client.disconnect() return super()._on_exit_app() diff --git a/src/locales.py b/src/locales.py index 3b31e54..94abb7a 100644 --- a/src/locales.py +++ b/src/locales.py @@ -1,3 +1,3 @@ -codes = ["auth_greeting", "phone_number", "code", "password", "you", "mention", "media", "message", "folders", "switch_focus", "enter", "back", "search", "start_converse"] -ru = ["Добро пожаловать в Тальк", "Номер телефона", "Код", "Пароль", "Вы", "Вас упомянули", "Медиа", "Сообщение", "Папки", "Переключение фокуса", "Открыть", "Назад", "Поиск", "Нажмите на чат в панели слева, чтобы начать общаться"] -en = ["Welcome to Talc", "Phone number", "Code", "Password", "You", "You got mentioned", "Media", "Message", "Folders", "Switch focus", "Enter", "Back", "Search", "Click on the chat to start conversation"] \ No newline at end of file +codes = ["auth_greeting", "phone_number", "code", "password", "you", "mention", "media", "message", "folders", "switch_focus", "enter", "back", "search", "start_converse", "talc"] +ru = ["Добро пожаловать в Тальк", "Номер телефона", "Код", "Пароль", "Вы", "Вас упомянули", "Медиа", "Сообщение", "Папки", "Переключение фокуса", "Открыть", "Назад", "Поиск", "Нажмите на чат в панели слева, чтобы начать общаться", "Тальк"] +en = ["Welcome to Talc", "Phone number", "Code", "Password", "You", "You got mentioned", "Media", "Message", "Folders", "Switch focus", "Enter", "Back", "Search", "Click on the chat to start conversation", "Talc"] \ No newline at end of file diff --git a/src/screens.py b/src/screens.py index 2abfd77..b214328 100644 --- a/src/screens.py +++ b/src/screens.py @@ -4,11 +4,12 @@ from textual.screen import Screen from textual.widgets import Label, Input, Footer, Static, ContentSwitcher from textual.containers import Vertical, Horizontal, VerticalScroll from textual.app import ComposeResult +from textual.css.query import NoMatches from telethon.errors import SessionPasswordNeededError +from telethon.utils import get_display_name from telethon import TelegramClient, events from src.widgets import Chat from os import system, getenv -from telethon.utils import get_display_name class AuthScreen(Screen): """Класс экрана логина в аккаунт""" @@ -131,7 +132,10 @@ class ChatScreen(Screen): if 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}", + number = i + chats_amount + 1 + ) self.chat_container.mount(chat) elif limit < chats_amount: # Удаление лишних, если чатов больше, чем нужно @@ -142,7 +146,7 @@ class ChatScreen(Screen): print("Виджеты чатов загружены") async def update_chat_list(self, event = None) -> None: - """Функция обновления чатов (и уведомления)""" + """Функция обновления чатов""" print("Запрос обновления чатов") if not self.is_chat_update_blocked: @@ -159,13 +163,18 @@ class ChatScreen(Screen): # Изменение надписей в виджетах чатов for i in range(limit): - chat = self.chat_container.query_one(f"#chat-{i + 1}") + chat = self.chat_container.query(".chat")[i] chat.peername = str(dialogs[i].name) chat.is_group = dialogs[i].is_group chat.is_channel = dialogs[i].is_channel chat.peer_id = dialogs[i].id + if not (self.switcher.current in ("start_label")) \ + and chat.peer_id == int(self.switcher.current[7:]): + self.remove_chat_lint() + chat.add_class("selected_chat") + try: is_my_msg = \ dialogs[i].message.from_id.user_id == self.me_id @@ -187,23 +196,15 @@ 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: + if not (self.switcher.current in ("start_label")): current_dialog = \ self.switcher.query_one(f"#{self.switcher.current}") - await current_dialog.update_dialog() + await current_dialog.update_dialog() def notify_send(self, event) -> None: if not event: @@ -211,16 +212,27 @@ class ChatScreen(Screen): if int(self.DO_NOTIFY) and not self.app.focused and event.mentioned: system(f"notify-send \"{self.locale["mention"]}\" Talc") + def remove_chat_lint(self) -> None: + try: + self.chat_container.query_one(".selected_chat")\ + .remove_class("selected_chat") + except NoMatches: + pass + def compose(self) -> ComposeResult: yield Footer() # Нижняя панель с подсказками with Horizontal(id="main_container"): # Основной контейнер with Horizontal(id="chats"): - yield VerticalScroll(id="chat_container") + yield VerticalScroll( + id="chat_container", + can_focus = False, + can_focus_children = True + ) #TODO: сделать кнопку, чтобы прогрузить больше чатов, # или ленивую прокрутку - yield ContentSwitcher(id="dialog_switcher") + with ContentSwitcher(id="dialog_switcher", initial="start_label"): # ↑ Внутри него как раз крутятся диалоги - #yield Label( - # self.locale["start_converse"], - # id="start_converse_label" - #) #TODO: не показывается надпись, надо будет исправить + yield Label( + self.locale["start_converse"], + id="start_label" + ) diff --git a/src/style.tcss b/src/style.tcss index 154caa1..ee2d462 100644 --- a/src/style.tcss +++ b/src/style.tcss @@ -74,12 +74,13 @@ ContentSwitcher { height: 100%; } -#start_converse_label { - width: 100%; +#start_label { + width: 70%; height: 100%; text-align: center; content-align: center middle; color: $panel; + padding: 3; } TopBar { diff --git a/src/widgets.py b/src/widgets.py index 229be3a..92a6f9e 100644 --- a/src/widgets.py +++ b/src/widgets.py @@ -7,7 +7,7 @@ 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 +from telethon import TelegramClient, utils, types class Chat(Widget): """Класс виджета чата для панели чатов""" @@ -21,9 +21,10 @@ class Chat(Widget): def __init__( self, + number: int, name: str | None = None, id: str | None = None, - classes: str | None = None, + classes: str = "chat", disabled: bool = False ) -> None: super().__init__( @@ -32,24 +33,24 @@ class Chat(Widget): classes=classes, disabled=disabled ) + self.number = number def on_mount(self) -> None: self.switcher = self.screen.query_one(Horizontal)\ .query_one("#dialog_switcher", ContentSwitcher) - if int(self.id[5:]) % 2 != 0: + if self.number % 2 != 0: self.add_class("odd") else: self.add_class("even") - def on_click(self) -> None: + async def on_click(self) -> None: # Получение ID диалога и создание DOM-ID на его основе - dialog_id = f"dialog-{str(self.peer_id)}" + dialog_id = f"dialog-{self.peer_id}" # Маунт диалога try: self.switcher.mount(Dialog( - telegram_client=self.app.telegram_client, chat_id=self.peer_id, id=dialog_id, is_channel=self.is_channel and not self.is_group @@ -59,6 +60,8 @@ class Chat(Widget): pass self.switcher.current = dialog_id + self.screen.remove_chat_lint() + self.add_class("selected_chat") self.switcher.recompose() def compose(self) -> ComposeResult: @@ -75,15 +78,14 @@ class Dialog(Widget): def __init__( self, - id=None, - classes=None, - disabled=None, - telegram_client: TelegramClient | None = None, + id = None, + classes = None, + disabled = None, chat_id: int | None = None, is_channel: bool | None = None ) -> None: super().__init__(id=id, classes=classes, disabled=disabled) - self.telegram_client = telegram_client + self.telegram_client: TelegramClient = self.app.telegram_client self.chat_id = chat_id self.is_msg_update_blocked = False self.timezone = self.app.timezone @@ -103,8 +105,6 @@ class Dialog(Widget): await self.update_dialog() - #self.dialog.scroll_down(animate=False, immediate=True) - def mount_messages(self, limit: int) -> None: print("Загрузка виджетов сообщений...") @@ -116,6 +116,7 @@ class Dialog(Widget): Message(id=f"msg-{i + msg_amount + 1}"), before=0 ) + self.dialog.scroll_end() elif limit < msg_amount: for i in range(msg_amount - limit): self.dialog.query(Message).last().remove() @@ -137,35 +138,34 @@ class Dialog(Widget): for i in range(limit): msg = self.dialog.query_one(f"#msg-{i + 1}") message = Content(str(messages[i].message)) - if str(messages[i].message): - entities = messages[i].entities - if entities: - for entity in entities: - match type(entity): - case types.MessageEntityBold: - message = message.stylize( - "bold", - entity.offset, - entity.offset + entity.length - ) - case types.MessageEntityUnderline: - message = message.stylize( - "underline", - entity.offset, - entity.offset + entity.length - ) - case types.MessageEntityItalic: - message = message.stylize( - "italic", - entity.offset, - entity.offset + entity.length - ) - case types.MessageEntityStrike: - message = message.stylize( - "strike", - entity.offset, - entity.offset + entity.length - ) + entities = messages[i].entities + if entities != None: + for entity in entities: + match type(entity): + case types.MessageEntityBold: + message = message.stylize( + "bold", + entity.offset, + entity.offset + entity.length + ) + case types.MessageEntityUnderline: + message = message.stylize( + "underline", + entity.offset, + entity.offset + entity.length + ) + case types.MessageEntityItalic: + message = message.stylize( + "italic", + entity.offset, + entity.offset + entity.length + ) + case types.MessageEntityStrike: + message = message.stylize( + "strike", + entity.offset, + entity.offset + entity.length + ) if messages[i].media and str(message): msg.message = Content(f"[{self.app.locale["media"]}] ")\ @@ -187,6 +187,7 @@ class Dialog(Widget): .date\ .astimezone(self.timezone)\ .strftime("%H:%M") + msg.msg_obj = messages[i] self.top_bar.peername = utils.get_display_name( await self.telegram_client.get_entity(self.chat_id) @@ -197,6 +198,29 @@ class Dialog(Widget): else: print("Обновление сообщений невозможно: уже выполняется") + async def on_button_pressed(self, event = None) -> None: + await self.send_message() + + async def on_input_submitted(self, event = None) -> None: + await self.send_message() + + async def send_message(self) -> None: + if self.app.msg_buffer is not None: + await self.telegram_client.forward_messages( + self.chat_id, + self.app.msg_buffer + ) + self.app.msg_buffer = None + try: + await self.telegram_client.send_message( + self.chat_id, + str(self.msg_input.value) + ) + except ValueError: + print("Ошибка отправки") + self.msg_input.value = "" + await self.update_dialog() + def compose(self) -> ComposeResult: with Vertical(): yield TopBar() @@ -209,23 +233,6 @@ class Dialog(Widget): ) yield Button(label="➤", id="send", variant="primary") - async def on_button_pressed(self, event = None) -> None: - await self.send_message() - - async def on_input_submitted(self, event = None) -> None: - await self.send_message() - - async def send_message(self) -> None: - try: - await self.telegram_client.send_message( - self.chat_id, - str(self.msg_input.value) - ) - except ValueError: - print("Ошибка отправки") - self.msg_input.value = "" - await self.update_dialog() - class Message(Widget): """Класс виджета сообщений для окна диалога""" @@ -234,13 +241,18 @@ class Message(Widget): username: reactive[str] = reactive("", recompose=True) info: reactive[str] = reactive("", recompose=True) - def __init__(self, id=None) -> None: + def __init__(self, id = None) -> None: super().__init__(id=id) + def on_click(self, event): + if event.chain == 2: + self.app.msg_buffer = self.msg_obj + self.app.notify("Сообщение в буфере.") + def compose(self) -> ComposeResult: label = Label(self.message, markup=False) - label.border_title = self.username * (not self.is_me) - label.border_subtitle = self.info + label.border_title = Content(self.username * (not self.is_me)) + label.border_subtitle = Content(self.info) with Container(): yield label @@ -254,8 +266,23 @@ class TopBar(Widget): """Класс виджета верхней панели для окна диалога""" peername: reactive[str] = reactive(" ", recompose=True) + + def __init__(self): + super().__init__() + self.telegram_client: TelegramClient = self.app.telegram_client + + def on_click(self, event = None) -> None: + """Обработка нажатия""" + + """pic = self.telegram_client.download_profile_photo( + self.screen.query_one(Horizontal)\ + .query_one("#dialog_switcher", ContentSwitcher)\ + .query_one(f"#{self.screen.query_one(Horizontal)\ + .query_one("#dialog_switcher", ContentSwitcher).current}").chat_id + ) + system(f"echo \"{pic}\" > picture.txt")""" # Debug штучка def compose(self) -> ComposeResult: with Horizontal(): - yield Label(self.peername[:1], classes="avatar") - yield Label(self.peername, classes="peername_top_bar") + yield Label(self.peername[:1], classes="avatar", markup=False) + yield Label(self.peername, classes="peername_top_bar", markup=False)