feat(settings): even out dropdowns and tidy the About page (#754)
* feat(settings): even out dropdowns and tidy the About page Pin combos to a shared right-hand column so long labels cannot stretch a row. Fold the dictation-tone preview into a two-stage Start/Stop button, quiet the model status card, shorten Performance copy, and rebuild About with combined links, channel+Check, family platform tiles, and Talk to us moved up next to the license. * fix(settings): keep tone preview on start if the cue fails Do not flip Start/Stop when preview_tone_cue returns false, so a missing file or silent player cannot skip the start cue. Also annotate PreferenceRow.set_title / set_subtitle. * fix(settings): paint About icons for dark dialogs Dark ink on Adwaita/Yaru-dark buttons made the family tiles and Talk to us marks disappear. Pick a light foreground on dark themes (and keep the original ink on light ones) so the marks match the labels. * fix(settings): show Android and iOS marks on VocaPhone VocaPhone covers both, so the family tile now uses the Android robot and the Apple mark instead of a single phone icon. * fix(settings): cut repeated About copy Name the AGPL once on the License row. The VocaHQ blurb is just the private-dictation line. VocaPhone says TestFlight, not source-only, and Gateway is self-hosted and headless. * fix(settings): space About icons from their labels Talk-to-us buttons and the family tiles were packing the mark against the text. Give the image an end margin and a wider gap in the family tile.
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Android</title><path fill="currentColor" d="M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path fill="currentColor" d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>
|
||||
|
After Width: | Height: | Size: 678 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-hidden="true">
|
||||
<title>Home</title>
|
||||
<path fill="currentColor" d="M12 3.1 2.7 10.9c-.4.3-.4.9-.1 1.3.3.4.9.4 1.3.1l.6-.5V19c0 .8.7 1.5 1.5 1.5h4.1v-5.4h4V20.5h4.1c.8 0 1.5-.7 1.5-1.5v-7.2l.6.5c.4.3 1 .3 1.3-.1.3-.4.3-1-.1-1.3L12 3.1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 322 B |
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Server</title><path fill="currentColor" fill-rule="evenodd" d="M4 5h16a1.5 1.5 0 0 1 1.5 1.5v2A1.5 1.5 0 0 1 20 10H4A1.5 1.5 0 0 1 2.5 8.5v-2A1.5 1.5 0 0 1 4 5zm1.25 2.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zM4 14h16a1.5 1.5 0 0 1 1.5 1.5v2a1.5 1.5 0 0 1-1.5 1.5H4a1.5 1.5 0 0 1-1.5-1.5v-2A1.5 1.5 0 0 1 4 14zm1.25 2.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 453 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Android</title><path fill="currentColor" d="M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path fill="currentColor" d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>
|
||||
|
After Width: | Height: | Size: 678 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-hidden="true">
|
||||
<title>Home</title>
|
||||
<path fill="currentColor" d="M12 3.1 2.7 10.9c-.4.3-.4.9-.1 1.3.3.4.9.4 1.3.1l.6-.5V19c0 .8.7 1.5 1.5 1.5h4.1v-5.4h4V20.5h4.1c.8 0 1.5-.7 1.5-1.5v-7.2l.6.5c.4.3 1 .3 1.3-.1.3-.4.3-1-.1-1.3L12 3.1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 322 B |
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Server</title><path fill="currentColor" fill-rule="evenodd" d="M4 5h16a1.5 1.5 0 0 1 1.5 1.5v2A1.5 1.5 0 0 1 20 10H4A1.5 1.5 0 0 1 2.5 8.5v-2A1.5 1.5 0 0 1 4 5zm1.25 2.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zM4 14h16a1.5 1.5 0 0 1 1.5 1.5v2a1.5 1.5 0 0 1-1.5 1.5H4a1.5 1.5 0 0 1-1.5-1.5v-2A1.5 1.5 0 0 1 4 14zm1.25 2.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 453 B |
@@ -223,6 +223,15 @@ def play_error_sound():
|
||||
return _play_sound_file(ERROR_SOUND) if _is_sound_effects_enabled() else False
|
||||
|
||||
|
||||
def preview_tone_cue(tone_id: Optional[str], kind: str) -> bool:
|
||||
"""Play one catalog cue. ``kind`` is ``start`` or ``stop``. Off plays nothing."""
|
||||
tone = normalize_sound_effect_tone(tone_id if tone_id is not None else _resolved_tone())
|
||||
if tone == "off":
|
||||
return False
|
||||
cue = "stop" if kind == "stop" else "start"
|
||||
return bool(_play_sound_file(tone_sound_path(tone, cue)))
|
||||
|
||||
|
||||
def preview_tone(tone_id: Optional[str] = None) -> bool:
|
||||
"""Play start then stop for a catalog tone. Off plays nothing."""
|
||||
tone = normalize_sound_effect_tone(tone_id if tone_id is not None else _resolved_tone())
|
||||
|
||||
@@ -537,3 +537,16 @@ class TestAudioFeedback(unittest.TestCase):
|
||||
mock_play.reset_mock()
|
||||
self.assertTrue(audio_feedback.preview_tone("not-a-tone"))
|
||||
self.assertTrue(mock_play.call_args_list[0].args[0].endswith("voca_start.wav"))
|
||||
|
||||
def test_preview_tone_cue_plays_one_side(self):
|
||||
import vocalinux.ui.audio_feedback as audio_feedback
|
||||
|
||||
with patch.object(audio_feedback, "_play_sound_file", return_value=True) as mock_play:
|
||||
self.assertTrue(audio_feedback.preview_tone_cue("voca", "start"))
|
||||
self.assertTrue(mock_play.call_args_list[0].args[0].endswith("voca_start.wav"))
|
||||
mock_play.reset_mock()
|
||||
self.assertTrue(audio_feedback.preview_tone_cue("voca", "stop"))
|
||||
self.assertTrue(mock_play.call_args_list[0].args[0].endswith("voca_stop.wav"))
|
||||
mock_play.reset_mock()
|
||||
self.assertFalse(audio_feedback.preview_tone_cue("off", "start"))
|
||||
mock_play.assert_not_called()
|
||||
|
||||
@@ -815,9 +815,12 @@ class TestLanguageComboSearch(unittest.TestCase):
|
||||
self.assertIn("SOUND_EFFECT_TONES", source_code)
|
||||
self.assertIn('title="Dictation Tone"', source_code)
|
||||
self.assertIn("def _on_preview_tone_clicked", source_code)
|
||||
self.assertIn("preview_tone(tone_id)", source_code)
|
||||
self.assertIn("preview_tone_cue(tone_id, kind)", source_code)
|
||||
self.assertIn("if not preview_tone_cue(tone_id, kind):", source_code)
|
||||
self.assertIn("self._tone_preview_kind", source_code)
|
||||
self.assertNotIn("fifth", source_code)
|
||||
self.assertNotIn("01-linux-glide", source_code)
|
||||
self.assertNotIn('title="Preview Tone"', source_code)
|
||||
|
||||
|
||||
class TestSettingsSearch(unittest.TestCase):
|
||||
@@ -1041,15 +1044,17 @@ class TestAboutPage(unittest.TestCase):
|
||||
def test_product_status_and_honest_network_copy(self):
|
||||
self.assertIn("Available now on Linux (X11 and Wayland)", self.about)
|
||||
self.assertIn("After a model is downloaded", self.about)
|
||||
self.assertIn("unsigned beta, v0.1.0-beta.1", self.about)
|
||||
self.assertIn("Android beta / iOS source build", self.about)
|
||||
self.assertIn("optional", self.about.lower())
|
||||
self.assertIn("self-hosted compute", self.about)
|
||||
self.assertIn("Private dictation for Linux, Mac, Windows, and phone.", self.about)
|
||||
self.assertIn("Android beta / iOS TestFlight", 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)
|
||||
self.assertNotIn("fully offline", self.about)
|
||||
self.assertNotIn("Coming soon", self.about)
|
||||
self.assertNotIn("releases/download", self.about)
|
||||
# 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 (
|
||||
@@ -1065,9 +1070,9 @@ class TestAboutPage(unittest.TestCase):
|
||||
):
|
||||
self.assertIn(url, self.source)
|
||||
self.assertIn('label="Report a bug or idea"', self.about)
|
||||
self.assertIn('"Discord"', self.about)
|
||||
self.assertIn('"X"', self.about)
|
||||
self.assertIn('"Email"', 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)
|
||||
@@ -1096,10 +1101,38 @@ class TestAboutPage(unittest.TestCase):
|
||||
self.assertNotIn("Open the Vocalinux GitHub page", self.about)
|
||||
|
||||
def test_source_code_row_sits_beside_website(self):
|
||||
self.assertIn('title="Source code"', self.about)
|
||||
self.assertIn('label="Website"', self.about)
|
||||
self.assertIn('label="Source code"', self.about)
|
||||
self.assertIn("GITHUB_REPO_URL", self.about)
|
||||
self.assertIn("Open the Vocalinux GitHub repository", self.about)
|
||||
self.assertLess(self.about.find("Website"), self.about.find("Source code"))
|
||||
self.assertLess(self.about.find('label="Website"'), self.about.find('label="Source code"'))
|
||||
|
||||
def test_talk_to_us_sits_above_updates(self):
|
||||
self.assertLess(self.about.find('title="Talk to us"'), self.about.find('title="Updates"'))
|
||||
self.assertLess(
|
||||
self.about.find("self.about_tab.pack_start(talk_group"),
|
||||
self.about.find("self.about_tab.pack_start(updates_group"),
|
||||
)
|
||||
|
||||
def test_channel_check_shares_one_row(self):
|
||||
self.assertIn("_combo_with_suffix", self.about)
|
||||
self.assertIn("self.check_updates_btn", self.about)
|
||||
self.assertNotIn('title="Status"', self.about)
|
||||
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-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_can_open_about_urls(self):
|
||||
from vocalinux.ui.settings_dialog import (
|
||||
@@ -1210,6 +1243,93 @@ class TestAboutPage(unittest.TestCase):
|
||||
self.assertNotIn("<rect", packaged_svg)
|
||||
self.assertIn('get_icon_path("vocalinux")', self.about)
|
||||
|
||||
def test_platform_marks_are_currentcolor_svgs(self):
|
||||
from pathlib import Path
|
||||
|
||||
icons = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "src"
|
||||
/ "vocalinux"
|
||||
/ "resources"
|
||||
/ "icons"
|
||||
/ "scalable"
|
||||
)
|
||||
root_icons = Path(__file__).resolve().parents[1] / "resources" / "icons" / "scalable"
|
||||
for name, title in (
|
||||
("platform-linux", "Linux"),
|
||||
("platform-apple", "Apple"),
|
||||
("platform-android", "Android"),
|
||||
("platform-server", "Server"),
|
||||
("platform-home", "Home"),
|
||||
):
|
||||
packaged_path = icons / f"{name}.svg"
|
||||
shipped_path = root_icons / f"{name}.svg"
|
||||
packaged = packaged_path.read_text(encoding="utf-8")
|
||||
shipped = shipped_path.read_text(encoding="utf-8")
|
||||
self.assertEqual(packaged, shipped, name)
|
||||
self.assertIn('viewBox="0 0 24 24"', packaged, name)
|
||||
self.assertIn('fill="currentColor"', packaged, name)
|
||||
self.assertIn(f"<title>{title}</title>", packaged, name)
|
||||
|
||||
def test_about_marks_use_theme_aware_ink(self):
|
||||
self.assertIn("_ABOUT_INK_ON_DARK", self.source)
|
||||
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 TestAboutInk(unittest.TestCase):
|
||||
"""Ink for About marks follows light vs dark surfaces."""
|
||||
|
||||
def test_dark_theme_pref_picks_light_ink(self):
|
||||
from vocalinux.ui.settings_dialog import (
|
||||
_ABOUT_INK,
|
||||
_ABOUT_INK_ON_DARK,
|
||||
_about_ink_hex,
|
||||
_about_surface_is_dark,
|
||||
)
|
||||
|
||||
self.assertNotEqual(_ABOUT_INK, _ABOUT_INK_ON_DARK)
|
||||
self.assertTrue(_ABOUT_INK_ON_DARK.startswith("#"))
|
||||
if _about_surface_is_dark():
|
||||
self.assertEqual(_about_ink_hex(), _ABOUT_INK_ON_DARK)
|
||||
else:
|
||||
self.assertIn(_about_ink_hex(), {_ABOUT_INK, _ABOUT_INK_ON_DARK})
|
||||
|
||||
|
||||
class TestSettingsControlColumn(unittest.TestCase):
|
||||
"""Dropdowns, steppers, and action buttons share one right-hand column."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.source = TestSettingsSearch._settings_source()
|
||||
|
||||
def test_style_helpers_exist(self):
|
||||
self.assertIn("def _style_combo(", self.source)
|
||||
self.assertIn("def _style_spin(", self.source)
|
||||
self.assertIn("def _combo_with_suffix(", self.source)
|
||||
self.assertIn("ellipsize", self.source)
|
||||
self.assertIn("Pango.EllipsizeMode.END", self.source)
|
||||
|
||||
def test_combos_use_style_helper(self):
|
||||
for name in (
|
||||
"self.engine_combo",
|
||||
"self.model_combo",
|
||||
"self.model_variant_combo",
|
||||
"self.language_combo",
|
||||
"self.paste_shortcut_combo",
|
||||
"self.shortcut_mode_combo",
|
||||
"self.shortcut_combo",
|
||||
"self.gpu_device_combo",
|
||||
"self.model_keepalive_timeout_combo",
|
||||
):
|
||||
self.assertIn(f"_style_combo({name})", self.source, name)
|
||||
|
||||
def test_model_card_has_no_legend(self):
|
||||
self.assertNotIn("✓ Downloaded ↓ Will download ★ Recommended", self.source)
|
||||
self.assertNotIn("Tip: <b>", self.source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -43,7 +43,7 @@ class TestSettingsDialogShortcutsSection(unittest.TestCase):
|
||||
|
||||
def test_shortcut_combo_size_request(self):
|
||||
"""Test that shortcut combo uses the shared control width."""
|
||||
self.assertIn("self.shortcut_combo.set_size_request(_CONTROL_WIDTH, -1)", self.source_code)
|
||||
self.assertIn("_style_combo(self.shortcut_combo)", self.source_code)
|
||||
|
||||
def test_shortcut_combo_tooltip(self):
|
||||
"""Test that shortcut combo has a tooltip."""
|
||||
|
||||