From 02deb8a77f8c853206295e0430215ad9311ebfc6 Mon Sep 17 00:00:00 2001 From: 2o Date: Fri, 18 Oct 2024 11:38:23 +0300 Subject: [PATCH] Update to 0.2.0 --- add.png | Bin 0 -> 122 bytes apply.png | Bin 0 -> 152 bytes delete.png | Bin 0 -> 159 bytes edit.png | Bin 0 -> 4356 bytes icon.png | Bin 0 -> 4808 bytes main.py | 12 ++ requirements.txt | 2 + save.png | Bin 0 -> 193 bytes scheduler.py | 135 +++++++++++++++++++++++ sound.py | 7 ++ start.png | Bin 0 -> 145 bytes stop.png | Bin 0 -> 105 bytes ui.py | 37 +++++++ widgets.py | 277 +++++++++++++++++++++++++++++++++++++++++++++++ windows.py | 176 ++++++++++++++++++++++++++++++ 15 files changed, 646 insertions(+) create mode 100644 add.png create mode 100644 apply.png create mode 100644 delete.png create mode 100644 edit.png create mode 100644 icon.png create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 save.png create mode 100644 scheduler.py create mode 100644 sound.py create mode 100644 start.png create mode 100644 stop.png create mode 100644 ui.py create mode 100644 widgets.py create mode 100644 windows.py diff --git a/add.png b/add.png new file mode 100644 index 0000000000000000000000000000000000000000..066480ba050d5f7c91734281e0a84f4a0346c4b4 GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`j-D=#Ar_~T6AmzciRb!%yzopr0Aa8vO#lD@ literal 0 HcmV?d00001 diff --git a/apply.png b/apply.png new file mode 100644 index 0000000000000000000000000000000000000000..4416fb9a7c438171b5929ba045be57c044b8ec93 GIT binary patch literal 152 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`@t!V@Ar_~T6C^SYcyuMr-hTYw z;T!HPYb7KlBqU~>xY)xp=iRa$;vE|~ zXQ%DpF7TWr)3GC_={%EKlISFn-W^G;Y77j8Z#Bd>95&w$w2Hyg)z4*}Q$iB}eaks2 literal 0 HcmV?d00001 diff --git a/delete.png b/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..5768a476b84990a54dd276f504358d24cac80ea4 GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`X`U{QAr_~T_pH+YGrzI1(ed}A zXg4+?1|NChT@0GDBqf00A}hyEmRMb7J_;SphaRo%GAVTp#6rk96^YHE;4vT;ji$k`WJj8f+l96X|Xj{$9D@O1TaS?83{ F1OOSQGCu$S literal 0 HcmV?d00001 diff --git a/edit.png b/edit.png new file mode 100644 index 0000000000000000000000000000000000000000..31b219644f8a7646945145cc9fd0501edd15d2f9 GIT binary patch literal 4356 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 literal 0 HcmV?d00001 diff --git a/main.py b/main.py new file mode 100644 index 0000000..2ee5558 --- /dev/null +++ b/main.py @@ -0,0 +1,12 @@ +# this will either work flawlessly, or fail to do anything... +# if you're trying to understand this, sorry for the spaghetti code + +from ui import Ui +from scheduler import Scheduler +import sys + +sc = Scheduler() +ui = Ui(sc.menu_event, sc.update) +sc.set_ui_class(ui) + +ui.run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9f3888e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PySide6 +playsound \ No newline at end of file diff --git a/save.png b/save.png new file mode 100644 index 0000000000000000000000000000000000000000..9a2487b3c20c865ffb202df2d41094091a742e04 GIT binary patch literal 193 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`9iA?ZAr`0iPBi3eHsEocA9(y; zhp*OJjZDSx?wKuA->q1A7{|J{Eug6V=?e0;;d&vr6giVO=@ z@x-Rga@X9u%xx=281uCW$7d#n{=I#hp-nGt-;#}W2YDiNd_Gs09;s#1TpE1(-tWf( sbBn)pqm&xUHx3vIVCg!0Et*h_W%F@ literal 0 HcmV?d00001 diff --git a/scheduler.py b/scheduler.py new file mode 100644 index 0000000..b2ec179 --- /dev/null +++ b/scheduler.py @@ -0,0 +1,135 @@ +import time, json, dataclasses +from enum import Enum +from playsound import playsound +from sound import play + +class SoundType(Enum): + FIRST_BELL = 1 + SECOND_BELL = 2 + BREAK = 3 + CUSTOM = 4 + +BELL_NAMES = { + SoundType.FIRST_BELL: "1-й дзвоник", + SoundType.SECOND_BELL: "2-й дзвоник", + SoundType.BREAK: "Перерва" +} + +@dataclasses.dataclass +class Bell: + sound: SoundType + hour: int + minute: int + played: bool = False + # only used when sound = SoundType.CUSTOM + custom_file: str = "" + custom_name: str = "" + +@dataclasses.dataclass +class CustomSound: + name: str + times: list[list[int]] + sound_file: str + +@dataclasses.dataclass +class Config: + # yes, this is permanently "borrowed" from the internet + # thx to https://stackoverflow.com/questions/53632152/why-cant-dataclasses-have-mutable-defaults-in-their-class-attributes-declaratio + lessons_start: list[int] = dataclasses.field(default_factory=lambda: [8, 0]) + lesson_length: int = dataclasses.field(default_factory=lambda: 40) + break_length: int = dataclasses.field(default_factory=lambda: 5) + first_bell: int = dataclasses.field(default_factory=lambda: 1) + num_lessons: int = dataclasses.field(default_factory=lambda: 12) + workdays: list[bool] = dataclasses.field(default_factory=lambda: [True, True, True, True, True, True, False]) + sound_files: list[str] = dataclasses.field(default_factory=lambda: ["", "", ""]) + first_bell_before_first_lesson: bool = dataclasses.field(default_factory=lambda: True) + custom_sounds: list[CustomSound] = dataclasses.field(default_factory=lambda: []) + +class Scheduler: + def __init__(self) -> None: + self.bells: list[Bell] = [] + self.bells_enabled: bool = True + self.load_config() + + def set_ui_class(self, ui_class) -> None: + self.ui = ui_class + self.generate_bells() + self.ui.set_settings(self.config) + + def apply_config(self) -> None: + self.ui.get_settings(self.config, CustomSound) + self.generate_bells() + + def generate_bells(self) -> None: + self.bells = [] + if not self.config.workdays[time.localtime().tm_wday]: + self.ui.set_schedule(self.get_bells_list()) + return + + day_minute: int = self.config.lessons_start[0] * 60 + self.config.lessons_start[1] - self.config.first_bell + for lesson_number in range(self.config.num_lessons): + if lesson_number != 0 or self.config.first_bell_before_first_lesson: + self.bells.append(Bell(SoundType.FIRST_BELL, day_minute // 60, day_minute % 60)) + day_minute += self.config.first_bell + self.bells.append(Bell(SoundType.SECOND_BELL, day_minute // 60, day_minute % 60)) + day_minute += self.config.lesson_length + self.bells.append(Bell(SoundType.BREAK, day_minute // 60, day_minute % 60)) + day_minute += self.config.break_length - self.config.first_bell + + for sound in self.config.custom_sounds: + for sound_time in sound.times: + self.bells.append(Bell(SoundType.CUSTOM, *sound_time, custom_file=sound.sound_file, custom_name=sound.name)) + + # so that the first bell before the first lesson can't roll time into the negatives + self.bells = [bell for bell in self.bells if bell.hour <= 23] + self.bells.sort(key=lambda sound: sound.hour * 60 + sound.minute) + + self.ui.set_schedule(self.get_bells_list()) + + def get_bells_list(self) -> list[str]: + bells_list: list[str] = [] + for bell in self.bells: + if bell.sound != SoundType.CUSTOM: bell_name: str = BELL_NAMES[bell.sound] + else: bell_name: str = '"' + bell.custom_name + '"' + bells_list.append(f"{bell.hour:02}:{bell.minute:02} - {bell_name}") + return bells_list + + def menu_event(self, button) -> None: + match button: + case 0: self.apply_config() + case 1: self.save_config() + case 2: self.bells_enabled = True + case 3: self.bells_enabled = False + + def update(self) -> None: + if not self.bells_enabled: + return + t = time.localtime() + for bell_n, bell in enumerate(self.bells): + if (not bell.played) and (bell.hour == t.tm_hour) and (bell.minute == t.tm_min): + bell.played = True + self.ui.select_bell(bell_n) + match bell.sound: + case SoundType.FIRST_BELL: play(self.config.sound_files[0]) + case SoundType.SECOND_BELL: play(self.config.sound_files[1]) + case SoundType.BREAK: play(self.config.sound_files[2]) + case SoundType.CUSTOM: play(bell.custom_file) + break + + def load_config(self) -> None: + try: + with open("config.json", "r") as fp: + self.config = Config(**json.load(fp)) + for i in range(len(self.config.custom_sounds)): + self.config.custom_sounds[i] = CustomSound(**self.config.custom_sounds[i]) + except: + # if something goes wrong, load the default values + # nothing should go wrong with the config file, if the user doesn't edit + # it manually. if that's the case - it's their fault, not mine :P + self.config = Config() + + def save_config(self) -> None: + temp_config = Config() + self.ui.get_settings(temp_config, CustomSound) + with open("config.json", "w") as fp: + json.dump(dataclasses.asdict(temp_config), fp) diff --git a/sound.py b/sound.py new file mode 100644 index 0000000..e920217 --- /dev/null +++ b/sound.py @@ -0,0 +1,7 @@ +import playsound + +def play(file_name): + try: + playsound.playsound(file_name, block=False) + except playsound.PlaysoundException: + pass diff --git a/start.png b/start.png new file mode 100644 index 0000000000000000000000000000000000000000..d6f95857eaf8b788007a19ae432f305c65e9d3a1 GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`5uPrNAr_~T6Be+2*tO!{{nv|r z)fas+I>h53c4t}r%aqphDT>y3>g@-40)vd+jE(KdKo-j{an^LB{Ts5 D&6^*X literal 0 HcmV?d00001 diff --git a/ui.py b/ui.py new file mode 100644 index 0000000..18be382 --- /dev/null +++ b/ui.py @@ -0,0 +1,37 @@ +from PySide6 import QtWidgets, QtGui, QtCore +from windows import MainWindow, MenuActions, ToolBar, MenuBar, Tray, SoundEditorWindow + +class Ui: + def __init__(self, menu_callback, periodic_task): + self.app = QtWidgets.QApplication([]) + self.app.setQuitOnLastWindowClosed(False) + self.sound_editor_window = SoundEditorWindow() + self.main_window = MainWindow(self.sound_editor_window.show) + self.menu_actions = MenuActions(self.main_window, menu_callback) + self.menu_bar = MenuBar(self.main_window, self.menu_actions) + self.toolbar = ToolBar(self.main_window, self.menu_actions) + self.tray = Tray(self.main_window) + + self.sound_editor_window.set_add_sound_callback(self.main_window.additional_sounds_box.add_sound) + + # timer for starting bells + self.timer = QtCore.QTimer() + self.timer.timeout.connect(periodic_task) + self.timer.start(1000) + + def run(self): + self.main_window.show() + self.tray.setVisible(True) + self.app.exec() + + def get_settings(self, *args, **kwargs): + return self.main_window.get_settings(*args, **kwargs) + + def set_settings(self, *args, **kwargs): + self.main_window.set_settings(*args, **kwargs) + + def set_schedule(self, *args, **kwargs): + self.main_window.set_schedule(*args, **kwargs) + + def select_bell(self, *args, **kwargs): + self.main_window.select_bell(*args, **kwargs) diff --git a/widgets.py b/widgets.py new file mode 100644 index 0000000..8d475ce --- /dev/null +++ b/widgets.py @@ -0,0 +1,277 @@ +from PySide6 import QtWidgets, QtGui, QtCore +import time + +DAYS_OF_WEEK = "Понеділок Вівторок Середа Четвер П'ятниця Субота Неділя".split() + +class BasicBox(QtWidgets.QScrollArea): + def __init__(self, title): + super().__init__() + self.widget = QtWidgets.QWidget() + self.setWidgetResizable(True) + self.setWidget(self.widget) + self.layout = QtWidgets.QVBoxLayout(self.widget) + font = QtGui.QFont() + font.setPointSize(16) + title = QtWidgets.QLabel(title) + title.setFont(font) + self.layout.addWidget(title) + +class WidgetArray(QtWidgets.QWidget): + def __init__(self, deleted_item_callback, edit_button=False): + super().__init__() + self.deleted_item_callback = deleted_item_callback + self.main_layout = QtWidgets.QVBoxLayout(self) + + self.control_widget = QtWidgets.QWidget() + self.control_layout = QtWidgets.QHBoxLayout(self.control_widget) + self.add_button = QtWidgets.QPushButton() + self.add_button.setIcon(QtGui.QIcon("add.png")) + self.add_button.setFixedSize(22, 22) + self.delete_button = QtWidgets.QPushButton() + self.delete_button.setIcon(QtGui.QIcon("delete.png")) + self.delete_button.setFixedSize(22, 22) + self.delete_button.clicked.connect(self.delete_items) + self.control_layout.addWidget(self.add_button) + self.control_layout.addWidget(self.delete_button) + if edit_button: + self.edit_button = QtWidgets.QPushButton() + self.edit_button.setIcon(QtGui.QIcon("edit.png")) + self.edit_button.setFixedSize(22, 22) + self.control_layout.addWidget(self.edit_button) + self.control_layout.addStretch(1) + + self.widget_list = QtWidgets.QListWidget() + + self.main_layout.addWidget(self.control_widget) + self.main_layout.addWidget(self.widget_list) + + def add_item(self, widget): + item = QtWidgets.QListWidgetItem() + self.widget_list.addItem(item) + self.widget_list.setItemWidget(item, widget) + + def get_items(self): + for i in range(self.widget_list.count()): + yield self.widget_list.itemWidget(self.widget_list.item(i)) + + def delete_items(self): + selected_items = self.widget_list.selectedItems() + if not selected_items: return + # if multiple items are selected, and the parent of this widget list keeps track of the widgets somehow, + # this makes so they are deleted in the correct order + selected_items.sort(reverse=True, key=lambda item: self.widget_list.row(item)) + for item in selected_items: + row = self.widget_list.row(item) + self.widget_list.takeItem(row) + self.deleted_item_callback(row) + +class BellStatusBox(BasicBox): + def __init__(self): + super().__init__("Розклад дзвінків") + self.bells_list = QtWidgets.QListWidget() + self.layout.addWidget(self.bells_list) + +class ScheduleBox(BasicBox): + def __init__(self): + super().__init__("Налаштування розкладу") + self.grid_widget = QtWidgets.QWidget() + self.grid_layout = QtWidgets.QGridLayout(self.grid_widget) + self.grid_widget.setLayout(self.grid_layout) + + self.first_lesson_input = QtWidgets.QTimeEdit() + self.lesson_length_input = QtWidgets.QSpinBox() + self.break_length_input = QtWidgets.QSpinBox() + self.first_bell_input = QtWidgets.QSpinBox() + self.num_lessons_input = QtWidgets.QSpinBox() + + self.lesson_length_input.setRange(20, 90) + self.break_length_input.setRange(2, 59) + self.first_bell_input.setRange(1, 4) + self.num_lessons_input.setRange(1, 20) + + self.break_length_input.valueChanged.connect(self.update_limits) + + self.grid_layout.addWidget(QtWidgets.QLabel("Перший урок о"), 0, 0) + self.grid_layout.addWidget(QtWidgets.QLabel("Тривалість уроку:"), 1, 0) + self.grid_layout.addWidget(QtWidgets.QLabel("Перерва"), 2, 0) + self.grid_layout.addWidget(QtWidgets.QLabel("Перший дзвоник за"), 3, 0) + self.grid_layout.addWidget(QtWidgets.QLabel("Кількість уроків:"), 4, 0) + self.grid_layout.addWidget(QtWidgets.QLabel("хвилин"), 1, 2) + self.grid_layout.addWidget(QtWidgets.QLabel("хвилин"), 2, 2) + self.grid_layout.addWidget(QtWidgets.QLabel("хвилин до уроку"), 3, 2) + self.grid_layout.addWidget(self.first_lesson_input, 0, 1) + self.grid_layout.addWidget(self.lesson_length_input, 1, 1) + self.grid_layout.addWidget(self.break_length_input, 2, 1) + self.grid_layout.addWidget(self.first_bell_input, 3, 1) + self.grid_layout.addWidget(self.num_lessons_input, 4, 1) + + self.first_bell_before_first_lesson_checkbox = QtWidgets.QCheckBox("Перший дзвоник перед першим уроком") + + self.layout.addWidget(self.grid_widget) + self.layout.addWidget(self.first_bell_before_first_lesson_checkbox) + self.layout.addStretch(1) + + def update_limits(self, value): + self.first_bell_input.setRange(1, value - 1) + +class DaysSelectBox(BasicBox): + def __init__(self): + super().__init__("Робочі дні") + self.days_checkboxes = [] + for day_name in DAYS_OF_WEEK: + self.days_checkboxes.append(QtWidgets.QCheckBox(day_name)) + self.layout.addWidget(self.days_checkboxes[-1]) + self.layout.addStretch(1) + +class AdditionalSoundsBox(BasicBox): + def __init__(self, show_sound_diag): + super().__init__("Додаткові звуки") + self.show_sound_diag = show_sound_diag + self.sound_list = WidgetArray(self.deleted_item, True) + self.sound_list.add_button.clicked.connect(lambda: self.show_sound_diag()) + self.sound_list.edit_button.clicked.connect(self.edit_sound) + self.layout.addWidget(self.sound_list) + self.sounds = [] + self.editing_sound = 0 + + def add_sound(self, name, times, sound_file, edit_n=None): + if edit_n is None: + self.sounds.append([name, times, sound_file]) + self.sound_list.add_item(QtWidgets.QLabel(name)) + else: + self.sounds[edit_n] = [name, times, sound_file] + self.sound_list.widget_list.itemWidget(self.sound_list.widget_list.item(edit_n)).setText(name) + + def deleted_item(self, n): + del self.sounds[n] + + def edit_sound(self): + edit_n = self.sound_list.widget_list.currentRow() + sound = self.sounds[edit_n] + self.show_sound_diag(edit_n, *sound) + +class StatusBox(BasicBox): + def __init__(self): + super().__init__("Стан") + self.grid_widget = QtWidgets.QWidget() + self.grid_layout = QtWidgets.QGridLayout(self.grid_widget) + self.grid_widget.setLayout(self.grid_layout) + + self.day_of_week_widget = QtWidgets.QLabel() + self.current_time_widget = QtWidgets.QLabel() + self.uptime_widget = QtWidgets.QLabel() + self.grid_layout.addWidget(QtWidgets.QLabel("День тижня:"), 0, 0) + self.grid_layout.addWidget(self.day_of_week_widget, 0, 1) + self.grid_layout.addWidget(QtWidgets.QLabel("Поточний час:"), 1, 0) + self.grid_layout.addWidget(self.current_time_widget, 1, 1) + self.grid_layout.addWidget(QtWidgets.QLabel("Зі запуску:"), 2, 0) + self.grid_layout.addWidget(self.uptime_widget, 2, 1) + + self.layout.addWidget(self.grid_widget) + self.layout.addStretch(1) + + self.uptime_timer = QtCore.QElapsedTimer() + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.update) + self.uptime_timer.start() + self.timer.start(1000) + self.update() + + def update(self): + t = time.localtime() + self.day_of_week_widget.setText(DAYS_OF_WEEK[t.tm_wday]) + self.current_time_widget.setText(f"{t.tm_hour:02}:{t.tm_min:02}:{t.tm_sec:02}") + seconds = self.uptime_timer.elapsed() / 1000 + minutes = seconds / 60 + hours = minutes / 60 + days = hours / 24 + self.uptime_widget.setText(f"{int(days)} днів, {int(hours % 24):02}:{int(minutes % 60):02}:{int(seconds % 60):02}") + +class SoundFilesBox(BasicBox): + def __init__(self): + super().__init__("Звуки") + self.grid_widget = QtWidgets.QWidget() + self.grid_layout = QtWidgets.QGridLayout(self.grid_widget) + self.grid_widget.setLayout(self.grid_layout) + + self.first_bell_file_text = QtWidgets.QLabel() + self.second_bell_file_text = QtWidgets.QLabel() + self.break_file_text = QtWidgets.QLabel() + self.first_bell_file_button = QtWidgets.QPushButton("Огляд") + self.second_bell_file_button = QtWidgets.QPushButton("Огляд") + self.break_file_button = QtWidgets.QPushButton("Огляд") + + self.first_bell_file_button .clicked.connect(lambda: self.file_select_diag(0)) + self.second_bell_file_button.clicked.connect(lambda: self.file_select_diag(1)) + self.break_file_button .clicked.connect(lambda: self.file_select_diag(2)) + + self.grid_layout.addWidget(QtWidgets.QLabel("Перший дзвоник:"), 0, 0) + self.grid_layout.addWidget(QtWidgets.QLabel("Другий дзвоник:"), 1, 0) + self.grid_layout.addWidget(QtWidgets.QLabel("Перерва:"), 2, 0) + self.grid_layout.addWidget(self.first_bell_file_text, 0, 1) + self.grid_layout.addWidget(self.second_bell_file_text, 1, 1) + self.grid_layout.addWidget(self.break_file_text, 2, 1) + self.grid_layout.addWidget(self.first_bell_file_button, 0, 2) + self.grid_layout.addWidget(self.second_bell_file_button, 1, 2) + self.grid_layout.addWidget(self.break_file_button, 2, 2) + + self.layout.addWidget(self.grid_widget) + self.layout.addStretch(1) + + def file_select_diag(self, sound_type): + file_name, _ = QtWidgets.QFileDialog.getOpenFileName(self.grid_widget, "SBC - Відкрити файл", "", "Підтримувані файли (*.wav *.mp3 *.flac)") + if file_name is None: + return + match sound_type: + case 0: self.first_bell_file_text .setText(file_name) + case 1: self.second_bell_file_text.setText(file_name) + case 2: self.break_file_text .setText(file_name) + +class SoundEditorBox(BasicBox): + def __init__(self, add_sound_callback, quit_callback): + super().__init__("Редактор звуку") + self.add_sound_callback = add_sound_callback + self.quit_callback = quit_callback + self.time_select_widget = WidgetArray(lambda _: None) + self.sound_name_input = QtWidgets.QLineEdit() + self.save_button = QtWidgets.QPushButton("Зберегти") + self.time_select_widget.add_button.clicked.connect(lambda: self.time_select_widget.add_item(QtWidgets.QTimeEdit())) + self.save_button.clicked.connect(self.save) + + self.file_select_widget = QtWidgets.QWidget() + self.file_select_layout = QtWidgets.QHBoxLayout(self.file_select_widget) + self.file_name_select_text = QtWidgets.QLabel() + self.file_name_select_button = QtWidgets.QPushButton("Огляд") + self.file_name_select_button.clicked.connect(self.file_select_diag) + self.file_select_layout.addWidget(self.file_name_select_text) + self.file_select_layout.addWidget(self.file_name_select_button) + self.file_select_layout.addStretch(1) + + self.layout.addWidget(self.sound_name_input) + self.layout.addWidget(self.time_select_widget) + self.layout.addWidget(self.file_select_widget) + self.layout.addStretch(1) + self.layout.addWidget(self.save_button) + self.edit_n = None + + def file_select_diag(self): + file_name, _ = QtWidgets.QFileDialog.getOpenFileName(self.file_select_widget, "SBC - Відкрити файл", "", "Підтримувані файли (*.wav *.mp3 *.flac)") + if file_name is None: + return + self.file_name_select_text.setText(file_name) + + def save(self): + self.add_sound_callback( + self.sound_name_input.text(), + [[item.time().hour(), item.time().minute()] for item in self.time_select_widget.get_items()], + self.file_name_select_text.text(), + self.edit_n + ) + self.clear_edit() + self.quit_callback() + + def clear_edit(self): + self.sound_name_input.setText("") + for i in range(self.time_select_widget.widget_list.count()): + self.time_select_widget.widget_list.takeItem(0) + self.edit_n = None diff --git a/windows.py b/windows.py new file mode 100644 index 0000000..010341d --- /dev/null +++ b/windows.py @@ -0,0 +1,176 @@ +from PySide6 import QtWidgets, QtGui, QtCore +from widgets import BellStatusBox, ScheduleBox, DaysSelectBox, AdditionalSoundsBox, StatusBox, SoundFilesBox, SoundEditorBox +import sys + +VERSION = "0.2.0" + +class MainWindow(QtWidgets.QMainWindow): + def __init__(self, show_sound_diag): + super().__init__() + self.setWindowTitle("SBC - Головне вікно") + self.setWindowIcon(QtGui.QIcon("icon.png")) + self.setMinimumSize(800, 768) + self.main_widget = QtWidgets.QWidget() + self.main_layout = QtWidgets.QGridLayout(self.main_widget) + + self.bell_status_box = BellStatusBox() + self.schedule_box = ScheduleBox() + self.days_select_box = DaysSelectBox() + self.additional_sounds_box = AdditionalSoundsBox(show_sound_diag) + self.status_box = StatusBox() + self.sound_files_box = SoundFilesBox() + + self.main_layout.addWidget(self.bell_status_box, 0, 0, 3, 1) + self.main_layout.addWidget(self.schedule_box, 0, 1, 1, 1) + self.main_layout.addWidget(self.days_select_box, 1, 1, 1, 1) + self.main_layout.addWidget(self.additional_sounds_box, 2, 1, 1, 1) + self.main_layout.addWidget(self.status_box, 0, 2, 1, 1) + self.main_layout.addWidget(self.sound_files_box, 1, 2, 1, 1) + self.setCentralWidget(self.main_widget) + + def get_settings(self, config, CustomSound): + first_lesson_input = self.schedule_box.first_lesson_input.time() + + config.lessons_start = [first_lesson_input.hour(), first_lesson_input.minute()] + config.lesson_length = self.schedule_box.lesson_length_input.value() + config.break_length = self.schedule_box.break_length_input .value() + config.first_bell = self.schedule_box.first_bell_input .value() + config.num_lessons = self.schedule_box.num_lessons_input .value() + config.first_bell_before_first_lesson = self.schedule_box.first_bell_before_first_lesson_checkbox.isChecked() + config.workdays = [checkbox.isChecked() for checkbox in self.days_select_box.days_checkboxes] + config.sound_files = [ + self.sound_files_box.first_bell_file_text .text(), + self.sound_files_box.second_bell_file_text .text(), + self.sound_files_box.break_file_text .text() + ] + config.custom_sounds = [CustomSound(*sound) for sound in self.additional_sounds_box.sounds] + + def set_settings(self, config): + self.schedule_box.first_lesson_input .setTime (QtCore.QTime(*config.lessons_start)) + self.schedule_box.lesson_length_input.setValue(config.lesson_length) + self.schedule_box.break_length_input .setValue(config.break_length) + self.schedule_box.first_bell_input .setValue(config.first_bell) + self.schedule_box.num_lessons_input .setValue(config.num_lessons) + self.schedule_box.first_bell_before_first_lesson_checkbox.setChecked(config.first_bell_before_first_lesson) + for i in range(0, 7): self.days_select_box.days_checkboxes[i].setChecked(config.workdays[i]) + self.sound_files_box.first_bell_file_text .setText(config.sound_files[0]) + self.sound_files_box.second_bell_file_text .setText(config.sound_files[1]) + self.sound_files_box.break_file_text .setText(config.sound_files[2]) + self.additional_sounds_box.sounds = [[sound.name, sound.times, sound.sound_file] for sound in config.custom_sounds] + for sound in config.custom_sounds: self.additional_sounds_box.sound_list.add_item(QtWidgets.QLabel(sound.name)) + + def set_schedule(self, bells): + self.bell_status_box.bells_list.clear() + for bell in bells: + self.bell_status_box.bells_list.addItem(bell) + + def select_bell(self, bell_n): + self.bell_status_box.bells_list.setCurrentItem(self.bell_status_box.bells_list.item(bell_n)) + +class MenuActions: + def __init__(self, window, callback): + self.window = window + self.callback = callback + self.button_apply = QtGui.QAction(QtGui.QIcon("apply.png"), "Застосувати", window) + self.button_save = QtGui.QAction(QtGui.QIcon("save.png"), "Зберегти налаштування", window) + self.button_start = QtGui.QAction(QtGui.QIcon("start.png"), "Запустити дзвоники", window) + self.button_stop = QtGui.QAction(QtGui.QIcon("stop.png"), "Зупинити все", window) + self.button_about = QtGui.QAction( "Про програму", window) + + self.button_start.setEnabled(False) + + self.button_apply.triggered.connect(lambda: self.handle_button(0)) + self.button_save .triggered.connect(lambda: self.handle_button(1)) + self.button_start.triggered.connect(lambda: self.handle_button(2)) + self.button_stop .triggered.connect(lambda: self.handle_button(3)) + self.button_about.triggered.connect(lambda: self.handle_button(4)) + + def handle_button(self, button): + match button: + case 0 | 1 | 2 | 3: + self.callback(button) + match button: + case 0: QtWidgets.QMessageBox.information(self.window, "SBC - Інформація", "Налаштування застосовано!") + case 1: QtWidgets.QMessageBox.information(self.window, "SBC - Інформація", "Налаштування збережено!") + case 2: + self.button_start.setEnabled(False) + self.button_stop .setEnabled(True) + QtWidgets.QMessageBox.information(self.window, "SBC - Інформація", "Дзвінки запущено. Якщо ви налаштували щось не так, самі винні!") + case 3: + self.button_start.setEnabled(True) + self.button_stop .setEnabled(False) + QtWidgets.QMessageBox.information(self.window, "SBC - Інформація", "Дзвінки зупинено. Щось пішло не так, еге ж? Піди і виправи це негайно!") + case 4: QtWidgets.QMessageBox.information(self.window, "SBC - Про програму", \ + f"SBC {VERSION}\nАвтор: 2o\nTelegram: @xfdtw\nDiscord: @2o___\nЯкщо щось не зрозуміло/не працює пишіть туди.") + +class ToolBar(QtWidgets.QToolBar): + def __init__(self, window, menu_actions): + super().__init__("Toolbar") + window.addToolBar(self) + self.setIconSize(QtCore.QSize(16, 16)) + + self.addAction(menu_actions.button_apply) + self.addAction(menu_actions.button_save) + self.addSeparator() + self.addAction(menu_actions.button_start) + self.addAction(menu_actions.button_stop) + +class MenuBar(QtWidgets.QMenuBar): + def __init__(self, window, menu_actions): + super().__init__() + window.setMenuBar(self) + self.settings_menu = self.addMenu("&Налаштування") + self.bells_menu = self.addMenu("&Дзвінки") + self.help_menu = self.addMenu("Д&опомога") + + self.settings_menu.addAction(menu_actions.button_apply) + self.settings_menu.addAction(menu_actions.button_save) + self.bells_menu.addAction(menu_actions.button_start) + self.bells_menu.addAction(menu_actions.button_stop) + self.help_menu.addAction(menu_actions.button_about) + +class Tray(QtWidgets.QSystemTrayIcon): + def __init__(self, window): + super().__init__() + self.window = window + self.setIcon(QtGui.QIcon("icon.png")) + self.setVisible(True) + + self.menu = QtWidgets.QMenu(self.window) + open_window = QtGui.QAction("Показати головне вікно", self) + close_all = QtGui.QAction("Вийти", self) + open_window.triggered.connect(self.window.show) + close_all.triggered.connect(self.close_all_diag) + self.menu.addAction(open_window) + self.menu.addAction(close_all) + self.setContextMenu(self.menu) + + def close_all_diag(self): + if QtWidgets.QMessageBox.question(self.window, "SBC - Вихід", \ + "Ви дійсно хочете вийти з програми? Після цього дітки кричатимуть чого в них уроки по 5 годин...", \ + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) == QtWidgets.QMessageBox.StandardButton.Yes: + sys.exit() + +class SoundEditorWindow(QtWidgets.QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("SBC - Редактор звуків") + self.setWindowIcon(QtGui.QIcon("icon.png")) + self.setMinimumSize(256, 384) + + def set_add_sound_callback(self, add_sound_callback): + self.sound_editor_box = SoundEditorBox(add_sound_callback, self.hide) + self.setCentralWidget(self.sound_editor_box) + + def show(self, edit_n=None, name="", times=[], sound_file=""): + self.sound_editor_box.edit_n = edit_n + self.sound_editor_box.sound_name_input.setText(name) + for sound_time in times: + self.sound_editor_box.time_select_widget \ + .add_item(QtWidgets.QTimeEdit(QtCore.QTime(*sound_time))) + self.sound_editor_box.file_name_select_text.setText(sound_file) + super().show() + + def closeEvent(self, *args, **kwargs): + self.sound_editor_box.clear_edit() + super().closeEvent(*args, **kwargs)