Вступление
Всем доброго дня! В предыдущей статье Kawai.Focus - приложение для фокусировки внимания (часть 1) я начал работу над приложением для фокусировки Kawai.Focus. В ней осветил идею приложения, рассказал о технике Pomodoro, а также сформулировал функционал для MVP1. Кроме того, провел анализ фреймворков и выбрал Kivy, на базе которого реализовал первую программу - "Hello world!".
Сегодня я реализую минимальный функционал таймера Pomodoro, а также создам блок-схему пользовательского пути. Заварите себе чай и приготовьте вкусняшки - время разбираться с "помидором"! =)
Пользовательский путь
Прежде чем приступать к реализации функций таймера Pomodoro в коде, необходимо составить блок-схему для пользовательского пути. Блок-схема для пользовательского пути – это визуальное представление шагов, через которые проходит пользователь при взаимодействии с продуктом или услугой.
Есть несколько видов блок-схем, и они все разные по применению. Вот некоторые из них:
- UseCase - как раз схема, описывающая пользовательский путь;
- ERD - диаграмма взаимосвязей в базе данных;
- UML - диаграмма классов;
- Схемы, описывающие именно логику, то, что происходит под капотом.
В нашем случае она называется UseCase, которая подробно покажет, как пользователь попадёт в ту или иную функцию таймера Pomodoro. Рисование блок-схем помогает наглядно увидеть путь пользователя, а также сделать этот процесс наиболее оптимальным и понятным.
Я буду использовать приложение Figma-linux для рисования блок-схемы. По сути, это обычная Figma, неофициально портированная на Linux. Мне нужно создать новую блок-схему. Для этого я открою приложение Figma-linux и нажму на кнопку "New Fig Jam board" для создания нового рабочего пространства блок-схемы.
Сама блок-схема в программе должна состоять из секций, которые будут связаны стрелочками между собой. Каждая секция будет иметь своё название, которое будет идентифицировать функционал, связанный с ней. Это чем-то похоже на игру, где есть старт и финиш, а фишка – это текущая позиция игрока.
Главным отличием игрока от пользователя программы является тот факт, что пользователь может пойти по любому пути и ходить как вперёд, так и назад по программе. Игрок же в настольной игре может ходить только по правилам игры.
Мне нужно отобразить на блок-схеме пользовательский путь для следующего функционала:
- Конструктор таймеров;
- Запуск/пауза/отмена серии таймеров;
- Настройка длительности таймера работы/таймера короткого перерыва/таймера длинного перерыва;
- Настройка количества помидоров до длинного перерыва;
- Текстовый гайд по использованию и технике Pomodoro;
- О программе.
Блок-схема
Поработав в Figma-linux некоторое время, у меня получилась блок-схема, описывающая пользовательский путь. Также схема описывает окна, кнопки, ввод и текст.
Меню и окна с текстом

На первом скриншоте схемы видно, что у нас есть меню с тремя кнопками. Это будет меню, которое доступно из любого окна приложения. Это позволит не делать отдельное окно для меню, сделает навигацию в приложении быстрее и будет экономить код на кнопках "назад". Кроме того, такой подход будет удобен и для мобильной версии приложения (если до него дойдёт).
Кнопки "Инструкция" и "О программе" ведут на окна с текстовым содержимым.
Таймеры
Кнопка "Таймеры" ведёт на окно со списком созданных таймеров и кнопкой "Новый таймер" для создания нового таймера.
Я хотел сделать приложение более гибким, чем классические Pomodoro таймеры, так как я человек ленивый =)
Дело в том, что постоянно настраивать один таймер под разные временные условия может довольно утомить. Поэтому я решил иметь возможность создавать сколько угодно таймеров, которые можно настроить по-разному.
Как видно из блок-схемы, пользователь может переключиться на готовый таймер. В текущей реализации в окне таймера доступно: общее время таймера, обратный отсчёт времени таймера, кнопка "Старт", кнопка "Пауза" и кнопка "Стоп". Кнопки "Стоп" и "Пауза" будут появляться, когда пользователь нажмёт на кнопку "Старт".
Окно таймера пока не имеет полного функционала. В следующих статьях я добавлю туда кнопку "Удалить". Также не помешал бы звуковой сигнал — это же таймер всё-таки! Настройка для включения/выключения звука тоже пригодится впридачу. Ещё не помешало бы подписывать сам таймер: помидор, перерыв, долгий отдых. Ну и конечно хотелось бы знать, сколько осталось помидоров =)
Конструктор

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

Я написал эту таблицу для чёткого понимания того, что есть что на блок-схеме. Сразу видно, что является кнопками, текстом, окном и полем ввода.
После создания блок-схемы настала пора писать сам код, но прежде мне нужно обновить Poetry.
Обновление Poetry
Недавно я переустановил Debian на SSD и заново установил всё, что нужно для программирования. На старой системе у меня стояла версия Poetry 1.8.3, и мне было лень её обновлять. Сейчас я установил самую новую версию Poetry 2.1.1, которая несовместима со старой 1.8.3. Как я понял, много изменений произошло в синтаксисе конфига pyproject.toml.
Я человек простой: я просто удалил старые файлы poetry.lock и pyproject.toml. После этого ввёл команды, которые вам уже известны. Более подробно об изменениях новой версии Poetry вы можете узнать в статье Обновление Poetry: 2.0.0 на сайте Кот на салфетке.
Код
Вначале я хочу реализовать логику самого таймера. Для этого я создам файл utils.py в пакете kawai_focus, который будет хранить все необходимые утилиты.
kawai_focus/utils.py
В файле utils.py я создам функцию custom_timer(), которая будет отсчитывать время в обратном порядке для заданного времени таймера.
Код для функции:
import time
import re
from typing import Generator
import logging
from kivy.logger import Logger
Logger.setLevel(logging.DEBUG)
def custom_timer(timer_str: str) -> Generator[str, None, None]:
"""Функция отсчитывает время, установленное для таймера в формате 'XhYmZs'."""
try:
match = re.match(r'(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?', timer_str)
if not match:
raise ValueError("Неверный формат времени, используйте 'XhYmZs'!")
hours, minutes, seconds = match.groups()
total_seconds = (int(hours or 0) * 3600) + (int(minutes or 0) * 60) + (int(seconds or 0))
if total_seconds 0:
raise ValueError("Не указано время таймера!")
for remaining in range(total_seconds, -1, -1):
yield f"{remaining // 3600:02d}:{(remaining % 3600) // 60:02d}:{remaining % 60:02d}"
time.sleep(1) # Todo: дублирует задержку нужно будет удалить!
except ValueError as err:
Logger.error(f'{err.__class__.__name__}: {err}')Разбор кода таймера:
from kivy.logger import Loggerнеобходим для создания логгера для kivy;def custom_timer(timer_str: str) -> Generator[str, None, None]:функция принимает аргументstrтипа для стартового времени таймера в параметрtimer_str. Возвращает функцияGeneratorсо строками;match = re.match(r'(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?', timer_str)регулярное выражения для строки с временем, которая будет приходить в параметреtimer_str(пример: 0h0m10s);raise ValueError("Неверный формат времени, используйте 'XhYmZs'!")сли строка не соответствует регулярному выражению, то будет выброшена ошибкаValueError;total_secondsхранит в себе результат сложения всех секунд таймера изtimer_str:(int(hours or 0) * 3600)для часов;(int(minutes or 0) * 60)для минут;(int(seconds or 0))для секунд;
raise ValueError("Не указано время таймера!")если сумма секунд для часов, минут и секунд равна нулю, то будет выброшенаValueError;for remaining in range(total_seconds, -1, -1):пройдётся по диапазону секунд до -1. Это необходимо для того, чтобы было включено в вывод"00:00:00";yield f"{remaining // 3600:02d}:{(remaining % 3600) // 60:02d}:{remaining % 60:02d}"создание строки времени и вычисление часов, минут и секунд. Формат02dнужен для автоматического добавления ведущих нулей (например, для 1 будет 01);time.sleep(1)используется для задержки 1 секунду, которая необходима для правильной работы таймера;Logger.error(f'{err.__class__.__name__}: {err}')логгируем ошибку.
Логирование в Kivy
Я создам ситуацию, когда будет выброшена ошибка, чтобы посмотреть, как выглядит кастомный лог в логгере Kivy. Для этого создам в файле utils.py следующий код:
if __name__ '__main__':
# Пример использования
for time_now in custom_timer(''):
print(time_now)Пояснения по коду:
for time_now in custom_timer(""):перебор значений, возвращаемых генераторной функцией, у которой в качестве параметра выступает пустая строка;print(time_now)для вывода в terminal текущего времени таймера.
Запускаем неисправный код через кнопку Run в IDE (у меня это VSCode):
[INFO ] [Logger ] Record log in /home/arduinum628/.kivy/logs/kivy_25-02-25_5.txt
[INFO ] [Kivy ] v2.3.1
[INFO ] [Kivy ] Installed at "/home/arduinum628/.pyenv/versions/3.12.0/envs/kawai_fokus/lib/python3.12/site-packages/kivy/__init__.py"
[INFO ] [Python ] v3.12.0 (main, Feb 18 2025, 21:30:11) [GCC 12.2.0]
[INFO ] [Python ] Interpreter at "/home/arduinum628/.pyenv/versions/3.12.0/envs/kawai_fokus/bin/python"
[INFO ] [Logger ] Purge log fired. Processing...
[INFO ] [Logger ] Purge finished!
[ERROR ] [ValueError ] Не указано время таймера!Как видно из лога в терминале, строка [ERROR ] [ValueError ] Не указано время таймера! выглядит как другие логи в Kivy.
Теперь я сделаю строку валидного вида для проверки работы функции:
for time_now in custom_timer('0h1m3s'):
print(time_now)Результат запуска кода:
00:01:03
00:01:02
00:01:01
00:01:00
00:00:59
00:00:58
00:00:57
00:00:56Теперь код функции отрабатывает верно. Как видно из вывода в terminal функция custom_timer() каждую секунду отдаёт сгенерированную строку с временем.
kawai_focus/screens/time_screen.py
Далее мне необходимо реализовать основу для экрана таймера. Пока речь пойдёт лишь о базовом функционале таймера. Как минимум, нужно реализовать запуск таймера, его полную остановку и паузу. При этом нужно обновлять экран таймера каждую секунду.
Для реализации экрана таймера я создам пакет screens, в котором будут храниться экраны. Экран или Screen - это окно приложения, внутри которого находятся сущности с кнопками, текстом и прочим. Экраны довольно прожорливы по строкам кода, поэтому я решил сразу создать отдельный пакет для них.
Пакет имеет следующую структуру:
├── screens
│ └── __init__.py
│ └── timer_screen.pyВ файле timer_screen.py я создам класс TimerScreen, который будет отвечать за экран таймера.
Код для класса:
from kivy.uix.screenmanager import Screen
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.clock import Clock
from kawai_focus.utils import custom_timer
class TimerScreen(Screen):
"""Экран таймера"""
def __init__(self, **kwargs):
super(TimerScreen, self).__init__(**kwargs)
self.layout = FloatLayout()
# Добавляем кнопку "Старт"
self.start_button = Button(
text='Старт',
size_hint=(None, None),
height=40,
width=100,
pos_hint={
'center_x': 0.3,
'center_y': 0.5
}
)
self.start_button.bind(on_release=self.start_timer)
self.layout.add_widget(self.start_button)
# Добавляем кнопку "Пауза"
self.pause_button = Button(
text='Пауза',
size_hint=(None, None),
height=40,
width=100,
pos_hint={
'center_x': 0.5,
'center_y': 0.5
}
)
self.pause_button.bind(on_release=self.pause_timer)
self.layout.add_widget(self.pause_button)
# Добавляем кнопку "Стоп"
self.stop_button = Button(
text='Стоп',
size_hint=(None, None),
height=40,
width=100,
pos_hint={
'center_x': 0.7,
'center_y': 0.5
}
)
self.stop_button.bind(on_release=self.stop_timer)
self.layout.add_widget(self.stop_button)
# Добавляем Label для отображения времени
self.time_label = Label(
text='00:00:10',
size_hint=(None, None),
pos_hint={
'center_x': 0.5,
'center_y': 0.6
}
)
self.layout.add_widget(self.time_label)
self.add_widget(self.layout)
# Переменные для управления таймером
self.timer_generator = None
self.paused = False
self.remaining_time = None
def start_timer(self, instance):
if self.paused:
self.paused = False
else:
# Инициализация генератора таймера
# Todo: тут пока харкодное время
self.timer_generator = custom_timer('0h0m10s')
self.remaining_time = next(self.timer_generator, '00:00:00')
# Запуск обновления времени каждую секунду
Clock.schedule_interval(self.update_time, 1)
def pause_timer(self, instance):
if not self.paused:
self.paused = True
# Остановка обновления времени
Clock.unschedule(self.update_time)
def stop_timer(self, instance):
# Остановка обновления времени
Clock.unschedule(self.update_time)
self.paused = False
self.remaining_time = '00:00:00'
self.time_label.text = self.remaining_time
def update_time(self, dt):
if self.paused:
return
try:
# Получение следующего значения времени из генератора
self.remaining_time = next(self.timer_generator, '00:00:00')
self.time_label.text = self.remaining_time
if self.remaining_time '00:00:00':
# Остановка обновления времени
Clock.unschedule(self.update_time)
except StopIteration:
# Остановка обновления времени при завершении генератора
Clock.unschedule(self.update_time)Разбор кода экрана таймера:
Импорты:
from kivy.uix.screenmanager import Screenнужен для создания экранов и переключения между ними;from kivy.uix.floatlayout import FloatLayoutодин из классов для компоновки layout в Kivy. Точно контролирует размер и положение элементов;from kivy.uix.button import Buttonвиджет для кнопки;from kivy.uix.label import Labelвиджет для текста;from kivy.clock import Clockпланировщик задач в Kivy. Способен вызвать функцию с заданным интервалом времени и на многое другое.
Метод init():
def __init__(self, **kwargs):конструктор класса с**kwargs;super(TimerScreen, self).__init__(**kwargs)вызов родительского классаScreen, для инициализации базового функционала экрана;self.layout = FloatLayout()создание компоновщика layout;self.start_buttonхранит кнопку с текстомtext, размерами высотыheightи шириныweight, положением кнопки на экранеpos_hint;self.start_button.bind(on_release=self.start_timer)привязывает событие к кнопке (отпускание кнопки), которая запускает старт таймера;self.layout.add_widget(self.start_button)добавление кнопки в компоновщикlayout;- Для остальных кнопок аналогичные действия;
self.time_labelхранит текст самого таймера, у которого стартовое значениеtext='00:00:10'пока захардкожено (позже будет задаваться в конструкторе таймера). Параметрsize_hint=(None, None)отключает адаптивный размер кнопки. Параметрpos_hintотвечает за позицию на экране;- На данный момент реализуется временный вариант вёрстки для тестирования самих функций таймера. В последующем внешний вид должен будет стать адаптивным и красивым;
self.timer_generator = Noneпеременная будет хранить генератор иNone, если генератор не запущен;self.paused = Falseпеременная-флаг для паузы таймера;self.remaining_time = Noneпеременная для хранения оставшегося времени в секундах;
Метод start_timer():
- Его назначение - запустить таймер;
def start_timer(self, instance):в котором аргумент в виде экземпляра кнопки для параметраinstanceбудет передаваться автоматически черезself.start_button.bind(on_release=self.start_timer);if self.paused:если флаг установлен вTrueна паузе переводим флаг вFalseвself.paused = False;else:если не на паузе, то происходитself.timer_generator = custom_timer('0h0m10s')установка заданного времени старта иself.remaining_time = next(self.timer_generator, '00:00:00')извлечение значения из генератора;Clock.schedule_interval(self.update_time, 1)вызов функции обновления времени таймераself.update_timeкаждую секунду. Во избежание дублирования задержки функцияsleep(1)будет удалена из логики функцииcustom_timer()!
Метод pause_timer():
- Предназначен для постановки обновления таймера на паузу;
if not self.paused:если флаг паузы вFalse, то делает егоTrueвself.paused = True;Clock.unschedule(self.update_time)останавливает обновление времени таймера;
Метод stop_timer():
- Нужен для полной остановки таймера;
Clock.unschedule(self.update_time)останавливаем обновление времени таймера;self.paused = Falseпереключает флаг паузы вFalse;self.remaining_time = '00:00:00'сбрасывает текущее время таймера в нуль;self.time_label.text = self.remaining_timeобновление на экране виджета сLabelтаймера;
Метод update_time():
- Нужен для обновления времени таймера и получения его из генератора таймера;
if self.paused:если таймер на паузе, то метод завершит своё выполнение черезreturn;self.remaining_time = next(self.timer_generator, '00:00:00')получает новое значение из генератора таймера, а если ничего не получает получаем дефолтное значение'00:00:00';self.time_label.text = self.remaining_timeобновляем значение таймера на экране;if self.remaining_time '00:00:00':если таймер в нулевом значении, то останавливаем обновление таймераClock.unschedule(self.update_time);except StopIteration:обработка исключения, в котором тоже останавливается обновление таймераClock.unschedule(self.update_time). Это нужно чтобы предотвратить ситуацию, если генератор завершится раньше, чемself.remaining_timeстанет'00:00:00'.
kawai_focus/main.py
Перейдём в уже созданный ранее файл main.py и добавим в него код для экрана:
from kivy.uix.screenmanager import ScreenManager
from kawai_focus.screens.timer_screen import TimerScreen
class KawaiFocusApp(App):
"""Класс для создания приложения"""
title = 'Kawai.Focus'
def build(self):
screen_manager = ScreenManager()
screen_manager.add_widget(TimerScreen(name='timers_screen'))
return screen_managerПояснения по новому коду:
Импорты:
from kivy.uix.screenmanager import ScreenManagerменеджер экранов, в который я буду добавлять новые экраны;from kawai_focus.screens.timer_screen import TimerScreenимпорт экрана таймера, который я писал выше;
Метод build():
screen_manager = ScreenManager()создаст менеджер экранов;screen_manager.add_widget(TimerScreen(name='timers_screen'))добавляет экран как виджет в менеджер экранов. Параметрname='timers_screen'потребуется в будущем для переключения на этот экран из меню;return screen_managerвернёт менеджер экранов.
Запуск приложения
Я запущу своё приложение с помощью Poetry в terminal:
poetry run kawai-focusВо время запуска появляется окно с временем 10 секунд. В последующем у нового таймера будет дефолтное значение в конструкторе таймера, которое можно будет поменять вручную. Пока 10 секунд установлено для быстрого тестирования.

Если нажать на кнопку "Старт", то начнётся обратный отсчёт времени. Обновление экрана и нажатие кнопок происходят довольно плавно.

Для фото выше я нажал на кнопку "Пауза", и таймер остановился секунда в секунду. Задержек не обнаружено.

Если нажать на кнопку "Стоп" или подождать до конца отсчёта времени, то таймер останется в нулевом положении. Пока никаких внешних или звуковых оповещений я ещё не добавил.
Механика простого таймера у меня получилась на приличного размера статью. В последующем кнопка "Пауза" будет появляться вместо кнопки "Старт" после старта таймера, а кнопка "Стоп" будет появляться при старте таймера и исчезать при полной его остановке.
Пояснения для последователей идеального кода: код и дизайн будут доработаны, а хардкод будет убран по мере работы над приложением. Структура будет разделяться по мере необходимости. Данный фреймворк ощущается сложнее, чем Flet, и я ещё нахожусь в процессе его изучения.
Анонс на следующие статьи
Следующая моя статья выйдет 20 марта, в которой я буду продолжать писать таймер. На этот раз я начну с его улучшений. Необходимо будет убрать хардкод текстовой информации, настроить появление кнопок на экране. Также я бы очень хотел настроить звуковое оповещение и сделать экран со списком таймеров с переходом непосредственно в таймер.
С ботом пока лёгкая пауза. Дело в том, что у меня сейчас занятость - три статьи за три недели для трёх сайтов. Мне приходится паять провода, снимать фото и видео для DIY проекта на Arduino, а это отнимает большое количество времени.
В данный момент работаю над тестовой статьёй, и если она понравится редактору, то у меня будет подработка на статьях. Если нет, то времени станет побольше, и я доделаю бота быстрее.
Читайте продолжение будет интересно =)
Заключение
- Создана блок-схема приложения для пользовательского пути;
- Написана минимальная версия функционала таймера;
- Обновлён Poetry.
Комментарии
Оставить комментарийВойдите, чтобы оставить комментарий.
Комментариев пока нет.