diff --git a/README.md b/README.md index 9c1543f..448be15 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,14 @@ All messages will saved in a SQLite DB and restored upon relaunch of the app. Y By navigating to Settings -> App Settings, you may customize your UI's icons, colors, and more! +### Notification sounds + +Add audio files to Contact's `contact/sounds/` folder, then choose one in Settings → App Settings → Notification sound. `alert.mp3` is the default selection; choose `None` to disable notification audio. Supported file types are MP3, WAV, OGG, AIFF, and FLAC. + +### Remote administration + +With the Nodes pane selected, press `` ` `` or `F12` and choose **Remote admin** for the highlighted node. Contact requests the node's settings through Meshtastic Remote Admin and displays an error if the node does not authorize your public key. Remote App Settings are intentionally unavailable because they apply only to the local Contact client. + For smaller displays you may wish to enable `single_pane_mode`: Screenshot 2025-08-22 at 11 15 54 PM diff --git a/contact/localisations/en.ini b/contact/localisations/en.ini index 38ceadc..b7f47d4 100644 --- a/contact/localisations/en.ini +++ b/contact/localisations/en.ini @@ -112,7 +112,7 @@ language, "Language", "UI language for labels and help text." message_prefix, "Message prefix", "" sent_message_prefix, "Sent message prefix", "" notification_symbol, "Notification symbol", "" -notification_sound, "Notification sound", "" +notification_sound, "Notification sound", "Select a sound file from Contact's sounds folder, or None to disable notification audio." ack_implicit_str, "ACK (implicit)", "" ack_str, "ACK", "" nak_str, "NAK", "" diff --git a/contact/message_handlers/rx_handler.py b/contact/message_handlers/rx_handler.py index 44c8b27..6ffb1e5 100644 --- a/contact/message_handlers/rx_handler.py +++ b/contact/message_handlers/rx_handler.py @@ -66,29 +66,15 @@ from contact.message_handlers.bot_handler import bot_respond def play_sound(): try: - system = platform.system() - sound_path = None - executable = None + selected_sound = str(getattr(config, "notification_sound", "None")) + if selected_sound.casefold() in {"none", "false", ""}: + return + sound_path = os.path.join(config.parent_dir, "sounds", selected_sound) + if not os.path.isfile(sound_path): + logging.warning("Configured notification sound is unavailable: %s", sound_path) + return - if system == "Darwin": # macOS - sound_path = "/System/Library/Sounds/Ping.aiff" - executable = "afplay" - - elif system == "Linux": - ogg_path = "/usr/share/sounds/freedesktop/stereo/complete.oga" - wav_path = "/usr/share/sounds/alsa/Front_Center.wav" # common fallback - - if shutil.which("paplay") and os.path.exists(ogg_path): - executable = "paplay" - sound_path = ogg_path - elif shutil.which("ffplay") and os.path.exists(ogg_path): - executable = "ffplay" - sound_path = ogg_path - elif shutil.which("aplay") and os.path.exists(wav_path): - executable = "aplay" - sound_path = wav_path - else: - logging.warning("No suitable sound player or sound file found on Linux") + executable = "afplay" if platform.system() == "Darwin" else shutil.which("ffplay") or shutil.which("mpg123") if executable and sound_path: cmd = [executable, sound_path] @@ -97,6 +83,7 @@ def play_sound(): subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return + logging.warning("No suitable sound player found for notification sound") except subprocess.CalledProcessError as e: logging.error(f"Sound playback failed: {e}") @@ -144,7 +131,7 @@ def on_receive(packet: Dict[str, Any], interface: Any) -> None: hops = hop_start - hop_limit - if config.notification_sound == "True": + if str(config.notification_sound).casefold() not in {"none", "false", ""}: schedule_notification_sound() message_bytes = packet["decoded"]["payload"] diff --git a/contact/sounds/alert.mp3 b/contact/sounds/alert.mp3 new file mode 100644 index 0000000..fc30cfd Binary files /dev/null and b/contact/sounds/alert.mp3 differ diff --git a/contact/ui/default_config.py b/contact/ui/default_config.py index 702b080..1a5587a 100644 --- a/contact/ui/default_config.py +++ b/contact/ui/default_config.py @@ -235,7 +235,7 @@ def initialize_config() -> Dict[str, object]: "message_prefix": ">>", "sent_message_prefix": ">> Sent", "notification_symbol": "*", - "notification_sound": "True", + "notification_sound": "alert.mp3", "ack_implicit_str": "[◌]", "ack_str": "[✓]", "nak_str": "[x]", @@ -264,6 +264,15 @@ def initialize_config() -> Dict[str, object]: # Check and add missing variables updated = update_dict(default_config_variables, loaded_config) + # Migrate the legacy boolean notification preference to the file-based + # sound selection introduced in 1.5.12. + if loaded_config.get("notification_sound") == "True": + loaded_config["notification_sound"] = "alert.mp3" + updated = True + elif loaded_config.get("notification_sound") == "False": + loaded_config["notification_sound"] = "None" + updated = True + # Update the JSON file if any variables were missing if updated: formatted_json = format_json_single_line_arrays(loaded_config) diff --git a/contact/ui/user_config.py b/contact/ui/user_config.py index ad04a2e..b0336c7 100644 --- a/contact/ui/user_config.py +++ b/contact/ui/user_config.py @@ -115,9 +115,23 @@ def edit_color_pair(key: str, display_label: str, current_value: List[str]) -> L def edit_value(key: str, display_label: str, current_value: str) -> str: - if key in ("notification_sound", "single_pane_mode", "enabled"): + if key in ("single_pane_mode", "enabled"): return get_list_input(display_label, current_value, ["True", "False"]) + if key == "notification_sound": + sounds_dir = os.path.join(config.parent_dir, "sounds") + try: + sound_options = sorted( + filename for filename in os.listdir(sounds_dir) + if filename.lower().endswith((".mp3", ".wav", ".ogg", ".aiff", ".flac")) + ) + except OSError: + sound_options = [] + sound_options = ["None"] + sound_options + if current_value not in sound_options: + sound_options.append(current_value) + return get_list_input(display_label, current_value, sound_options) + w = get_effective_width() height = 10 input_width = w - 16 # Allow space for "New Value: "