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