From e6e7180b734e05acc66b17e1c57bcd2aa1688933 Mon Sep 17 00:00:00 2001 From: mahmoud atef <109464635+MahmoudAtefFarook@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:25:15 +0200 Subject: [PATCH 1/5] Remove duplication in the method of displaying a random message. --- ui/dialogs/info_dialog.py | 33 ++++++++++++++++++++++----------- ui/quran_interface.py | 18 ++++-------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/ui/dialogs/info_dialog.py b/ui/dialogs/info_dialog.py index c2501d1..29e7231 100644 --- a/ui/dialogs/info_dialog.py +++ b/ui/dialogs/info_dialog.py @@ -1,12 +1,14 @@ import sys import json import random -from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QTextEdit, QPushButton, QApplication +from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QTextEdit, QPushButton, QApplication, QMessageBox from PyQt6.QtCore import QTimer from PyQt6.QtGui import QKeySequence, QClipboard from ui.widgets.qText_edit import ReadOnlyTextEdit from utils.universal_speech import UniversalSpeech from utils.const import Globals, data_folder +from exceptions.json import JSONFileNotFoundError, InvalidJSONFormatError +from exceptions.error_decorators import exception_handler class InfoDialog(QDialog): def __init__(self, parent, title: str, label: str, text: str, is_html_content: bool = False, show_message_button: bool = False): @@ -44,7 +46,7 @@ def init_ui(self): # Message to you button (conditionally added) message_to_you_button = QPushButton('رسالة لك', self) - message_to_you_button.clicked.connect(self.message_to_you) + message_to_you_button.clicked.connect(self.OnNewMessage) message_to_you_button.setShortcut(QKeySequence("Ctrl+M")) message_to_you_button.setStyleSheet('background-color: red; color: white;') message_to_you_button.setVisible(self.show_message_button) @@ -82,14 +84,23 @@ def copy_text(self): def reject(self): Globals.effects_manager.play("clos") self.deleteLater() + + def choose_QuotesMessage(self): + file_path = data_folder/"quotes/QuotesMessages.json" + if not file_path.exists(): + raise JSONFileNotFoundError(file_path) + + try: + with open(file_path, "r", encoding="utf-8") as file: + quotes_list = json.load(file) + message = random.choice(quotes_list) + except json.JSONDecodeError as e: + raise InvalidJSONFormatError(file_path, e) + + self.text_edit.setText(message) + UniversalSpeech.say(message) - def message_to_you(self): - # Load the quotes and choose a random message - with open(data_folder/"quotes/QuotesMessages.json", "r", encoding="utf-8") as file: - quotes_list = json.load(file) - new_message = random.choice(quotes_list) - - # Update the text without opening a new dialog - self.text_edit.setText(new_message) - UniversalSpeech.say(new_message) + def OnNewMessage(self): + self.choose_QuotesMessage() Globals.effects_manager.play("message") + diff --git a/ui/quran_interface.py b/ui/quran_interface.py index d429b33..e63c2a7 100644 --- a/ui/quran_interface.py +++ b/ui/quran_interface.py @@ -38,7 +38,6 @@ from utils.const import program_name, program_icon, user_db_path, data_folder, Globals from utils.audio_player import bass, SoundEffectPlayer from exceptions.error_decorators import exception_handler -from exceptions.json import JSONFileNotFoundError, InvalidJSONFormatError class QuranInterface(QMainWindow): @@ -364,17 +363,8 @@ def closeEvent(self, event): self.tray_manager.hide_icon() bass.BASS_Free() - @exception_handler(ui_element=QMessageBox ) + @exception_handler(ui_element=QMessageBox) def OnRandomMessages(self, event): - file_path = data_folder/"quotes/QuotesMessages.json" - if not file_path.exists(): - raise JSONFileNotFoundError(file_path) - - try: - with open(file_path, "r", encoding="utf-8") as file: - quotes_list = json.load(file) - message = random.choice(quotes_list) - except json.JSONDecodeError as e: - raise InvalidJSONFormatError(file_path, e) - - InfoDialog(self, 'رسالة لك', '', message, is_html_content=False, show_message_button=True).exec() + info_dialog = InfoDialog(self, 'رسالة لك', '', "", is_html_content=False, show_message_button=True) + info_dialog.choose_QuotesMessage() + info_dialog.exec() From 65be69dc67fa29b394555a8d878388b86695025c Mon Sep 17 00:00:00 2001 From: mahmoud atef <109464635+MahmoudAtefFarook@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:43:43 +0200 Subject: [PATCH 2/5] Used exception_handler with showing info, tafaseer, search and quick_access dialog. --- ui/quran_interface.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ui/quran_interface.py b/ui/quran_interface.py index e63c2a7..e8773da 100644 --- a/ui/quran_interface.py +++ b/ui/quran_interface.py @@ -213,8 +213,9 @@ def set_text_ctrl_label(self): self.menu_bar.previous_action.setEnabled(back_status) self.toolbar.navigation.reset_position() self.toolbar.set_buttons_status() - - def OnQuickAccess(self): + + @exception_handler(ui_element=QMessageBox) + def OnQuickAccess(self, event): dialog = QuickAccess(self, "الوصول السريع") if not dialog.exec(): return @@ -222,7 +223,8 @@ def OnQuickAccess(self): self.set_text_ctrl_label() self.quran_view.setFocus() - def OnSearch(self): + @exception_handler(ui_element=QMessageBox) + def OnSearch(self, event): search_dialog = SearchDialog(self, "بحث") if search_dialog.exec(): self.set_text_ctrl_label() @@ -235,6 +237,7 @@ def get_current_ayah_info(self) -> list: return ayah_info + @exception_handler(ui_element=QMessageBox) def OnInterpretation(self): selected_category = self.sender().text() @@ -295,15 +298,16 @@ def on_copy_verse(self): UniversalSpeech.say("تم نسخ الآية.") Globals.effects_manager.play("copy") - - def OnSyntax(self): + @exception_handler(ui_element=QMessageBox) + def OnSyntax(self, event): aya_info = self.get_current_ayah_info() title = "إعراب آية رقم {} من {}".format(aya_info[3], aya_info[2]) label = "الإعراب" text = E3rab(aya_info[0], aya_info[1]).text InfoDialog(self, title, label, text).exec() - def OnVerseReasons(self): + @exception_handler(ui_element=QMessageBox) + def OnVerseReasons(self, event): aya_info = self.get_current_ayah_info() title = "أسباب نزول آية رقم {} من {}".format(aya_info[3], aya_info[2]) label = "الأسباب" @@ -314,14 +318,16 @@ def OnVerseReasons(self): else: QMessageBox.information(self, "لا يتوفر معلومات للآية", "للأسف لا يتوفر في الوقت الحالي معلومات لهذه الآية.") - def OnAyahInfo(self): + @exception_handler(ui_element=QMessageBox) + def OnAyahInfo(self, event): aya_info = self.get_current_ayah_info() title = "معلومات آية رقم {} من {}".format(aya_info[3], aya_info[2]) label = "معلومات الآية:" text = AyaInfo(aya_info[1]).text InfoDialog(self, title, label, text, is_html_content=True).exec() - - def OnSaveBookmark(self): + + @exception_handler(ui_element=QMessageBox) + def OnSaveBookmark(self, event): bookmark_manager= BookmarkManager() aya_info = self.get_current_ayah_info() From 4c6a5854b563f44ceb941ea3cec41838df05d100 Mon Sep 17 00:00:00 2001 From: mahmoud atef <109464635+MahmoudAtefFarook@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:48:54 +0200 Subject: [PATCH 3/5] Fixed a bug related to enable search button. --- ui/dialogs/find.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/dialogs/find.py b/ui/dialogs/find.py index 9442717..f587f54 100644 --- a/ui/dialogs/find.py +++ b/ui/dialogs/find.py @@ -131,6 +131,7 @@ def initUI(self): self.search_type_radio_quarter.toggled.connect(self.on_radio_toggled) self.on_radio_toggled() + self.OnEdit() def OnEdit(self): self.search_button.setEnabled(bool(self.search_box.text())) From b5f4afe1decefe0a6bfb1060671514f7dddb47ce Mon Sep 17 00:00:00 2001 From: mahmoud atef <109464635+MahmoudAtefFarook@users.noreply.github.com> Date: Tue, 10 Dec 2024 23:01:18 +0200 Subject: [PATCH 4/5] Allow to set start time greater than end time in athkar. --- core_functions/athkar/athkar_scheduler.py | 63 ++++++++++++++++------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/core_functions/athkar/athkar_scheduler.py b/core_functions/athkar/athkar_scheduler.py index 4af55ed..4e5fcc0 100644 --- a/core_functions/athkar/athkar_scheduler.py +++ b/core_functions/athkar/athkar_scheduler.py @@ -6,11 +6,13 @@ from random import choice from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger -from sqlalchemy.exc import IntegrityError +from sqlalchemy.exc import IntegrityError from .athkar_db_manager import AthkarDBManager from .athkar_refresher import AthkarRefresher from utils.audio_player import AthkarPlayer from utils.const import Globals +from utils.logger import Logger +from exceptions.base import ErrorMessage class AthkarScheduler: def __init__(self, athkar_db_folder: Union[Path, str], default_category_path: Optional[Union[Path, str]] = None, text_athkar_path: Optional[Union[Path, str]] = None, default_category_settings: Optional[Dict[str, int]] = None) -> None: @@ -19,11 +21,10 @@ def __init__(self, athkar_db_folder: Union[Path, str], default_category_path: Op self.default_category_settings = default_category_settings if default_category_settings is not None else {} self.categories = None self.db_manager = AthkarDBManager(athkar_db_folder) - self.scheduler = BackgroundScheduler() + self.scheduler = BackgroundScheduler() self.setup() def setup(self) -> None: - if self.default_category_path: self.default_category_path.mkdir(parents=True, exist_ok=True) try: @@ -31,7 +32,7 @@ def setup(self) -> None: if self.text_athkar_path and self.text_athkar_path.exists(): with open(self.text_athkar_path, encoding="UTF-8") as f: self.db_manager.add_text_athkar(json.load(f), category_id) - except IntegrityError as e: + except IntegrityError: pass self.categories = self.db_manager.get_all_categories() @@ -46,38 +47,62 @@ def audio_athkar_job(self, category_id: int, audio_path: str) -> None: def text_athkar_job(self, category_id: int) -> None: random_text_athkar = choice(self.db_manager.get_text_athkar(category_id)) text = random_text_athkar.text - + if len(text) > 256: - # Extract the first 5 words for the title title = " ".join(text.split()[:10]) description = " ".join(text.split()[10:]) else: title = "البيان" description = text - + Globals.TRAY_ICON.showMessage(title, description, msecs=5000) @staticmethod def _parse_time(time_str: str) -> time: - time_ob = datetime.strptime(time_str, "%H:%M") - return time_ob + return datetime.strptime(time_str, "%H:%M").time() - def start(self,) -> None: + def _create_triggers(self, from_time: time, to_time: time, play_interval: int): + minute = "0" if play_interval == 60 else f"*/{play_interval}" + + if from_time < to_time: + return [CronTrigger(minute=minute, hour=f"{from_time.hour}-{to_time.hour}")] + else: + return [ + CronTrigger(minute=minute, hour=f"{from_time.hour}-23"), + CronTrigger(minute=minute, hour=f"0-{to_time.hour}") + ] + + def _add_jobs(self, category, trigger): + if category.audio_athkar_enabled: + self.scheduler.add_job( + self.audio_athkar_job, + trigger, + args=[category.id, category.audio_path], + id=f"audio_athkar_{category.id}_{trigger}" + ) + if category.text_athkar_enabled: + self.scheduler.add_job( + self.text_athkar_job, + trigger, + args=[category.id], + id=f"text_athkar_{category.id}_{trigger}" + ) + + def start(self) -> None: for category in self.categories: - from_time = self._parse_time(category.from_time) - to_time = self._parse_time(category.to_time) - minute = "0" if category.play_interval == 60 else f"*/{category.play_interval}" - trigger = CronTrigger(minute=minute, hour=f"{from_time.hour}-{to_time.hour}") - if category.audio_athkar_enabled : - self.scheduler.add_job(self.audio_athkar_job, trigger, args=[category.id, category.audio_path]) - if category.text_athkar_enabled: - self.scheduler.add_job(self.text_athkar_job, trigger, args=[category.id]) + try: + from_time = self._parse_time(category.from_time) + to_time = self._parse_time(category.to_time) + triggers = self._create_triggers(from_time, to_time, category.play_interval) + for trigger in triggers: + self._add_jobs(category, trigger) + except Exception as e: + Logger.error(ErrorMessage(e)) if not self.scheduler.running: self.scheduler.start() def refresh(self) -> None: - if self.scheduler is not None: self.scheduler.remove_all_jobs() From a287bf9fb308d6c4a3f4d3aaa5c15d900a8505ff Mon Sep 17 00:00:00 2001 From: mahmoud atef <109464635+MahmoudAtefFarook@users.noreply.github.com> Date: Wed, 11 Dec 2024 01:29:49 +0200 Subject: [PATCH 5/5] Created set_position method. --- utils/audio_player/bass_player.py | 37 ++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/utils/audio_player/bass_player.py b/utils/audio_player/bass_player.py index ce5744f..7a1e56c 100644 --- a/utils/audio_player/bass_player.py +++ b/utils/audio_player/bass_player.py @@ -102,19 +102,40 @@ def decrease_volume(self, step: float = 0.01) -> None: new_volume = max(self.volume - step, 0.0) self.set_volume(new_volume) - def forward(self, seconds: float = 10) -> None: + def set_position(self, new_seconds: float) -> None: + """Sets the playback position to the specified number of seconds.""" + if self.current_channel: + new_seconds = max(0.0, new_seconds) + + duration = bass.BASS_ChannelBytes2Seconds(self.current_channel, bass.BASS_ChannelGetLength( self.current_channel, 0)) + new_seconds = min(new_seconds, duration) + new_position = bass.BASS_ChannelSeconds2Bytes(self.current_channel, new_seconds, 0) + bass.BASS_ChannelSetPosition(self.current_channel, new_position, 0) + + def forward(self, seconds: float = 0.5) -> None: """Forwards the audio playback by the specified number of seconds.""" if self.current_channel: - current_position = bass.BASS_ChannelGetPosition(self.current_channel) - new_position = current_position + seconds - bass.BASS_ChannelSetPosition(self.current_channel, new_position) + current_position = bass.BASS_ChannelGetPosition(self.current_channel, 0) + if current_position == -1: + print("Error getting current position.") + return + + current_seconds = bass.BASS_ChannelBytes2Seconds(self.current_channel, current_position) + new_seconds = current_seconds + seconds + new_seconds = 111 + self.set_position(new_seconds) - def rewind(self, seconds: float = 10) -> None: + def rewind(self, seconds: float = 0.5) -> None: """Rewinds the audio playback by the specified number of seconds.""" if self.current_channel: - current_position = bass.BASS_ChannelGetPosition(self.current_channel) - new_position = max(0.0, current_position - seconds) - bass.BASS_ChannelSetPosition(self.current_channel, new_position) + current_position = bass.BASS_ChannelGetPosition(self.current_channel, 0) + if current_position == -1: + print("Error getting current position.") + return + + current_seconds = bass.BASS_ChannelBytes2Seconds(self.current_channel, current_position) + new_seconds = current_seconds - seconds + self.set_position(new_seconds) def get_playback_status(self) -> PlaybackStatus: """Returns the current playback status."""