Cat

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

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

  • Продолжению работы над Open Source приложением для фокусировки внимания "Kawai.Focus";
  • Реализации минимального функционала таймера Pomodoro;
  • Работе с фреймворком Kivy.

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

Icon Link

Реклама

Icon Link
Kawai.Focus Arduinum628 26 Февраль 2025 Просмотров: 65

Вступление

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

В данный момент работаю над тестовой статьёй, и если она понравится редактору, то у меня будет подработка на статьях. Если нет, то времени станет побольше, и я доделаю бота быстрее.

Читайте продолжение будет интересно =)

Заключение

  1. Создана блок-схема приложения для пользовательского пути;
  2. Написана минимальная версия функционала таймера;
  3. Обновлён Poetry.

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

Автор

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

    Реклама