diff --git a/kawai_focus/main.py b/kawai_focus/main.py index 225860d..4f9bfab 100644 --- a/kawai_focus/main.py +++ b/kawai_focus/main.py @@ -7,6 +7,7 @@ from kivy.metrics import dp from kivy.uix.behaviors import ButtonBehavior from kivy.logger import Logger from kivy.properties import StringProperty +from kivy import Config import logging @@ -19,10 +20,13 @@ from kivymd.uix.navigationrail import MDNavigationRailItem from kawai_focus.menu_app import MenuApp from kawai_focus.screens.timers_screen import TimersScreen +from kawai_focus.screens.timer_screen import TimerScreen Logger.setLevel(logging.DEBUG) +Config.set('kivy', 'audio', 'ffpyplayer') # вместо audio_sdl2 + class TrailingPressedIconButton( ButtonBehavior, RotateBehavior, MDListItemTrailingIcon @@ -49,9 +53,11 @@ class KawaiFocusApp(MDApp, MenuApp): 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 diff --git a/kawai_focus/screens/timer_screen.py b/kawai_focus/screens/timer_screen.py index df1d753..954ba32 100644 --- a/kawai_focus/screens/timer_screen.py +++ b/kawai_focus/screens/timer_screen.py @@ -1,137 +1,183 @@ -from kivy.uix.screenmanager import Screen +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.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): - super(TimerScreen, self).__init__(**kwargs) + super().__init__(**kwargs) + Clock.schedule_once(self.load_sound) + # Переменные для управления таймером self.zero_time = data_json.get_text('zero_time') - self.timer_generator = None + 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 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.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.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 - 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: 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: - """Метод для паузы таймера""" + def pause_timer(self, *args) -> None: + """Пауза таймера""" if not self.paused: self.paused = True - # Остановка обновления времени 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) 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: + 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 back(self, instance) -> None: - """Метод для кнопки назад - возврат в меню таймеров""" + def play_sound(self, *args) -> None: + """Воспроизведение звука""" - if self.ids.title_warning.text: - self.ids.title_warning.text = '' - self.ids.del_timer.color = (1, 1, 1, 1) - - 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) + 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) diff --git a/kv/navigation_panel.kv b/kv/navigation_panel.kv index eaa5874..d9d528f 100644 --- a/kv/navigation_panel.kv +++ b/kv/navigation_panel.kv @@ -24,14 +24,17 @@ icon: "clock-outline" CommonNavigationRailItem: + id: timers_nav icon: "timer-outline" text: "Таймеры" CommonNavigationRailItem: + id: guide_nav icon: "compass-outline" text: "Гид" CommonNavigationRailItem: + id: info_nav icon: "information-outline" text: "Инфо" @@ -40,5 +43,6 @@ y: "12dp" MDNavigationRailMenuButton: + id: menu icon: "menu" on_release: app.open_menu(self) diff --git a/kv/timer_screen.kv b/kv/timer_screen.kv index 4b39f9c..2ef121c 100644 --- a/kv/timer_screen.kv +++ b/kv/timer_screen.kv @@ -1,72 +1,96 @@ #:kivy 2.3.1 +#:include kv/navigation_panel.kv : - FloatLayout: - Label: - id: title_label - size_hint: None, None - pos_hint: {"center_x": 0.3, "center_y": 0.7} + NavigationPanel: + id: nav_panel + name: "timer_screen" - Label: - id: type_timer_label - text: "Помидор" - size_hint: None, None - pos_hint: {"center_x": 0.7, "center_y": 0.7} + MDAnchorLayout: + orientation: "vertical" + anchor_x: "center" + anchor_y: "center" + md_bg_color: app.theme_cls.secondaryContainerColor - 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) + # Центральный блок таймера + 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" - 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) + # Заголовок блока таймера (слева вверху) + MDBoxLayout: + orientation: "horizontal" + size_hint_y: None + height: "24dp" + spacing: "4dp" - 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) + MDLabel: + id: title_label + text: "Таймер" + halign: "left" + valign: "center" + role: "small" - Label: - id: time_label - size_hint: None, None - pos_hint: {"center_x": 0.5, "center_y": 0.6} + MDLabel: + id: type_timer_label + text: "Помидор" + halign: "right" + valign: "center" + role: "small" - Label: - id: title_warning - text: "" - color: (1, 0.3, 0.3, 1) - font_size: 14 - size_hint_y: None - height: 20 - pos_hint: {"center_x": 0.5, "center_y": 0.4} + # Крупный таймер + MDLabel: + id: time_label + text: "00:00:00" + halign: "center" + valign: "center" + font_style: "Display" + role: "large" - Button: - text: "Назад" - size_hint: None, None - height: 40 - width: 100 - pos_hint: {"center_x": 0.3, "center_y": 0.1} - on_press: root.back(self) + # Кнопки + MDBoxLayout: + orientation: "horizontal" + size_hint_y: None + height: "48dp" + spacing: "12dp" + pos_hint: {"center_x": .5} - Button: - id: del_timer - text: "Удалить" - size_hint: None, None - height: 40 - width: 120 - pos_hint: {"center_x": 0.7, "center_y": 0.1} - on_press: root.delete_timer(self) \ No newline at end of file + 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: "Стоп" diff --git a/poetry.lock b/poetry.lock index cdb5177..a210c2e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -213,6 +213,47 @@ files = [ [package.dependencies] 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]] name = "filetype" version = "1.2.0" @@ -1213,5 +1254,5 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.1" -python-versions = ">=3.12,<4.0" -content-hash = "816dd58321a6a8a40acd2b7654f2ebc31aa77336462f61bfbbff781f0585b1cb" +python-versions = ">=3.12,<3.15" +content-hash = "4a7c690b6c5e8432b3b7d8b36e5411560f5778581bef9cc70d84fe1a2b5edb1a" diff --git a/pyproject.toml b/pyproject.toml index 3b54c30..8bbf73a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "ruff (>=0.11.8,<0.12.0)", "kivymd @ https://github.com/kivymd/KivyMD/archive/master.zip", "faker (>=37.6.0,<38.0.0)", + "ffpyplayer (>=4.5.3,<5.0.0)", ] diff --git a/sounds/alarm-beep.mp3 b/sounds/alarm-beep.mp3 index b9bfeba..fde2d49 100644 Binary files a/sounds/alarm-beep.mp3 and b/sounds/alarm-beep.mp3 differ