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

Для скачивания материалов необходимо войти или зарегистрироваться
Файлы также можно получить в Telegram-боте по коду: 939422
Вступление
Всем доброго дня! В предыдущей статье 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_label
—id
для изменения названия из Python;pos_hint: {'center_x': 0.3, 'center_y': 0.7}
— расположение по двум осям X и Y: визуально выше времени таймера и слева на экране;id: type_timer_label
—id
для изменения типа таймера из 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. В ней я реализую экран «Таймеры», который позволит выбрать любой из созданных таймеров и использовать его. Также я добавлю кнопку «Назад», которая позволит пользователю вернуться из любого таймера цепочки на экран «Таймеры».
Если у вас есть мысли о том, как можно улучшить проект, пишите в комментариях — с удовольствием ознакомлюсь с вашими предложениями!
Читайте продолжение — не пропустите!
Заключение
- Реализован функционал запуска таймеров друг за другом по цепочке;
- На экран таймера выведены его название и тип;
- Добавлен механизм скрытия и показа кнопки «Стоп».
Ссылки к статье
- Мои статьи Arduinum628 на Код на салфетке;
- Репозиторий проекта Kawai.Focus.
Все статьи