Вступление
Всем доброго дня! В предыдущей статье 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.
 
           
                    
Комментарии
Оставить комментарийВойдите, чтобы оставить комментарий.
Комментариев пока нет.