mirror of
https://github.com/avitoras/telegram-tui.git
synced 2025-07-27 11:20:31 +00:00
Compare commits
4 Commits
7ecdf5ea39
...
54704d48e3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
54704d48e3 | ||
![]() |
e6a49fa3c3 | ||
![]() |
262fdcba59 | ||
![]() |
2a18f625c0 |
10
.env.example
10
.env.example
@ -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
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
Тальк — клиент Telegram с текстовым пользовательским интерфейсом, написанный на Python, Telethon и Textual.
|
Тальк — клиент Telegram с текстовым пользовательским интерфейсом, написанный на Python, Telethon и Textual.
|
||||||
|
|
||||||
|
Будьте добры, по-русски | [In English, please](readme/README-en.md)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
- Python 3.12
|
- Python 3.12
|
||||||
|
46
readme/README-en.md
Normal file
46
readme/README-en.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Talc
|
||||||
|
|
||||||
|
Talc is a Telegram client with TUI, written in Python, Telethon and Textual.
|
||||||
|
|
||||||
|
[Будьте добры, по-русски](../README.md) | In English, please
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.12
|
||||||
|
- pyenv (recommended for managing Python versions)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Install Python 3.12 via pyenv:
|
||||||
|
```bash
|
||||||
|
pyenv install 3.12
|
||||||
|
pyenv local 3.12
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create and activate virtual enviroment:
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate # for Linux/macOS
|
||||||
|
# or
|
||||||
|
.venv\Scripts\activate # for Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Configure enviroment variables:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Configure .env file and add your API-keys
|
||||||
|
# Get keys on https://my.telegram.org/apps
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./main.py
|
||||||
|
```
|
21
src/app.py
21
src/app.py
@ -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()
|
||||||
|
|
||||||
|
@ -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,15 +86,17 @@ 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")
|
||||||
# Получение объекта контейнера чатов
|
# Получение объекта контейнера чатов
|
||||||
self.chat_container = self\
|
self.chat_container = self\
|
||||||
.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: не показывается надпись, надо будет исправить
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
@ -38,6 +36,11 @@ class Chat(Widget):
|
|||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
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 на его основе
|
||||||
@ -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,10 +183,14 @@ 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("Сообщения обновлены")
|
||||||
@ -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")
|
||||||
|
119
style.tcss
119
style.tcss
@ -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;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user