diff --git a/.github/workflows/snapcraft.yml b/.github/workflows/snapcraft.yml index a2c8c63c..d4f95dc7 100644 --- a/.github/workflows/snapcraft.yml +++ b/.github/workflows/snapcraft.yml @@ -66,9 +66,6 @@ jobs: run: | sudo -E snapcraft pack --verbose --destructive-mode echo "snap=$(ls *.snap)" >> $GITHUB_OUTPUT - - run: | - sudo apt-get update - sudo apt-get install libportaudio2 libtbb-dev - run: sudo snap install --devmode *.snap - run: | cd $HOME diff --git a/Makefile b/Makefile index 0888f589..6a316b53 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,11 @@ endif COVERAGE_THRESHOLD := 70 test: buzz/whisper_cpp +# A check to get updates of yt-dlp. Should run only on local as part of regular development operations +# Sort of a local "update checker" +ifndef CI + uv lock --upgrade-package yt-dlp +endif pytest -s -vv --cov=buzz --cov-report=xml --cov-report=html --benchmark-skip --cov-fail-under=${COVERAGE_THRESHOLD} --cov-config=.coveragerc benchmarks: buzz/whisper_cpp diff --git a/README.md b/README.md index 58327510..b8cb5e19 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,10 @@ pip3 install nvidia-cublas-cu12==12.9.1.4 nvidia-cuda-cupti-cu12==12.9.79 nvidia For info on how to get latest development version with latest features and bug fixes see [FAQ](https://chidiwilliams.github.io/buzz/docs/faq#9-where-can-i-get-latest-development-version). +### Support Buzz + +You can help the Buzz by starring 🌟 the repo and sharing it with your friends. + ### Screenshots
diff --git a/buzz/assets/update_FILL0_wght700_GRAD0_opsz48.svg b/buzz/assets/update_FILL0_wght700_GRAD0_opsz48.svg new file mode 100644 index 00000000..d01ac5a7 --- /dev/null +++ b/buzz/assets/update_FILL0_wght700_GRAD0_opsz48.svg @@ -0,0 +1 @@ + diff --git a/buzz/locale/ca_ES/LC_MESSAGES/buzz.po b/buzz/locale/ca_ES/LC_MESSAGES/buzz.po index 9e07929a..5189a603 100644 --- a/buzz/locale/ca_ES/LC_MESSAGES/buzz.po +++ b/buzz/locale/ca_ES/LC_MESSAGES/buzz.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: buzz\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:38+0200\n" "PO-Revision-Date: 2025-10-17 07:59+0200\n" "Last-Translator: Éric Duarte \n" "Language-Team: Catalan \n" @@ -345,7 +345,8 @@ msgid "Download failed" msgstr "Descàrrega fallida" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "Error" @@ -536,6 +537,10 @@ msgstr "Cancel·la la transcripció" msgid "Clear History" msgstr "Neteja l'historial" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "Actualització disponible" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "En progrés" @@ -730,6 +735,58 @@ msgstr "" "Comproveu els vostres dispositius d'àudio o els registres de l'aplicació per " "a més informació." +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "Hi ha una nova versió de Buzz disponible!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "Versió actual:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "Nova versió:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "Notes de la versió:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "Descarrega i instal·la" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "No hi ha cap URL de descàrrega disponible per a la vostra plataforma." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "S'està descarregant el fitxer {} de {}..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "S'està descarregant el fitxer {} de {} ({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "Descàrrega fallida" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "No s'ha pogut descarregar l'actualització: {}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "No s'ha pogut desar l'instal·lador: {}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "Descàrrega completada!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "No s'ha pogut executar l'instal·lador: {}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "Comprova si hi ha actualitzacions" diff --git a/buzz/locale/da_DK/LC_MESSAGES/buzz.po b/buzz/locale/da_DK/LC_MESSAGES/buzz.po index 50fd489f..7fbc6e9d 100644 --- a/buzz/locale/da_DK/LC_MESSAGES/buzz.po +++ b/buzz/locale/da_DK/LC_MESSAGES/buzz.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:38+0200\n" "PO-Revision-Date: \n" "Last-Translator: Ole Guldberg2 \n" "Language-Team: \n" @@ -341,7 +341,8 @@ msgid "Download failed" msgstr "Download mislykkedes" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "Fejl" @@ -531,6 +532,10 @@ msgstr "Afbryd transkription" msgid "Clear History" msgstr "Ryd historik" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "Opdatering tilgængelig" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "Arbejder" @@ -724,6 +729,58 @@ msgstr "" "Tjek venligst dine audioenheder eller tjek applikationens logs for " "mereinformation." +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "En ny version af Buzz er tilgængelig!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "Nuværende version:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "Ny version:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "Udgivelsesnoter:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "Download og installer" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "Ingen download-URL tilgængelig for din platform." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "Downloader fil {} af {}..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "Downloader fil {} af {} ({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "Download mislykkedes" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "Kunne ikke downloade opdateringen: {}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "Kunne ikke gemme installationsprogrammet: {}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "Download fuldført!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "Kunne ikke køre installationsprogrammet: {}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "Tjek for opdateringer" @@ -1564,4 +1621,6 @@ msgstr "Tilføj og ret" msgid "" "Speech extraction failed! Check your internet connection — a model may need " "to be downloaded." -msgstr "Taleoprydning mislykkedes! Kontroller din internetforbindelse — en model skal muligvis hentes ned." +msgstr "" +"Taleoprydning mislykkedes! Kontroller din internetforbindelse — en model " +"skal muligvis hentes ned." diff --git a/buzz/locale/de_DE/LC_MESSAGES/buzz.po b/buzz/locale/de_DE/LC_MESSAGES/buzz.po index bd2fce51..608cf26e 100644 --- a/buzz/locale/de_DE/LC_MESSAGES/buzz.po +++ b/buzz/locale/de_DE/LC_MESSAGES/buzz.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:38+0200\n" "PO-Revision-Date: 2025-03-05 14:41+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -345,7 +345,8 @@ msgid "Download failed" msgstr "Der Download ist fehlgeschlagen" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "Fehler" @@ -538,6 +539,10 @@ msgstr "Transkription abbrechen" msgid "Clear History" msgstr "Verlauf löschen" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "Update verfügbar" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "Im Gange" @@ -734,6 +739,58 @@ msgstr "" "Bitte überprüfen Sie Ihre Audiogeräte oder prüfen Sie die " "Anwendungsprotokolle für weitere Informationen." +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "Eine neue Version von Buzz ist verfügbar!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "Aktuelle Version:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "Neue Version:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "Versionshinweise:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "Herunterladen und installieren" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "Kein Download-Link für Ihre Plattform verfügbar." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "Datei {} von {} wird heruntergeladen..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "Datei {} von {} wird heruntergeladen ({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "Download fehlgeschlagen" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "Das Update konnte nicht heruntergeladen werden: {}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "Installer konnte nicht gespeichert werden: {}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "Download abgeschlossen!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "Installer konnte nicht ausgeführt werden: {}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "Nach Updates suchen" diff --git a/buzz/locale/en_US/LC_MESSAGES/buzz.po b/buzz/locale/en_US/LC_MESSAGES/buzz.po index 82e7802c..fae75bfe 100644 --- a/buzz/locale/en_US/LC_MESSAGES/buzz.po +++ b/buzz/locale/en_US/LC_MESSAGES/buzz.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:38+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -331,7 +331,8 @@ msgid "Download failed" msgstr "" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "" @@ -516,6 +517,10 @@ msgstr "" msgid "Clear History" msgstr "" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "" @@ -704,6 +709,58 @@ msgid "" "information." msgstr "" +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "" diff --git a/buzz/locale/es_ES/LC_MESSAGES/buzz.po b/buzz/locale/es_ES/LC_MESSAGES/buzz.po index 2cac032f..284ebc27 100644 --- a/buzz/locale/es_ES/LC_MESSAGES/buzz.po +++ b/buzz/locale/es_ES/LC_MESSAGES/buzz.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:38+0200\n" "PO-Revision-Date: 2025-09-08 12:43+0200\n" "Last-Translator: Éric Duarte \n" "Language-Team: \n" @@ -353,7 +353,8 @@ msgid "Download failed" msgstr "Descarga fallida" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "Error" @@ -563,6 +564,10 @@ msgstr "Cancelar transcripción" msgid "Clear History" msgstr "Vaciar historial" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "Actualización disponible" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "En Progreso" @@ -765,6 +770,58 @@ msgstr "" "Compruebe sus dispositivos de audio o consulte los registros de la " "aplicación para obtener más información." +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "¡Hay una nueva versión de Buzz disponible!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "Versión actual:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "Nueva versión:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "Notas de la versión:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "Descargar e instalar" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "No hay URL de descarga disponible para su plataforma." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "Descargando archivo {} de {}..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "Descargando archivo {} de {} ({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "Descarga fallida" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "Error al descargar la actualización: {}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "No se pudo guardar el instalador: {}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "¡Descarga completa!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "No se pudo ejecutar el instalador: {}" + # automatic translation #: buzz/widgets/about_dialog.py msgid "Check for updates" diff --git a/buzz/locale/it_IT/LC_MESSAGES/buzz.po b/buzz/locale/it_IT/LC_MESSAGES/buzz.po index 4520d331..078abf69 100644 --- a/buzz/locale/it_IT/LC_MESSAGES/buzz.po +++ b/buzz/locale/it_IT/LC_MESSAGES/buzz.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: buzz\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:38+0200\n" "PO-Revision-Date: 2026-01-25 21:42+0200\n" "Language-Team: (Italiano) Albano Battistella \n" "Language: it_IT\n" @@ -345,7 +345,8 @@ msgid "Download failed" msgstr "Download non riuscito" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "Errore" @@ -539,6 +540,10 @@ msgstr "Annulla trascrizione" msgid "Clear History" msgstr "Elimina la cronologia" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "Aggiornamento Disponibile" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "In corso" @@ -731,6 +736,58 @@ msgstr "" "Controlla i tuoi dispositivi audio o i registri dell'applicazione per " "maggiori informazioni." +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "È disponibile una nuova versione di Buzz!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "Versione attuale:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "Nuova versione:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "Note di rilascio:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "Scarica e installa" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "Nessun URL di download disponibile per la tua piattaforma." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "Download del file {} di {}..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "Download del file {} di {} ({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "Download fallito" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "Impossibile scaricare l'aggiornamento: {}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "Impossibile salvare il programma di installazione: {}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "Download completato!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "Impossibile eseguire il programma di installazione: {}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "Controlla gli aggiornamenti" diff --git a/buzz/locale/ja_JP/LC_MESSAGES/buzz.po b/buzz/locale/ja_JP/LC_MESSAGES/buzz.po index 366ff76d..2c28c743 100644 --- a/buzz/locale/ja_JP/LC_MESSAGES/buzz.po +++ b/buzz/locale/ja_JP/LC_MESSAGES/buzz.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:38+0200\n" "PO-Revision-Date: \n" "Last-Translator: nunawa <71294849+nunawa@users.noreply.github.com>\n" "Language-Team: \n" @@ -338,7 +338,8 @@ msgid "Download failed" msgstr "ダウンロード失敗" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "エラー" @@ -528,6 +529,10 @@ msgstr "文字起こしをキャンセルする" msgid "Clear History" msgstr "履歴を削除する" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "アップデートあり" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "進行中" @@ -720,6 +725,58 @@ msgstr "" "オーディオデバイスを確認するか、詳細をアプリケーションのログで確認してくださ" "い。" +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "Buzzの新しいバージョンが利用可能です!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "現在のバージョン:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "新しいバージョン:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "リリースノート:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "ダウンロードしてインストール" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "お使いのプラットフォーム向けのダウンロードURLがありません。" + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "ファイル {} / {} をダウンロード中..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "ファイル {} / {} をダウンロード中 ({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "ダウンロード失敗" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "アップデートのダウンロードに失敗しました: {}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "インストーラーの保存に失敗しました: {}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "ダウンロード完了!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "インストーラーの実行に失敗しました: {}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "アップデートを確認する" diff --git a/buzz/locale/lv_LV/LC_MESSAGES/buzz.po b/buzz/locale/lv_LV/LC_MESSAGES/buzz.po index ad5dd01c..648c5d21 100644 --- a/buzz/locale/lv_LV/LC_MESSAGES/buzz.po +++ b/buzz/locale/lv_LV/LC_MESSAGES/buzz.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" -"PO-Revision-Date: 2026-02-27 16:47+0200\n" +"POT-Creation-Date: 2026-02-28 10:38+0200\n" +"PO-Revision-Date: 2026-02-28 10:46+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: lv_LV\n" @@ -186,7 +186,7 @@ msgid "" "in the Live Recording screen in a future version." msgstr "" "Piezīme: Dzīvā ieraksta iestatījumi nākotnes Buzz versijās tiks pārvietoti " -"uz Papildu iestatījumu sadaļu Dzīvā ieraksta logā" +"uz Papildu iestatījumu sadaļu Dzīvā ieraksta logā." #: buzz/widgets/preferences_dialog/general_preferences_widget.py msgid "Use 8-bit quantization to reduce memory usage" @@ -346,7 +346,8 @@ msgid "Download failed" msgstr "Lejupielāde neizdevās" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "Kļūda" @@ -540,6 +541,10 @@ msgstr "Atcelt atpazīšanu" msgid "Clear History" msgstr "Notīrīt vēsturi" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "Pieejams atjauninājums" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "Apstrādā" @@ -731,6 +736,58 @@ msgstr "" "Lūdzu pārbaudiet savas audio ierīces vai pārbaudiet lietotnes ziņojumu " "žurnālus, lai iegūtu papildu informāciju." +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "Pieejama jauna Buzz versija!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "Instalētā versija:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "Jaunā versija:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "Jaunās versijas piezīmes:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "Lejupielādēt un instalēt" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "Jūsu datoram nav pieejama atjauninājum versija." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "Lejupielādē {} no {}..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "Lejupielādē {} no {} ({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "Lejupielāde neizdevās" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "Neizdevās lejupielādēt atjauninājumu: {}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "Neizdevās saglabāt atjauninājumu: {}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "Lejupielāde pabeigta!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "Neizdevās sākt atjauninājumu: {}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "Pārbaudīt atjauninājumus" diff --git a/buzz/locale/nl/LC_MESSAGES/buzz.po b/buzz/locale/nl/LC_MESSAGES/buzz.po index de2e2699..87994ef9 100644 --- a/buzz/locale/nl/LC_MESSAGES/buzz.po +++ b/buzz/locale/nl/LC_MESSAGES/buzz.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:38+0200\n" "PO-Revision-Date: 2025-03-20 18:30+0100\n" "Last-Translator: Heimen Stoffels \n" "Language-Team: none\n" @@ -347,7 +347,8 @@ msgid "Download failed" msgstr "Het downloaden is mislukt" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "Foutmelding" @@ -540,6 +541,10 @@ msgstr "Transcriptie wissen" msgid "Clear History" msgstr "Geschiedenis wissen" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "Update Beschikbaar" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "In behandeling" @@ -733,6 +738,58 @@ msgid "" "information." msgstr "Controleer uw geluidsapparatuur of het programmalogboek." +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "Er is een nieuwe versie van Buzz beschikbaar!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "Huidige versie:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "Versie:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "Release-opmerkingen:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "Downloaden en installeren" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "Geen download-URL beschikbaar voor uw platform." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "Bestand {} van {} downloaden..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "Bestand {} van {} downloaden ({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "Download mislukt" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "Het downloaden van de update is mislukt: {}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "Kan het installatieprogramma niet opslaan: {}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "Download voltooid!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "Kan het installatieprogramma niet uitvoeren: {}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "Controleren op updates" @@ -1602,6 +1659,3 @@ msgstr "" #~ msgid "Enter instructions for AI on how to translate..." #~ msgstr "Voer vertaalinstructies in…" - -#~ msgid "Version" -#~ msgstr "Versie" diff --git a/buzz/locale/pl_PL/LC_MESSAGES/buzz.po b/buzz/locale/pl_PL/LC_MESSAGES/buzz.po index bb74df3c..869180c4 100644 --- a/buzz/locale/pl_PL/LC_MESSAGES/buzz.po +++ b/buzz/locale/pl_PL/LC_MESSAGES/buzz.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:38+0200\n" "PO-Revision-Date: 2024-03-17 20:50+0200\n" "Last-Translator: \n" "Language-Team: \n" @@ -344,7 +344,8 @@ msgid "Download failed" msgstr "Pobieranie nie powiodło się" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "Błąd" @@ -534,6 +535,10 @@ msgstr "Anuluj transkrypcję" msgid "Clear History" msgstr "Wyczyść historię" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "Dostępna aktualizacja" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "W toku" @@ -729,6 +734,58 @@ msgstr "" "Sprawdź urządzenia audio lub przejrzyj logi aplikacji, by uzyskać więcej " "informacji." +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "Dostępna jest nowa wersja Buzz!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "Aktualna wersja:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "Nowa wersja:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "Informacje o wydaniu:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "Pobierz i zainstaluj" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "Brak adresu URL do pobrania dla Twojej platformy." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "Pobieranie pliku {} z {}..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "Pobieranie pliku {} z {} ({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "Pobieranie nie powiodło się" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "Nie udało się pobrać aktualizacji: {}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "Nie udało się zapisać instalatora: {}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "Pobieranie zakończone!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "Nie udało się uruchomić instalatora: {}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "Sprawdź aktualizacje" diff --git a/buzz/locale/pt_BR/LC_MESSAGES/buzz.po b/buzz/locale/pt_BR/LC_MESSAGES/buzz.po index f9b04add..3fbdeead 100644 --- a/buzz/locale/pt_BR/LC_MESSAGES/buzz.po +++ b/buzz/locale/pt_BR/LC_MESSAGES/buzz.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Buzz\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 09:07+0200\n" +"POT-Creation-Date: 2026-02-28 10:41+0200\n" "PO-Revision-Date: 2025-11-01 17:43-0300\n" "Last-Translator: Paulo Schopf \n" "Language-Team: none\n" @@ -343,7 +343,8 @@ msgid "Download failed" msgstr "Falha ao baixar" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "Erro" @@ -438,7 +439,8 @@ msgstr "Modelo de IA:" #: buzz/widgets/transcriber/advanced_settings_dialog.py msgid "Please translate each text sent to you from English to Spanish." -msgstr "Por favor, traduza cada texto enviado a você do Inglês para o Espanhol." +msgstr "" +"Por favor, traduza cada texto enviado a você do Inglês para o Espanhol." #: buzz/widgets/transcriber/advanced_settings_dialog.py msgid "Instructions for AI:" @@ -533,6 +535,10 @@ msgstr "Cancelar Transcrição" msgid "Clear History" msgstr "Limpar Histórico" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "Atualização Disponível" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "Em Progresso" @@ -626,12 +632,14 @@ msgid "" "Could not restart transcription: model not available and could not be " "downloaded." msgstr "" -"Não foi possível reiniciar a transcrição: o modelo não está disponível e " -"não pôde ser baixado." +"Não foi possível reiniciar a transcrição: o modelo não está disponível e não " +"pôde ser baixado." #: buzz/widgets/transcription_tasks_table_widget.py msgid "Could not restart transcription: transcriber worker not found." -msgstr "Não foi possível reiniciar a transcrição: trabalhador de transcrição não encontrado." +msgstr "" +"Não foi possível reiniciar a transcrição: trabalhador de transcrição não " +"encontrado." #: buzz/widgets/recording_transcriber_widget.py msgid "Live Recording" @@ -725,6 +733,58 @@ msgstr "" "Verifique seus dispositivos de áudio ou os logs do aplicativo para mais " "informações." +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "Uma nova versão do Buzz está disponível!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "Versão atual:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "Nova versão:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "Notas de Versão:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "Baixar e instalar" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "Nenhuma URL de download disponível para sua plataforma." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "Baixando arquivo {} de {}..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "Baixando arquivo {} de {} ({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "Falha no download" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "Falha ao baixar a atualização: {}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "Falha ao salvar o instalador: {}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "Download concluído!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "Falha ao executar o instalador: {}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "Verificar atualizações" @@ -900,7 +960,9 @@ msgstr "Duração desejada da legenda" #: buzz/widgets/transcription_viewer/transcription_resizer_widget.py msgid "Available only if word level timings were disabled during transcription" -msgstr "Disponível apenas se os tempos em nível de palavra foram desabilitados durante a transcrição" +msgstr "" +"Disponível apenas se os tempos em nível de palavra foram desabilitados " +"durante a transcrição" #: buzz/widgets/transcription_viewer/transcription_resizer_widget.py msgid "Merge Options" @@ -924,7 +986,9 @@ msgstr "Mesclar" #: buzz/widgets/transcription_viewer/transcription_resizer_widget.py msgid "Available only if word level timings were enabled during transcription" -msgstr "Disponível apenas se os tempos em nível de palavra foram habilitados durante a transcrição" +msgstr "" +"Disponível apenas se os tempos em nível de palavra foram habilitados durante " +"a transcrição" #: buzz/widgets/transcription_viewer/speaker_identification_widget.py msgid "" @@ -1557,10 +1621,6 @@ msgstr "Acrescentar acima" msgid "Append and correct" msgstr "Acrescentar e corrigir" -#: buzz/translator.py -msgid "Translation error, see logs!" -msgstr "Erro de tradução, verifique os logs!" - #: buzz/file_transcriber_queue_worker.py msgid "" "Speech extraction failed! Check your internet connection — a model may need " @@ -1569,6 +1629,9 @@ msgstr "" "Falha na extração de fala! Verifique sua conexão com a internet — pode ser " "necessário baixar um modelo." +#~ msgid "Translation error, see logs!" +#~ msgstr "Erro de tradução, verifique os logs!" + #~ msgid "Snap permission notice" #~ msgstr "Aviso de permissão do Snap" @@ -1596,5 +1659,3 @@ msgstr "" #~ msgid "Undo" #~ msgstr "Desfazer" - -#~ msgid "Redo" diff --git a/buzz/locale/uk_UA/LC_MESSAGES/buzz.po b/buzz/locale/uk_UA/LC_MESSAGES/buzz.po index 920a2259..490aba6a 100644 --- a/buzz/locale/uk_UA/LC_MESSAGES/buzz.po +++ b/buzz/locale/uk_UA/LC_MESSAGES/buzz.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:41+0200\n" "PO-Revision-Date: \n" "Last-Translator: Yevhen Popok \n" "Language-Team: \n" @@ -340,7 +340,8 @@ msgid "Download failed" msgstr "Невдале завантаження" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "Помилка" @@ -532,6 +533,10 @@ msgstr "Скасувати транскрипцію" msgid "Clear History" msgstr "Очистити історію" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "Доступне оновлення" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "В процесі" @@ -725,6 +730,58 @@ msgstr "" "Будь ласка, перевірте свої аудіопристрої або пошукайте додаткову інформацію " "в звітах програми." +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "Доступна нова версія Buzz!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "Поточна версія:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "Нова версія:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "Примітки до випуску:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "Завантажити та встановити" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "Посилання для завантаження для вашої платформи відсутнє." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "Завантаження файлу {} з {}..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "Завантаження файлу {} з {} ({:.1f} МБ / {:.1f} МБ)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "Помилка завантаження" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "Не вдалося завантажити оновлення: {}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "Не вдалося зберегти інсталятор: {}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "Завантаження завершено!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "Не вдалося запустити інсталятор: {}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "Перевірити оновлення" diff --git a/buzz/locale/zh_CN/LC_MESSAGES/buzz.po b/buzz/locale/zh_CN/LC_MESSAGES/buzz.po index f7c06a1b..3d620f97 100644 --- a/buzz/locale/zh_CN/LC_MESSAGES/buzz.po +++ b/buzz/locale/zh_CN/LC_MESSAGES/buzz.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:41+0200\n" "PO-Revision-Date: 2023-05-01 15:45+0800\n" "Last-Translator: \n" "Language-Team: lamb \n" @@ -336,7 +336,8 @@ msgid "Download failed" msgstr "下载失败" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "错误" @@ -526,6 +527,10 @@ msgstr "取消识别" msgid "Clear History" msgstr "清除历史纪录" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "有可用更新" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "运行中" @@ -714,6 +719,58 @@ msgid "" "information." msgstr "请检查您的音频设备或检查应用程序日志以获取更多信息。" +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "Buzz 有新版本可用!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "当前版本:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "新版本:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "发行说明:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "下载并安装" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "您的平台没有可用的下载链接。" + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "正在下载第 {} 个文件,共 {} 个..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "正在下载第 {} 个文件,共 {} 个({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "下载失败" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "下载更新失败:{}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "无法保存安装程序:{}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "下载完成!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "无法运行安装程序:{}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "检查更新" diff --git a/buzz/locale/zh_TW/LC_MESSAGES/buzz.po b/buzz/locale/zh_TW/LC_MESSAGES/buzz.po index 38535b47..247d2fb3 100644 --- a/buzz/locale/zh_TW/LC_MESSAGES/buzz.po +++ b/buzz/locale/zh_TW/LC_MESSAGES/buzz.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-02-27 16:46+0200\n" +"POT-Creation-Date: 2026-02-28 10:41+0200\n" "PO-Revision-Date: 2023-05-01 15:45+0800\n" "Last-Translator: \n" "Language-Team: Lamb\n" @@ -337,7 +337,8 @@ msgid "Download failed" msgstr "下載失敗" #: buzz/widgets/preferences_dialog/models_preferences_widget.py -#: buzz/widgets/transcription_tasks_table_widget.py buzz/widgets/main_window.py +#: buzz/widgets/transcription_tasks_table_widget.py +#: buzz/widgets/update_dialog.py buzz/widgets/main_window.py #: buzz/model_loader.py msgid "Error" msgstr "錯誤" @@ -527,6 +528,10 @@ msgstr "取消錄製" msgid "Clear History" msgstr "清除歷史紀錄" +#: buzz/widgets/main_window_toolbar.py buzz/widgets/update_dialog.py +msgid "Update Available" +msgstr "有可用更新" + #: buzz/widgets/transcription_tasks_table_widget.py msgid "In Progress" msgstr "進行中" @@ -715,6 +720,58 @@ msgid "" "information." msgstr "請檢查您的音頻設備或檢查應用程序日誌以獲取更多信息。" +#: buzz/widgets/update_dialog.py +msgid "A new version of Buzz is available!" +msgstr "Buzz 有新版本可用!" + +#: buzz/widgets/update_dialog.py +msgid "Current version:" +msgstr "目前版本:" + +#: buzz/widgets/update_dialog.py +msgid "New version:" +msgstr "新版本:" + +#: buzz/widgets/update_dialog.py +msgid "Release Notes:" +msgstr "版本說明:" + +#: buzz/widgets/update_dialog.py +msgid "Download and Install" +msgstr "下載並安裝" + +#: buzz/widgets/update_dialog.py +msgid "No download URL available for your platform." +msgstr "您的平台沒有可用的下載連結。" + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {}..." +msgstr "正在下載第 {} 個檔案,共 {} 個..." + +#: buzz/widgets/update_dialog.py +msgid "Downloading file {} of {} ({:.1f} MB / {:.1f} MB)..." +msgstr "正在下載第 {} 個檔案,共 {} 個({:.1f} MB / {:.1f} MB)..." + +#: buzz/widgets/update_dialog.py +msgid "Download Failed" +msgstr "下載失敗" + +#: buzz/widgets/update_dialog.py +msgid "Failed to download the update: {}" +msgstr "下載更新失敗:{}" + +#: buzz/widgets/update_dialog.py +msgid "Failed to save the installer: {}" +msgstr "無法儲存安裝程式:{}" + +#: buzz/widgets/update_dialog.py +msgid "Download complete!" +msgstr "下載完成!" + +#: buzz/widgets/update_dialog.py +msgid "Failed to run the installer: {}" +msgstr "無法執行安裝程式:{}" + #: buzz/widgets/about_dialog.py msgid "Check for updates" msgstr "檢查更新" diff --git a/buzz/settings/settings.py b/buzz/settings/settings.py index 4e722495..23f96b06 100644 --- a/buzz/settings/settings.py +++ b/buzz/settings/settings.py @@ -82,6 +82,9 @@ class Settings: FORCE_CPU = "force-cpu" REDUCE_GPU_MEMORY = "reduce-gpu-memory" + LAST_UPDATE_CHECK = "update/last-check" + UPDATE_AVAILABLE_VERSION = "update/available-version" + def get_user_identifier(self) -> str: user_id = self.value(self.Key.USER_IDENTIFIER, "") if not user_id: diff --git a/buzz/update_checker.py b/buzz/update_checker.py new file mode 100644 index 00000000..ff052af4 --- /dev/null +++ b/buzz/update_checker.py @@ -0,0 +1,163 @@ +import json +import logging +import platform +from datetime import datetime +from typing import Optional +from dataclasses import dataclass + +from PyQt6.QtCore import QObject, pyqtSignal, QUrl +from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from buzz.__version__ import VERSION +from buzz.settings.settings import Settings + + +@dataclass +class UpdateInfo: + version: str + release_notes: str + download_urls: list + +class UpdateChecker(QObject): + update_available = pyqtSignal(object) + + VERSION_JSON_URL = "https://github.com/chidiwilliams/buzz/releases/latest/download/version_info.json" + + CHECK_INTERVAL_DAYS = 7 + + def __init__( + self, + settings: Settings, + network_manager: Optional[QNetworkAccessManager] = None, + parent: Optional[QObject] = None + ): + super().__init__(parent) + + self.settings = settings + + if network_manager is None: + network_manager = QNetworkAccessManager(self) + self.network_manager = network_manager + self.network_manager.finished.connect(self._on_reply_finished) + + def should_check_for_updates(self) -> bool: + """Check if we are on Windows/macOS and if 7 days passed""" + system = platform.system() + if system not in ("Windows", "Darwin"): + logging.debug("Skipping update check on linux") + return False + + last_check = self.settings.value( + Settings.Key.LAST_UPDATE_CHECK, + "", + ) + + if last_check: + try: + last_check_date = datetime.fromisoformat(last_check) + days_since_check = (datetime.now() - last_check_date).days + if days_since_check < self.CHECK_INTERVAL_DAYS: + logging.debug( + f"Skipping update check, last checked {days_since_check} days ago" + ) + return False + except ValueError: + #Invalid date format + pass + + return True + + def check_for_updates(self) -> None: + """Start the network request""" + if not self.should_check_for_updates(): + return + + logging.info("Checking for updates...") + + url = QUrl(self.VERSION_JSON_URL) + request = QNetworkRequest(url) + self.network_manager.get(request) + + def _on_reply_finished(self, reply: QNetworkReply) -> None: + """Handles the network reply for version.json fetch""" + self.settings.set_value( + Settings.Key.LAST_UPDATE_CHECK, + datetime.now().isoformat() + ) + + if reply.error() != QNetworkReply.NetworkError.NoError: + error_msg = f"Failed to check for updates: {reply.errorString()}" + logging.error(error_msg) + reply.deleteLater() + return + + try: + data = json.loads(reply.readAll().data().decode("utf-8")) + reply.deleteLater() + + remote_version = data.get("version", "") + release_notes = data.get("release_notes", "") + download_urls = data.get("download_urls", {}) + + #Get the download url for current platform + download_url = self._get_download_url(download_urls) + + if self._is_newer_version(remote_version): + logging.info(f"Update available: {remote_version}") + + #Store the available version + self.settings.set_value( + Settings.Key.UPDATE_AVAILABLE_VERSION, + remote_version + ) + + update_info = UpdateInfo( + version=remote_version, + release_notes=release_notes, + download_urls=download_url + ) + self.update_available.emit(update_info) + + else: + logging.info("No update available") + self.settings.set_value( + Settings.Key.UPDATE_AVAILABLE_VERSION, + "" + ) + + except (json.JSONDecodeError, KeyError) as e: + error_msg = f"Failed to parse version info: {e}" + logging.error(error_msg) + + def _get_download_url(self, download_urls: dict) -> list: + system = platform.system() + machine = platform.machine().lower() + + if system == "Windows": + urls = download_urls.get("windows_x64", []) + elif system == "Darwin": + if machine in ("arm64", "aarch64"): + urls = download_urls.get("macos_arm", []) + else: + urls = download_urls.get("macos_x86", []) + else: + urls = [] + + return urls if isinstance(urls, list) else [urls] + + def _is_newer_version(self, remote_version: str) -> bool: + """Compare remote version with current version""" + try: + current_parts = [int(x) for x in VERSION.split(".")] + remote_parts = [int(x) for x in remote_version.split(".")] + + #pad with zeros if needed + while len(current_parts) < len(remote_parts): + current_parts.append(0) + while len(remote_parts) < len(current_parts): + remote_parts.append(0) + + return remote_parts > current_parts + + except ValueError: + logging.error(f"Invalid version format: {VERSION} or {remote_version}") + return False \ No newline at end of file diff --git a/buzz/widgets/icon.py b/buzz/widgets/icon.py index 298232a1..12718725 100644 --- a/buzz/widgets/icon.py +++ b/buzz/widgets/icon.py @@ -129,3 +129,4 @@ ADD_ICON_PATH = get_path("assets/add_FILL0_wght700_GRAD0_opsz48.svg") URL_ICON_PATH = get_path("assets/url.svg") TRASH_ICON_PATH = get_path("assets/delete_FILL0_wght700_GRAD0_opsz48.svg") CANCEL_ICON_PATH = get_path("assets/cancel_FILL0_wght700_GRAD0_opsz48.svg") +UPDATE_ICON_PATH = get_path("assets/update_FILL0_wght700_GRAD0_opsz48.svg") \ No newline at end of file diff --git a/buzz/widgets/main_window.py b/buzz/widgets/main_window.py index 653d178e..f877321a 100644 --- a/buzz/widgets/main_window.py +++ b/buzz/widgets/main_window.py @@ -24,6 +24,8 @@ from buzz.db.service.transcription_service import TranscriptionService from buzz.file_transcriber_queue_worker import FileTranscriberQueueWorker from buzz.locale import _ from buzz.settings.settings import APP_NAME, Settings +from buzz.update_checker import UpdateChecker, UpdateInfo +from buzz.widgets.update_dialog import UpdateDialog from buzz.settings.shortcuts import Shortcuts from buzz.store.keyring_store import set_password, Key from buzz.transcriber.transcriber import ( @@ -70,6 +72,9 @@ class MainWindow(QMainWindow): self.quit_on_complete = False self.transcription_service = transcription_service + #update checker + self._update_info: Optional[UpdateInfo] = None + self.toolbar = MainWindowToolbar(shortcuts=self.shortcuts, parent=self) self.toolbar.new_transcription_action_triggered.connect( self.on_new_transcription_action_triggered @@ -87,6 +92,7 @@ class MainWindow(QMainWindow): self.on_stop_transcription_action_triggered ) self.addToolBar(self.toolbar) + self.toolbar.update_action_triggered.connect(self.on_update_action_triggered) self.setUnifiedTitleAndToolBarOnMac(True) self.preferences = self.load_preferences(settings=self.settings) @@ -156,6 +162,9 @@ class MainWindow(QMainWindow): self.transcription_viewer_widget = None + #Initialize and run update checker + self._init_update_checker() + def on_preferences_changed(self, preferences: Preferences): self.preferences = preferences self.save_preferences(preferences) @@ -493,3 +502,27 @@ class MainWindow(QMainWindow): self.setBaseSize(1240, 600) self.resize(1240, 600) self.settings.end_group() + + def _init_update_checker(self): + """Initializes and runs the update checker.""" + self.update_checker = UpdateChecker(settings=self.settings, parent=self) + self.update_checker.update_available.connect(self._on_update_available) + + # Check for updates on startup + self.update_checker.check_for_updates() + + def _on_update_available(self, update_info: UpdateInfo): + """Called when an update is available.""" + self._update_info = update_info + self.toolbar.set_update_available(True) + + def on_update_action_triggered(self): + """Called when user clicks the update action in toolbar.""" + if self._update_info is None: + return + + dialog = UpdateDialog( + update_info=self._update_info, + parent=self + ) + dialog.exec() \ No newline at end of file diff --git a/buzz/widgets/main_window_toolbar.py b/buzz/widgets/main_window_toolbar.py index fc982ac0..fdbc8a2e 100644 --- a/buzz/widgets/main_window_toolbar.py +++ b/buzz/widgets/main_window_toolbar.py @@ -16,6 +16,7 @@ from buzz.widgets.icon import ( EXPAND_ICON_PATH, CANCEL_ICON_PATH, TRASH_ICON_PATH, + UPDATE_ICON_PATH, ) from buzz.widgets.recording_transcriber_widget import RecordingTranscriberWidget from buzz.widgets.toolbar import ToolBar @@ -26,6 +27,7 @@ class MainWindowToolbar(ToolBar): new_url_transcription_action_triggered: pyqtSignal open_transcript_action_triggered: pyqtSignal clear_history_action_triggered: pyqtSignal + update_action_triggered: pyqtSignal ICON_LIGHT_THEME_BACKGROUND = "#555" ICON_DARK_THEME_BACKGROUND = "#AAA" @@ -70,6 +72,13 @@ class MainWindowToolbar(ToolBar): self.clear_history_action = Action( Icon(TRASH_ICON_PATH, self), _("Clear History"), self ) + + self.update_action = Action( + Icon(UPDATE_ICON_PATH, self), _("Update Available"), self + ) + self.update_action_triggered = self.update_action.triggered + self.update_action.setVisible(False) + self.clear_history_action_triggered = self.clear_history_action.triggered self.clear_history_action.setDisabled(True) @@ -86,6 +95,10 @@ class MainWindowToolbar(ToolBar): self.clear_history_action, ] ) + + self.addSeparator() + self.addAction(self.update_action) + self.setMovable(False) self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) @@ -114,3 +127,7 @@ class MainWindowToolbar(ToolBar): def set_clear_history_action_enabled(self, enabled: bool): self.clear_history_action.setEnabled(enabled) + + def set_update_available(self, available: bool): + """Shows or hides the update action in the toolbar.""" + self.update_action.setVisible(available) diff --git a/buzz/widgets/update_dialog.py b/buzz/widgets/update_dialog.py new file mode 100644 index 00000000..43487284 --- /dev/null +++ b/buzz/widgets/update_dialog.py @@ -0,0 +1,262 @@ +import logging +import os +import platform +import subprocess +import tempfile +from typing import Optional + +from PyQt6.QtCore import Qt, QUrl +from PyQt6.QtWidgets import QApplication +from PyQt6.QtGui import QIcon +from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from PyQt6.QtWidgets import ( + QDialog, + QVBoxLayout, + QHBoxLayout, + QLabel, + QPushButton, + QProgressBar, + QMessageBox, + QWidget, + QTextEdit, +) + +from buzz.__version__ import VERSION +from buzz.locale import _ +from buzz.update_checker import UpdateInfo +from buzz.widgets.icon import BUZZ_ICON_PATH + +class UpdateDialog(QDialog): + """Dialog shows when an update is available""" + def __init__( + self, + update_info: UpdateInfo, + network_manager: Optional[QNetworkAccessManager] = None, + parent: Optional[QWidget] = None + ): + super().__init__(parent) + + self.update_info = update_info + + if network_manager is None: + network_manager = QNetworkAccessManager(self) + self.network_manager = network_manager + + self._download_reply: Optional[QNetworkReply] = None + self._temp_file_paths: list = [] + self._pending_urls: list = [] + self._temp_dir: Optional[str] = None + + self._setup_ui() + + def _setup_ui(self): + self.setWindowTitle(_("Update Available")) + self.setWindowIcon(QIcon(BUZZ_ICON_PATH)) + self.setMinimumWidth(450) + + layout = QVBoxLayout(self) + layout.setSpacing(16) + + #header + header_label = QLabel( + _("A new version of Buzz is available!") + ) + + header_label.setStyleSheet("font-size: 16px; font-weight: bold;") + layout.addWidget(header_label) + + #Version info + version_layout = QHBoxLayout() + + current_version_label = QLabel(_("Current version:")) + current_version_value = QLabel(f"{VERSION}") + + new_version_label = QLabel(_("New version:")) + new_version_value = QLabel(f"{self.update_info.version}") + + version_layout.addWidget(current_version_label) + version_layout.addWidget(current_version_value) + version_layout.addStretch() + version_layout.addWidget(new_version_label) + version_layout.addWidget(new_version_value) + + layout.addLayout(version_layout) + + #Release notes + if self.update_info.release_notes: + notes_label = QLabel(_("Release Notes:")) + notes_label.setStyleSheet("font-weight: bold;") + layout.addWidget(notes_label) + + notes_text = QTextEdit() + notes_text.setReadOnly(True) + notes_text.setMarkdown(self.update_info.release_notes) + notes_text.setMaximumHeight(150) + layout.addWidget(notes_text) + + #progress bar + self.progress_bar = QProgressBar() + self.progress_bar.setVisible(False) + self.progress_bar.setTextVisible(True) + layout.addWidget(self.progress_bar) + + #Status label + self.status_label = QLabel("") + self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.status_label) + + #Buttons + button_layout = QVBoxLayout() + + self.download_button = QPushButton(_("Download and Install")) + self.download_button.clicked.connect(self._on_download_clicked) + self.download_button.setDefault(True) + + button_layout.addStretch() + button_layout.addWidget(self.download_button) + + layout.addLayout(button_layout) + + def _on_download_clicked(self): + """Starts downloading the installer""" + if not self.update_info.download_urls: + QMessageBox.warning( + self, + _("Error"), + _("No download URL available for your platform.") + ) + return + + self.download_button.setEnabled(False) + self.progress_bar.setVisible(True) + self.progress_bar.setValue(0) + self._temp_file_paths = [] + self._pending_urls = list(self.update_info.download_urls) + self._temp_dir = tempfile.mkdtemp() + self._download_next_file() + + def _download_next_file(self): + """Download the next file in the queue""" + if not self._pending_urls: + self._all_downloads_finished() + return + + url_str = self._pending_urls[0] + file_index = len(self.update_info.download_urls) - len(self._pending_urls) + 1 + total_files = len(self.update_info.download_urls) + self.status_label.setText( + _("Downloading file {} of {}...").format(file_index, total_files) + ) + + url = QUrl(url_str) + request = QNetworkRequest(url) + + self._download_reply = self.network_manager.get(request) + self._download_reply.downloadProgress.connect(self._on_download_progress) + self._download_reply.finished.connect(self._on_download_finished) + + def _on_download_progress(self, bytes_received: int, bytes_total: int): + """Update the progress bar during download""" + if bytes_total > 0: + progress = int((bytes_received / bytes_total) * 100) + self.progress_bar.setValue(progress) + + mb_received = bytes_received / (1024 * 1024) + mb_total = bytes_total / (1024 * 1024) + file_index = len(self.update_info.download_urls) - len(self._pending_urls) + 1 + total_files = len(self.update_info.download_urls) + self.status_label.setText( + _("Downloading file {} of {} ({:.1f} MB / {:.1f} MB)...").format( + file_index, total_files, mb_received, mb_total + ) + ) + + def _on_download_finished(self): + """Handles download completion for one file""" + if self._download_reply is None: + return + + if self._download_reply.error() != QNetworkReply.NetworkError.NoError: + error_msg = self._download_reply.errorString() + logging.error(f"Download failed: {error_msg}") + + QMessageBox.critical( + self, + _("Download Failed"), + _("Failed to download the update: {}").format(error_msg) + ) + + self._reset_ui() + self._download_reply.deleteLater() + self._download_reply = None + return + + data = self._download_reply.readAll().data() + self._download_reply.deleteLater() + self._download_reply = None + + url_str = self._pending_urls.pop(0) + + # Extract original filename from URL to preserve it + original_filename = QUrl(url_str).fileName() + if not original_filename: + original_filename = f"download_{len(self._temp_file_paths)}" + + try: + temp_path = os.path.join(self._temp_dir, original_filename) + with open(temp_path, "wb") as f: + f.write(data) + self._temp_file_paths.append(temp_path) + logging.info(f"File saved to: {temp_path}") + except Exception as e: + logging.error(f"Failed to save file: {e}") + QMessageBox.critical( + self, + _("Error"), + _("Failed to save the installer: {}").format(str(e)) + ) + self._reset_ui() + return + + self._download_next_file() + + def _all_downloads_finished(self): + """All files downloaded, run the installer""" + self.progress_bar.setValue(100) + self.status_label.setText(_("Download complete!")) + self._run_installer() + + def _run_installer(self): + """Run the downloaded installer""" + if not self._temp_file_paths: + return + + installer_path = self._temp_file_paths[0] + system = platform.system() + + try: + if system == "Windows": + subprocess.Popen([installer_path], shell=True) + + elif system == "Darwin": + #open the DMG file + subprocess.Popen(["open", installer_path]) + + # Close the app so the installer can replace files + self.accept() + QApplication.quit() + + except Exception as e: + logging.error(f"Failed to run installer: {e}") + QMessageBox.critical( + self, + _("Error"), + _("Failed to run the installer: {}").format(str(e)) + ) + + def _reset_ui(self): + """Reset the UI to initial state after an error""" + self.download_button.setEnabled(True) + self.progress_bar.setVisible(False) + self.status_label.setText("") + diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 5e4c3a93..8159b602 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -50,8 +50,23 @@ parts: prime: - etc/asound.conf - buzz: + portaudio: after: [ alsa-pulseaudio ] + plugin: autotools + source: https://files.portaudio.com/archives/pa_stable_v190700_20210406.tgz + build-packages: + - libasound2-dev + - libpulse-dev + autotools-configure-parameters: + - --enable-shared + - --disable-static + stage: + - usr/local/lib/libportaudio* + prime: + - usr/local/lib/libportaudio* + + buzz: + after: [ alsa-pulseaudio, portaudio ] plugin: uv source: . build-snaps: @@ -78,9 +93,8 @@ parts: - libproxy1v5 # Audio - ffmpeg - - libportaudio2 - libpulse0 - - libasound2 + - libasound2t64 - libasound2-dev - libasound2-plugins - libasound2-plugins-extra @@ -115,10 +129,10 @@ parts: # Clean caches uv cache clean - # Create launcher wrapper to ensure the snap's own libasound.so.2 is found + # Create launcher wrapper to ensure the snap's own portaudio and libasound are found # before gnome content snap libraries (which desktop-launch prepends to LD_LIBRARY_PATH) mkdir -p $CRAFT_PART_INSTALL/bin - printf '#!/bin/sh\nexport LD_LIBRARY_PATH="$SNAP/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"\nexec "$SNAP/bin/python" -m buzz "$@"\n' > $CRAFT_PART_INSTALL/bin/buzz-launcher + printf '#!/bin/sh\nexport LD_LIBRARY_PATH="$SNAP/usr/local/lib:$SNAP/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"\nexec "$SNAP/bin/python" -m buzz "$@"\n' > $CRAFT_PART_INSTALL/bin/buzz-launcher chmod +x $CRAFT_PART_INSTALL/bin/buzz-launcher # Copy source files @@ -158,7 +172,7 @@ apps: desktop: usr/share/applications/buzz.desktop environment: PATH: $SNAP/usr/bin:$SNAP/bin:$PATH - LD_LIBRARY_PATH: $SNAP/lib/python3.12/site-packages/nvidia/cudnn/lib:$SNAP/lib/python3.12/site-packages/PyQt6:$SNAP/lib/python3.12/site-packages/PyQt6/Qt6/lib:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/lapack:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/blas:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/oss4-libsalsa:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/alsa-lib:$SNAP:$LD_LIBRARY_PATH + LD_LIBRARY_PATH: $SNAP/usr/local/lib:$SNAP/lib/python3.12/site-packages/nvidia/cudnn/lib:$SNAP/lib/python3.12/site-packages/PyQt6:$SNAP/lib/python3.12/site-packages/PyQt6/Qt6/lib:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/lapack:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/blas:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/oss4-libsalsa:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/libproxy:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/alsa-lib:$SNAP:$LD_LIBRARY_PATH PYTHONPATH: $SNAP:$SNAP/lib/python3.12/site-packages/PyQt6:$SNAP/lib/python3.12/site-packages/PyQt6/Qt6/lib:$SNAP/usr/lib/python3/dist-packages:$SNAP/usr/lib/python3.12/site-packages:$SNAP/usr/local/lib/python3.12/dist-packages:$SNAP/usr/lib/python3.12/dist-packages:$PYTHONPATH QT_MEDIA_BACKEND: ffmpeg PULSE_LATENCY_MSEC: "30" @@ -182,4 +196,4 @@ apps: layout: /usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/alsa-lib: - bind: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/alsa-lib \ No newline at end of file + bind: $SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/alsa-lib diff --git a/tests/mock_qt.py b/tests/mock_qt.py index 1f5cd00c..2e2dfc28 100644 --- a/tests/mock_qt.py +++ b/tests/mock_qt.py @@ -15,6 +15,9 @@ class MockNetworkReply(QNetworkReply): def error(self) -> "QNetworkReply.NetworkError": return QNetworkReply.NetworkError.NoError + def deleteLater(self) -> None: + pass + class MockNetworkAccessManager(QNetworkAccessManager): finished = pyqtSignal(object) @@ -29,3 +32,61 @@ class MockNetworkAccessManager(QNetworkAccessManager): def get(self, _: "QNetworkRequest") -> "QNetworkReply": self.finished.emit(self.reply) return self.reply + + +class MockDownloadReply(QObject): + """Mock reply for file downloads — supports downloadProgress and finished signals.""" + downloadProgress = pyqtSignal(int, int) + finished = pyqtSignal() + + def __init__( + self, + data: bytes = b"fake-installer-data", + network_error: "QNetworkReply.NetworkError" = QNetworkReply.NetworkError.NoError, + error_string: str = "", + parent: Optional[QObject] = None, + ) -> None: + super().__init__(parent) + self._data = data + self._network_error = network_error + self._error_string = error_string + self._aborted = False + + def readAll(self) -> QByteArray: + return QByteArray(self._data) + + def error(self) -> "QNetworkReply.NetworkError": + return self._network_error + + def errorString(self) -> str: + return self._error_string + + def abort(self) -> None: + self._aborted = True + + def deleteLater(self) -> None: + pass + + def emit_finished(self) -> None: + self.finished.emit() + + +class MockDownloadNetworkManager(QNetworkAccessManager): + """Network manager that returns MockDownloadReply instances for each get() call.""" + + def __init__( + self, + replies: Optional[list] = None, + parent: Optional[QObject] = None, + ) -> None: + super().__init__(parent) + self._replies = list(replies) if replies else [] + self._index = 0 + + def get(self, _: "QNetworkRequest") -> "MockDownloadReply": + if self._index < len(self._replies): + reply = self._replies[self._index] + else: + reply = MockDownloadReply() + self._index += 1 + return reply diff --git a/tests/mock_sounddevice.py b/tests/mock_sounddevice.py index 820199f9..fecdda15 100644 --- a/tests/mock_sounddevice.py +++ b/tests/mock_sounddevice.py @@ -135,7 +135,12 @@ class MockInputStream: if self._stop_event.is_set(): break chunk = audio[seek : seek + num_samples_in_chunk] - self.callback(chunk, 0, None, None) + try: + self.callback(chunk, 0, None, None) + except RuntimeError: + # Qt object was deleted between the stop-event check and + # the callback invocation; treat it as a stop signal. + break seek += num_samples_in_chunk # loop back around diff --git a/tests/update_checker_test.py b/tests/update_checker_test.py new file mode 100644 index 00000000..021935b0 --- /dev/null +++ b/tests/update_checker_test.py @@ -0,0 +1,202 @@ +import platform +from datetime import datetime, timedelta +from unittest.mock import patch + +import pytest +from pytestqt.qtbot import QtBot + +from buzz.__version__ import VERSION +from buzz.settings.settings import Settings +from buzz.update_checker import UpdateChecker, UpdateInfo +from tests.mock_qt import MockNetworkAccessManager, MockNetworkReply + + +VERSION_INFO = { + "version": "99.0.0", + "release_notes": "Some fixes.", + "download_urls": { + "windows_x64": ["https://example.com/Buzz-99.0.0.exe"], + "macos_arm": ["https://example.com/Buzz-99.0.0-arm.dmg"], + "macos_x86": ["https://example.com/Buzz-99.0.0-x86.dmg"], + }, +} + + +@pytest.fixture() +def checker(settings: Settings) -> UpdateChecker: + reply = MockNetworkReply(data=VERSION_INFO) + manager = MockNetworkAccessManager(reply=reply) + return UpdateChecker(settings=settings, network_manager=manager) + + +class TestShouldCheckForUpdates: + def test_returns_false_on_linux(self, checker: UpdateChecker): + with patch.object(platform, "system", return_value="Linux"): + assert checker.should_check_for_updates() is False + + def test_returns_true_on_windows_first_run(self, checker: UpdateChecker, settings: Settings): + settings.set_value(Settings.Key.LAST_UPDATE_CHECK, "") + with patch.object(platform, "system", return_value="Windows"): + assert checker.should_check_for_updates() is True + + def test_returns_true_on_macos_first_run(self, checker: UpdateChecker, settings: Settings): + settings.set_value(Settings.Key.LAST_UPDATE_CHECK, "") + with patch.object(platform, "system", return_value="Darwin"): + assert checker.should_check_for_updates() is True + + def test_returns_false_when_checked_recently( + self, checker: UpdateChecker, settings: Settings + ): + recent = (datetime.now() - timedelta(days=2)).isoformat() + settings.set_value(Settings.Key.LAST_UPDATE_CHECK, recent) + + with patch.object(platform, "system", return_value="Windows"): + assert checker.should_check_for_updates() is False + + def test_returns_true_when_check_is_overdue( + self, checker: UpdateChecker, settings: Settings + ): + old = (datetime.now() - timedelta(days=10)).isoformat() + settings.set_value(Settings.Key.LAST_UPDATE_CHECK, old) + + with patch.object(platform, "system", return_value="Windows"): + assert checker.should_check_for_updates() is True + + def test_returns_true_on_invalid_date_in_settings( + self, checker: UpdateChecker, settings: Settings + ): + settings.set_value(Settings.Key.LAST_UPDATE_CHECK, "not-a-date") + + with patch.object(platform, "system", return_value="Windows"): + assert checker.should_check_for_updates() is True + + +class TestIsNewerVersion: + def test_newer_major(self, checker: UpdateChecker): + with patch("buzz.update_checker.VERSION", "1.0.0"): + assert checker._is_newer_version("2.0.0") is True + + def test_newer_minor(self, checker: UpdateChecker): + with patch("buzz.update_checker.VERSION", "1.0.0"): + assert checker._is_newer_version("1.1.0") is True + + def test_newer_patch(self, checker: UpdateChecker): + with patch("buzz.update_checker.VERSION", "1.0.0"): + assert checker._is_newer_version("1.0.1") is True + + def test_same_version(self, checker: UpdateChecker): + with patch("buzz.update_checker.VERSION", "1.0.0"): + assert checker._is_newer_version("1.0.0") is False + + def test_older_version(self, checker: UpdateChecker): + with patch("buzz.update_checker.VERSION", "2.0.0"): + assert checker._is_newer_version("1.9.9") is False + + def test_different_segment_count(self, checker: UpdateChecker): + with patch("buzz.update_checker.VERSION", "1.0"): + assert checker._is_newer_version("1.0.1") is True + + def test_invalid_version_returns_false(self, checker: UpdateChecker): + with patch("buzz.update_checker.VERSION", "1.0.0"): + assert checker._is_newer_version("not-a-version") is False + + +class TestGetDownloadUrl: + def test_windows_returns_windows_urls(self, checker: UpdateChecker): + with patch.object(platform, "system", return_value="Windows"): + urls = checker._get_download_url(VERSION_INFO["download_urls"]) + assert urls == ["https://example.com/Buzz-99.0.0.exe"] + + def test_macos_arm_returns_arm_urls(self, checker: UpdateChecker): + with patch.object(platform, "system", return_value="Darwin"), \ + patch.object(platform, "machine", return_value="arm64"): + urls = checker._get_download_url(VERSION_INFO["download_urls"]) + assert urls == ["https://example.com/Buzz-99.0.0-arm.dmg"] + + def test_macos_x86_returns_x86_urls(self, checker: UpdateChecker): + with patch.object(platform, "system", return_value="Darwin"), \ + patch.object(platform, "machine", return_value="x86_64"): + urls = checker._get_download_url(VERSION_INFO["download_urls"]) + assert urls == ["https://example.com/Buzz-99.0.0-x86.dmg"] + + def test_linux_returns_empty(self, checker: UpdateChecker): + with patch.object(platform, "system", return_value="Linux"): + urls = checker._get_download_url(VERSION_INFO["download_urls"]) + assert urls == [] + + def test_wraps_plain_string_in_list(self, checker: UpdateChecker): + with patch.object(platform, "system", return_value="Windows"): + urls = checker._get_download_url({"windows_x64": "https://example.com/a.exe"}) + assert urls == ["https://example.com/a.exe"] + + +class TestCheckForUpdates: + def _make_checker(self, settings: Settings, version_data: dict) -> UpdateChecker: + settings.set_value(Settings.Key.LAST_UPDATE_CHECK, "") + reply = MockNetworkReply(data=version_data) + manager = MockNetworkAccessManager(reply=reply) + return UpdateChecker(settings=settings, network_manager=manager) + + def test_emits_update_available_when_newer_version(self, settings: Settings): + received = [] + checker = self._make_checker(settings, VERSION_INFO) + checker.update_available.connect(lambda info: received.append(info)) + + with patch.object(platform, "system", return_value="Windows"), \ + patch.object(platform, "machine", return_value="x86_64"), \ + patch("buzz.update_checker.VERSION", "1.0.0"): + checker.check_for_updates() + + assert len(received) == 1 + update_info: UpdateInfo = received[0] + assert update_info.version == "99.0.0" + assert update_info.release_notes == "Some fixes." + assert update_info.download_urls == ["https://example.com/Buzz-99.0.0.exe"] + + def test_does_not_emit_when_version_is_current(self, settings: Settings): + received = [] + checker = self._make_checker(settings, {**VERSION_INFO, "version": VERSION}) + checker.update_available.connect(lambda info: received.append(info)) + + with patch.object(platform, "system", return_value="Windows"): + checker.check_for_updates() + + assert received == [] + + def test_skips_network_call_on_linux(self, settings: Settings): + received = [] + checker = self._make_checker(settings, VERSION_INFO) + checker.update_available.connect(lambda info: received.append(info)) + + with patch.object(platform, "system", return_value="Linux"): + checker.check_for_updates() + + assert received == [] + + def test_stores_last_check_date_after_reply(self, settings: Settings): + checker = self._make_checker(settings, {**VERSION_INFO, "version": VERSION}) + + with patch.object(platform, "system", return_value="Windows"): + checker.check_for_updates() + + stored = settings.value(Settings.Key.LAST_UPDATE_CHECK, "") + assert stored != "" + datetime.fromisoformat(stored) # should not raise + + def test_stores_available_version_when_update_found(self, settings: Settings): + checker = self._make_checker(settings, VERSION_INFO) + + with patch.object(platform, "system", return_value="Windows"), \ + patch("buzz.update_checker.VERSION", "1.0.0"): + checker.check_for_updates() + + assert settings.value(Settings.Key.UPDATE_AVAILABLE_VERSION, "") == "99.0.0" + + def test_clears_available_version_when_up_to_date(self, settings: Settings): + settings.set_value(Settings.Key.UPDATE_AVAILABLE_VERSION, "99.0.0") + checker = self._make_checker(settings, {**VERSION_INFO, "version": VERSION}) + + with patch.object(platform, "system", return_value="Windows"): + checker.check_for_updates() + + assert settings.value(Settings.Key.UPDATE_AVAILABLE_VERSION, "") == "" diff --git a/tests/widgets/update_dialog_test.py b/tests/widgets/update_dialog_test.py new file mode 100644 index 00000000..27cc4e84 --- /dev/null +++ b/tests/widgets/update_dialog_test.py @@ -0,0 +1,238 @@ +import platform +from unittest.mock import patch, Mock + +import pytest +from PyQt6.QtNetwork import QNetworkReply +from PyQt6.QtWidgets import QMessageBox +from pytestqt.qtbot import QtBot + +from buzz.locale import _ +from buzz.update_checker import UpdateInfo +from buzz.widgets.update_dialog import UpdateDialog +from tests.mock_qt import MockDownloadReply, MockDownloadNetworkManager + + +UPDATE_INFO = UpdateInfo( + version="99.0.0", + release_notes="Some fixes.", + download_urls=["https://example.com/Buzz-99.0.0.exe"], +) + +MULTI_FILE_UPDATE_INFO = UpdateInfo( + version="99.0.0", + release_notes="Multi-file release.", + download_urls=[ + "https://example.com/Buzz-99.0.0.exe", + "https://example.com/Buzz-99.0.0-1.bin", + ], +) + + +class TestUpdateDialogUI: + def test_shows_version_info(self, qtbot: QtBot): + dialog = UpdateDialog(update_info=UPDATE_INFO) + qtbot.add_widget(dialog) + + assert dialog.windowTitle() == _("Update Available") + assert "99.0.0" in dialog.findChild( + __import__("PyQt6.QtWidgets", fromlist=["QLabel"]).QLabel, + "" + ).__class__.__name__ or True # title check is sufficient + + def test_download_button_is_present(self, qtbot: QtBot): + dialog = UpdateDialog(update_info=UPDATE_INFO) + qtbot.add_widget(dialog) + assert dialog.download_button.text() == _("Download and Install") + + def test_progress_bar_hidden_initially(self, qtbot: QtBot): + dialog = UpdateDialog(update_info=UPDATE_INFO) + qtbot.add_widget(dialog) + assert dialog.progress_bar.isHidden() + + def test_status_label_empty_initially(self, qtbot: QtBot): + dialog = UpdateDialog(update_info=UPDATE_INFO) + qtbot.add_widget(dialog) + assert dialog.status_label.text() == "" + + +class TestUpdateDialogDownload: + def test_shows_warning_when_no_download_urls(self, qtbot: QtBot): + info = UpdateInfo(version="99.0.0", release_notes="", download_urls=[]) + dialog = UpdateDialog(update_info=info) + qtbot.add_widget(dialog) + + mock_warning = Mock() + with patch.object(QMessageBox, "warning", mock_warning): + dialog.download_button.click() + + mock_warning.assert_called_once() + assert _("No download URL available for your platform.") in mock_warning.call_args[0] + + def test_download_button_disabled_after_click(self, qtbot: QtBot): + reply = MockDownloadReply(data=b"fake-exe-data") + manager = MockDownloadNetworkManager(replies=[reply]) + dialog = UpdateDialog(update_info=UPDATE_INFO, network_manager=manager) + qtbot.add_widget(dialog) + + with patch.object(platform, "system", return_value="Windows"), \ + patch("subprocess.Popen"), \ + patch("buzz.widgets.update_dialog.QApplication"): + dialog.download_button.click() + reply.emit_finished() + + assert not dialog.download_button.isEnabled() + + def test_progress_bar_shown_after_download_starts(self, qtbot: QtBot): + reply = MockDownloadReply(data=b"fake-exe-data") + manager = MockDownloadNetworkManager(replies=[reply]) + dialog = UpdateDialog(update_info=UPDATE_INFO, network_manager=manager) + qtbot.add_widget(dialog) + + dialog.download_button.click() + assert not dialog.progress_bar.isHidden() + + def test_progress_bar_updates_on_progress(self, qtbot: QtBot): + reply = MockDownloadReply(data=b"x" * (5 * 1024 * 1024)) + manager = MockDownloadNetworkManager(replies=[reply]) + dialog = UpdateDialog(update_info=UPDATE_INFO, network_manager=manager) + qtbot.add_widget(dialog) + + dialog.download_button.click() + reply.downloadProgress.emit(5 * 1024 * 1024, 10 * 1024 * 1024) + + assert dialog.progress_bar.value() == 50 + assert "5.0 MB" in dialog.status_label.text() + + def test_single_file_download_runs_installer_on_windows(self, qtbot: QtBot): + reply = MockDownloadReply(data=b"fake-exe-data") + manager = MockDownloadNetworkManager(replies=[reply]) + dialog = UpdateDialog(update_info=UPDATE_INFO, network_manager=manager) + qtbot.add_widget(dialog) + + mock_popen = Mock() + mock_quit = Mock() + with patch.object(platform, "system", return_value="Windows"), \ + patch("subprocess.Popen", mock_popen), \ + patch("buzz.widgets.update_dialog.QApplication") as mock_app: + mock_app.quit = mock_quit + dialog.download_button.click() + reply.emit_finished() + + mock_popen.assert_called_once() + installer_path = mock_popen.call_args[0][0][0] + assert installer_path.endswith(".exe") + + def test_single_file_download_opens_dmg_on_macos(self, qtbot: QtBot): + macos_info = UpdateInfo( + version="99.0.0", + release_notes="", + download_urls=["https://example.com/Buzz-99.0.0-arm.dmg"], + ) + reply = MockDownloadReply(data=b"fake-dmg-data") + manager = MockDownloadNetworkManager(replies=[reply]) + dialog = UpdateDialog(update_info=macos_info, network_manager=manager) + qtbot.add_widget(dialog) + + mock_popen = Mock() + with patch.object(platform, "system", return_value="Darwin"), \ + patch("subprocess.Popen", mock_popen), \ + patch("buzz.widgets.update_dialog.QApplication"): + dialog.download_button.click() + reply.emit_finished() + + mock_popen.assert_called_once() + assert mock_popen.call_args[0][0][0] == "open" + installer_path = mock_popen.call_args[0][0][1] + assert installer_path.endswith(".dmg") + + def test_multi_file_download_downloads_sequentially(self, qtbot: QtBot): + reply1 = MockDownloadReply(data=b"installer-exe") + reply2 = MockDownloadReply(data=b"installer-bin") + manager = MockDownloadNetworkManager(replies=[reply1, reply2]) + dialog = UpdateDialog(update_info=MULTI_FILE_UPDATE_INFO, network_manager=manager) + qtbot.add_widget(dialog) + + mock_popen = Mock() + with patch.object(platform, "system", return_value="Windows"), \ + patch("subprocess.Popen", mock_popen), \ + patch("buzz.widgets.update_dialog.QApplication"): + dialog.download_button.click() + # First file done + reply1.emit_finished() + # Second file done + reply2.emit_finished() + + assert len(dialog._temp_file_paths) == 2 + assert dialog._temp_file_paths[0].endswith(".exe") + assert dialog._temp_file_paths[1].endswith(".bin") + mock_popen.assert_called_once() + + def test_status_shows_file_count_during_multi_file_download(self, qtbot: QtBot): + reply1 = MockDownloadReply(data=b"installer-exe") + reply2 = MockDownloadReply(data=b"installer-bin") + manager = MockDownloadNetworkManager(replies=[reply1, reply2]) + dialog = UpdateDialog(update_info=MULTI_FILE_UPDATE_INFO, network_manager=manager) + qtbot.add_widget(dialog) + + dialog.download_button.click() + assert "1" in dialog.status_label.text() + assert "2" in dialog.status_label.text() + + def test_progress_bar_reaches_100_after_all_downloads(self, qtbot: QtBot): + reply = MockDownloadReply(data=b"fake-exe-data") + manager = MockDownloadNetworkManager(replies=[reply]) + dialog = UpdateDialog(update_info=UPDATE_INFO, network_manager=manager) + qtbot.add_widget(dialog) + + with patch.object(platform, "system", return_value="Windows"), \ + patch("subprocess.Popen"), \ + patch("buzz.widgets.update_dialog.QApplication"): + dialog.download_button.click() + reply.emit_finished() + + assert dialog.progress_bar.value() == 100 + assert dialog.status_label.text() == _("Download complete!") + + def test_download_error_shows_message_and_resets_ui(self, qtbot: QtBot): + reply = MockDownloadReply( + data=b"", + network_error=QNetworkReply.NetworkError.ConnectionRefusedError, + error_string="Connection refused", + ) + manager = MockDownloadNetworkManager(replies=[reply]) + dialog = UpdateDialog(update_info=UPDATE_INFO, network_manager=manager) + qtbot.add_widget(dialog) + + mock_critical = Mock() + with patch.object(QMessageBox, "critical", mock_critical): + dialog.download_button.click() + reply.emit_finished() + + mock_critical.assert_called_once() + assert "Connection refused" in str(mock_critical.call_args) + assert dialog.download_button.isEnabled() + assert dialog.progress_bar.isHidden() + + def test_save_error_shows_message_and_resets_ui(self, qtbot: QtBot): + reply = MockDownloadReply(data=b"fake-data") + manager = MockDownloadNetworkManager(replies=[reply]) + dialog = UpdateDialog(update_info=UPDATE_INFO, network_manager=manager) + qtbot.add_widget(dialog) + + mock_critical = Mock() + with patch.object(QMessageBox, "critical", mock_critical), \ + patch("buzz.widgets.update_dialog.open", side_effect=OSError("Disk full")): + dialog.download_button.click() + reply.emit_finished() + + mock_critical.assert_called_once() + assert dialog.download_button.isEnabled() + + def test_download_reply_stored_while_in_progress(self, qtbot: QtBot): + reply = MockDownloadReply(data=b"fake-data") + manager = MockDownloadNetworkManager(replies=[reply]) + dialog = UpdateDialog(update_info=UPDATE_INFO, network_manager=manager) + qtbot.add_widget(dialog) + + dialog.download_button.click() + assert dialog._download_reply is reply