From 56d3052d927296c257083658c713d8dc54b99599 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 26 Dec 2025 19:33:30 +0000 Subject: [PATCH 1/5] feat: Add dark mode support and fix log window legibility - Implement automatic system theme detection (Light/Dark mode) - Refactor hardcoded stylesheets to dynamic `apply_theme` method - Fix log context window contrast issues in Dark Mode - Ensure banners and dynamic widgets respect the current theme - Add `QEvent` import for palette change detection - Fix linting issues (bare excepts and f-string usage) --- pdf2mp3_gui.py | 217 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 167 insertions(+), 50 deletions(-) diff --git a/pdf2mp3_gui.py b/pdf2mp3_gui.py index 2000442..2de66a9 100644 --- a/pdf2mp3_gui.py +++ b/pdf2mp3_gui.py @@ -36,7 +36,7 @@ def get_resource_path(filename): try: import edge_tts from pypdf import PdfReader - from PyQt6.QtCore import Qt, QThread, pyqtSignal + from PyQt6.QtCore import Qt, QThread, pyqtSignal, QEvent from PyQt6.QtGui import QDragEnterEvent, QDropEvent, QPixmap try: @@ -60,7 +60,7 @@ def get_resource_path(filename): ) except ImportError as e: missing = str(e).split("'")[1] if "'" in str(e) else "dependencies" - print(f"Installing required dependencies...") + print("Installing required dependencies...") packages = ["edge-tts", "pypdf", "PyQt6"] subprocess.check_call([sys.executable, "-m", "pip", "install"] + packages) print("Dependencies installed. Please run the script again.\n") @@ -317,13 +317,142 @@ def __init__(self): self.app_download_url = "" self.init_ui() + def get_theme_colors(self): + """Get color palette based on system theme.""" + app = QApplication.instance() + is_dark = app.styleHints().colorScheme() == Qt.ColorScheme.Dark + + if is_dark: + return { + "window_bg": "#1e1e1e", + "subtitle_color": "#aaaaaa", + "drop_bg": "#2d2d2d", + "drop_border": "#00BCD4", + "drop_text": "#e0e0e0", + "log_bg": "#121212", + "log_text": "#e0e0e0", + "log_border": "#333333", + "banner_update_bg": "#0D47A1", + "banner_update_border": "#00BCD4", + "banner_app_bg": "#1B5E20", + "banner_app_border": "#4CAF50", + "dismiss_btn": "#aaaaaa", + "dismiss_btn_hover": "#ffffff", + } + else: + return { + "window_bg": "#f0f0f0", + "subtitle_color": "#666", + "drop_bg": "#f5f5f5", + "drop_border": "#00BCD4", + "drop_text": "#000000", + "log_bg": "#ffffff", + "log_text": "#000000", + "log_border": "#cccccc", + "banner_update_bg": "#E3F2FD", + "banner_update_border": "#00BCD4", + "banner_app_bg": "#E8F5E9", + "banner_app_border": "#4CAF50", + "dismiss_btn": "#666", + "dismiss_btn_hover": "#000", + } + + def apply_theme(self): + """Apply current theme colors to all widgets.""" + colors = self.get_theme_colors() + + # Main Window + if self.centralWidget(): + self.centralWidget().setStyleSheet(f"background-color: {colors['window_bg']};") + + # Subtitle + if hasattr(self, "subtitle_label"): + self.subtitle_label.setStyleSheet(f""" + QLabel {{ + font-size: 14px; + color: {colors['subtitle_color']}; + padding-bottom: 15px; + padding-top: 5px; + }} + """) + + # Drop Label + if hasattr(self, "drop_label"): + self.drop_label.setStyleSheet(f""" + QLabel {{ + border: 3px dashed {colors['drop_border']}; + border-radius: 10px; + padding: 50px; + font-size: 18px; + background-color: {colors['drop_bg']}; + color: {colors['drop_text']}; + }} + """) + + # Log Window + if hasattr(self, "status_text"): + self.status_text.setStyleSheet(f""" + QTextEdit {{ + background-color: {colors['log_bg']}; + color: {colors['log_text']}; + border: 1px solid {colors['log_border']}; + border-radius: 4px; + }} + """) + + # Update Banners if they exist + if self.update_banner: + self.update_banner_style(self.update_banner, colors, "update") + if self.app_update_banner: + self.update_banner_style(self.app_update_banner, colors, "app") + + def update_banner_style(self, banner, colors, type_): + """Helper to update banner styles.""" + bg = colors[f"banner_{type_}_bg"] + border = colors[f"banner_{type_}_border"] + + # Apply style to the banner container + banner.setStyleSheet(f""" + QWidget {{ + background-color: {bg}; + border: 1px solid {border}; + border-radius: 5px; + }} + """) + + # Update dismiss button color + for btn in banner.findChildren(QPushButton): + if btn.text() == "Γ—": + btn.setStyleSheet(f""" + QPushButton {{ + background: transparent; + border: none; + font-size: 20px; + font-weight: bold; + color: {colors['dismiss_btn']}; + padding: 0px 5px; + }} + QPushButton:hover {{ + color: {colors['dismiss_btn_hover']}; + }} + """) + # Ensure update/download buttons keep their specific styling if needed, + # but they usually have their own inline style. + # We should verify if apply_theme overwrites them. + # The banner.setStyleSheet might affect children if using QWidget selector without ID. + # The current implementation sets style on the banner widget itself. + + def changeEvent(self, event): + if event.type() == QEvent.Type.PaletteChange: + self.apply_theme() + super().changeEvent(event) + def init_ui(self): self.setWindowTitle("TextWave") self.setGeometry(100, 100, 700, 600) # Central widget central_widget = QWidget() - central_widget.setStyleSheet("background-color: #f0f0f0;") self.setCentralWidget(central_widget) layout = QVBoxLayout() central_widget.setLayout(layout) @@ -351,32 +480,15 @@ def init_ui(self): layout.addWidget(logo_label) # Subtitle (logo already contains "TextWave" text) - subtitle_label = QLabel("Convert PDFs to MP3 Audio") - subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - subtitle_label.setStyleSheet(""" - QLabel { - font-size: 14px; - color: #666; - padding-bottom: 15px; - padding-top: 5px; - } - """) - layout.addWidget(subtitle_label) + self.subtitle_label = QLabel("Convert PDFs to MP3 Audio") + self.subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.subtitle_label) # Drop area label self.drop_label = QLabel( "πŸ“„ Drag & Drop PDF Here\n\nor click 'Select PDF' below" ) self.drop_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.drop_label.setStyleSheet(""" - QLabel { - border: 3px dashed #00BCD4; - border-radius: 10px; - padding: 50px; - font-size: 18px; - background-color: #f5f5f5; - } - """) self.drop_label.setAcceptDrops(True) self.drop_label.dragEnterEvent = self.drag_enter_event self.drop_label.dropEvent = self.drop_event @@ -432,16 +544,20 @@ def init_ui(self): self.app_update_check_thread.finished.connect(self.app_update_check_complete) self.app_update_check_thread.start() + # Apply initial theme + self.apply_theme() + def create_update_banner(self): """Create the update notification banner widget.""" + colors = self.get_theme_colors() banner = QWidget() - banner.setStyleSheet(""" - QWidget { - background-color: #E3F2FD; - border: 1px solid #00BCD4; + banner.setStyleSheet(f""" + QWidget {{ + background-color: {colors['banner_update_bg']}; + border: 1px solid {colors['banner_update_border']}; border-radius: 5px; padding: 10px; - } + }} """) banner_layout = QHBoxLayout() @@ -487,18 +603,18 @@ def create_update_banner(self): # Dismiss button dismiss_btn = QPushButton("Γ—") - dismiss_btn.setStyleSheet(""" - QPushButton { + dismiss_btn.setStyleSheet(f""" + QPushButton {{ background: transparent; border: none; font-size: 20px; font-weight: bold; - color: #666; + color: {colors['dismiss_btn']}; padding: 0px 5px; - } - QPushButton:hover { - color: #000; - } + }} + QPushButton:hover {{ + color: {colors['dismiss_btn_hover']}; + }} """) dismiss_btn.clicked.connect(self.dismiss_update_banner) banner_layout.addWidget(dismiss_btn) @@ -592,14 +708,15 @@ def version_check_complete(self, has_update, installed_version, latest_version): def create_app_update_banner(self): """Create the app update notification banner widget.""" + colors = self.get_theme_colors() banner = QWidget() - banner.setStyleSheet(""" - QWidget { - background-color: #E8F5E9; - border: 1px solid #4CAF50; + banner.setStyleSheet(f""" + QWidget {{ + background-color: {colors['banner_app_bg']}; + border: 1px solid {colors['banner_app_border']}; border-radius: 5px; padding: 10px; - } + }} """) banner_layout = QHBoxLayout() @@ -642,18 +759,18 @@ def create_app_update_banner(self): # Dismiss button dismiss_btn = QPushButton("Γ—") - dismiss_btn.setStyleSheet(""" - QPushButton { + dismiss_btn.setStyleSheet(f""" + QPushButton {{ background: transparent; border: none; font-size: 20px; font-weight: bold; - color: #666; + color: {colors['dismiss_btn']}; padding: 0px 5px; - } - QPushButton:hover { - color: #000; - } + }} + QPushButton:hover {{ + color: {colors['dismiss_btn_hover']}; + }} """) dismiss_btn.clicked.connect(self.dismiss_app_update_banner) banner_layout.addWidget(dismiss_btn) @@ -788,7 +905,7 @@ def closeEvent(self, event): if hasattr(self, "version_check_thread") and self.version_check_thread: try: self.version_check_thread.finished.disconnect() - except: + except Exception: pass if self.version_check_thread.isRunning(): self.version_check_thread.requestInterruption() @@ -802,7 +919,7 @@ def closeEvent(self, event): ): try: self.app_update_check_thread.finished.disconnect() - except: + except Exception: pass if self.app_update_check_thread.isRunning(): self.app_update_check_thread.requestInterruption() @@ -814,7 +931,7 @@ def closeEvent(self, event): try: self.update_thread.status.disconnect() self.update_thread.finished.disconnect() - except: + except Exception: pass if self.update_thread.isRunning(): self.update_thread.requestInterruption() From af7ce6507606915ea16bbcba076498828d65be18 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 26 Dec 2025 19:42:17 +0000 Subject: [PATCH 2/5] feat: Add persistence and smart defaults for file dialogs - Initialize `QSettings` in `PDF2MP3App` to store application state. - Update `select_pdf` to default to `~/Downloads` initially, and persist the last used directory for subsequent opens. - Update `convert` to default the Save MP3 dialog to the input PDF's directory, ensuring a writable default location. - This resolves issues with restricted permissions in bundled apps and improves UX. --- pdf2mp3_gui.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pdf2mp3_gui.py b/pdf2mp3_gui.py index 2000442..e91e4aa 100644 --- a/pdf2mp3_gui.py +++ b/pdf2mp3_gui.py @@ -36,7 +36,7 @@ def get_resource_path(filename): try: import edge_tts from pypdf import PdfReader - from PyQt6.QtCore import Qt, QThread, pyqtSignal + from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings from PyQt6.QtGui import QDragEnterEvent, QDropEvent, QPixmap try: @@ -305,6 +305,7 @@ def stop_caffeinate(self): class PDF2MP3App(QMainWindow): def __init__(self): super().__init__() + self.settings = QSettings("TextWave", "PDF2MP3") self.pdf_path = None self.update_banner = None self.update_dismissed = False @@ -708,10 +709,17 @@ def drop_event(self, event: QDropEvent): self.set_pdf(files[0]) def select_pdf(self): + # Get last input directory or default to Downloads + default_dir = self.settings.value( + "last_input_dir", str(Path.home() / "Downloads") + ) + file_path, _ = QFileDialog.getOpenFileName( - self, "Select PDF File", "", "PDF Files (*.pdf)" + self, "Select PDF File", default_dir, "PDF Files (*.pdf)" ) if file_path: + # Save the directory for next time + self.settings.setValue("last_input_dir", str(Path(file_path).parent)) self.set_pdf(file_path) def set_pdf(self, path): @@ -728,9 +736,12 @@ def convert(self): return # Ask where to save - default_name = Path(self.pdf_path).stem + ".mp3" + # Default to the same folder as the input PDF + default_path = Path(self.pdf_path).parent / ( + Path(self.pdf_path).stem + ".mp3" + ) output_path, _ = QFileDialog.getSaveFileName( - self, "Save MP3 As", default_name, "MP3 Files (*.mp3)" + self, "Save MP3 As", str(default_path), "MP3 Files (*.mp3)" ) if not output_path: From 02dda64a4bc3b77f679b67ff80a7f89776085a4b Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Fri, 26 Dec 2025 15:09:54 -0600 Subject: [PATCH 3/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b42115..a641800 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## πŸ—ΊοΈ Roadmap -- [ ] App signing and notarization +- [X] App signing and notarization - [ ] Custom regex support for text removal (i.e., Page 21) - [ ] Support for additional TTS voices - [ ] Support for other TTS providers From 3947f2d9eae2accc21aac82e946e0930a5d1d8d7 Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Fri, 26 Dec 2025 15:53:26 -0600 Subject: [PATCH 4/5] Improve DMG UX --- .github/assets/dmg-background.png | Bin 0 -> 1571 bytes .github/assets/dmg-background.tiff | Bin 0 -> 28464 bytes .github/assets/dmg-background@2x.png | Bin 0 -> 4412 bytes .github/scripts/create-dmg-background.py | 159 +++++++++++++++++++++++ .github/scripts/create-dmg-background.sh | 39 ++++++ .github/scripts/create-dmg.sh | 36 +++-- .github/workflows/release.yml | 9 ++ 7 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 .github/assets/dmg-background.png create mode 100644 .github/assets/dmg-background.tiff create mode 100644 .github/assets/dmg-background@2x.png create mode 100644 .github/scripts/create-dmg-background.py create mode 100755 .github/scripts/create-dmg-background.sh diff --git a/.github/assets/dmg-background.png b/.github/assets/dmg-background.png new file mode 100644 index 0000000000000000000000000000000000000000..4425ffbc525f290ed5b243720f059cd323d09b38 GIT binary patch literal 1571 zcmeAS@N?(olHy`uVBq!ia0y~yV2WU1V4T3g1Qhuj-g%yZf%TQAi(^Q|t+#g!vzP)U zS}!i&W6HsH$8%Fxis;F*M_MJKyF|DT9?M*l-?fs7lV#%b8RdSde-D2sJUH{_(;`iV z3#(@x5oLIB?G7h{fD`pm##*&PRt6m!m<(}k#)2VX{)=1B)4uP#xs#bg@x(%zCIP4Q zVgB5fEQ&4nqBYtTS`^xgIGi|&K+Fc9EE9-#LeV5-@(!jg-?ksm+Zr<^i0}C28;AP0 zi%)0n7Wft2e)?#{k*{U@@4b^{X%To5z~{)}$oEtLiMh^Py6vFoi-Svo4B6ha0d@a4 zZ0~(giRJYLE0%77y2uvu`ANblvQ4fW*A|w-Y)5hkgn2@dL-B(%pR1rE%dT?^vlUt# z%0Whb+Z+D=Wn?MS3**M8zptln5m02Y@>SqaRA>bHfCcCSA)tvKP=OEq%7O3xs_lk| ze+Rp3!OV}zY34C4LL6d>5se_#LL4#Thg8-v@J~PR{rdBuuIJ)9PA^Ov8w8qIm_V`| ziXIBUkisT#C`DTXXz>M2&X$)m=AKUvXIKzjtj^%T!AL54;CKb-ga%g*1}EAe-l1&% k+M(=tV1y3sz^`F6Nt7%)7r^-oSokw|y85}Sb4q9e0H1O=rvLx| literal 0 HcmV?d00001 diff --git a/.github/assets/dmg-background.tiff b/.github/assets/dmg-background.tiff new file mode 100644 index 0000000000000000000000000000000000000000..e00cdd5765f0d23a5cba654fdd50c279a1638c2c GIT binary patch literal 28464 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 /dev/null; then echo "Using create-dmg..." - create-dmg \ - --volname "TextWave" \ + + # Build create-dmg command with background if available + CREATE_DMG_CMD="create-dmg \ + --volname \"TextWave\" \ --window-pos 200 120 \ --window-size 600 400 \ --icon-size 100 \ - --icon "TextWave.app" 175 120 \ - --hide-extension "TextWave.app" \ - --app-drop-link 425 120 \ - "TextWave-${VERSION}.dmg" \ - "dist/TextWave.app" \ - || true # create-dmg sometimes exits with error even on success + --icon \"TextWave.app\" 175 120 \ + --hide-extension \"TextWave.app\" \ + --app-drop-link 425 120" + + # Add background parameter if image exists + if [ -f "$BACKGROUND_IMAGE" ]; then + echo "Using custom background: $BACKGROUND_IMAGE" + CREATE_DMG_CMD="$CREATE_DMG_CMD --background \"$BACKGROUND_IMAGE\"" + else + echo "No custom background found, using default" + fi + + # Complete the command + CREATE_DMG_CMD="$CREATE_DMG_CMD \"TextWave-${VERSION}.dmg\" \"dist/TextWave.app\"" + + # Execute + eval $CREATE_DMG_CMD || true # create-dmg sometimes exits with error even on success fi # If create-dmg isn't available or failed, use hdiutil diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a3c686..5860d0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,6 +54,15 @@ jobs: chmod +x .github/scripts/build-app.sh .github/scripts/build-app.sh + - name: Generate DMG background image + if: steps.check_tag.outputs.exists == 'false' + run: | + mkdir -p .github/assets + python3 .github/scripts/create-dmg-background.py + chmod +x .github/scripts/create-dmg-background.sh + .github/scripts/create-dmg-background.sh + ls -lh .github/assets/ + - name: Code sign and notarize app if: steps.check_tag.outputs.exists == 'false' env: From 7535f05718fde0dd3a999feb22e1fdad75a6a20c Mon Sep 17 00:00:00 2001 From: Josh Roskos Date: Fri, 26 Dec 2025 15:56:28 -0600 Subject: [PATCH 5/5] Fix retaining output dir --- pdf2mp3_gui.py | 56 +++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/pdf2mp3_gui.py b/pdf2mp3_gui.py index 0d4b986..7e429d4 100644 --- a/pdf2mp3_gui.py +++ b/pdf2mp3_gui.py @@ -36,7 +36,7 @@ def get_resource_path(filename): try: import edge_tts from pypdf import PdfReader - from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings, QEvent + from PyQt6.QtCore import QEvent, QSettings, Qt, QThread, pyqtSignal from PyQt6.QtGui import QDragEnterEvent, QDropEvent, QPixmap try: @@ -364,14 +364,16 @@ def apply_theme(self): # Main Window if self.centralWidget(): - self.centralWidget().setStyleSheet(f"background-color: {colors['window_bg']};") + self.centralWidget().setStyleSheet( + f"background-color: {colors['window_bg']};" + ) # Subtitle if hasattr(self, "subtitle_label"): self.subtitle_label.setStyleSheet(f""" QLabel {{ font-size: 14px; - color: {colors['subtitle_color']}; + color: {colors["subtitle_color"]}; padding-bottom: 15px; padding-top: 5px; }} @@ -381,12 +383,12 @@ def apply_theme(self): if hasattr(self, "drop_label"): self.drop_label.setStyleSheet(f""" QLabel {{ - border: 3px dashed {colors['drop_border']}; + border: 3px dashed {colors["drop_border"]}; border-radius: 10px; padding: 50px; font-size: 18px; - background-color: {colors['drop_bg']}; - color: {colors['drop_text']}; + background-color: {colors["drop_bg"]}; + color: {colors["drop_text"]}; }} """) @@ -394,9 +396,9 @@ def apply_theme(self): if hasattr(self, "status_text"): self.status_text.setStyleSheet(f""" QTextEdit {{ - background-color: {colors['log_bg']}; - color: {colors['log_text']}; - border: 1px solid {colors['log_border']}; + background-color: {colors["log_bg"]}; + color: {colors["log_text"]}; + border: 1px solid {colors["log_border"]}; border-radius: 4px; }} """) @@ -430,11 +432,11 @@ def update_banner_style(self, banner, colors, type_): border: none; font-size: 20px; font-weight: bold; - color: {colors['dismiss_btn']}; + color: {colors["dismiss_btn"]}; padding: 0px 5px; }} QPushButton:hover {{ - color: {colors['dismiss_btn_hover']}; + color: {colors["dismiss_btn_hover"]}; }} """) # Ensure update/download buttons keep their specific styling if needed, @@ -554,8 +556,8 @@ def create_update_banner(self): banner = QWidget() banner.setStyleSheet(f""" QWidget {{ - background-color: {colors['banner_update_bg']}; - border: 1px solid {colors['banner_update_border']}; + background-color: {colors["banner_update_bg"]}; + border: 1px solid {colors["banner_update_border"]}; border-radius: 5px; padding: 10px; }} @@ -610,11 +612,11 @@ def create_update_banner(self): border: none; font-size: 20px; font-weight: bold; - color: {colors['dismiss_btn']}; + color: {colors["dismiss_btn"]}; padding: 0px 5px; }} QPushButton:hover {{ - color: {colors['dismiss_btn_hover']}; + color: {colors["dismiss_btn_hover"]}; }} """) dismiss_btn.clicked.connect(self.dismiss_update_banner) @@ -713,8 +715,8 @@ def create_app_update_banner(self): banner = QWidget() banner.setStyleSheet(f""" QWidget {{ - background-color: {colors['banner_app_bg']}; - border: 1px solid {colors['banner_app_border']}; + background-color: {colors["banner_app_bg"]}; + border: 1px solid {colors["banner_app_border"]}; border-radius: 5px; padding: 10px; }} @@ -766,11 +768,11 @@ def create_app_update_banner(self): border: none; font-size: 20px; font-weight: bold; - color: {colors['dismiss_btn']}; + color: {colors["dismiss_btn"]}; padding: 0px 5px; }} QPushButton:hover {{ - color: {colors['dismiss_btn_hover']}; + color: {colors["dismiss_btn_hover"]}; }} """) dismiss_btn.clicked.connect(self.dismiss_app_update_banner) @@ -853,10 +855,15 @@ def convert(self): return # Ask where to save - # Default to the same folder as the input PDF - default_path = Path(self.pdf_path).parent / ( - Path(self.pdf_path).stem + ".mp3" - ) + # Use last output directory if available, otherwise use input PDF's directory + last_output_dir = self.settings.value("last_output_dir", None) + if last_output_dir: + default_path = Path(last_output_dir) / (Path(self.pdf_path).stem + ".mp3") + else: + default_path = Path(self.pdf_path).parent / ( + Path(self.pdf_path).stem + ".mp3" + ) + output_path, _ = QFileDialog.getSaveFileName( self, "Save MP3 As", str(default_path), "MP3 Files (*.mp3)" ) @@ -864,6 +871,9 @@ def convert(self): if not output_path: return + # Save the output directory for next time + self.settings.setValue("last_output_dir", str(Path(output_path).parent)) + # Disable UI during conversion self.convert_btn.setEnabled(False) self.select_btn.setEnabled(False)