feat(ui): About page for this app, VocaHQ, and talk to us (#718)

* feat(ui): About page for this app, VocaHQ, and talk to us

Settings About now has three groups. Vocalinux keeps the existing icon,
version, AGPL-3.0, updater, and What's New. The website button opens
vocalinux.com.

Part of VocaHQ lists the family sites with PRODUCT.md status. Talk to us
opens GitHub issues, Discord, X, and email. Official Design marks from
VocaHQ/.github are vendored as-is and painted with iron-white ink.

* fix(ui): unique About Open names and pin Design SVG hashes

Open buttons on About still say Open. Each one now uses its tooltip as
the accessible name so a screen reader can tell vocahq.com from
vocalinux.com from Latest Release.

The Talk-to-us SVG test pins sha256 of the four official marks from
VocaHQ/.github 61c8eee.

* fix(ui): circular app icon and About source code link

The launcher and About header used the square teal fill. Switch
both to the circular family logo (voca-logo.svg). Website stays
vocalinux.com; Source code next to it opens the GitHub repo.

* fix(appimage): pin pywhispercpp to the version install.sh declares

* fix(appimage): pin pywhispercpp at both install sites

* test(docs): match AGENTS.md uv.lock wording

#727 dropped "unused until those phases land". The test still
looked for that line, so any PR that runs Python tests against
current main failed. Pin the current contract instead: uv.lock
is authoritative, and requirements/*.txt are not hand-edited.

---------

Co-authored-by: Sergey Silaev <hello@8am.dev>
This commit is contained in:
Jatin K Malik
2026-08-23 00:50:40 -07:00
committed by GitHub
co-authored by Sergey Silaev
parent a5e723e1bd
commit a37469c8d5
15 changed files with 488 additions and 32 deletions
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-hidden="true">
<title>Discord</title>
<path fill="currentColor" d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-hidden="true">
<title>GitHub</title>
<path fill="currentColor" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>

After

Width:  |  Height:  |  Size: 869 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-hidden="true">
<title>Email</title>
<path fill="currentColor" d="M3 5h18a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2zm0 2v.2l9 6.3 9-6.3V7H3zm18 2.4-8.4 5.9a1 1 0 0 1-1.2 0L3 9.4V17h18V9.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 299 B

@@ -1,11 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024" role="img" aria-label="Voca">
<rect width="1024" height="1024" fill="#0F6B57"/>
<g transform="translate(28.386 32.619) scale(2.116474)">
<path d="M 164 127 A 65 65 0 0 1 294 127 L 294 224 A 65 65 0 0 1 164 224 Z" fill="#F2F6F2"/>
<path d="M 144.87 202.02 A 87 87 0 1 0 312.13 202.02" fill="none" stroke="#F2F6F2" stroke-width="18" stroke-linecap="round"/>
<path d="M 228.5 313 L 228.5 382" fill="none" stroke="#F2F6F2" stroke-width="18" stroke-linecap="round"/>
<path d="M 170 382 L 287 382" fill="none" stroke="#F2F6F2" stroke-width="18" stroke-linecap="round"/>
<path d="M 123.12 168.78 A 129.0 99.2 0 0 0 123.12 283.22" fill="none" stroke="#F2F6F2" stroke-width="18" stroke-linecap="round"/>
<path d="M 333.88 168.78 A 129.0 99.2 0 0 1 333.88 283.22" fill="none" stroke="#F2F6F2" stroke-width="18" stroke-linecap="round"/>
</g>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 460 460" width="460" height="460" role="img" aria-label="Vocalinux">
<circle cx="229" cy="231.5" r="221.5" fill="#0F6B57"/>
<path d="M 164 127 A 65 65 0 0 1 294 127 L 294 224 A 65 65 0 0 1 164 224 Z" fill="#F2F6F2"/>
<path d="M 144.87 202.02 A 87 87 0 1 0 312.13 202.02" fill="none" stroke="#F2F6F2" stroke-width="18" stroke-linecap="round"/>
<path d="M 228.5 313 L 228.5 382" fill="none" stroke="#F2F6F2" stroke-width="18" stroke-linecap="round"/>
<path d="M 170 382 L 287 382" fill="none" stroke="#F2F6F2" stroke-width="18" stroke-linecap="round"/>
<path d="M 123.12 168.78 A 129.0 99.2 0 0 0 123.12 283.22" fill="none" stroke="#F2F6F2" stroke-width="18" stroke-linecap="round"/>
<path d="M 333.88 168.78 A 129.0 99.2 0 0 1 333.88 283.22" fill="none" stroke="#F2F6F2" stroke-width="18" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 959 B

After

Width:  |  Height:  |  Size: 887 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-hidden="true">
<title>X</title>
<path fill="currentColor" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/>
</svg>

After

Width:  |  Height:  |  Size: 299 B

+215 -9
View File
@@ -68,7 +68,7 @@ from ..utils.whispercpp_model_info import is_model_downloaded as is_whispercpp_m
from ..utils.whispercpp_model_info import (
list_downloaded_models as list_downloaded_whispercpp_models,
)
from ..version import __copyright__, __description__, __url__, __version__ # noqa: E402
from ..version import __copyright__, __url__, __version__ # noqa: E402
from .config_manager import ( # noqa: E402
DEFAULT_CONFIG,
DEFAULT_SOUND_EFFECT_TONE,
@@ -261,6 +261,69 @@ def _default_whispercpp_variant_for_size(model_size: str, language_id: str) -> O
# Uniform width for right-hand row controls so they align down a page
_CONTROL_WIDTH = 230
VOCALINUX_SITE_URL = "https://vocalinux.com"
VOCAHQ_SITE_URL = "https://vocahq.com"
VOCAMAC_SITE_URL = "https://vocamac.com"
VOCAPHONE_SITE_URL = "https://vocaphone.vocahq.com"
VOCAGATEWAY_SITE_URL = "https://vocagateway.vocahq.com"
GITHUB_REPO_URL = __url__
GITHUB_ISSUES_URL = "https://github.com/VocaHQ/vocalinux/issues"
VOCAHQ_DISCORD_URL = "https://discord.gg/UMJduhcqn"
VOCAHQ_X_URL = "https://x.com/vocahq"
VOCAHQ_MAILTO_URL = "mailto:hello@vocahq.com"
# About page links that are not GitHub release URLs.
_ABOUT_OPEN_URLS = frozenset(
{
VOCALINUX_SITE_URL,
VOCAHQ_SITE_URL,
VOCAMAC_SITE_URL,
VOCAPHONE_SITE_URL,
VOCAGATEWAY_SITE_URL,
GITHUB_REPO_URL,
GITHUB_ISSUES_URL,
VOCAHQ_DISCORD_URL,
VOCAHQ_X_URL,
VOCAHQ_MAILTO_URL,
}
)
# Official Talk-to-us marks from VocaHQ/.github brand/vocahq/social (commit 61c8eee).
# Do not redraw. fill is currentColor; paint iron-white ink at load time.
_ABOUT_INK = "#14231C"
_ABOUT_ICON_TEXT_PX = 16
_VOCAHQ_FAMILY_LINKS = (
(VOCAHQ_SITE_URL, "vocahq.com", "Family site", "Open vocahq.com"),
(VOCALINUX_SITE_URL, "vocalinux.com", "Linux, Available now", "Open vocalinux.com"),
(VOCAMAC_SITE_URL, "vocamac.com", "macOS, Beta", "Open vocamac.com"),
(
VOCAPHONE_SITE_URL,
"vocaphone.vocahq.com",
"Android beta / iOS source build",
"Open vocaphone.vocahq.com",
),
(
VOCAGATEWAY_SITE_URL,
"vocagateway.vocahq.com",
"Optional self-hosted compute",
"Open vocagateway.vocahq.com",
),
)
def _can_open_url(url: str) -> bool:
"""Return True for GitHub project URLs or the About page allowlist."""
return bool(url) and (url in _ABOUT_OPEN_URLS or is_trusted_release_url(url))
def _set_accessible_name(widget: Gtk.Widget, name: str) -> None:
"""Set the ATK name so identical visible labels stay distinguishable."""
accessible = widget.get_accessible()
if accessible is not None:
accessible.set_name(name)
MODEL_SIZE_TOOLTIP = (
"Choose the largest model your computer can run comfortably. Tiny/Base are fastest, "
"Small is balanced, and Medium/Large can be more accurate but need more memory."
@@ -1571,7 +1634,7 @@ class SettingsDialog(Gtk.Dialog):
def _open_web_url(self, url: str) -> None:
"""Open a trusted project URL in the user's default browser."""
if not url or not is_trusted_release_url(url):
if not _can_open_url(url):
logger.warning("Refusing to open untrusted URL: %s", url)
return
try:
@@ -3360,6 +3423,58 @@ class SettingsDialog(Gtk.Dialog):
self.power_user_switch.connect("state-set", self._on_power_user_toggled)
def _about_mark_image(self, icon_name: str) -> Optional[Gtk.Image]:
"""Load a Design Talk-to-us SVG and paint currentColor as iron-white ink."""
from gi.repository import GdkPixbuf
from ..utils.resource_manager import ResourceManager
path = ResourceManager().get_icon_path(icon_name)
if not os.path.exists(path):
return None
try:
with open(path, encoding="utf-8") as handle:
svg = handle.read().replace("currentColor", _ABOUT_INK)
loader = GdkPixbuf.PixbufLoader.new_with_type("svg")
loader.set_size(_ABOUT_ICON_TEXT_PX, _ABOUT_ICON_TEXT_PX)
loader.write(svg.encode("utf-8"))
loader.close()
pixbuf = loader.get_pixbuf()
if pixbuf is None:
return None
image = Gtk.Image.new_from_pixbuf(pixbuf)
image.set_pixel_size(_ABOUT_ICON_TEXT_PX)
return image
except Exception as exc:
logger.warning("Failed to load About mark %s: %s", icon_name, exc)
return None
def _about_open_button(
self,
url: str,
tooltip: str,
label: str = "Open",
icon_name: Optional[str] = None,
width: int = 100,
) -> Gtk.Button:
"""Button that opens a trusted About URL.
Unique labels (Report a bug or idea, Discord, X, Email) are the
accessible name. Shared "Open" labels use the tooltip instead.
"""
button = Gtk.Button(label=label)
button.set_size_request(width, -1)
button.set_tooltip_text(tooltip)
if label == "Open":
_set_accessible_name(button, tooltip)
if icon_name:
image = self._about_mark_image(icon_name)
if image is not None:
button.set_image(image)
button.set_always_show_image(True)
button.connect("clicked", lambda *_args, dest=url: self._open_web_url(dest))
return button
def _build_about_section(self):
"""Build the About page using the same PreferenceRow cards as other pages."""
from gi.repository import GdkPixbuf
@@ -3379,7 +3494,10 @@ class SettingsDialog(Gtk.Dialog):
app_group = PreferencesGroup(
title="Vocalinux",
description=__description__,
description=(
"Available now on Linux (X11 and Wayland). After a model is downloaded, "
"speech is processed on this PC."
),
keywords=("about", "version", "app"),
header_icon=about_icon,
)
@@ -3395,18 +3513,30 @@ class SettingsDialog(Gtk.Dialog):
)
app_group.add_row(version_row)
website_btn = Gtk.Button(label="Open")
website_btn.set_size_request(100, -1)
website_btn.set_tooltip_text("Open the Vocalinux GitHub page")
website_btn.connect("clicked", lambda *_: self._open_web_url(__url__))
website_btn = self._about_open_button(
VOCALINUX_SITE_URL,
"Open vocalinux.com website",
)
website_row = PreferenceRow(
title="Website",
subtitle="Source code, issues, and documentation on GitHub",
subtitle="Product site at vocalinux.com",
widget=website_btn,
keywords=("github", "website", "repo"),
keywords=("website", "vocalinux.com"),
)
app_group.add_row(website_row)
source_btn = self._about_open_button(
GITHUB_REPO_URL,
"Open the Vocalinux GitHub repository",
)
source_row = PreferenceRow(
title="Source code",
subtitle=GITHUB_REPO_URL,
widget=source_btn,
keywords=("github", "source", "repo", "code"),
)
app_group.add_row(source_row)
license_row = PreferenceRow(
title="License",
subtitle=f"GNU AGPL v3 · {__copyright__}",
@@ -3461,6 +3591,10 @@ class SettingsDialog(Gtk.Dialog):
self.open_release_btn.set_tooltip_text(
"Open the release page in your browser for download links and install steps"
)
_set_accessible_name(
self.open_release_btn,
"Open the latest release page",
)
self.open_release_btn.connect("clicked", self._on_open_release_clicked)
self.latest_release_row = PreferenceRow(
title="Latest Release",
@@ -3489,6 +3623,78 @@ class SettingsDialog(Gtk.Dialog):
self.about_tab.pack_start(self.release_notes_group, False, False, 0)
self.release_notes_group.hide()
family_group = PreferencesGroup(
title="Part of VocaHQ",
description=(
"Vocalinux is part of VocaHQ. The same private dictation idea also ships "
"as VocaMac (macOS, beta), VocaWin (Windows, unsigned developer alpha), "
"and VocaPhone (Android beta / iOS source build). VocaGateway is optional "
"self-hosted compute. It is not on-device."
),
keywords=("vocahq", "vocamac", "vocawin", "vocaphone", "vocagateway", "family"),
)
for url, title, subtitle, open_name in _VOCAHQ_FAMILY_LINKS:
family_group.add_row(
PreferenceRow(
title=title,
subtitle=subtitle,
widget=self._about_open_button(url, open_name),
keywords=(title, subtitle.lower()),
)
)
self.about_tab.pack_start(family_group, False, False, 0)
talk_group = PreferencesGroup(
title="Talk to us",
description=(
"Bugs, feedback, and feature ideas open a GitHub issue. You pick the "
"template on the next screen."
),
keywords=("github", "discord", "contact", "bug", "feedback", "email"),
)
report_btn = self._about_open_button(
GITHUB_ISSUES_URL,
"Open GitHub issues for a bug or idea",
label="Report a bug or idea",
icon_name="github",
width=200,
)
talk_group.add_row(
PreferenceRow(
title="GitHub issues",
subtitle="https://github.com/VocaHQ/vocalinux/issues",
widget=report_btn,
keywords=("github", "issue", "bug", "idea"),
)
)
contact_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
for url, label, tooltip, icon_name in (
(VOCAHQ_DISCORD_URL, "Discord", "Open the VocaHQ Discord", "discord"),
(VOCAHQ_X_URL, "X", "Open X @vocahq", "x"),
(VOCAHQ_MAILTO_URL, "Email", "Email hello@vocahq.com", "mail"),
):
contact_box.pack_start(
self._about_open_button(
url,
tooltip,
label=label,
icon_name=icon_name,
width=110,
),
False,
False,
0,
)
talk_group.add_row(
PreferenceRow(
title="Community",
subtitle="Discord, X, and email",
widget=contact_box,
keywords=("discord", "x", "twitter", "email", "mail"),
)
)
self.about_tab.pack_start(talk_group, False, False, 0)
def _set_about_update_badge(self, visible: bool) -> None:
"""Show or hide the green New badge on the About sidebar row."""
about_page = next((page for page in self._pages if page.name == "about"), None)