feat: added new disign for timer_screen; deleted back and delete_timer fuctions; added id for navigation panel; added new backend for sounds ffpyplayer, converted alarm-beep.mp3 into CBR mp3

This commit is contained in:
Arduinum628
2025-11-13 11:59:44 +03:00
parent 7795bc6455
commit b748708751
7 changed files with 258 additions and 136 deletions

View File

@@ -7,6 +7,7 @@ from kivy.metrics import dp
from kivy.uix.behaviors import ButtonBehavior from kivy.uix.behaviors import ButtonBehavior
from kivy.logger import Logger from kivy.logger import Logger
from kivy.properties import StringProperty from kivy.properties import StringProperty
from kivy import Config
import logging import logging
@@ -19,10 +20,13 @@ from kivymd.uix.navigationrail import MDNavigationRailItem
from kawai_focus.menu_app import MenuApp from kawai_focus.menu_app import MenuApp
from kawai_focus.screens.timers_screen import TimersScreen from kawai_focus.screens.timers_screen import TimersScreen
from kawai_focus.screens.timer_screen import TimerScreen
Logger.setLevel(logging.DEBUG) Logger.setLevel(logging.DEBUG)
Config.set('kivy', 'audio', 'ffpyplayer') # вместо audio_sdl2
class TrailingPressedIconButton( class TrailingPressedIconButton(
ButtonBehavior, RotateBehavior, MDListItemTrailingIcon ButtonBehavior, RotateBehavior, MDListItemTrailingIcon
@@ -49,9 +53,11 @@ class KawaiFocusApp(MDApp, MenuApp):
self.theme_cls.theme_style = 'Dark' self.theme_cls.theme_style = 'Dark'
# Загрузка kv файла # Загрузка kv файла
Builder.load_file('kv/timers_screen.kv') Builder.load_file('kv/timers_screen.kv')
Builder.load_file('kv/timer_screen.kv')
self.screen_manager = MDScreenManager() self.screen_manager = MDScreenManager()
self.screen_manager.add_widget(TimersScreen(name='timers_screen')) self.screen_manager.add_widget(TimersScreen(name='timers_screen'))
self.screen_manager.add_widget(TimerScreen(name='timer_screen'))
return self.screen_manager return self.screen_manager

View File

@@ -1,20 +1,20 @@
from kivy.uix.screenmanager import Screen from kivymd.uix.screen import MDScreen
from kivy.clock import Clock from kivy.clock import Clock
from kivy.core.audio import SoundLoader 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.utils.utils import data_json, custom_timer, calculate_time
from kawai_focus.database.cruds import del_timer, list_timers from kawai_focus.main import Logger
class TimerScreen(Screen): class TimerScreen(MDScreen):
"""Экран таймера""" """Экран таймера"""
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): def __init__(self, **kwargs):
super(TimerScreen, self).__init__(**kwargs) super().__init__(**kwargs)
Clock.schedule_once(self.load_sound)
# Переменные для управления таймером # Переменные для управления таймером
self.zero_time = data_json.get_text('zero_time') self.zero_time = data_json.get_text('zero_time')
self.timer_generator = None self.timer_generator = None
@@ -24,114 +24,160 @@ class TimerScreen(Screen):
self.timer = None self.timer = None
self.timer_start_time = None self.timer_start_time = None
self.source_timer_names = None self.source_timer_names = None
self.sound = None # Заглушка
def choice_timer(self) -> None: def load_sound(self, dt) -> None:
"""Метод для выбора таймера""" sound_timer_name = data_json.get_text('sound_timer')
path_file = f'sounds/{sound_timer_name}'
current_timer_name = self.manager.state_machine.pop(0) if not isfile(path_file):
Logger.error(f'[TimerScreen] Файл звука не найден: {path_file}')
return
if current_timer_name == 'pomodoro': self.sound = SoundLoader.load(path_file)
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 = 'Перерывище'
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.opacity = 0
self.ids.stop_button.disabled = True self.ids.stop_button.disabled = True
def start_timer(self, instance) -> None: 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: if self.paused:
self.paused = False self.paused = False
else: else:
# Инициализация генератора таймера
if len(self.manager.state_machine) and self.timer_generator is None: if len(self.manager.state_machine) and self.timer_generator is None:
self.choice_timer() 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) self.remaining_time = next(self.timer_generator, self.zero_time)
# Запуск обновления времени каждую секунду
Clock.schedule_interval(self.update_time, 1) Clock.schedule_interval(self.update_time, 1)
def pause_timer(self, instance) -> None: def pause_timer(self, *args) -> None:
"""Метод для паузы таймера""" """Пауза таймера"""
if not self.paused: if not self.paused:
self.paused = True self.paused = True
# Остановка обновления времени
Clock.unschedule(self.update_time) Clock.unschedule(self.update_time)
def stop_timer(self, instance) -> None: # Активируем кнопку "Старт"
"""Метод для остановки таймера""" 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) Clock.unschedule(self.update_time)
self.paused = False self.paused = False
# Сбрасываем оставшееся время
self.remaining_time = next(self.timer_generator, self.zero_time) self.remaining_time = next(self.timer_generator, self.zero_time)
self.ids.time_label.text = self.timer_start_time self.ids.time_label.text = self.timer_start_time
self.sound.stop()
# Останавливаем звук, если играет
if self.sound:
self.sound.stop()
if self.sound_stop_event: if self.sound_stop_event:
Clock.unschedule(self.sound_stop_event) Clock.unschedule(self.sound_stop_event)
self.sound_stop_event = None self.sound_stop_event = None
# Возврат цикла таймеров
if not len(self.manager.state_machine): if not len(self.manager.state_machine):
self.manager.state_machine = self.source_timer_names.copy() 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() self.choice_timer()
def back(self, instance) -> None: def play_sound(self, *args) -> None:
"""Метод для кнопки назад - возврат в меню таймеров""" """Воспроизведение звука"""
if self.ids.title_warning.text: if self.sound:
self.ids.title_warning.text = '' self.sound.play()
self.ids.del_timer.color = (1, 1, 1, 1) self.sound_stop_event = Clock.schedule_once(lambda dt: self.sound.stop(), 20)
self.manager.current = 'timers_screen'
def delete_timer(self, instance) -> None:
"""Метод для удаления таймера"""
if not self.ids.title_warning.text:
self.ids.title_warning.text = ('Вы действительно хотите удалить таймер?\n'
'Если да, то нажмите кнопку "удалить" ещё раз.')
self.ids.del_timer.color = (1, 0.3, 0.3, 1)
else:
del_timer(timer_id=self.timer.id)
timers = list_timers()
timers_screen = self.manager.get_screen('timers_screen')
timers_screen.ids.timers_view.data = timers
self.manager.current = 'timers_screen'
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: def update_time(self, dt) -> None:
"""Метод для обновления времени на экране""" """Обновление времени"""
if self.paused: if self.paused:
return return
# Получение следующего значения времени из генератора
self.remaining_time = next(self.timer_generator, self.zero_time) self.remaining_time = next(self.timer_generator, self.zero_time)
self.ids.time_label.text = self.remaining_time self.ids.time_label.text = self.remaining_time
if self.remaining_time == self.zero_time: if self.remaining_time == self.zero_time:
# Остановка обновления времени
Clock.unschedule(self.update_time) Clock.unschedule(self.update_time)
Clock.schedule_once(self.play_sound) Clock.schedule_once(self.play_sound)

View File

@@ -24,14 +24,17 @@
icon: "clock-outline" icon: "clock-outline"
CommonNavigationRailItem: CommonNavigationRailItem:
id: timers_nav
icon: "timer-outline" icon: "timer-outline"
text: "Таймеры" text: "Таймеры"
CommonNavigationRailItem: CommonNavigationRailItem:
id: guide_nav
icon: "compass-outline" icon: "compass-outline"
text: "Гид" text: "Гид"
CommonNavigationRailItem: CommonNavigationRailItem:
id: info_nav
icon: "information-outline" icon: "information-outline"
text: "Инфо" text: "Инфо"
@@ -40,5 +43,6 @@
y: "12dp" y: "12dp"
MDNavigationRailMenuButton: MDNavigationRailMenuButton:
id: menu
icon: "menu" icon: "menu"
on_release: app.open_menu(self) on_release: app.open_menu(self)

View File

@@ -1,72 +1,96 @@
#:kivy 2.3.1 #:kivy 2.3.1
#:include kv/navigation_panel.kv
<TimerScreen>: <TimerScreen>:
FloatLayout: NavigationPanel:
Label: id: nav_panel
id: title_label name: "timer_screen"
size_hint: None, None
pos_hint: {"center_x": 0.3, "center_y": 0.7}
Label: MDAnchorLayout:
id: type_timer_label orientation: "vertical"
text: "Помидор" anchor_x: "center"
size_hint: None, None anchor_y: "center"
pos_hint: {"center_x": 0.7, "center_y": 0.7} md_bg_color: app.theme_cls.secondaryContainerColor
Button: # Центральный блок таймера
text: "Старт" MDBoxLayout:
size_hint: None, None orientation: "vertical"
height: 40 size_hint: None, None
width: 100 size: "310dp", "200dp"
pos_hint: {"center_x": 0.3, "center_y": 0.5} md_bg_color: app.theme_cls.surfaceColor
on_release: root.start_timer(self) radius: [20]
elevation: 4
padding: "16dp"
spacing: "12dp"
Button: # Заголовок блока таймера (слева вверху)
text: "Пауза" MDBoxLayout:
size_hint: None, None orientation: "horizontal"
height: 40 size_hint_y: None
width: 100 height: "24dp"
pos_hint: {"center_x": 0.5, "center_y": 0.5} spacing: "4dp"
on_release: root.pause_timer(self)
Button: MDLabel:
id: stop_button id: title_label
text: "Стоп" text: "Таймер"
opacity: 0 halign: "left"
disabled: True valign: "center"
size_hint: None, None role: "small"
height: 40
width: 100
pos_hint: {"center_x": 0.7, "center_y": 0.5}
on_release: root.stop_timer(self)
Label: MDLabel:
id: time_label id: type_timer_label
size_hint: None, None text: "Помидор"
pos_hint: {"center_x": 0.5, "center_y": 0.6} halign: "right"
valign: "center"
role: "small"
Label: # Крупный таймер
id: title_warning MDLabel:
text: "" id: time_label
color: (1, 0.3, 0.3, 1) text: "00:00:00"
font_size: 14 halign: "center"
size_hint_y: None valign: "center"
height: 20 font_style: "Display"
pos_hint: {"center_x": 0.5, "center_y": 0.4} role: "large"
Button: # Кнопки
text: "Назад" MDBoxLayout:
size_hint: None, None orientation: "horizontal"
height: 40 size_hint_y: None
width: 100 height: "48dp"
pos_hint: {"center_x": 0.3, "center_y": 0.1} spacing: "12dp"
on_press: root.back(self) pos_hint: {"center_x": .5}
Button: MDButton:
id: del_timer style: "outlined"
text: "Удалить" id: start_button
size_hint: None, None on_release: root.start_timer()
height: 40 theme_line_color: "Custom"
width: 120 line_color: 0, 1, 0, 1
pos_hint: {"center_x": 0.7, "center_y": 0.1}
on_press: root.delete_timer(self) 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: "Стоп"

45
poetry.lock generated
View File

@@ -213,6 +213,47 @@ files = [
[package.dependencies] [package.dependencies]
tzdata = "*" tzdata = "*"
[[package]]
name = "ffpyplayer"
version = "4.5.3"
description = "A cython implementation of an ffmpeg based player."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "ffpyplayer-4.5.3-cp310-cp310-macosx_10_13_universal2.whl", hash = "sha256:9c8cc4bbbfe5f01ab0568a941927972310b63fad4663f8c08712c25d23b2a8e8"},
{file = "ffpyplayer-4.5.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:3a5fbae10ce1de3946856dc19937daae3631c076ee98c248d4d889be8ca06320"},
{file = "ffpyplayer-4.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0db8f81229c5f53e3c4cd60b378b5ecd6a9c40edde12e7f64eab67a52bd49ec3"},
{file = "ffpyplayer-4.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4343948aa3f031e3ba44593587b81f89ce15f8c756cad3d52fc75df1d71b02a0"},
{file = "ffpyplayer-4.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa7d590016e95258dcbe82e972a2bea0fea1ec408cbd9f19e710bfcde6b4a397"},
{file = "ffpyplayer-4.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:73caa0c12ce57dfa033280e9b4b3dfb7965d15c18f190b56d57b16b4b014c3b9"},
{file = "ffpyplayer-4.5.3-cp311-cp311-macosx_10_13_universal2.whl", hash = "sha256:7c9799e86a4c197c647e3ece6a8a1d026b2deb21e0a5b5bffbf49eac2a876168"},
{file = "ffpyplayer-4.5.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:a5728d1b3e4a2e449893b766eab4e35fcccb1e6e53a2c3321bf745f1a86c27cf"},
{file = "ffpyplayer-4.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b592ee9f6c71ba8d768ebc032ce30e3aaeff3be3c4cba2fcf7fed4a36071f169"},
{file = "ffpyplayer-4.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2d4f6c779ae5038e321e84e1da7570f9ea9ae3d7993054c60ad1338c2d5da38"},
{file = "ffpyplayer-4.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29490c57b86c5e48ed0d12c974ca83ee4dd7d2cc360f400115e64e8afb4436ef"},
{file = "ffpyplayer-4.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:3e613fe88c2fd1f3f7c9f84b3e153c90b1222f7e4dbf92a280d449c6217256bf"},
{file = "ffpyplayer-4.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:666bfed646b52014895f30c8d9d10b416a616911ae496a643f9980b45cc24b74"},
{file = "ffpyplayer-4.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bb4bd6d9d1ee37cfe8f470aa9b49dc79f1b4761c2c5b44e4784740c8efb8cc22"},
{file = "ffpyplayer-4.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c79aa4c82b414689377b1e0111c61b12c8b2b3b0ed961670ec5f1fbd873b967"},
{file = "ffpyplayer-4.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2545160450f8dacb1de0f37b2438a61a301610a1f50c1878edf87d19ed9782a1"},
{file = "ffpyplayer-4.5.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b92f2b399b25404b688093b605109ef647a7b4314d6bbb24a81c738646e77b87"},
{file = "ffpyplayer-4.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:7eefb1060e2df8534eec245e1f7e228b4b53853a0b4f508a124b4b83c6f2c56c"},
{file = "ffpyplayer-4.5.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:441888f42b76dbd7c3005f44d7145f713a1d8ce23351bbc480b9fa05e4662afb"},
{file = "ffpyplayer-4.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:549151bb125c9f2a6d7ebddf62290459a1624cf1a9562e78ff07ead537fdbbae"},
{file = "ffpyplayer-4.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50917a4b25fd648ed76991d1703cd5940bcce9b3b0e824108483b2f4fce244f9"},
{file = "ffpyplayer-4.5.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e012cc1c943570009fad024a675bdf43b31e8e1d0c0709715e611bdf5ab3959"},
{file = "ffpyplayer-4.5.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586b04cd59b1bc3a759d9772de281172480e166778693474249ae1e1254a427f"},
{file = "ffpyplayer-4.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:2dc1dc9dc6fddc7812c8451e14544aa01ecdaaee8c2ca705187925c3ff75d12d"},
{file = "ffpyplayer-4.5.3-cp39-cp39-macosx_10_13_universal2.whl", hash = "sha256:6ab4283201c6e07fd976f830ae0fb96157d904c33e5366415b50fcdec80a3247"},
{file = "ffpyplayer-4.5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:951e65db450aec868df31c84b2d8b10ed854f80688f7360d829d641709917067"},
{file = "ffpyplayer-4.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f750f0b9e1274bf4db89ef1c83681f40474a37636a76ebfdb729ff99611de66c"},
{file = "ffpyplayer-4.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ddb132009b3109e6d6978bd5c89b1d9ca04c19966fe2bebd4a1cccf2ff8bbab"},
{file = "ffpyplayer-4.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f37e5eba45bbf042a4756e72f4621864f51f949399960037438171a4bd47675b"},
{file = "ffpyplayer-4.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:0335ee8e0c603799a2b76cd8e125632942f48990ee55c9747c5541744184b218"},
{file = "ffpyplayer-4.5.3.tar.gz", hash = "sha256:8b9623e04997ba7bbf5476313aa8d9eae2665c65f403b5deff4ac51f16155e7e"},
]
[[package]] [[package]]
name = "filetype" name = "filetype"
version = "1.2.0" version = "1.2.0"
@@ -1213,5 +1254,5 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.12,<4.0" python-versions = ">=3.12,<3.15"
content-hash = "816dd58321a6a8a40acd2b7654f2ebc31aa77336462f61bfbbff781f0585b1cb" content-hash = "4a7c690b6c5e8432b3b7d8b36e5411560f5778581bef9cc70d84fe1a2b5edb1a"

View File

@@ -18,6 +18,7 @@ dependencies = [
"ruff (>=0.11.8,<0.12.0)", "ruff (>=0.11.8,<0.12.0)",
"kivymd @ https://github.com/kivymd/KivyMD/archive/master.zip", "kivymd @ https://github.com/kivymd/KivyMD/archive/master.zip",
"faker (>=37.6.0,<38.0.0)", "faker (>=37.6.0,<38.0.0)",
"ffpyplayer (>=4.5.3,<5.0.0)",
] ]

Binary file not shown.