Перейти к контенту

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

Kawai.Focus Eugene Kaddo 13

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

  • Фреймворку Kivy в проекте Kawai.Focus;
  • Material Design для Kivy на библиотеке KivyMD 2.0.0;
  • Обновлению дизайна экрана «Таймер»;
  • Устранению проблем с запуском backends связанных с библиотекой SDL2.

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

Вступление

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

  1. Подключена боковая панель NavigationRail;
  2. Добавлены новые кнопки на экран «Таймеры».

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

Так же у меня возникла неожиданная проблема, с бекендом звука и связанной с ним SDL2 библиотекой, которой не возникало при использовании чистого Kivy без KivyMD. Я вынужденно буду решать её сегодня.

Ещё мне предстоит заняться конвертацией звука.

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


Проблема запуска двух backends на SDL2

Как только я сделал наследование от MDScreen класса, подключил экран в main.py у меня перестала работать отрисовка окна приложения и всё зависало. Выглядит визуально это следующим образом.

На экране просто появляется полоска от окна с названием приложения и больше ничего. Когда я закомментировал код от загрузки звука приложение загрузилось как обычно. По началу я не понял что произошло так как видимых ошибок не было в логах приложения.

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

Что такое SDL2?

SDL2 (Simple DirectMedia Layer) — это кроссплатформенная библиотека, которая предоставляет низкоуровневый доступ к мультимедиа:

  • графика (OpenGL/2D);
  • звук;
  • обработка ввода с клавиатуры, мыши, геймпада;
  • таймеры и события.

SDL2 в Kivy:

В Kivy есть несколько компонентов, которые могут использовать SDL2:

  1. Window backend (window_sdl2) :
    Kivy может рендерить интерфейс через SDL2. Это значит, что графический контекст (окна, OpenGL) создаётся через SDL2;
  2. Audio backend (audio_sdl2) :
    Kivy может воспроизводить звуки через SDL2. При этом используется SDL2 mixer для загрузки и проигрывания аудио;
  3. Ввод :
    SDL2 обрабатывает события клавиатуры, мыши и сенсоров.

О баге:

  • В KivyMD 2.0.0/2.0.1.dev вызов SoundLoader.load() блокирует главный поток приложения при использовании audio_sdl2;
  • Это происходит даже если звук валидный, без ошибок — приложение просто «виснет»;
  • Причина — конфликт инициализации SDL2 аудио с контекстом графики KivyMD;
  • Решение — использовать ffpyplayer для аудио вместо audio_sdl2.

Для проверки звука я решил запустить его для проверки из главного приложения в main.py для этого я временно написал функцию и метод, которые помогут быстро протестировать запуск звука.

utils.py

В utils.py я написал функцию, которая вернёт объект с загруженной мелодией.

from kivy.core.audio import SoundLoader, Sound
def load_sound() -> Sound | None:
    """Функция для загрузки звука"""
    sound_timer_name = data_json.get_text('sound_timer')
    path_file = f'sounds/{sound_timer_name}'
    sound = SoundLoader.load(path_file)
    return sound

Никакого нового кода тут я не написал, а просто скопировал строки кода из класса TimerScreen.

main.py

В главном файле main.py я решил попробовать загружать звук в отдельный поток. Сам по себе отдельный поток не решил проблему, но я попробовал на всякий случай такой вариант.

# Другой код
from kivy.clock import Clock
from utils.utils import load_sound
class KawaiFocusApp(MDApp, MenuApp):
    """Главный класс приложения"""
    title = 'Kawai-Focus'
    # Другой код
    def on_start(self):
        # Загружаем звук в отдельном потоке, чтобы не блокировать UI
        Thread(target=self.load_sound_timer).start()
    def load_sound_timer(self):
        try:
            sound = load_sound()  # твоя функция для получения Sound
            if sound:
                # Воспроизведение в главном потоке
                Clock.schedule_once(lambda dt: sound.play())
                # Остановка через 3 секунды
                Clock.schedule_once(lambda dt: sound.stop(), 3)
        except Exception as err:
            print(err)

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

  • def on_start(self): — метод, вызываемый при старте приложения; здесь запускается отдельный поток для загрузки звука, чтобы не блокировать UI;
  • Thread(target=self.load_sound_timer).start() — создаётся и запускается поток, выполняющий метод load_sound_timer;
  • def load_sound_timer(self): — метод для загрузки и воспроизведения звука:
    • sound = load_sound() — загружается объект звука через пользовательскую функцию load_sound;
    • Clock.schedule_once(lambda dt: sound.play()) — воспроизведение звука в главном потоке, безопасно для UI;
    • Clock.schedule_once(lambda dt: sound.stop(), 3) — остановка звука через 3 секунды;
    • try/except — отлавливает и выводит возможные ошибки при загрузке или воспроизведении.

Переход с audio_sdl2 на ffpyplayer

В качестве альтернативы audio_sdl2 воспользуюсь ffpyplayer.

ffpyplayer — это библиотека Python для работы с мультимедиа, основанная на FFmpeg. Она позволяет воспроизводить аудио и видео в приложениях без блокировки главного потока.

Основные характеристики ffpyplayer:

  • Асинхронное воспроизведение:
    Звук и видео могут загружаться и воспроизводиться в фоновом потоке, не мешая интерфейсу. Это особенно важно для Kivy/KivyMD, где главный поток отвечает за UI;
  • Поддержка форматов: 
    Любые форматы, поддерживаемые FFmpeg: mp3, wav, ogg, flac, mp4, mkv и другие;
  • Высокая производительность:
    Использует C-библиотеки FFmpeg для декодирования, что быстрее, чем чисто Python-решения;
  • Интеграция с Kivy: 
    Можно использовать вместе с SoundLoader (через бэкенд ffpyplayer) вместо audio_sdl2, чтобы избежать подвисаний при загрузке аудио.

Установка в Debian/Ubuntu:

sudo apt install ffmpeg

Для установки на другие операционные системы есть инструкция на сайте ffmpeg. Есть поддержка Windows и Mac OS.

Добабляю ffpyplayer в проект:

poetry add ffpyplayer
from kivy import Config
Config.set('kivy', 'audio', 'ffpyplayer')

Разбор кода:

  • from kivy import Config — импортируется объект конфигурации Kivy для настройки параметров приложения;
  • Config.set('kivy', 'audio', 'ffpyplayer') — указывается, что аудиобэкенд Kivy должен использовать ffpyplayer вместо стандартного audio_sdl2.

Видео с проверкой звука:

Окно приложения запустилось, а звук воспроизвёлся. Это не может не радовать.

Основные логи ffpyplayer:

[INFO   ] [ImageLoaderFFPy] Using ffpyplayer 4.5.3

Using ffpyplayer 4.5.3 — говорит о том, что для звука используется новый бекенд для аудио ffpyplayer.

[WARNING] [ffpyplayer  ] [mp3 @ 0x7fb86c000d80] Estimating duration from bitrate, this may be inaccurate

Сообщение [WARNING] переводится как "Оценка длительности по битрейту может быть неточной". Это предупреждение всего лишь информационное и имеет смысл только тогда, когда есть необходимость в точно длине файла, например, в отображении продолжительности или регулировке. В остальном это просто предупреждение, которое ни на что не влияет.

MP3-файлы бывают двух типов:

  1. CBR (Constant Bitrate) — постоянный битрейт, длительность можно рассчитать точно;
  2. VBR (Variable Bitrate) — переменный битрейт, и в таких файлах нет точных метаданных о длине.

Когда FFmpeg (а значит и ffpyplayer) не находит эту метаинформацию, он пишет:

“Estimating duration from bitrate, this may be inaccurate”

То есть — «оценил длительность по среднему битрейту, может быть неточно».

FFmpeg — это мощная кроссплатформенная утилита и библиотека для работы с аудио и видео.

Конвертирую .mp3 формат в CBR .mp3 чтобы убрать это предупреждение:

ffmpeg -i alarm.mp3 -b:a 256k -ar 44100 -ac 2 -codec:a libmp3lame -abr 0 alarm_cbr.mp3

Разбор команды:

  • ffmpeg -i alarm.mp3 — указывает исходный файл alarm.mp3 для обработки;
  • -b:a 256k — задаёт битрейт аудио 256 кбит/с (для постоянного или целевого качества);
  • -ar 44100 — устанавливает частоту дискретизации 44,1 кГц;
  • -ac 2 — устанавливает количество аудиоканалов (2 — стерео);
  • -codec:a libmp3lame — выбирает кодек MP3 libmp3lame для перекодирования;
  • -abr 0 — включает CBR (Constant Bit Rate), т.е. постоянный битрейт;
  • alarm_cbr.mp3 — имя выходного файла с новыми параметрами.

Вывод конвертации:

ffmpeg version 5.1.7-0+deb12u1 Copyright (c) 2000-2025 the FFmpeg developers
  built with gcc 12 (Debian 12.2.0-14+deb12u1)
  configuration: --prefix=/usr --extra-version=0+deb12u1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libglslang --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librist --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --disable-sndio --enable-libjxl --enable-pocketsphinx --enable-librsvg --enable-libmfx --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-libplacebo --enable-librav1e --enable-shared
  libavutil      57. 28.100 / 57. 28.100
  libavcodec     59. 37.100 / 59. 37.100
  libavformat    59. 27.100 / 59. 27.100
  libavdevice    59.  7.100 / 59.  7.100
  libavfilter     8. 44.100 /  8. 44.100
  libswscale      6.  7.100 /  6.  7.100
  libswresample   4.  7.100 /  4.  7.100
  libpostproc    56.  6.100 / 56.  6.100
[mp3 @ 0x5632de7534c0] Estimating duration from bitrate, this may be inaccurate
Input #0, mp3, from 'alarm-beep.mp3':
  Duration: 00:00:18.47, start: 0.000000, bitrate: 255 kb/s
  Stream #0:0: Audio: mp3, 44100 Hz, mono, fltp, 256 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (mp3 (mp3float) -> mp3 (libmp3lame))
Press [q] to stop, [?] for help
Output #0, mp3, to 'alarm-beep_cbr.mp3':
  Metadata:
    TSSE            : Lavf59.27.100
  Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 256 kb/s
    Metadata:
      encoder         : Lavc59.37.100 libmp3lame
size=     579kB time=00:00:18.46 bitrate= 256.7kbits/s speed=  94x    
video:0kB audio:578kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.148691%

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


Новый дизайн экрана «Таймер»

На экран «Таймер» пользователь попадёт нажав на кнопку «Открыть» на кране «Таймеры».

Так выглядел экран «Таймер» в старом дизайне.

Что я хочу видеть в новом дизайне:

  • Большой шрифт у цифры таймера, который будет более удобным в отслеживании времени;
  • Material Design на KivyMD 2.0.0;
  • Кнопка «Пауза» будет появляться только после нажатия на копку старт так же как и кнопка «Стоп». На старом дизайне я забыл это сделать и до старта кнопка «Пауза» не имеет функционального смысла. Так же сделаю уместным появление кнопки «Старт» при нужных ситуациях.

Подбор образца

В образцах из папки examples , которую я взял из репозитория форка KivyMD 2.0.0 нет конкретного экрана под таймер. Зато там есть экран timepicker, который используют для установки времени часов.

Образец timepicker (input):

Что может мне пригодится в образце?

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

При вызове элемента с настройкой часов блокируются остальные элементы приложения такие как меню бургер. Эту блокировку можно использовать для того чтобы пользователь не отвлекался на остальные элементы и кнопки приложения во время использования таймера.

Вместо кнопок «Cancel» и «Ok» я добавлю кнопки «Стоп» и «Пауза». Остальные ненужные кнопки и надписи такие как «AM» и «PM» я уберу ибо в таймере они не нужны.

В данном случае я буду скорее вдохновляться визуальной частью, и почти не буду использовать код из образца.

timer_screen.kv

Код из .kv файла я переписал основательно поэтому я его покажу целиком. С вёрсткой пришлось повозиться так как очень много отличий от стандартной вёрстки Kivy.

#:kivy 2.3.1
#:include kv/navigation_panel.kv
<TimerScreen>:
    NavigationPanel:
        id: nav_panel
        name: "timer_screen"
        MDAnchorLayout:
            orientation: "vertical"
            anchor_x: "center"
            anchor_y: "center"
            md_bg_color: app.theme_cls.secondaryContainerColor
            # Центральный блок таймера
            MDBoxLayout:
                orientation: "vertical"
                size_hint: None, None
                size: "310dp", "200dp"
                md_bg_color: app.theme_cls.surfaceColor
                radius: [20]
                elevation: 4
                padding: "16dp"
                spacing: "12dp"
                # Заголовок блока таймера (слева вверху)
                MDBoxLayout:
                    orientation: "horizontal"
                    size_hint_y: None
                    height: "24dp"
                    spacing: "4dp"
                    MDLabel:
                        id: title_label
                        text: "Таймер"
                        halign: "left"
                        valign: "center"
                        role: "small"
                    MDLabel:
                        id: type_timer_label
                        text: "Помидор"
                        halign: "right"
                        valign: "center"
                        role: "small"
                # Крупный таймер
                MDLabel:
                    id: time_label
                    text: "00:00:00"
                    halign: "center"
                    valign: "center"
                    font_style: "Display"
                    role: "large"
                # Кнопки
                MDBoxLayout:
                    orientation: "horizontal"
                    size_hint_y: None
                    height: "48dp"
                    spacing: "12dp"
                    pos_hint: {"center_x": .5}
                    MDButton:
                        style: "outlined"
                        id: start_button
                        on_release: root.start_timer()
                        theme_line_color: "Custom"
                        line_color: 0, 1, 0, 1 
                        MDButtonText:
                            text: "Старт"
                    MDButton:
                        style: "outlined"
                        id: pause_button
                        on_release: root.pause_timer()
                        opacity: 0
                        disabled: True
                        theme_line_color: "Custom"
                        line_color: 1, 1, 0, 1
                        MDButtonText:
                            text: "Пауза"
                    MDButton:
                        style: "outlined"
                        id: stop_button
                        on_release: root.stop_timer()
                        opacity: 0
                        disabled: True
                        theme_line_color: "Custom"
                        line_color: 1, 0, 0, 1
                        MDButtonText:
                            text: "Стоп"

Разбор .kv файла:

  • <TimerScreen> — экран с таймером, использующий NavigationPanel для бокового меню/навигации;
  • MDAnchorLayout — центрирует основной блок таймера на экране, задаёт фон через secondaryContainerColor;
  • MDBoxLayout (центральный блок таймера) — вертикальный контейнер с фоном, скруглёнными углами, тенью, внутренним отступом и промежутками между элементами;
  • Заголовок таймера (MDBoxLayout с двумя MDLabel) — слева название «Таймер», справа тип таймера («Помидор»);
  • Крупный таймер (MDLabel id: time_label) — показывает текущее время, центрирован, большой шрифт;
  • Кнопки управления (MDBoxLayout с MDButton) — три кнопки:
    • Старт — зелёная, активная;
    • Пауза — жёлтая, изначально скрыта и неактивна;
    • Стоп — красная, изначально скрыта и неактивна.

timer_screen.py

Теперь осталось самое сложное переписать логику класса TimerScreen:

  • Убрать лишние методы delete_timer() и back() так как они не будут больше использоваться в данном экране;
  • Улучшить код загрузки аудио;
  • Переписать логику метода choice_timer() для работы с match ... case;
  • Добавить код для скрытия и появления на экране кнопок управления таймером;
  • Написать код, который сделает кнопки боковой панели навигации неактивными во время работы таймера.

Импорты:

from kivymd.uix.screen import MDScreen
from kivy.clock import Clock
from kivy.core.audio import SoundLoader
from os.path import isfile
from kawai_focus.utils.utils import data_json, custom_timer, calculate_time
from kawai_focus.main import Logger

Разбор новых импортов:

  • from os.path import isfile — импорт функции для проверки, существует ли файл по заданному пути;
  • from kivymd.uix.screen import MDScreen — импорт виджета экрана KivyMD, используется как контейнер для интерфейса;
  • from kivy.clock import Clock — импорт планировщика событий Kivy для запуска функций с задержкой или периодически в главном потоке.

Обновлённый класс TimerScreen:

class TimerScreen(MDScreen):
    """Экран таймера"""
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Clock.schedule_once(self.load_sound)
        # Переменные для управления таймером
        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
        self.sound = None  # Заглушка
    def load_sound(self, dt) -> None:
        sound_timer_name = data_json.get_text('sound_timer')
        path_file = f'sounds/{sound_timer_name}'
        if not isfile(path_file):
            Logger.error(f'[TimerScreen] Файл звука не найден: {path_file}')
            return
        self.sound = SoundLoader.load(path_file)
        if not self.sound:
            Logger.error(f'[TimerScreen] Не удалось загрузить файл звука (неподдерживаемый формат?): {path_file}')
            return
    def on_pre_enter(self, *args) -> None:
        """Вызывается перед появлением экрана"""
        # Прячем кнопки "Стоп" и "Пауза"
        self.ids.stop_button.opacity = 0
        self.ids.stop_button.disabled = True
        self.ids.pause_button.opacity = 0
        self.ids.pause_button.disabled = True
        # Проверяем состояние state_machine
        if not hasattr(self.manager, 'state_machine') or self.manager.state_machine is None:
            self.manager.state_machine = []
    def choice_timer(self) -> None:
        """Выбор таймера (помидор / перерыв / длинный перерыв)"""
        if not self.manager.state_machine:
            return
        current_timer_name = self.manager.state_machine.pop(0)
        match current_timer_name:
            case 'pomodoro':
                self.timer_generator = custom_timer(mm_user=self.timer.pomodoro_time)
                self.ids.time_label.text = calculate_time(self.timer.pomodoro_time)
                self.ids.type_timer_label.text = 'Помидор'
            case 'break':
                self.timer_generator = custom_timer(mm_user=self.timer.break_time)
                self.ids.time_label.text = calculate_time(self.timer.break_time)
                self.ids.type_timer_label.text = 'Перерыв'
            case _:
                self.timer_generator = custom_timer(mm_user=self.timer.break_long_time)
                self.ids.time_label.text = calculate_time(self.timer.break_long_time)
                self.ids.type_timer_label.text = 'Перерывище'
    def start_timer(self, *args) -> None:
        """Запуск таймера"""
        # Делаем кнопки панели навигации неактивными
        self.ids.nav_panel.ids.menu.disabled = True
        self.ids.nav_panel.ids.timers_nav.disabled = True
        self.ids.nav_panel.ids.guide_nav.disabled = True
        self.ids.nav_panel.ids.info_nav.disabled = True
        # Активируем кнопки "Стоп" и "Пауза"
        self.ids.stop_button.opacity = 1
        self.ids.stop_button.disabled = False
        self.ids.pause_button.opacity = 1
        self.ids.pause_button.disabled = False
        # Прячем кнопку "Старт"
        self.ids.start_button.opacity = 0
        self.ids.start_button.disabled = True
        if self.paused:
            self.paused = False
        else:
            if len(self.manager.state_machine) and self.timer_generator is None:
                self.choice_timer()
            self.remaining_time = next(self.timer_generator, self.zero_time)
        Clock.schedule_interval(self.update_time, 1)
    def pause_timer(self, *args) -> None:
        """Пауза таймера"""
        if not self.paused:
            self.paused = True
            Clock.unschedule(self.update_time)
        # Активируем кнопку "Старт"
        self.ids.start_button.opacity = 1
        self.ids.start_button.disabled = False
        # Прячем кнопку "Пауза"
        self.ids.pause_button.opacity = 0
        self.ids.pause_button.disabled = True
    def stop_timer(self, *args) -> 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
        # Останавливаем звук, если играет
        if self.sound:
            self.sound.stop()
        if self.sound_stop_event:
            Clock.unschedule(self.sound_stop_event)
            self.sound_stop_event = None
        # Возврат цикла таймеров
        if not len(self.manager.state_machine):
            self.manager.state_machine = self.source_timer_names.copy()
        # Делаем кнопки панели навигации активными
        self.ids.nav_panel.ids.menu.disabled = False
        self.ids.nav_panel.ids.timers_nav.disabled = False
        self.ids.nav_panel.ids.guide_nav.disabled = False
        self.ids.nav_panel.ids.info_nav.disabled = False
        # Прячем кнопки "Стоп" и "Пауза"
        self.ids.stop_button.opacity = 0
        self.ids.stop_button.disabled = True
        self.ids.pause_button.opacity = 0
        self.ids.pause_button.disabled = True
        # Активируем кнопку "Старт"
        self.ids.start_button.opacity = 1
        self.ids.start_button.disabled = False
        self.choice_timer()
    def play_sound(self, *args) -> None:
        """Воспроизведение звука"""
        if self.sound:
            self.sound.play()
            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)

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

  • Clock.schedule_once(self.load_sound) — планирует однократный вызов метода load_sound после старта экрана; выполняется в главном потоке, безопасно для UI; используется для предварительной загрузки звука без блокировки интерфейса;
  • if not isfile(path_file): — проверка существования файла звука. Если файла нет, выводится ошибка через Logger.error и загрузка не выполняется;
  • if not self.sound: — проверка успешной загрузки аудио через SoundLoader. Если звук не поддерживается или произошла ошибка, выводится сообщение об ошибке;
  • Примеры скрытия/появления кнопок:
    • Скрыть кнопку: self.ids.start_button.opacity = 0; self.ids.start_button.disabled = True;
    • Показать кнопку: self.ids.start_button.opacity = 1; self.ids.start_button.disabled = False.
  • Преимущество match-case перед if-else — улучшает читаемость при множественных вариантах таймера (pomodoro, break, break_long). Легко добавлять новые типы таймеров без вложенных условий;
  • Clock.schedule_interval(self.update_time, 1) — планирует вызов метода update_time каждую секунду. Используется для обновления оставшегося времени таймера в реальном времени.

main.py

Обратно добавляю TimerScreen в виджеты главного приложения. Тут нет нового кода всё как и было раньше.

# Другой код
from kawai_focus.screens.timer_screen import TimerScreen
class KawaiFocusApp(MDApp, MenuApp):
    """Главный класс приложения"""
    title = 'Kawai-Focus'
    def build(self) -> MDScreenManager:
        """Создаёт и возвращает менеджер экранов приложения"""
        self.theme_cls.theme_style = 'Dark'
        # Загрузка kv файла
        Builder.load_file('kv/timers_screen.kv')
        Builder.load_file('kv/timer_screen.kv')
        self.screen_manager = MDScreenManager()
        self.screen_manager.add_widget(TimersScreen(name='timers_screen'))
        self.screen_manager.add_widget(TimerScreen(name='timer_screen'))
        return self.screen_manager
    # Другой код

Запуск с новым дизайном

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

Запускаю приложение в terminal:

poetry run kawai-focus

Выбираю «Таймер 1»:

Нажимаю на кнопку «Открыть»:

Открылся обновлённый экран «Таймер». Видно, что название и тип таймера теперь маленького компактного размера. Цифры таймера теперь гораздо больше и заметнее, а кнопки «Пауза» и «Стоп» теперь скрыты.

Нажимаю на кноку «Старт»:

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

Протестирую кнопку «Пауза»:

Теперь время остановилось, а кнопка «Пауза» пропала с экрана. Зато появилась кнопка «Старт», которой легко можно продолжить работу таймера.

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

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

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

Пока это всё по новому дизайну и функционалу таймера. Кнопка для экрана «Таймеры» на боковом меню ещё не подключена.


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

Сегодня я переписал дизайн экрана «Таймер» к Material Design виду. Так же логика работы таймера и кнопок стала более удобной. В целом мне нравится как преображается моё приложение в плане удобства и внешнего вида.

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

У меня есть хорошая новость =) Я отправил свой проект на конкурс от Gitverse Код без границ! Хоть MVP1 сейчас ещё не дописан до конца я уже борюсь за призовое место в номинации «Для всех и каждого». Так как моё приложение я пишу абсолютно для всех. Любой может использовать его для работы и хобби чтобы не выгорать. Пожелайте удачи =)

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

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


Заключение

  1. Обновлён дизайн экрана «Таймер»;
  2. Устранены проблемы с запуском backends связанных с библиотекой SDL2.

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

Аватар автора

Автор

Eugene Kaddo

Программист фрилансер. Пишу боты, парсеры и скрипты на Python3. По вопросам фриланс заказа программы пишите на почту, указанную в профиле. Автор статей по программированию. Увлекаюсь lego, робототехникой на arduino, рок музыкой, программированием на Python3 и C.

Войдите, чтобы оставить комментарий.

Комментариев пока нет.