add "add word" dialog with handlers, windows, and states integration

This commit is contained in:
ruslangilfanov 2025-12-20 13:17:12 +02:00
parent 40ed52777e
commit 6c34219a6f
No known key found for this signature in database
11 changed files with 200 additions and 4 deletions

View File

@ -11,7 +11,8 @@ from ..database.base import Base
from ..languages import LanguageEnum
class LexicalCategoryEnum(str, enum.Enum):
@enum.unique
class LexicalCategoryEnum(enum.StrEnum):
noun = "noun"
verb = "verb"
adjective = "adjective"

View File

@ -2,7 +2,7 @@ import enum
@enum.unique
class LanguageEnum(str, enum.Enum):
class LanguageEnum(enum.StrEnum):
ru = "ru"
en = "en"
el = "el"

View File

@ -16,6 +16,7 @@ from sqlalchemy.orm import Mapped, mapped_column
from greek_lang.database.base import Base
@enum.unique
class ReviewState(enum.StrEnum):
learning = "learning"
review = "review"

View File

@ -1,8 +1,11 @@
from aiogram_dialog import Dialog
from .main_menu import windows as main_windows
from .add_word import windows as add_word_windows
dialog = Dialog(
main_windows.main_window,
add_word_windows.add_word_window,
add_word_windows.add_word_result_window,
)

View File

@ -0,0 +1,65 @@
from aiogram.types import Message, CallbackQuery, BufferedInputFile
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Button
from greek_lang.languages import LanguageEnum
from greek_lang.translator import translate
from ..states import States
async def add_word(
message: Message,
source: MessageInput | Button,
manager: DialogManager,
) -> None:
if not message.text:
return
word = message.text.strip()
if not word:
return
source_lang = LanguageEnum.ru
target_lang = LanguageEnum.el
glossary_word = await translate(word, source_lang, target_lang=target_lang)
# Try to send audio pronunciation back to the user
try:
audio_bytes = getattr(glossary_word, "audio_file", None)
if audio_bytes:
# aiogTTS produces MP3 data; send as audio
caption = (
f"<b>{glossary_word.term}</b> → <b>{glossary_word.translation}</b>"
)
input_file = BufferedInputFile(
audio_bytes, filename=f"{glossary_word.term}.mp3"
)
await message.answer_audio(
audio=input_file, caption=caption, parse_mode="HTML"
)
except Exception:
# Silently ignore audio sending issues to not break the flow
pass
# Store data for the result window
manager.dialog_data.update(
{
"term": glossary_word.term,
"translation": glossary_word.translation,
"transcription": glossary_word.transcription or "",
"lexical_category": getattr(glossary_word, "lexical_category", ""),
"description": glossary_word.description or "",
"example": glossary_word.example or "",
"note": glossary_word.note or "",
}
)
# Switch to the result window state
await manager.switch_to(States.add_word_result)
async def on_add_another(
callback: CallbackQuery,
button: Button,
manager: DialogManager,
) -> None:
await callback.answer()
await manager.switch_to(States.add_word)

View File

@ -0,0 +1,73 @@
from typing import Any
from aiogram.enums import ParseMode
from aiogram_dialog import DialogManager, Window
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.markup.reply_keyboard import ReplyKeyboardFactory
from aiogram_dialog.widgets.text import Format, Const
from ..states import States
from ..base_handlers import cancel_handler
from . import handlers
async def add_word_window_getter(
dialog_manager: DialogManager, **kwargs: Any
) -> dict[str, Any]:
return {}
add_word_window = Window(
Const("<b>Введите слово</b>:"),
MessageInput(func=handlers.add_word),
Row(
Button(
Format("❌ Отмена"),
on_click=cancel_handler, # type: ignore[arg-type]
id="cancel_add_word",
),
),
markup_factory=ReplyKeyboardFactory(
input_field_placeholder=Format("Слово..."),
resize_keyboard=True,
one_time_keyboard=True,
),
state=States.add_word,
parse_mode=ParseMode.HTML,
getter=add_word_window_getter,
)
async def add_word_result_getter(
dialog_manager: DialogManager, **kwargs: Any
) -> dict[str, Any]:
# Expose dialog_data fields directly for Format widgets
return dialog_manager.dialog_data
add_word_result_window = Window(
Format(
"✅ <b>Слово добавлено</b>!\n\n"
"<b>{term}</b> → <b>{translation}</b>\n"
"{transcription}\n"
"<b>Часть речи</b>: {lexical_category!s}\n"
"<b>Описание</b>: {description}\n"
"<b>Пример</b>: {example}"
),
Row(
Button(
Const(" Добавить ещё"),
id="add_another",
on_click=handlers.on_add_another,
),
Button(
Const("🏠 В меню"),
on_click=cancel_handler, # type: ignore[arg-type]
id="to_main_menu",
),
),
state=States.add_word_result,
parse_mode=ParseMode.HTML,
getter=add_word_result_getter,
)

View File

@ -0,0 +1,16 @@
from aiogram_dialog import DialogManager, ShowMode
from aiogram_dialog.api.internal import ReplyCallbackQuery
from aiogram_dialog.widgets.kbd import Cancel
from .states import States
async def cancel_handler(
callback_query: ReplyCallbackQuery,
button: Cancel,
manager: DialogManager,
) -> None:
await manager.switch_to(
States.main_menu,
show_mode=ShowMode.DELETE_AND_SEND,
)

View File

@ -0,0 +1,26 @@
from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager, ShowMode
from aiogram_dialog.api.internal import ReplyCallbackQuery
from aiogram_dialog.widgets.kbd import Cancel, Button
from ..states import States
async def on_add_word(
callback: CallbackQuery,
button: Button,
manager: DialogManager,
) -> None:
await callback.answer()
await manager.switch_to(States.add_word)
async def cancel_handler(
callback_query: ReplyCallbackQuery,
button: Cancel,
manager: DialogManager,
) -> None:
await manager.switch_to(
States.add_word,
show_mode=ShowMode.DELETE_AND_SEND,
)

View File

@ -2,9 +2,11 @@ from typing import Any
from aiogram.enums import ParseMode
from aiogram_dialog import DialogManager, Window
from aiogram_dialog.widgets.text import Format
from aiogram_dialog.widgets.kbd import Row, Button
from aiogram_dialog.widgets.text import Format, Const
from ..states import States
from . import handlers
async def main_getter(dialog_manager: DialogManager, **kwargs: Any) -> dict[str, Any]:
@ -13,10 +15,17 @@ async def main_getter(dialog_manager: DialogManager, **kwargs: Any) -> dict[str,
main_window = Window(
Format(
"Выбери действие:",
"<b>Выбери действие</b>:",
when=lambda data, widget, dialog_manager: data["dialog_data"].get("action")
is None,
),
Row(
Button(
Const("Добавить слово"),
id="add_word",
on_click=handlers.on_add_word,
),
),
state=States.main_menu,
getter=main_getter,
parse_mode=ParseMode.HTML,

View File

@ -3,3 +3,5 @@ from aiogram.fsm.state import State, StatesGroup
class States(StatesGroup):
main_menu = State()
add_word = State()
add_word_result = State()