Cat

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

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

  • Работе с фреймворком Kivy в проекте Kawai.Focus;
  • Функционалу запуска таймеров друг за другом по цепочке;
  • Выводу остальной информации таймера на экран;
  • Механизму скрытия и показа кнопки «Стоп».

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

Icon Link
Kawai.Focus Arduinum628 31 Июль 2025 Просмотров: 35

Вступление

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

  • Написан код для подсчёта часов, минут и секунд из введённых минут;
  • Написана и проверена валидация для конструктора таймера;
  • Обновлён код.

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

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

Кнопка «Стоп» будет появляться и исчезать автоматически — почти как по волшебству. 😉

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


Цепочка из таймеров

Цепочка из таймеров состоит из таймеров «помидоров», коротких перерывов и одного длительного перерыва. В прошлой статье я создал пример таймера с такими настройками:

  • Помидор — 55 минут;
  • Короткий перерыв — 10 минут;
  • Длительный перерыв — 15 минут;
  • Количество помидоров — 4 шт.

Цепочка для этого таймера будет выглядеть следующим образом:

  • Помидор -> короткий перерыв -> помидор -> короткий перерыв -> помидор -> короткий перерыв -> помидор -> длительный перерыв.

Таймеры идут друг за другом в определённом порядке. В процессе можно нажать кнопку «Пауза» для приостановки или перейти к следующему таймеру цепочки, нажав «Стоп».

Как реализовать цепочку в коде?

  • Хранение цепочки таймеров в state_machine (машине состояний) в виде списка:
    • ['pomodoro', 'break', 'pomodoro', 'break', 'pomodoro', 'long_break'].
  • Логика смены состояния:
    • Извлечение состояния из списка методом list_timers.pop(0);
    • Передача данных в экран таймера и переключение на него;
    • Если список таймеров пуст, перенаправить пользователя на экран первого таймера из цепи. Там пользователь сможет решить, запустить цепь заново или выйти в меню таймеров (экран «Таймеры» пока не реализован).
  • Завершение текущего таймера при нажатии кнопки «Стоп».

Машина состояний — это модель поведения, при которой система может находиться в одном из предопределённых состояний и переходить между ними по определённым правилам в ответ на события. В Kivy машина состояний обычно реализуется вручную (например, через списки или словари) и управляет переходами между экранами, действиями пользователя или этапами логики, как в данном случае с цепочкой таймеров.

Функция gen_types_timers()

Для начала напишу функцию gen_types_timers() в модуле utils.py, которая будет генерировать список типов, идущих в нужном порядке для цепочки таймеров pomodoro:

def gen_types_timers(count_pomodoro: int) -> list[str]:
    """Функция для генерирования списка типов таймеров для очереди"""

    types_timers_list = ['break' if num % 2  0 else 'pomodoro' for num in range(1, count_pomodoro * 2)]
    types_timers_list.append('long_break')

    return types_timers_list

Разбор кода:

  • def gen_types_timers(count_pomodoro: int) -> list[str]: — функция принимает количество помидоров в виде числа int и возвращает список строк с названиями типов;
  • types_timers_list = ['break' if num % 2 0 else 'pomodoro' for num in range(1, count_pomodoro * 2)] — list comprehension, который добавляет:
    • 'break' — если число num чётное;
    • 'pomodoro' — если число num нечётное.
  • types_timers_list.append('long_break') — добавляет строку 'long_break' в конец списка;
  • return types_timers_list — возвращает созданный список с типами таймеров.

Класс TimerConstructorScreen

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

Удаляю строку:

screen_timer.timer_generator = custom_timer(mm_user=timer.pomodoro_time)

Теперь я добавлю на экран таймера через конструктор таймеров:

  • Название таймера;
  • Сгенерированный список типов таймеров;
  • Копию списка типов таймеров в машину состояний.
screen_timer.ids.title_label.text = timer.title
screen_timer.source_timer_names = gen_types_timers(count_pomodoro=timer.count_pomodoro)

self.manager.state_machine = screen_timer.source_timer_names.copy()

Разбор кода:

  • screen_timer.ids.title_label.text = timer.title — задаём название для лейбла таймера, которое получаем из модели нового таймера;
  • screen_timer.source_timer_names = gen_types_timers(count_pomodoro=timer.count_pomodoro) — атрибуту экрана таймера присваивается сгенерированный список типов таймеров;
  • self.manager.state_machine = screen_timer.source_timer_names.copy() — машине состояний state_machine, к которой идёт обращение через manager, присваивается копия сгенерированного списка типов таймеров.

Класс TimerScreen

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

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

self.source_timer_names = None

Исходный список типов таймеров будет храниться в атрибуте self.source_timer_names, который до передачи в него списка с данными содержит None. Этот исходный список нужен для того, чтобы при необходимости снова скопировать из него типы таймеров в машину состояний и запустить цепочку заново — не выходя из таймера в меню.

Теперь нужно реализовать механизм переключения таймеров, для чего я создам метод choice_timer():

def choice_timer(self) -> None:
    """Метод для выбора таймера"""

    current_timer_name = self.manager.state_machine.pop(0)

    if current_timer_name  'pomodoro':
        self.timer_generator = custom_timer(mm_user=self.timer.pomodoro_time)
        self.ids.time_label.text = calculate_time(mm_user=self.timer.pomodoro_time)
    elif current_timer_name  'break':
        self.timer_generator = custom_timer(mm_user=self.timer.break_time)
        self.ids.time_label.text = calculate_time(mm_user=self.timer.break_time)
    else:
        self.timer_generator = custom_timer(mm_user=self.timer.break_long_time)
        self.ids.time_label.text = calculate_time(mm_user=self.timer.break_long_time)

    self.ids.stop_button.opacity = 0
    self.ids.stop_button.disabled = True

Разбор кода:

  • def choice_timer(self) -> None: — метод ничего не возвращает;
  • current_timer_name = self.manager.state_machine.pop(0) — из списка, который находится в машине состояний, извлекается текущий тип таймера методом pop() по нулевому индексу;
  • if current_timer_name 'pomodoro': — если текущий тип равен 'pomodoro':
    • self.timer_generator = custom_timer(mm_user=self.timer.pomodoro_time) — создаётся генератор таймера с временем «помидора»;
    • self.ids.time_label.text = calculate_time(mm_user=self.timer.pomodoro_time) — задаётся стартовое значение времени для типа таймера «помидор»;
    • для других типов таймеров код работает аналогичным образом;
  • self.ids.stop_button.opacity = 0 — делает кнопку невидимой визуально;
  • self.ids.stop_button.disabled = True — делает кнопку с id stop_button неактивной (да, на невидимую кнопку в Kivy можно нажать).

Далее я обновлю метод start_timer(), в котором добавится вызов метода для переключения таймера, а также код для того, чтобы сделать кнопку «Стоп» видимой и активной:

else:
    # Инициализация генератора таймера
    if len(self.manager.state_machine) and self.timer_generator is None:
        self.choice_timer()

    self.ids.stop_button.opacity = 1
    self.ids.stop_button.disabled = False

    self.remaining_time = next(self.timer_generator, self.zero_time)

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

  • if len(self.manager.state_machine) and self.timer_generator is None: — условие истинно, если в списке машины состояний есть данные и если генератор таймера равен None:
    • self.choice_timer() — вызов метода переключения таймера;
  • self.ids.stop_button.opacity = 1 — делает кнопку с id stop_button видимой;
  • self.ids.stop_button.disabled = False — делает кнопку активной.

Теперь я добавлю новую логику в метод stop_timer(), который будет переключать на другой таймер при нажатии кнопки «Стоп», а если список в машине состояний пуст — скопирует его заново для нового запуска цепочки таймеров:

if not len(self.manager.state_machine):
    self.manager.state_machine = self.source_timer_names.copy()

self.choice_timer()

Разбор кода:

  • if not len(self.manager.state_machine): — если список в машине состояний пуст:
    • self.manager.state_machine = self.source_timer_names.copy() — копируется исходный список типов таймеров;
  • self.choice_timer() — метод для переключения таймера.

Логика цепочки таймеров полностью готова. Теперь осталось вывести информацию о текущем таймере.


Вывод остальной информации на экран

Помимо времени таймера и кнопок управления, на экран таймера необходимо вывести его название и тип.

timer_screen.kv

Начну с файла UI-экрана timer_screen.kv, в котором добавлю два Label для названия и типа таймера:

# Label для названия таймера
Label:
    id: title_label
    size_hint: None, None
    pos_hint: {'center_x': 0.3, 'center_y': 0.7}

# Label для типа таймера
Label:
    id: type_timer_label
    text: 'Помидор'
    size_hint: None, None
    pos_hint: {'center_x': 0.7, 'center_y': 0.7}

Разбор UI-шаблона:

  • id: title_labelid для изменения названия из Python;
  • pos_hint: {'center_x': 0.3, 'center_y': 0.7} — расположение по двум осям X и Y: визуально выше времени таймера и слева на экране;
  • id: type_timer_labelid для изменения типа таймера из Python;
  • pos_hint: {'center_x': 0.7, 'center_y': 0.7} — расположение по осям X и Y: визуально выше времени таймера и справа на экране.

Когда я писал новый код для класса TimerScreen, я добавил строки, которые управляют отображением кнопки «Стоп». В самом UI-экране я сделаю кнопку «Стоп» неактивной и невидимой по умолчанию:

# Кнопка "Стоп"
Button:
    id: stop_button
    text: 'Стоп'
    opacity: 0
    disabled: True
    size_hint: None, None
    height: 40
    width: 100
    pos_hint: {'center_x': 0.7, 'center_y': 0.5}
    on_release: root.stop_timer(self)

Разбор новых строк UI-шаблона:

  • opacity: 0 — делает кнопку невидимой;
  • disabled: True — делает кнопку неактивной.

Весь timer_screen.kv

#:kivy 2.3.1

<TimerScreen>:
    FloatLayout:
        # Label для названия таймера
        Label:
            id: title_label
            size_hint: None, None
            pos_hint: {'center_x': 0.3, 'center_y': 0.7}

        # Label для типа таймера
        Label:
            id: type_timer_label
            text: 'Помидор'
            size_hint: None, None
            pos_hint: {'center_x': 0.7, 'center_y': 0.7}

        # Кнопка "Старт"
        Button:
            text: 'Старт'
            size_hint: None, None
            height: 40
            width: 100
            pos_hint: {'center_x': 0.3, 'center_y': 0.5}
            on_release: root.start_timer(self)

        # Кнопка "Пауза"
        Button:
            text: 'Пауза'
            size_hint: None, None
            height: 40
            width: 100
            pos_hint: {'center_x': 0.5, 'center_y': 0.5}
            on_release: root.pause_timer(self)

        # Кнопка "Стоп"
        Button:
            text: 'Стоп'
            size_hint: None, None
            height: 40
            width: 100
            pos_hint: {'center_x': 0.7, 'center_y': 0.5}
            on_release: root.stop_timer(self)

        # Label для отображения времени
        Label:
            id: time_label
            size_hint: None, None
            pos_hint: {'center_x': 0.5, 'center_y': 0.6}

Класс TimerConstructorScreen

После того как я расширил UI-шаблон новыми полями пришло время обновить код Python для работы с ними. Сначала начну с конструктора таймера класса TimerConstructorScreen, в котором добавлю название таймера на экран таймера:

screen_timer.ids.title_label.text = timer.title

Строка screen_timer.ids.title_label.text = timer.title добавит на экран название, которое возьмёт из timer.title модели таймера.

Весь код для класса TimerConstructorScreen

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
from kawai_focus.utils.utils import calculate_time, gen_types_timers
from kawai_focus.screens.validators_fields import validate_title


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

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

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

        validate_title(self, self.ids.title.text)

        # прекратить создание таймера если поле пустое
        if not self.ids.title.text:
            return  

        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
            time_culc = calculate_time(mm_user=timer.pomodoro_time)
            screen_timer.timer_start_time = time_culc
            screen_timer.ids.time_label.text = time_culc
            screen_timer.ids.title_label.text = timer.title
            screen_timer.source_timer_names = gen_types_timers(count_pomodoro=timer.count_pomodoro)

            self.manager.state_machine = screen_timer.source_timer_names.copy()
            self.manager.current = 'timers_screen'


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

        pass

Класс TimerScreen

Далее добавлю типы таймера в логику метода choice_timer():

self.ids.type_timer_label.text = 'Помидор'
self.ids.type_timer_label.text = 'Перерыв'
self.ids.type_timer_label.text = 'Перерывище'

Каждая из этих строк присваивает type_timer_label название типа для соответствующего вида таймера.

Обновлённый метод choice_timer():

def choice_timer(self) -> None:
    """Метод для выбора таймера"""

    current_timer_name = self.manager.state_machine.pop(0)

    if current_timer_name  'pomodoro':
        self.timer_generator = custom_timer(mm_user=self.timer.pomodoro_time)
        self.ids.time_label.text = calculate_time(mm_user=self.timer.pomodoro_time)
        self.ids.type_timer_label.text = 'Помидор'
    elif current_timer_name  'break':
        self.timer_generator = custom_timer(mm_user=self.timer.break_time)
        self.ids.time_label.text = calculate_time(mm_user=self.timer.break_time)
        self.ids.type_timer_label.text = 'Перерыв'
    else:
        self.timer_generator = custom_timer(mm_user=self.timer.break_long_time)
        self.ids.time_label.text = calculate_time(mm_user=self.timer.break_long_time)
        self.ids.type_timer_label.text = 'Перерывище'

    self.ids.stop_button.opacity = 0
    self.ids.stop_button.disabled = True

Теперь при переключении таймера будет отображаться соответствующий тип.

Весь код для класса TimerScreen

from kivy.uix.screenmanager import Screen
from kivy.clock import Clock
from kivy.core.audio import SoundLoader

from kawai_focus.utils.utils import data_json, custom_timer, calculate_time


class TimerScreen(Screen):
    """Экран таймера"""

    sound_timer_name = data_json.get_text('sound_timer')
    path_file = f'sounds/{sound_timer_name}'
    sound = SoundLoader.load(path_file)

    def __init__(self, **kwargs):
        super(TimerScreen, self).__init__(**kwargs)
        # Переменные для управления таймером
        self.zero_time = data_json.get_text('zero_time')
        self.timer_generator = None 
        self.paused = False
        self.remaining_time = None
        self.sound_stop_event = None
        self.timer = None
        self.timer_start_time = None
        self.source_timer_names = None

    def choice_timer(self) -> None:
        """Метод для выбора таймера"""

        current_timer_name = self.manager.state_machine.pop(0)

        if current_timer_name  'pomodoro':
            self.timer_generator = custom_timer(mm_user=self.timer.pomodoro_time)
            self.ids.time_label.text = calculate_time(mm_user=self.timer.pomodoro_time)
            self.ids.type_timer_label.text = 'Помидор'
        elif current_timer_name  'break':
            self.timer_generator = custom_timer(mm_user=self.timer.break_time)
            self.ids.time_label.text = calculate_time(mm_user=self.timer.break_time)
            self.ids.type_timer_label.text = 'Перерыв'
        else:
            self.timer_generator = custom_timer(mm_user=self.timer.break_long_time)
            self.ids.time_label.text = calculate_time(mm_user=self.timer.break_long_time)
            self.ids.type_timer_label.text = 'Перерывище'

        self.ids.stop_button.opacity = 0
        self.ids.stop_button.disabled = True

    def start_timer(self, instance) -> None:
        """Метод для запуска таймера"""

        if self.paused:
            self.paused = False
        else:
            # Инициализация генератора таймера
            if len(self.manager.state_machine) and self.timer_generator is None:
                self.choice_timer()

            self.ids.stop_button.opacity = 1
            self.ids.stop_button.disabled = False

            self.remaining_time = next(self.timer_generator, self.zero_time)

        # Запуск обновления времени каждую секунду
        Clock.schedule_interval(self.update_time, 1)

    def pause_timer(self, instance) -> None:
        """Метод для паузы таймера"""

        if not self.paused:
            self.paused = True
            # Остановка обновления времени
            Clock.unschedule(self.update_time)

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

        # Остановка обновления времени
        Clock.unschedule(self.update_time)
        self.paused = False
        self.remaining_time = next(self.timer_generator, self.zero_time)
        self.ids.time_label.text = self.timer_start_time
        self.sound.stop()

        if self.sound_stop_event:
            Clock.unschedule(self.sound_stop_event)
            self.sound_stop_event = None

        if len(self.manager.state_machine):
            self.choice_timer()
        else:
            self.manager.state_machine = self.source_timer_names.copy()
            self.choice_timer()

    def play_sound(self, dt) -> None:
        """Метод для воспроизведения звука"""

        self.sound.play()
        # Планируем остановку звука через 20 секунд
        self.sound_stop_event = Clock.schedule_once(lambda dt: self.sound.stop(), 20)

    def update_time(self, dt) -> None:
        """Метод для обновления времени на экране"""

        if self.paused:
            return

        # Получение следующего значения времени из генератора
        self.remaining_time = next(self.timer_generator, self.zero_time)
        self.ids.time_label.text = self.remaining_time

        if self.remaining_time  self.zero_time:
            # Остановка обновления времени
            Clock.unschedule(self.update_time)
            Clock.schedule_once(self.play_sound)

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


Проверка работы цепочки таймеров

Запуск в terminal:

poetry run kawai-focus

Создаю таймер с самыми минимальными настройками:

На экране таймера видна новая информация: название, тип, отсутствует визуально кнопка «Стоп». В базе данных появилась запись с новым таймером.

Нажимаю на кнопку "Старт":

После нажатия на кнопку «Старт» появилась кнопка «Стоп». Можно нажать на кнопку «Пауза», а потом на «Стоп», а можно сразу на «Стоп». Это будет одинаково завершать текущий таймер цепи. Пользователь может не дожидаться сигнала и перейти к следующему таймеру цепочки, когда пожелает.

Нажимаю на кнопку "Стоп":

Логика программы обновила экран таймера, сменив время и тип. Теперь пользователь видит, что он попал в таймер перерыва:

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

Нажимаю на кнопку "Старт":

Нажимаю на кнопку "Стоп":

После нажатия на кнопку «Стоп» экран переключился на первый таймер цепочки, которым является «Помидор»:

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


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

Моя следующая статья выйдет 21.08.2025. В ней я реализую экран «Таймеры», который позволит выбрать любой из созданных таймеров и использовать его. Также я добавлю кнопку «Назад», которая позволит пользователю вернуться из любого таймера цепочки на экран «Таймеры».

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

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


Заключение

  1. Реализован функционал запуска таймеров друг за другом по цепочке;
  2. На экран таймера выведены его название и тип;
  3. Добавлен механизм скрытия и показа кнопки «Стоп».

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

Автор

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