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
375 changes: 375 additions & 0 deletions README.de.md

Large diffs are not rendered by default.

294 changes: 149 additions & 145 deletions README.md

Large diffs are not rendered by default.

193 changes: 122 additions & 71 deletions app/blitztext_linux.py

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

from app.writing_presets import DEFAULT_PRESET_KEY, WRITING_PRESET_KEYS

from app.i18n import LANGUAGES as I18N_LANGUAGES, DEFAULT_LANGUAGE as I18N_DEFAULT_LANGUAGE

logger = logging.getLogger("blitztext.config")

DEFAULTS: dict[str, Any] = {
Expand Down Expand Up @@ -45,6 +47,7 @@
"custom_terms": [],
"writing_preset": DEFAULT_PRESET_KEY,
},
"ui_language": I18N_DEFAULT_LANGUAGE,
}

VALID_MODELS = {"tiny", "base", "small", "medium", "large", "large-v2", "large-v3", "large-v3-turbo"}
Expand All @@ -56,6 +59,7 @@
VALID_LLM_PROVIDERS = {"openai", "openrouter", "custom"}
VALID_TTS_PROVIDERS = {"piper", "openai"}
VALID_OPENAI_TTS_VOICES = {"alloy", "ash", "ballad", "coral", "echo", "fable", "nova", "onyx", "sage", "shimmer", "verse", "marin", "cedar"}
VALID_UI_LANGUAGES = set(I18N_LANGUAGES)
BASE_URL_RE = re.compile(r"^https?://", re.IGNORECASE)
VALID_HOTKEY_KEYS = {
"KEY_LEFTALT", "KEY_RIGHTALT", "KEY_RIGHTCTRL", "KEY_LEFTCTRL",
Expand Down Expand Up @@ -199,6 +203,18 @@ def language(self) -> str:
def language(self, value: str) -> None:
self._data["language"] = value

@property
def ui_language(self) -> str:
"""Gebe die UI-Sprache zurück, validiert."""
value = self._data.get("ui_language", DEFAULTS["ui_language"])
return value if value in VALID_UI_LANGUAGES else DEFAULTS["ui_language"]

@ui_language.setter
def ui_language(self, value: str) -> None:
if value not in VALID_UI_LANGUAGES:
raise ValueError(f"Ungueltige UI-Sprache: {value!r}. Gueltig: {sorted(VALID_UI_LANGUAGES)}")
self._data["ui_language"] = value

@property
def backend(self) -> str:
return self._data["backend"]
Expand Down Expand Up @@ -375,6 +391,8 @@ def _validate_and_sanitize(self) -> None:
self._data["model"] = "base"
if self._data.get("backend") not in VALID_BACKENDS:
self._data["backend"] = "openai-whisper"
if self._data.get("ui_language") not in VALID_UI_LANGUAGES:
self._data["ui_language"] = DEFAULTS["ui_language"]
if self._data.get("hotkey_mode") not in VALID_HOTKEY_MODES:
self._data["hotkey_mode"] = "toggle"
if self._data.get("transcription_hotkey") not in VALID_HOTKEY_KEYS:
Expand Down
34 changes: 18 additions & 16 deletions app/history_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
QWidget,
)

from app.i18n import t

logger = logging.getLogger("blitztext.history")


Expand Down Expand Up @@ -58,7 +60,7 @@ def save_dictation_note(folder: str, text: str) -> Optional[str]:
filename = now.strftime("%Y-%m-%d_%H-%M-%S") + ".md"
heading = now.strftime("%Y-%m-%d %H:%M:%S")
path = os.path.join(resolved, filename)
content = f"# Diktat {heading}\n\n{text}\n"
content = f"# {t('history.note.heading').format(heading=heading)}\n\n{text}\n"
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(content)
Expand Down Expand Up @@ -92,10 +94,10 @@ def save_merged_dictation(folder: str, combined: str) -> Optional[str]:
try:
os.makedirs(resolved, exist_ok=True)
now = datetime.now()
filename = "Diktat-zusammengefuehrt_" + now.strftime("%Y-%m-%d_%H-%M-%S") + ".md"
filename = t("history.note.merged_filename_prefix") + now.strftime("%Y-%m-%d_%H-%M-%S") + ".md"
heading = now.strftime("%Y-%m-%d %H:%M:%S")
path = os.path.join(resolved, filename)
content = f"# Diktat (zusammengefuehrt) {heading}\n\n{combined}\n"
content = f"# {t('history.note.merged_heading').format(heading=heading)}\n\n{combined}\n"
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(content)
Expand Down Expand Up @@ -171,21 +173,21 @@ def __init__(self, entry: HistoryEntry, parent: Optional[QWidget] = None) -> Non
layout.setSpacing(3)

top_row = QHBoxLayout()
meta_text = f"{entry.timestamp} · {entry.word_count} Wörter"
meta_text = t("history.entry.meta").format(timestamp=entry.timestamp, count=entry.word_count)
if entry.is_dictation:
meta_text = f"\U0001f3a4 {meta_text}"
meta_label = QLabel(meta_text)
meta_label.setStyleSheet("color: palette(mid); font-size: 11px;")
top_row.addWidget(meta_label, 1)

self._btn_copy = QPushButton("\U0001f4cb")
self._btn_copy.setToolTip("In Zwischenablage kopieren")
self._btn_copy.setToolTip(t("history.tooltip.copy"))
self._btn_copy.setFixedSize(28, 24)
self._btn_copy.clicked.connect(self._copy_to_clipboard)
top_row.addWidget(self._btn_copy)

btn_delete = QPushButton("✕")
btn_delete.setToolTip("Eintrag löschen")
btn_delete.setToolTip(t("history.tooltip.delete"))
btn_delete.setFixedSize(28, 24)
btn_delete.clicked.connect(lambda: self.deleted.emit(self.entry))
top_row.addWidget(btn_delete)
Expand Down Expand Up @@ -244,17 +246,17 @@ def _setup_ui(self) -> None:
layout.setSpacing(4)

header_row = QHBoxLayout()
self._header_label = QLabel("Verlauf (0)")
self._header_label = QLabel(t("history.header").format(count=0))
self._header_label.setStyleSheet("font-size: 13px; font-weight: bold;")
header_row.addWidget(self._header_label, 1)

self._btn_merge = QPushButton("Zusammenführen")
self._btn_merge.setToolTip("Alle Diktat-Einträge kombinieren, als Datei speichern und kopieren")
self._btn_merge = QPushButton(t("history.button.merge"))
self._btn_merge.setToolTip(t("history.tooltip.merge"))
self._btn_merge.clicked.connect(self._merge_dictation)
self._btn_merge.hide()
header_row.addWidget(self._btn_merge)

self._btn_clear = QPushButton("Alles löschen")
self._btn_clear = QPushButton(t("history.button.clear_all"))
self._btn_clear.clicked.connect(self._on_clear_clicked)
header_row.addWidget(self._btn_clear)

Expand Down Expand Up @@ -319,15 +321,15 @@ def _remove_widget_for_entry(self, entry: HistoryEntry) -> None:
def _on_clear_clicked(self) -> None:
if not self._clear_armed:
self._clear_armed = True
self._btn_clear.setText("Sicher?")
self._btn_clear.setText(t("history.button.confirm_clear"))
self._btn_clear.setStyleSheet("color: #f44336; font-weight: bold;")
QTimer.singleShot(3000, self._disarm_clear)
else:
self.clear_all()

def _disarm_clear(self) -> None:
self._clear_armed = False
self._btn_clear.setText("Alles löschen")
self._btn_clear.setText(t("history.button.clear_all"))
self._btn_clear.setStyleSheet("")

def clear_all(self) -> None:
Expand All @@ -337,7 +339,7 @@ def clear_all(self) -> None:
widget.deleteLater()
self._entry_widgets.clear()
self._clear_armed = False
self._btn_clear.setText("Alles löschen")
self._btn_clear.setText(t("history.button.clear_all"))
self._btn_clear.setStyleSheet("")
self._update_header()
self._update_merge_button()
Expand All @@ -352,17 +354,17 @@ def _merge_dictation(self) -> None:
if path:
self.merged.emit(path)

self._btn_merge.setText("✓ Gespeichert" if path else "✓ Kopiert")
self._btn_merge.setText(t("history.status.saved") if path else t("history.status.copied"))
self._btn_merge.setStyleSheet("color: #4caf50; font-weight: bold;")
QTimer.singleShot(2500, self._reset_merge_button)

def _reset_merge_button(self) -> None:
self._btn_merge.setText("Zusammenführen")
self._btn_merge.setText(t("history.button.merge"))
self._btn_merge.setStyleSheet("")

def _update_header(self) -> None:
count = len(self._entries)
self._header_label.setText(f"Verlauf ({count})")
self._header_label.setText(t("history.header").format(count=count))
self.count_changed.emit(count)

def _update_merge_button(self) -> None:
Expand Down
Loading