Cat

Kawai.Focus - приложение для фокусировки внимания (часть 7)

Данная статья посвящена:

  • Работе с фреймворком Kivy в проекте Kawai.Focus;
  • Созданию экрана конструктора таймера;
  • Реализации перехода с одного экрана на другой;
  • Доработке экрана таймера.

Дополнительные материалы

Icon Link

Реклама

Icon Link
Kawai.Focus Arduinum628 19 Июнь 2025 Просмотров: 55

Вступление

Всем доброго дня! В предыдущей статье Kawai.Focus - приложение для фокусировки внимания (часть 6) было сделано:

  • Созданы и обновлены crud функции для таймера;
  • Написан декоратор для обработки ошибок @crud_error_guard;
  • Добавлена pydantic схема TimerListModel для списка таймеров;
  • Написаны тесты на pytest для crud функций;
  • Тесты из validators_tests.py превращены в тесты для схем schemas_tests.py.

Было написано много нового функционала, который мне предстоит связать с новыми экранами.

Сегодня я создам экран конструктора таймера, подключу к нему CRUD-функцию new_timer() и покажу, как реализовать переход от одного таймера к другому. Затем я запущу конструктор и проверю, как он справляется с созданием нового таймера.

Обратите внимание: в этой статье конструктор и таймер представлены в предварительной версии. Это скорее демо, чем завершённый продукт — работа над доработкой ещё продолжается.

Заваривайте чай, доставайте вкусняшки — пора “выращивать новые помидоры”! 🍅


Конструктор таймера

Конструктор таймера нужен для того что-бы создать новый таймер с уникальным именем и гибкими настройками под пользователя. У таймера должны настраиваться следующие поля:

  • title — название таймера;
  • pomodoro_time — время/длительность одного помидора;
  • break_time — время/длительность перерыва между помидорами;
  • break_long_time — время/длительность длительного перерыва;
  • count_pomodoro — количество помидоров.

Пока экран таймера TimerScreen будет поддерживает только поле pomodoro_time. Для использования остальных полей и их функциональности в дальнейшем потребуется модернизация кода таймера и его логики. Пока же я сосредоточусь на реализации конструктора таймера.

timer_constructor_screen.kv

Сначала в папке kv, в котором находится UI экранов я создам файл timer_constructor_screen.kv, который будет отвечать за отрисовку элементов конструктора таймера:

#:kivy 2.3.1

<TimerConstructorScreen>:
    FloatLayout:        
        # поле вовода "название"
        TextInput:
            size_hint: None, None
            size: 220, 30
            pos_hint: {"center_x": 0.5, "center_y": 0.9}
            hint_text: "название"

        # числовой счётчик "время помидора"
        Label:
            text: "помидор"
            size_hint: None, None
            pos_hint: {"center_x": 0.3, "center_y": 0.7}

        Button:
            text: "-"
            size_hint: None, None
            size: 40, 40
            pos_hint: {"center_x": 0.5, "center_y": 0.7}
            on_press: time_tomato_input.decrement()

        TimeTomatoInput:
            id: time_tomato_input
            size_hint: None, None
            size: 40, 30
            pos_hint: {"center_x": 0.6, "center_y": 0.7}

        Button:
            text: "+"
            size_hint: None, None
            size: 40, 40
            pos_hint: {"center_x": 0.7, "center_y": 0.7}
            on_press: time_tomato_input.increment()

        # числовой счётчик "время прерыва"
        Label:
            text: "перерыв"
            size_hint: None, None
            pos_hint: {"center_x": 0.3, "center_y": 0.6}

        Button:
            text: "-"
            size_hint: None, None
            size: 40, 40
            pos_hint: {"center_x": 0.5, "center_y": 0.6}
            on_press: time_break_input.decrement()

        TimeBreakInput:
            id: time_break_input
            size_hint: None, None
            size: 40, 30
            pos_hint: {"center_x": 0.6, "center_y": 0.6}

        Button:
            text: "+"
            size_hint: None, None
            size: 40, 40
            pos_hint: {"center_x": 0.7, "center_y": 0.6}
            on_press: time_break_input.increment()

        # числовой счётчик "время длительного перерыва"
        Label:
            text: "перерывище"
            size_hint: None, None
            pos_hint: {"center_x": 0.3, "center_y": 0.5}

        Button:
            text: "-"
            size_hint: None, None
            size: 40, 40
            pos_hint: {"center_x": 0.5, "center_y": 0.5}
            on_press: time_long_break_input.decrement()

        TimeLoongBreakInput:
            id: time_long_break_input
            size_hint: None, None
            size: 40, 30
            pos_hint: {"center_x": 0.6, "center_y": 0.5}

        Button:
            text: "+"
            size_hint: None, None
            size: 40, 40
            pos_hint: {"center_x": 0.7, "center_y": 0.5}
            on_press: time_long_break_input.increment()

        # числовой счётчик "колличество помидоров"
        Label:
            text: "помидоров"
            size_hint: None, None
            pos_hint: {"center_x": 0.3, "center_y": 0.4}

        Button:
            text: "-"
            size_hint: None, None
            size: 40, 40
            pos_hint: {"center_x": 0.5, "center_y": 0.4}
            on_press: count_tomatos_input.decrement()

        CountTomatosInput:
            id: count_tomatos_input
            size_hint: None, None
            size: 40, 30
            pos_hint: {"center_x": 0.6, "center_y": 0.4}

        Button:
            text: "+"
            size_hint: None, None
            size: 40, 40
            pos_hint: {"center_x": 0.7, "center_y": 0.4}
            on_press: count_tomatos_input.increment()

        # Кнопка "Создать"
        Button:
            text: 'Создать'
            size_hint: None, None
            height: 40
            width: 100
            pos_hint: {'center_x': 0.7, 'center_y': 0.1}
            on_press: root.create_timer(self)

        # Кнопка "Назад"
        Button:
            text: 'Назад'
            size_hint: None, None
            height: 40
            width: 100
            pos_hint: {'center_x': 0.3, 'center_y': 0.1}
            on_press: root.back(self)

Разбор файла разметки UI:

  • TextInput "название" — поле для ввода названия таймера:
    • hint_text: "название" — чем-то похож на плейсхолдер html, который нужен для отображения текста в поле (сотрётся при начале ввода).
  • Label "помидор" — подпись для счётчика рабочего времени;
  • TimeTomatoInput — кастомный числовой счётчик длительности одного "помидора";
  • Button "+" — кнопки для увеличения значения на 1:
    • on_press: time_long_break_input.increment() — по нажатию на кнопку вызовет метод increment(), который увеличит число ввода на 1.
  • Button "-" — кнопки для уменьшения значения на 1:
    • on_press: time_tomato_input.decrement() — по нажатию на кнопку вызовет метод decrement(), который уменьшит число ввода на 1;
  • Label "перерыв" — подпись для счётчика короткого перерыва;
  • TimeBreakInput — кастомный числовой счётчик короткого перерыва;
  • Label "перерывище" — подпись для счётчика длинного перерыва;
  • TimeLoongBreakInput — кастомный числовой счётчик длительного перерыва;
  • Label "помидоров" — подпись для счётчика количества рабочих циклов;
  • CountTomatosInput — кастомный числовой счётчик количества "помидоров";
  • Button "Создать" — кнопка для запуска создания таймера;
    • on_press: root.create_timer(self) — по нажатию на кнопку вызовет метод create_timer(), который создаст новый таймер.
  • Button "Назад" — кнопка возврата на предыдущий экран:
    • on_press: root.back(self) — по нажатию на кнопку вызовет метод back(), который перенесёт на предыдущий экран "Список таймеров" (пока там будет заглушка).

Теперь, когда kv-файл конструктора таймера готов, я начну писать Python-код, который будет с ним взаимодействовать.

timer_wigets.py

Теперь я создам кастомные виджеты для числовых счётчиков. По крайней мере, я не нашёл в Kivy подходящего виджета под свои нужды, поэтому решил реализовать свой собственный.:

from kivy.uix.textinput import TextInput


class BaseNumInput(TextInput):
    """Базовый класс для поля числа"""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.text = '0'
        self.halign = 'center'

    def increment(self):
        """Метод для прибавки 1"""

        self.text = str(int(self.text) + 1)

    def decrement(self):
        """Метод для вычитания 1"""

        self.text = str(int(self.text) - 1)


class TimeTomatoInput(BaseNumInput):
    """Класс для поля ввода колличества помидорово"""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.text = '25'


class TimeBreakInput(BaseNumInput):
    """Класс для поля ввода времени перерыва"""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.text = '5'

class TimeLoongBreakInput(BaseNumInput):
    """Класс для поля ввода времени длительного перерыва"""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.text = '15'


class CountTomatosInput(BaseNumInput):
    """Класс для поля ввода количества помидоров"""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.text = '4'

Разбор кода:

  • `BaseNumInput:
    • Базовый класс для виджета числового счётчика;
    • Наследует от TextInput (класс для ввода текста в поле);
    • Устанавливает текст по умолчанию в '0';
    • Центрирует ввод в self.halign = 'center';
    • Содержит методы increment() и decrement() для изменения значения на ±1.
  • TimeTomatoInput:
    • Класс для числового счётчика время помидора;
    • Инициализируется со значением по умолчанию '25' минут.
  • TimeBreakInput:
    • Устанавливает значение по умолчанию '5' минут;
    • Используется для настройки коротких перерывов между сессиями.
  • TimeLoongBreakInput:
    • Значение по умолчанию — '15' минут;
    • Предназначен для длительных восстановительных пауз.
  • CountTomatosInput:
    • Инициализируется числом '4' минуты;
    • Предназначен для настройки количества помидоров.

timer_constructor_screen.py

После того как я закончил создания UI создаю .py файл для экрана timer_constructor_screen.py в пакете screens:

from kivy.uix.screenmanager import Screen

from kawai_focus.custom_widgets.timer_wigets import TimeTomatoInput
from kawai_focus.schemas import TimerModel
from kawai_focus.database.cruds import new_timer


class TimerConstructorScreen(Screen):
    """Экран конструктора таймера"""

    def __init__(self, **kwargs):
        super(TimerConstructorScreen, self).__init__(**kwargs)

    def create_timer(self, instance):
        """Метод для создания таймера"""

        timer = new_timer(
            data=TimerModel(
                title=self.ids.title.text,
                pomodoro_time=int(self.ids.time_tomato_input.text),
                break_time=int(self.ids.time_break_input.text),
                break_long_time=int(self.ids.time_long_break_input.text),
                count_pomodoro=int(self.ids.count_tomatos_input.text)
            )
        )

        if timer:
            screen_timer = self.manager.get_screen('timers_screen')
            screen_timer.timer = timer
            # Todo: временное решение: нужно сделать механизм для
            # автоматического рассчёта часов, минут и секунд из
            # колличества минут, введённого пользователем
            screen_timer.ids.time_label.text = f'00:{
                "0" + str(timer.pomodoro_time) if timer.pomodoro_time <= 9 else timer.pomodoro_time
            }:00'
            self.manager.current = 'timers_screen'

    def back(self, instance) -> None:
        """Метод для кнопки назад - возврат в меню таймеров"""

        pass

Разбор кода:

  • Импортируется класс Screen из модуля kivy.uix.screenmanager для создания пользовательского экрана ;
  • Импортируются пользовательские классы и функции: TimeTomatoInput (виджет ввода), TimerModel (схема таймера), new_timer (CRUD-функция создания таймера) ;
  • Создаётся класс TimerConstructorScreen, наследующий Screen, представляющий экран конструктора таймера ;
  • В конструкторе __init__ вызывается инициализация родительского класса через super() ;
  • Метод create_timer реализует функциональность по созданию нового таймера на основе данных, введённых пользователем ;
  • Значения для TimerModel берутся из виджетов экрана через self.ids, все поля преобразуются в int ;
  • Созданный объект таймера сохраняется через функцию new_timer и, если успешно, передаётся экрану с таймерами (timers_screen) ;
  • Поле time_label на экране таймера обновляется вручную, чтобы отобразить минуты в формате 00:MM:00 ;
  • После установки таймера активируется экран timers_screen путём изменения текущего экрана self.manager.current (переход на экран созданного таймера после его создания);
  • Метод back определён, но не реализован — предназначен для возврата в главное меню таймеров (пока не будет создан экран "Список таймеров" тут будет заглушка pass).

main.py

После того как новый экран написан его нужно подключить в главном файле main.py:

# Другой код
from kawai_focus.screens.timer_constructor_screen import TimerConstructorScreen


class KawaiFocusApp(App):
    """Класс для создания приложения"""

    title = 'Kawai.Focus'

    def build(self):
        # Другой код
        Builder.load_file('kv/timer_constructor_screen.kv')
        # Другой код
        screen_manager.add_widget(TimerConstructorScreen(name='timer_constructor_screen')
        # Другой код

Разбор нового кода:

  • Импорт TimerConstructorScreen конструктора класса из kawai_focus.screens.timer_constructor_screen;
  • Builder.load_file('kv/timer_constructor_screen.kv') — загрузка kv файла конструктора таймера;
  • screen_manager.add_widget(TimerConstructorScreen(name='timer_constructor_screen') — добавление экрана конструктора таймера в screen_manager.

Таймер

Теперь осталось доработать таймер так, чтобы он принимал значения, передаваемые через его конструктор. Пока он работает только с минутами и секундами, но в следующей статье я планирую добавить поддержку часов — они будут вычисляться на основе минут. В текущей версии программа завершится с ошибкой, если ввести более 59 минут.

Изменения в TimerScreen:

class TimerScreen(Screen):
    """Экран таймера"""
    # Другой код

    def __init__(self, **kwargs):
        # Другой код
        self.timer = None

    def start_timer(self, instance) -> None:
        """Метод для запуска таймера"""
        # Другой код
        self.valid_timer_data = TimerTimeModel(mm=self.timer.pomodoro_time)
        # Другой код

    def stop_timer(self, instance) -> None:
        """Метод для остановки таймера"""
        # Другой код
        self.remaining_time = TimerTimeModel(mm=self.timer.pomodoro_time).mm
        # Todo: временное решение: нужно сделать механизм для
        # автоматического рассчёта часов, минут и секунд из
        # колличества минут, введённого пользователем
        self.ids.time_label.text = f'00:{
            "0" + str(self.remaining_time) if self.remaining_time <= 9 else self.remaining_time
        }:00'
        # Другой код

Разбор нового кода:

  • self.timer = None Инициализация атрибута timer со значением None. Это означает, что таймер ещё не создан или не запущен. Позже этот атрибут должен получить объект с параметрами таймера;
  • self.valid_timer_data = TimerTimeModel(mm=self.timer.pomodoro_time) Создание экземпляра класса TimerTimeModel, используя значение pomodoro_time из текущего таймера;
  • self.remaining_time = TimerTimeModel(mm=self.timer.pomodoro_time).mm Повторное создание объекта TimerTimeModel, но теперь сразу извлекается количество минут (mm) и сохраняется в переменную remaining_time. Это значение будет использоваться для обновления интерфейса;
  • self.ids.time_label.text = f'00:{ "0" + str(self.remaining_time) if self.remaining_time <= 9 else self.remaining_time }:00' Форматирование и отображение времени в формате 00:MM:00, где MM — это remaining_time. Если значение меньше 10, добавляется ведущий ноль для корректного отображения времени (например, 00:05:00).

Создание нового таймера

Настало время проверить работу конструктора таймера и продемонстрировать этот процесс. Для этого запускаю приложение через терминал с помощью Poetry:

poetry run kawai-focus

Таймер успешно создан

Этот сценарий предполагает, что конструктор успешно создаёт новый таймер и переводит пользователя на экран его настройки. При запуске отображается экран конструктора:

Для числовых значений установлены значения по умолчанию — они представляют собой рабочий пример настроек. Пользователь может быстро создать таймер, просто введя название, либо изменить часть параметров по своему усмотрению.

Далее я создаю таймер, редактирую несколько полей и нажимаю кнопку "Создать":

Как видно на скриншоте, таймер успешно сохранён в базе данных, и пользователь перенаправлен на экран нового таймера. Для проверки нажимаю кнопку "Старт":

Таймер корректно запускается и начинает обратный отсчёт минут и секунд:

Таймер не удалось создать

Теперь проверим поведение приложения при некорректном вводе. Пока в коде нет валидации и механизма, предотвращающего создание ошибочного таймера. Я решил использовать это как возможность показать, как отсутствие валидации может повлиять на поведение приложения.

Для моделирования ошибки: не ввожу название таймера, уменьшаю одно из числовых значений до отрицательного значения (с помощью кнопки "−") и нажимаю кнопку "Cоздать":

Приложение не упало — запись сохранилась в базе данных, а пользователь был перенаправлен на экран таймера. Название отсутствует, а "время помидора" оказалось равным -16. Выглядит это неожиданно: 00:0-16:00. Я понимаю, что при запуске такого таймера приложение наверняка упадёт. Но в этом и была моя цель — протестировать на прочность. Итак, запускаю таймер:

Как и ожидалось, сработала валидация Pydantic — отрицательное значение -16 вызвало ошибку:

Выводы:

  • Необходима валидация данных конструктора;
  • Поля с числовыми значениями должны иметь ограничения, исключающие отрицательные числа;
  • Поле «Название» должно выделяться при отсутствии введённого текста, и сохранение пустого названия нужно запретить;
  • Это хороший пример того, зачем нужна валидация и обработка ошибок в интерфейсах и формах.

Анонс на следующие статьи

Следующая моя статья выйдет 10 июля 2025 года. В ней я продолжу разработку функционала таймера и его конструктора.

На текущий момент в экране таймера реализованы не все возможности и настройки. В частности, необходимо добавить механизм воспроизведения цепочки таймеров — они должны запускаться последовательно, с предусмотренными перерывами между ними.

Также требуется реализовать валидацию вводимых данных в конструкторе таймеров, чтобы избежать некорректного ввода и повысить стабильность приложения.

Если у вас есть мысли о том, как можно улучшить проект, пишите в комментариях — с удовольствием ознакомлюсь с вашими предложениями!

Читайте продолжение — не пропустите!


Поздравление канала

Поздравляю канал Код на салфетке с двухлетием! День рождения канала — 18 июня 2025 года. Желаю ему развития и процветания в наше непростое время. За время своего существования канал многое мне дал, и я с радостью продолжу поддерживать его и замечательное комьюнити.

Сейчас на канале проходит конкурс — присоединяйтесь! Подписывайтесь на Код на салфетке в Telegram — будет весело=)


Заключение

  1. Написана демонстрационная версия конструктора таймера;
  2. Реализован переход между экранами;
  3. Обновлён код таймера для работы с новыми данными;
  4. Продемонстрировано создание нового таймера в двух сценариях:
    1. Таймер успешно создан;
    2. Таймер не удалось создать;

Ссылки к статье

Автор

    Нет комментариев

    Реклама