Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Release assets

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Existing tag to build and validate (for example v0.9.0); no publication'
required: true
type: string

permissions:
contents: read

concurrency:
group: release-assets-${{ github.event.release.tag_name || inputs.tag }}
cancel-in-progress: false

jobs:
build:
# The release builder only supports stable numeric versions. Manual runs
# remain available for validating an existing tag, including prereleases.
if: github.event_name != 'release' || github.event.release.prerelease == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.release.tag_name || inputs.tag }}
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: '3.12'
- name: Build and validate tag against application version
env:
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: python scripts/build_release.py --tag "$RELEASE_TAG" --output dist
- name: Check archive and checksum
run: |
cd dist
sha256sum --check *.sha256
python -c 'import glob, tarfile; t = tarfile.open(glob.glob("*.tar.gz")[0]); assert all(n in t.getnames() for n in ("run.sh", "release.json", "app/blitztext_linux.py"))'
- uses: actions/upload-artifact@v4
with:
name: release-assets
path: dist/*
if-no-files-found: error
publish:
if: github.event_name == 'release' && !github.event.release.draft && !github.event.release.prerelease
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: release-assets
path: dist
- name: Attach public updater assets to the published release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: gh release upload "$RELEASE_TAG" dist/*.tar.gz dist/*.sha256
16 changes: 15 additions & 1 deletion README.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,11 +383,25 @@ systemctl --user enable --now ydotool.service # nutzt /usr/local/bin/ydotoold

---

## Aufnahme verwerfen

Während einer Aufnahme erscheint **Verwerfen** direkt bei der Aufnahmesteuerung und im Tray-Menü. Die Aktion beendet die Aufnahme und löscht das temporäre Audio, ohne Transkription oder KI-Verarbeitung zu starten. **Stopp** beendet und verarbeitet die Aufnahme weiterhin. Vorhandener Text und Verlauf bleiben beim Verwerfen erhalten.

## Updates aus den Einstellungen

Unter **Einstellungen → Updates → Nach Updates suchen** eine neue Version prüfen und mit **Update herunterladen und installieren** einspielen. Öffentliche stabile GitHub-Releases benötigen weder GitHub-Konto noch Token, Git-Installation oder `git pull`. Die Prüfung erfolgt auf Knopfdruck; beim bloßen Öffnen der Einstellungen wird nichts geladen. Nach der Installation Blitztext beenden und erneut öffnen.

Updates werden separat im Datenverzeichnis des Benutzers vorbereitet. Konfiguration, Zugangsdaten und Modell-Caches bleiben an ihren bisherigen Orten. Schlägt Download oder Vorbereitung fehl, bleibt die bisherige Version ausgewählt. Die erste Vorbereitung kann mehrere Minuten dauern und benötigt freien Speicher für eine separate Python-Umgebung. Die Erstinstallation stellt weiterhin die Systempakete bereit; ein App-Update benötigt keine Administratorrechte und führt `install.sh` nicht erneut aus.

Der Updatekanal setzt eine Version voraus, die den Updater bereits enthält. Ältere Installationen müssen diese einmal über den bisherigen Installationsweg beziehen. Experimentelle Flatpak-Builds verwenden ihren eigenen Paketweg.

Für Release-Ersteller: Ein stabiles Release mit Tag `vX.Y.Z` veröffentlichen, passend zu `app/__init__.py`. Der Release-Workflow baut das versionierte Updatearchiv samt Prüfsumme und hängt es an das Release. Solange diese Dateien fehlen, meldet die App, dass das Release noch nicht installierbar ist. Paketaufbau und Wiederherstellung stehen in der [Update-Dokumentation](docs/release-updates.md).

## Konfiguration

Alles wird lokal unter `~/.config/blitztext-linux/config.json` gespeichert. Diese Datei enthält keine Secrets — der OpenAI-/OpenRouter-Key wird aus einer Umgebungsvariable gelesen (siehe [Secrets](#secrets)). Die Konfigurationsdatei lässt sich direkt aus den Einstellungen öffnen: **Einstellungen → Allgemein → „Konfigurationsdatei öffnen"**.

Der Einstellungs-Dialog hat drei Tabs:
Der Einstellungs-Dialog enthält Spracherkennung, Text & KI, Allgemein und Updates:

<div align="center">
<img src="docs/screenshots/linux/settings-speech-de.png" alt="Einstellungen: Spracherkennung" width="480">
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,25 @@ systemctl --user enable --now ydotool.service # uses /usr/local/bin/ydotoold

---

## Discard a recording

While recording, **Discard** is visible next to the recording controls and in the tray menu. It stops capture and deletes the temporary audio without starting transcription or AI processing. **Stop** still finishes the recording and processes it. Discarding does not clear existing text or history.

## Updates from Settings

Open **Settings → Updates → Check for updates**, then **Download and install update**. Public stable GitHub releases are checked without a GitHub account, token, Git installation, or `git pull`. Checking is manual; nothing is downloaded when you merely open Settings. After installation, quit and reopen Blitztext to use the new version.

Updates are prepared separately in your user data directory. Configuration, credentials and model caches stay in their existing locations. A download or preparation failure keeps the previous version selected. The first preparation can take several minutes and requires free disk space for a separate Python environment. The initial installation still supplies system packages; an app update does not request administrator permissions or rerun `install.sh`.

This update channel requires a version that already includes the updater. Older installations need to acquire that version once through the existing installation path. Experimental Flatpak builds use their packaging channel instead.

Release maintainers: publish a stable release with a `vX.Y.Z` tag matching `app/__init__.py`. The release workflow builds and attaches the versioned update archive and checksum. Until those assets are attached, the app reports that the release is not yet available for installation. See [release update details](docs/release-updates.md) for packaging and recovery.

## Configuration

Everything is stored locally under `~/.config/blitztext-linux/config.json`. This file holds no secrets — the OpenAI/OpenRouter key is read from an environment variable (see [Secrets](#secrets)). The configuration file can be opened directly from the settings: **Settings → General → "Open configuration file"**.

The settings dialog has three tabs:
The settings dialog includes Speech Recognition, Text & AI, General, and Updates:

<div align="center">
<img src="docs/screenshots/linux/settings-speech-en.png" alt="Settings: Speech Recognition" width="480">
Expand Down
13 changes: 13 additions & 0 deletions app/blitztext_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,10 @@ def init_ui(self) -> None:

self.tabs.addTab(self._scrollable(tab_general), tq("settings.tab.general"))

from app.update_widget import UpdateWidget
self.update_widget = UpdateWidget(self)
self.tabs.addTab(self._scrollable(self.update_widget), t("updates.tab"))

layout.addWidget(self.tabs)

def _target_widgets(item) -> list[QWidget]:
Expand Down Expand Up @@ -895,6 +899,11 @@ def setup_tray(self) -> None:
self.action_custom_prompt.setIcon(theme.create_ui_icon("edit", theme.BLITZ_400))
self.action_custom_prompt.triggered.connect(self.show_custom_prompt)
self.menu.addAction(self.action_custom_prompt)
self.action_discard = QAction(t("mainwindow.button.discard"), self)
self.action_discard.setVisible(self.state == "RECORDING")
self.action_discard.setEnabled(self.state == "RECORDING")
self.action_discard.triggered.connect(self.gui_discard)
self.menu.addAction(self.action_discard)
self.menu.addSeparator()

# Actions für die fünf Workflows
Expand Down Expand Up @@ -1017,6 +1026,8 @@ def _refresh_i18n_texts(self) -> None:
self.action_compose.setText(t("tray.compose"))
if hasattr(self, "action_custom_prompt"):
self.action_custom_prompt.setText(t("compose.custom.edit"))
if hasattr(self, "action_discard"):
self.action_discard.setText(t("mainwindow.button.discard"))
if hasattr(self, "action_transcription"):
self.action_transcription.setText(
f"{tq('workflow.transcription.name')}\t{hotkey_display_name(self.config.transcription_hotkey)}"
Expand Down Expand Up @@ -1655,6 +1666,8 @@ def _on_state_changed(self) -> None:
self._main_window.update_state(self.state, self.current_workflow, self._tray_error_message)

def update_tray_state(self) -> None:
self.action_discard.setVisible(self.state == "RECORDING")
self.action_discard.setEnabled(self.state == "RECORDING")
if self._tray_error_message:
self.tray_icon.setIcon(self._tray_icons["ERROR"])
self.tray_icon.setToolTip(t("tray.tooltip.error").format(message=self._tray_error_message))
Expand Down
24 changes: 24 additions & 0 deletions app/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@

TRANSLATIONS: dict[str, dict[str, str]] = {
"de": {
"updates.tab": "Updates",
"updates.help": "Öffentliche Releases ohne GitHub-Konto laden. Die neue Version wird beim nächsten App-Start verwendet. Einstellungen und Modelle bleiben erhalten.",
"updates.check": "Nach Updates suchen",
"updates.install": "Update herunterladen und installieren",
"updates.ready": "Updates werden nur auf Knopfdruck geprüft.",
"updates.status": "Update-Status",
"updates.checking": "Suche nach einer neuen Version …",
"updates.installing": "Update wird geladen und eingerichtet. Dies kann einige Minuten dauern …",
"updates.installed": "Version {version} ist installiert. Blitztext beenden und erneut starten, um sie zu verwenden.",
"updates.available": "Version {version} ist verfügbar.",
"updates.current": "Sie verwenden die aktuelle Version.",
"updates.failed": "Update fehlgeschlagen: {error}",
"app.name": "Blitztext",
"tray.settings": "Einstellungen",
"tray.quit": "Beenden",
Expand Down Expand Up @@ -259,6 +271,18 @@
"preset.custom.name": "Eigene Anweisung",
},
"en": {
"updates.tab": "Updates",
"updates.help": "Download public releases without a GitHub account. The new version is used the next time the app starts. Settings and models are preserved.",
"updates.check": "Check for updates",
"updates.install": "Download and install update",
"updates.ready": "Updates are checked only when requested.",
"updates.status": "Update status",
"updates.checking": "Checking for a new version …",
"updates.installing": "Downloading and preparing the update. This may take several minutes …",
"updates.installed": "Version {version} is installed. Quit and reopen Blitztext to use it.",
"updates.available": "Version {version} is available.",
"updates.current": "You are using the latest version.",
"updates.failed": "Update failed: {error}",
"app.name": "Blitztext",
"tray.settings": "Settings",
"tray.quit": "Quit",
Expand Down
39 changes: 32 additions & 7 deletions app/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def _mic_body_path() -> QPainterPath:
_PANEL_MIN_WIDTH = 212
_PANEL_PADDING = (10, 6, 10, 10) # plus the 1 px QSS border
_TOOL_GAP = 6
_TOOLBAR_TOP_GAP = 8
_TOOLBAR_TOP_GAP = 0 # Keep the always-reserved discard row within the compact window height
_HISTORY_BUTTON_WIDTH = 48


Expand Down Expand Up @@ -301,6 +301,8 @@ def _setup_ui(self) -> None:
header.addWidget(self._btn_close)
panel.addLayout(header)

capture_controls = QHBoxLayout()
capture_controls.setSpacing(8)
self._btn_toggle = RecordButton(self._main_panel)
self._btn_toggle.clicked.connect(self._on_toggle_clicked)
panel.addWidget(self._btn_toggle, 0, Qt.AlignmentFlag.AlignHCenter)
Expand All @@ -316,7 +318,6 @@ def _setup_ui(self) -> None:
self._timer_label.setObjectName("timerLabel")
status.addWidget(self._timer_label)
panel.addLayout(status)
panel.addSpacing(4)
switch = QHBoxLayout()
switch.setSpacing(0)
self._seg_local = self._segment_button(t("mainwindow.mode.switch.local"), "left")
Expand All @@ -342,6 +343,34 @@ def _setup_ui(self) -> None:
self._mode_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed)
self._mode_label.clicked.connect(self._show_options_popup)
panel.addWidget(self._mode_label)
self._btn_discard = QPushButton(t("mainwindow.button.discard"), self._main_panel)
self._btn_discard.setObjectName("discardButton")
# Fit the existing 22px summary row, keeping the recording controls stationary.
self._btn_discard.setStyleSheet(
f"""
QPushButton#discardButton {{
background: {theme.APP_SURFACE}; color: {theme.STATE_ERROR};
border: 1px solid {theme.APP_LINE}; border-radius: 5px;
padding: 0 8px; min-height: 22px; max-height: 22px;
}}
QPushButton#discardButton:hover {{
background: {theme.APP_SURFACE_HOVER}; border-color: {theme.STATE_ERROR};
}}
QPushButton#discardButton:focus {{ border-color: {theme.BLITZ_300}; }}
QPushButton#discardButton:pressed {{ background: {theme.APP_SURFACE_SUNK}; }}
"""
)
self._btn_discard.setMinimumWidth(82)
self._btn_discard.setEnabled(False)
self._btn_discard.hide()
self._btn_discard.clicked.connect(self._on_discard_clicked)
discard_container = QWidget(self._main_panel)
discard_container.setMinimumHeight(24)
discard_row = QHBoxLayout(discard_container)
discard_row.setContentsMargins(0, 0, 0, 0)
discard_row.addStretch(1)
discard_row.addWidget(self._btn_discard)
panel.addWidget(discard_container)
panel.addSpacing(_TOOLBAR_TOP_GAP)

# The tool row is part of the same surface; its width sets the panel width.
Expand Down Expand Up @@ -484,11 +513,6 @@ def _setup_options_popup(self) -> None:
self._btn_dictation.setCheckable(True)
self._btn_dictation.clicked.connect(self._on_dictation_clicked)
popup.addWidget(self._btn_dictation)
self._btn_discard = QPushButton(t("mainwindow.button.discard"), self._popup_surface)
self._btn_discard.setObjectName("discardButton")
self._btn_discard.setEnabled(False)
self._btn_discard.clicked.connect(self._on_discard_clicked)
popup.addWidget(self._btn_discard)
self._sync_mode_widgets()
self._options_popup.adjustSize()
self._options_popup.hide()
Expand Down Expand Up @@ -672,6 +696,7 @@ def update_state(self, state: str, workflow: Optional[WorkflowType], error: Opti
self._btn_toggle.setEnabled(state in ("IDLE", "RECORDING"))
self._btn_toggle.set_mode("RECORDING" if recording else "PROCESSING" if busy else "IDLE")
self._btn_discard.setEnabled(recording)
self._btn_discard.setVisible(recording)
for control in (
self._workflow_combo, self._action_combo, self._btn_prompt,
self._provider_combo, self._model_combo, self._preset_combo,
Expand Down
Loading
Loading