fix(ui): point About and update links at the fork

The About page links the fork's website, source and issue tracker. The
Voca family tiles and Discord, X and mail links are gone. A single
Origin row credits upstream Vocalinux. The model-selection doc link and
the project URL now point at the fork host.
This commit is contained in:
2026-11-06 21:25:42 +05:30
parent 91708fdf4a
commit bc47ffecf6
5 changed files with 112 additions and 267 deletions
+42 -181
View File
@@ -1,4 +1,4 @@
"""About page content and trusted family links for Settings."""
"""About page content and trusted project links for Settings."""
import logging
import os
@@ -20,41 +20,29 @@ from ..version import __copyright__, __url__, __version__
logger = logging.getLogger(__name__)
VOCULINUX_SITE_URL = "https://voculinux.com"
VOCAHQ_SITE_URL = "https://vocahq.com"
VOCAMAC_SITE_URL = "https://vocamac.com"
VOCAWIN_SITE_URL = "https://vocawin.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/t6muquAJbm"
VOCAHQ_X_URL = "https://x.com/vocahq"
VOCAHQ_MAILTO_URL = "mailto:hello@vocahq.com"
# The fork's own source, issue and release host. The update checker reads the
# same host, so About and the updater agree on where releases live.
SOURCE_REPO_URL = __url__
SOURCE_ISSUES_URL = f"{SOURCE_REPO_URL}/issues"
# The one upstream credit: the project this fork was made from.
UPSTREAM_REPO_URL = "https://github.com/VocaHQ/vocalinux"
UPSTREAM_CREDIT_LINE = f"Voculinux is a hard fork of Vocalinux by VocaHQ: {UPSTREAM_REPO_URL}"
# About page links that are not release URLs.
_ABOUT_OPEN_URLS = frozenset(
{
VOCULINUX_SITE_URL,
VOCAHQ_SITE_URL,
VOCAMAC_SITE_URL,
VOCAWIN_SITE_URL,
VOCAPHONE_SITE_URL,
VOCAGATEWAY_SITE_URL,
GITHUB_REPO_URL,
GITHUB_ISSUES_URL,
VOCAHQ_DISCORD_URL,
VOCAHQ_X_URL,
VOCAHQ_MAILTO_URL,
SOURCE_REPO_URL,
SOURCE_ISSUES_URL,
UPSTREAM_REPO_URL,
}
)
# Official Talk-to-us marks from VocaHQ/.github brand/vocahq/social (commit 61c8eee).
# Do not redraw. fill is currentColor; paint to match the dialog foreground so
# the marks stay visible on both light and dark surfaces.
# Currentcolor brand marks. fill is currentColor; paint to match the dialog
# foreground so the marks stay visible on both light and dark surfaces.
_ABOUT_INK = "#14231C"
_ABOUT_INK_ON_DARK = "#E6E1D8"
_ABOUT_ICON_TEXT_PX = 18
_FAMILY_ICON_PX = 22
def _color_luminance(color: Any) -> float:
@@ -119,55 +107,6 @@ def _about_ink_hex(
return _ABOUT_INK_ON_DARK if dark else _ABOUT_INK
# Official platform marks from VocaHQ/.github brand/promo/cards/platform
# (currentColor copies live in web/public/brand/platforms). Home is a simple
# house so HQ does not reuse a product lockup.
_VOCAHQ_FAMILY_LINKS = (
(
VOCAHQ_SITE_URL,
"VocaHQ",
"Family site",
("platform-home",),
"Open vocahq.com",
),
(
VOCULINUX_SITE_URL,
"Voculinux",
"Linux, available now",
("platform-linux",),
"Open voculinux.com",
),
(
VOCAMAC_SITE_URL,
"VocaMac",
"macOS, available now",
("platform-apple",),
"Open vocamac.com",
),
(
VOCAWIN_SITE_URL,
"VocaWin",
"Windows, unsigned beta",
("platform-windows",),
"Open vocawin.com",
),
(
VOCAPHONE_SITE_URL,
"VocaPhone",
"Android beta / iOS TestFlight",
("platform-android", "platform-apple"),
"Open vocaphone.vocahq.com",
),
(
VOCAGATEWAY_SITE_URL,
"VocaGateway",
"Self-hosted, headless",
("platform-server",),
"Open vocagateway.vocahq.com",
),
)
def _can_open_url(url: str) -> bool:
"""Return True for this project's release URLs or the About page allowlist."""
return bool(url) and (url in _ABOUT_OPEN_URLS or is_trusted_release_url(url))
@@ -239,50 +178,6 @@ class AboutSectionMixin(Gtk.Dialog):
button.connect("clicked", lambda *_args, dest=url: self._open_web_url(dest))
return button
def _family_tile(
self: Any,
url: str,
title: str,
subtitle: str,
icon_names: tuple[str, ...],
tooltip: str,
dark: Optional[bool] = None,
) -> Gtk.Button:
"""Compact icon + name tile that opens a family site."""
from .settings_dialog import _set_accessible_name
inner = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16)
marks = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
marks.set_valign(Gtk.Align.CENTER)
for icon_name in icon_names:
icon = self._about_mark_image(
icon_name,
pixel_size=_FAMILY_ICON_PX,
dark=dark,
)
if icon is not None:
marks.pack_start(icon, False, False, 0)
inner.pack_start(marks, False, False, 0)
text = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
name_label = Gtk.Label(label=title, xalign=0)
name_label.get_style_context().add_class("family-tile-title")
status_label = Gtk.Label(label=subtitle, xalign=0)
status_label.get_style_context().add_class("family-tile-subtitle")
status_label.set_line_wrap(True)
text.pack_start(name_label, False, False, 0)
text.pack_start(status_label, False, False, 0)
inner.pack_start(text, True, True, 0)
button = Gtk.Button()
button.add(inner)
button.get_style_context().add_class("family-tile")
button.set_tooltip_text(tooltip)
button.set_hexpand(True)
button.set_halign(Gtk.Align.FILL)
_set_accessible_name(button, tooltip)
button.connect("clicked", lambda *_args, dest=url: self._open_web_url(dest))
return button
def _build_about_section(self: Any) -> None:
"""Build the About page using the same PreferenceRow cards as other pages."""
about_tab = getattr(self, "about_tab")
@@ -320,7 +215,7 @@ class AboutSectionMixin(Gtk.Dialog):
app_group = PreferencesGroup(
title="Voculinux",
description=(
"Available now on Linux (X11 and Wayland). After a model is downloaded, "
"Voice dictation for Linux (X11 and Wayland). After a model is downloaded, "
"speech is processed on this PC."
),
keywords=("about", "version", "app"),
@@ -353,8 +248,8 @@ class AboutSectionMixin(Gtk.Dialog):
)
link_box.pack_start(
self._about_open_button(
GITHUB_REPO_URL,
"Open the Voculinux GitHub repository",
SOURCE_REPO_URL,
"Open the Voculinux source repository",
label="Source code",
width=120,
dark=dark,
@@ -365,9 +260,9 @@ class AboutSectionMixin(Gtk.Dialog):
)
links_row = PreferenceRow(
title="Open source",
subtitle="Product site at voculinux.com, source on GitHub.",
subtitle="Product site at voculinux.com, source on the project's Gitea host.",
widget=link_box,
keywords=("website", "voculinux.com", "github", "source", "repo", "code", "agpl"),
keywords=("website", "voculinux.com", "gitea", "source", "repo", "code", "agpl"),
)
app_group.add_row(links_row)
@@ -381,53 +276,22 @@ class AboutSectionMixin(Gtk.Dialog):
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"),
description=("Bugs, feedback, and feature ideas go to the Voculinux issue tracker."),
keywords=("issues", "bug", "feedback", "contact", "report", "idea"),
)
report_btn = self._about_open_button(
GITHUB_ISSUES_URL,
"Open GitHub issues for a bug or idea",
SOURCE_ISSUES_URL,
"Open Voculinux issues for a bug or idea",
label="Report a bug or idea",
icon_name="github",
width=200,
dark=dark,
)
talk_group.add_row(
PreferenceRow(
title="GitHub issues",
subtitle="https://github.com/VocaHQ/vocalinux/issues",
title="Issues",
subtitle=SOURCE_ISSUES_URL,
widget=report_btn,
keywords=("github", "issue", "bug", "idea"),
)
)
contact_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
for url, label, tooltip, icon_name, width in (
(VOCAHQ_DISCORD_URL, "Join Discord", "Open the VocaHQ Discord", "discord", 140),
(VOCAHQ_X_URL, "Follow on X", "Open X @vocahq", "x", 130),
(VOCAHQ_MAILTO_URL, "Email us", "Email hello@vocahq.com", "mail", 120),
):
contact_box.pack_start(
self._about_open_button(
url,
tooltip,
label=label,
icon_name=icon_name,
width=width,
dark=dark,
),
False,
False,
0,
)
talk_group.add_row(
PreferenceRow(
title="Community",
subtitle="Discord, X, and email",
widget=contact_box,
keywords=("discord", "x", "twitter", "email", "mail"),
keywords=("issue", "bug", "idea"),
)
)
about_tab.pack_start(talk_group, False, False, 0)
@@ -519,24 +383,21 @@ class AboutSectionMixin(Gtk.Dialog):
about_tab.pack_start(self.release_notes_group, False, False, 0)
self.release_notes_group.hide()
family_group = PreferencesGroup(
title="Part of VocaHQ",
description="Private dictation for Linux, Mac, Windows, and phone.",
keywords=("vocahq", "vocamac", "vocawin", "vocaphone", "vocagateway", "family"),
origin_group = PreferencesGroup(
title="Origin",
keywords=("upstream", "fork", "origin", "credit", "project"),
)
family_grid = Gtk.Grid()
family_grid.set_column_spacing(8)
family_grid.set_row_spacing(4)
family_grid.set_column_homogeneous(True)
family_grid.set_margin_start(8)
family_grid.set_margin_end(8)
family_grid.set_margin_top(4)
family_grid.set_margin_bottom(8)
for index, (url, title, subtitle, icon_names, open_name) in enumerate(_VOCAHQ_FAMILY_LINKS):
tile = self._family_tile(url, title, subtitle, icon_names, open_name, dark=dark)
family_grid.attach(tile, index % 2, index // 2, 1, 1)
family_row = Gtk.ListBoxRow()
family_row.set_activatable(False)
family_row.add(family_grid)
family_group.add_row(family_row)
about_tab.pack_start(family_group, False, False, 0)
upstream_row = PreferenceRow(
title="Upstream project",
subtitle=UPSTREAM_CREDIT_LINE,
widget=self._about_open_button(
UPSTREAM_REPO_URL,
"Open the upstream project repository",
label="Upstream",
width=110,
dark=dark,
),
keywords=("upstream", "fork", "credit"),
)
origin_group.add_row(upstream_row)
about_tab.pack_start(origin_group, False, False, 0)
+7 -13
View File
@@ -151,18 +151,10 @@ from .settings_about import ( # noqa: F401
_ABOUT_INK,
_ABOUT_INK_ON_DARK,
_ABOUT_OPEN_URLS,
_FAMILY_ICON_PX,
_VOCAHQ_FAMILY_LINKS,
GITHUB_ISSUES_URL,
GITHUB_REPO_URL,
VOCAGATEWAY_SITE_URL,
VOCAHQ_DISCORD_URL,
VOCAHQ_MAILTO_URL,
VOCAHQ_SITE_URL,
VOCAHQ_X_URL,
VOCAMAC_SITE_URL,
VOCAPHONE_SITE_URL,
VOCAWIN_SITE_URL,
SOURCE_ISSUES_URL,
SOURCE_REPO_URL,
UPSTREAM_CREDIT_LINE,
UPSTREAM_REPO_URL,
VOCULINUX_SITE_URL,
AboutSectionMixin,
_about_ink_hex,
@@ -318,7 +310,9 @@ def _engine_from_display(display_name: str) -> str:
return display_name.lower()
MODEL_SELECTION_DOC_URL = "https://github.com/VocaHQ/vocalinux/blob/main/docs/MODEL_SELECTION.md"
MODEL_SELECTION_DOC_URL = (
"https://git.bongbetic.com/xavierk/voculinux/src/branch/main/docs/MODEL_SELECTION.md"
)
def _model_display_name(model_name: str) -> str:
+1 -1
View File
@@ -8,5 +8,5 @@ __author__ = "Jatin K Malik"
__email__ = "jatinkrmalik@gmail.com"
__license__ = "AGPL-3.0-only"
__copyright__ = "Copyright 2023-2026 Jatin K Malik"
__url__ = "https://github.com/VocaHQ/vocalinux"
__url__ = "https://git.bongbetic.com/xavierk/voculinux"
__description__ = "Free, offline voice dictation for Linux - speak and type anywhere"
+1 -1
View File
@@ -131,5 +131,5 @@ def test_recommendation_does_not_change_picker_or_config_state(
def test_model_selection_docs_url_is_stable(settings_dialog: Any) -> None:
assert (
settings_dialog.MODEL_SELECTION_DOC_URL
== "https://github.com/VocaHQ/vocalinux/blob/main/docs/MODEL_SELECTION.md"
== "https://git.bongbetic.com/xavierk/voculinux/src/branch/main/docs/MODEL_SELECTION.md"
)
+61 -71
View File
@@ -1329,7 +1329,7 @@ class TestSettingsNavigation(unittest.TestCase):
class TestAboutPage(unittest.TestCase):
"""About page: this app, VocaHQ family, talk-to-us. Updater stays."""
"""About page: this app, talk-to-us, origin credit. Updater stays."""
@classmethod
def setUpClass(cls):
@@ -1343,7 +1343,7 @@ class TestAboutPage(unittest.TestCase):
def test_three_groups_keep_updater(self) -> None:
self.assertIn('title="Voculinux"', self.about)
self.assertIn('title="Part of VocaHQ"', self.about)
self.assertIn('title="Origin"', self.about)
self.assertIn('title="Talk to us"', self.about)
self.assertIn('title="Updates"', self.about)
self.assertIn("What's New", self.about)
@@ -1352,12 +1352,8 @@ class TestAboutPage(unittest.TestCase):
self.assertIn("self.update_checks_switch", self.about)
def test_product_status_and_honest_network_copy(self):
self.assertIn("Available now on Linux (X11 and Wayland)", self.about)
self.assertIn("Voice dictation for Linux (X11 and Wayland)", self.about)
self.assertIn("After a model is downloaded", self.about)
self.assertIn("Private dictation for Linux, Mac, Windows, and phone.", self.about)
self.assertIn("Android beta / iOS TestFlight", self.source)
self.assertIn("Windows, unsigned beta", self.source)
self.assertIn("Self-hosted, headless", self.source)
self.assertNotIn("iOS source build", self.source)
self.assertNotIn("SmartScreen", self.about)
self.assertNotIn("100% offline", self.about)
@@ -1367,28 +1363,44 @@ class TestAboutPage(unittest.TestCase):
# License is named once, on the License row, not in the intro or links.
self.assertEqual(self.about.count("GNU AGPL v3"), 1)
def test_family_and_talk_urls(self):
for url in (
"https://voculinux.com",
"https://vocahq.com",
"https://vocamac.com",
"https://vocawin.com",
"https://vocaphone.vocahq.com",
"https://vocagateway.vocahq.com",
"https://github.com/VocaHQ/vocalinux/issues",
"https://discord.gg/t6muquAJbm",
"https://x.com/vocahq",
"mailto:hello@vocahq.com",
):
self.assertIn(url, self.source)
def test_links_point_at_the_fork_and_one_credit(self):
from vocalinux.ui.settings_about import (
SOURCE_ISSUES_URL,
SOURCE_REPO_URL,
UPSTREAM_CREDIT_LINE,
UPSTREAM_REPO_URL,
VOCULINUX_SITE_URL,
)
self.assertEqual(VOCULINUX_SITE_URL, "https://voculinux.com")
self.assertEqual(SOURCE_REPO_URL, "https://git.bongbetic.com/xavierk/voculinux")
self.assertEqual(SOURCE_ISSUES_URL, f"{SOURCE_REPO_URL}/issues")
self.assertEqual(UPSTREAM_REPO_URL, "https://github.com/VocaHQ/vocalinux")
self.assertEqual(UPSTREAM_CREDIT_LINE.count(UPSTREAM_REPO_URL), 1)
self.assertIn('label="Report a bug or idea"', self.about)
self.assertIn("Join Discord", self.about)
self.assertIn("Follow on X", self.about)
self.assertIn("Email us", self.about)
self.assertIn("https://github.com/VocaHQ/vocalinux/issues", self.about)
self.assertIn("https://discord.gg/t6muquAJbm", self.source)
self.assertIn("https://x.com/vocahq", self.source)
self.assertIn("mailto:hello@vocahq.com", self.source)
self.assertIn("SOURCE_ISSUES_URL", self.about)
self.assertIn("UPSTREAM_CREDIT_LINE", self.about)
def test_family_and_social_links_are_gone(self):
"""Voca family sites and social channels are not Voculinux's to offer."""
for banned in (
"Part of VocaHQ",
"_VOCAHQ_FAMILY_LINKS",
"_family_tile",
"family_grid",
"Join Discord",
"Follow on X",
"Email us",
"discord.gg",
"x.com/",
"mailto:",
"vocahq.com",
"vocamac.com",
"vocawin.com",
"vocaphone.vocahq.com",
"vocagateway.vocahq.com",
):
self.assertNotIn(banned, self.source, banned)
def test_open_buttons_have_unique_accessible_names(self):
helper = self.source.split("def _about_open_button")[1].split("\n def ")[0]
@@ -1396,13 +1408,9 @@ class TestAboutPage(unittest.TestCase):
self.assertIn('if label == "Open"', helper)
for name in (
"Open voculinux.com website",
"Open vocahq.com",
"Open voculinux.com",
"Open vocamac.com",
"Open vocawin.com",
"Open vocaphone.vocahq.com",
"Open vocagateway.vocahq.com",
"Open the Voculinux GitHub repository",
"Open the Voculinux source repository",
"Open Voculinux issues for a bug or idea",
"Open the upstream project repository",
"Open the latest release page",
):
self.assertIn(name, self.source)
@@ -1412,12 +1420,13 @@ class TestAboutPage(unittest.TestCase):
self.assertIn("VOCULINUX_SITE_URL", self.about)
self.assertIn("Product site at voculinux.com", self.about)
self.assertNotIn("Open the Voculinux GitHub page", self.about)
self.assertNotIn("GitHub", self.about)
def test_source_code_row_sits_beside_website(self):
self.assertIn('label="Website"', self.about)
self.assertIn('label="Source code"', self.about)
self.assertIn("GITHUB_REPO_URL", self.about)
self.assertIn("Open the Voculinux GitHub repository", self.about)
self.assertIn("SOURCE_REPO_URL", self.about)
self.assertIn("Open the Voculinux source repository", self.about)
self.assertLess(self.about.find('label="Website"'), self.about.find('label="Source code"'))
def test_talk_to_us_sits_above_updates(self):
@@ -1434,52 +1443,34 @@ class TestAboutPage(unittest.TestCase):
self.assertNotIn('title="Latest Release"', self.about)
self.assertIn("self.latest_release_row = self.release_notes_row", self.about)
def test_family_uses_platform_icons(self):
self.assertIn("def _family_tile", self.source)
self.assertIn("platform-linux", self.source)
self.assertIn("platform-apple", self.source)
self.assertIn("platform-windows", self.source)
self.assertIn("platform-android", self.source)
self.assertIn("platform-server", self.source)
self.assertIn("platform-home", self.source)
self.assertIn('("platform-android", "platform-apple")', self.source)
self.assertIn("family_grid.attach", self.about)
self.assertIn("image.set_margin_end(8)", self.source)
helper = self.source.split("def _family_tile")[1].split("\n def ")[0]
self.assertIn("spacing=16", helper)
def test_origin_group_names_the_upstream_credit_once(self):
self.assertIn("origin_group = PreferencesGroup(", self.about)
self.assertIn("UPSTREAM_CREDIT_LINE", self.about)
self.assertIn("UPSTREAM_REPO_URL", self.about)
self.assertLess(
self.about.find('title="Updates"'),
self.about.find('title="Origin"'),
)
def test_can_open_about_urls(self):
from vocalinux.ui.settings_dialog import (
GITHUB_ISSUES_URL,
GITHUB_REPO_URL,
VOCAGATEWAY_SITE_URL,
VOCAHQ_DISCORD_URL,
VOCAHQ_MAILTO_URL,
VOCAHQ_SITE_URL,
VOCAHQ_X_URL,
VOCAMAC_SITE_URL,
VOCAPHONE_SITE_URL,
VOCAWIN_SITE_URL,
SOURCE_ISSUES_URL,
SOURCE_REPO_URL,
UPSTREAM_REPO_URL,
VOCULINUX_SITE_URL,
_can_open_url,
)
for url in (
VOCULINUX_SITE_URL,
VOCAHQ_SITE_URL,
VOCAMAC_SITE_URL,
VOCAWIN_SITE_URL,
VOCAPHONE_SITE_URL,
VOCAGATEWAY_SITE_URL,
GITHUB_REPO_URL,
GITHUB_ISSUES_URL,
VOCAHQ_DISCORD_URL,
VOCAHQ_X_URL,
VOCAHQ_MAILTO_URL,
SOURCE_REPO_URL,
SOURCE_ISSUES_URL,
UPSTREAM_REPO_URL,
"https://git.bongbetic.com/xavierk/voculinux/releases/tag/v0.15.0",
):
self.assertTrue(_can_open_url(url), url)
self.assertFalse(_can_open_url("https://github.com/VocaHQ/vocalinux/releases/tag/v0.15.0"))
self.assertFalse(_can_open_url("https://vocahq.com"))
self.assertFalse(_can_open_url("https://evil.example/payload"))
self.assertFalse(_can_open_url("mailto:evil@example.com"))
self.assertFalse(_can_open_url(""))
@@ -1594,7 +1585,6 @@ class TestAboutPage(unittest.TestCase):
self.assertIn("def _about_ink_hex(", self.source)
self.assertIn("def _about_surface_is_dark(", self.source)
self.assertIn('replace("currentColor", ink)', self.source)
self.assertIn("_FAMILY_ICON_PX", self.source)
class TestUpdatePreferences(unittest.TestCase):