diff --git a/actions/ChangeTextChannel.py b/actions/ChangeTextChannel.py index 794c13c..0a2e88e 100644 --- a/actions/ChangeTextChannel.py +++ b/actions/ChangeTextChannel.py @@ -1,60 +1,46 @@ -from gi.repository import Gtk, Adw - -from src.backend.PluginManager.ActionBase import ActionBase -from ..discordrpc.commands import VOICE_CHANNEL_SELECT +from enum import StrEnum from loguru import logger as log +from .DiscordCore import DiscordCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input + +from GtkHelper.GenerativeUI.EntryRow import EntryRow -class ChangeTextChannel(ActionBase): + +class ChangeTextChannel(DiscordCore): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.channel_id: str = None self.has_configuration = True - def on_ready(self): - self.load_config() - self.plugin_base.add_callback( - VOICE_CHANNEL_SELECT, self.update_display) - self.plugin_base.backend.register_callback( - VOICE_CHANNEL_SELECT, self.update_display) - - def update_display(self, value: dict): - if not self.plugin_base.backend: - self.show_error() - return - else: - self.hide_error() - - def on_tick(self): - self.update_display({}) - if self.channel_id: - self.set_label(self.channel_id) - else: - self.set_label(self.plugin_base.lm.get( - "actions.changetextchannel.update_channel")) - - def load_config(self): - settings = self.get_settings() - self.channel_id = settings.get('channel_id') + def create_generative_ui(self): + self._channel_row = EntryRow( + action_core=self, + var_name="change_text_channel.text", + default_value="", + title="change-channel-text", + auto_add=False, + complex_var_name=True, + ) def get_config_rows(self): - super_rows = super().get_config_rows() - - self.channel_id_row = Adw.EntryRow(title=self.plugin_base.lm.get( - "actions.changetextchannel.channel_id"), text=self.channel_id) - self.channel_id_row.connect("notify::text", self.on_change_channel_id) - - super_rows.append(self.channel_id_row) - return super_rows - - def on_change_channel_id(self, entry, _): - settings = self.get_settings() - settings["channel_id"] = entry.get_text() - self.set_settings(settings) - - def on_key_down(self): - settings = self.get_settings() - channel_id = settings.get('channel_id') - if not self.plugin_base.backend.change_text_channel(channel_id): - self.show_error(5) + return [self._channel_row._widget] + + def create_event_assigners(self): + self.event_manager.add_event_assigner( + EventAssigner( + id="change-channel", + ui_label="change-channel", + default_event=Input.Key.Events.DOWN, + callback=self._on_change_channel + ) + ) + + def _on_change_channel(self, _): + channel = self._channel_row.get_value() + try: + self.backend.change_text_channel(channel) + except Exception as ex: + log.error(ex) + self.show_error(3) diff --git a/actions/ChangeVoiceChannel.py b/actions/ChangeVoiceChannel.py new file mode 100644 index 0000000..ef9463f --- /dev/null +++ b/actions/ChangeVoiceChannel.py @@ -0,0 +1,50 @@ +from enum import StrEnum + +from loguru import logger as log + +from .DiscordCore import DiscordCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input + +from GtkHelper.GenerativeUI.EntryRow import EntryRow + + +class Icons(StrEnum): + CHANGE_VOICE = "" + + +class ChangeVoiceChannel(DiscordCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.has_configuration = True + + def create_generative_ui(self): + self._channel_row = EntryRow( + action_core=self, + var_name="change_voice_channel.text", + default_value="", + title="change-channel-voice", + auto_add=False, + complex_var_name=True, + ) + + def get_config_rows(self): + return [self._channel_row._widget] + + def create_event_assigners(self): + self.event_manager.add_event_assigner( + EventAssigner( + id="change-channel", + ui_label="change-channel", + default_event=Input.Key.Events.DOWN, + callback=self._on_change_channel + ) + ) + + def _on_change_channel(self, _): + channel = self._channel_row.get_value() + try: + self.backend.change_voice_channel(channel) + except Exception as ex: + log.error(ex) + self.show_error(3) diff --git a/actions/ChangeVoiceChannelAction.py b/actions/ChangeVoiceChannelAction.py deleted file mode 100644 index 47a70f7..0000000 --- a/actions/ChangeVoiceChannelAction.py +++ /dev/null @@ -1,77 +0,0 @@ -from gi.repository import Gtk, Adw - -from src.backend.PluginManager.ActionBase import ActionBase -from ..discordrpc.commands import VOICE_CHANNEL_SELECT -from src.backend.DeckManagement.InputIdentifier import InputEvent, Input - -from loguru import logger as log - - -class ChangeVoiceChannelAction(ActionBase): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.current_channel: str = None - self.channel_id: str = None - self.has_configuration = True - - def on_ready(self): - self.load_config() - self.plugin_base.add_callback( - VOICE_CHANNEL_SELECT, self.update_display) - self.plugin_base.backend.register_callback( - VOICE_CHANNEL_SELECT, self.update_display) - - def update_display(self, value: dict): - if not self.plugin_base.backend: - self.show_error() - return - else: - self.hide_error() - - def on_tick(self): - self.update_display({}) - if self.current_channel: - self.set_label(self.current_channel) - else: - self.set_label(self.plugin_base.lm.get( - "actions.changevoicechannel.update_channel")) - - def load_config(self): - settings = self.get_settings() - self.channel_id = settings.get('channel_id') - - def get_config_rows(self): - super_rows = super().get_config_rows() - - self.channel_id_row = Adw.EntryRow(title=self.plugin_base.lm.get( - "actions.changevoicechannel.channel_id"), text=self.channel_id) - self.channel_id_row.connect("notify::text", self.on_change_channel_id) - - super_rows.append(self.channel_id_row) - return super_rows - - def on_change_channel_id(self, entry, _): - settings = self.get_settings() - settings["channel_id"] = entry.get_text() - self.set_settings(settings) - - def on_key_down(self): - settings = self.get_settings() - channel_id = settings.get('channel_id') - self.plugin_base.backend.change_voice_channel(channel_id) - - def event_callback(self, event: InputEvent, data: dict = None): - if event == Input.Key.Events.DOWN: - self.on_key_down() - if event == Input.Key.Events.HOLD_START or event == Input.Dial.Events.HOLD_START: - self.on_key_hold_start() - if event == Input.Dial.Events.TURN_CW: - self.on_dial_turn(+1) - if event == Input.Dial.Events.TURN_CCW: - self.on_dial_turn(-1) - if event == Input.Dial.Events.DOWN: - self.on_dial_down() - - def on_key_hold_start(self): - if not self.plugin_base.backend.change_voice_channel(None): - self.show_error(5) diff --git a/actions/Deafen.py b/actions/Deafen.py new file mode 100644 index 0000000..f7d7905 --- /dev/null +++ b/actions/Deafen.py @@ -0,0 +1,62 @@ +from enum import StrEnum + +from loguru import logger as log + +from .DiscordCore import DiscordCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input + +from GtkHelper.GenerativeUI.EntryRow import EntryRow + +from ..discordrpc.commands import VOICE_SETTINGS_UPDATE + + +class Icons(StrEnum): + DEAFEN = "deafen" + UNDEAFEN = "undeafen" + + +class Deafen(DiscordCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.has_configuration = False + self._deafened: bool = False + self.icon_keys = [Icons.DEAFEN, Icons.UNDEAFEN] + self.current_icon = self.get_icon(Icons.DEAFEN) + self.icon_name = Icons.DEAFEN + + def on_ready(self): + super().on_ready() + self.plugin_base.add_callback( + VOICE_SETTINGS_UPDATE, self._update_display) + self.backend.register_callback( + VOICE_SETTINGS_UPDATE, self._update_display) + + def create_event_assigners(self): + self.event_manager.add_event_assigner( + EventAssigner( + id="toggle-deafen", + ui_label="toggle-deafen", + default_event=Input.Key.Events.DOWN, + callback=self._on_toggle + ) + ) + + def _on_toggle(self, _): + try: + self.backend.set_deafen(not self._deafened) + except Exception as ex: + log.error(ex) + self.show_error(3) + + def _update_display(self, value: dict): + if not self.backend: + self.show_error() + return + else: + self.hide_error() + self._deafened = value["deaf"] + icon = Icons.DEAFEN if not self._deafened else Icons.UNDEAFEN + self.icon_name = Icons(icon) + self.current_icon = self.get_icon(self.icon_name) + self.display_icon() diff --git a/actions/DeafenAction.py b/actions/DeafenAction.py deleted file mode 100644 index 1e63aa2..0000000 --- a/actions/DeafenAction.py +++ /dev/null @@ -1,115 +0,0 @@ -import os - -from gi.repository import Gtk, Adw - -from src.backend.PluginManager.ActionBase import ActionBase -from ..discordrpc.commands import VOICE_SETTINGS_UPDATE - -from loguru import logger as log - - -class DeafenAction(ActionBase): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.mode: str = 'Toggle' - self.deafened: bool = False - self.label_location: str = 'Bottom' - - def on_ready(self): - self.load_config() - self.plugin_base.add_callback( - VOICE_SETTINGS_UPDATE, self.update_display) - self.plugin_base.backend.register_callback( - VOICE_SETTINGS_UPDATE, self.update_display) - - def update_display(self, value: dict): - if not self.plugin_base.backend: - self.show_error() - return - else: - self.hide_error() - self.deafened = value['deaf'] - image = "undeafen.png" - if self.deafened: - image = "deafen.png" - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", image), size=0.85) - - def on_tick(self): - # We need to do this to help make sure icons are set on - # page transition. I don't like it, but it works for now - self.update_display({'deaf': self.deafened}) - if self.deafened: - self.set_label("Deafened", position=self.label_location.lower()) - else: - self.set_label( - "Not\nDeafened", position=self.label_location.lower()) - - def load_config(self): - settings = self.get_settings() - self.mode = settings.get('mode') - if not self.mode: - self.mode = 'Toggle' - self.label_location = settings.get('label_location') - if not self.label_location: - self.label_location = 'Bottom' - - def get_config_rows(self): - super_rows = super().get_config_rows() - self.action_model = Gtk.StringList() - self.mode_row = Adw.ComboRow( - model=self.action_model, title=self.plugin_base.lm.get("actions.deafen.choice.title")) - - index = 0 - found = 0 - for k in ['Deafen', 'Undeafen', 'Toggle']: - self.action_model.append(k) - if self.mode == k: - found = index - index += 1 - self.mode_row.set_selected(found) - self.mode_row.connect("notify::selected", self.on_change_mode) - - index = 0 - found = 0 - self.label_model = Gtk.StringList() - self.label_row = Adw.ComboRow( - model=self.label_model, title=self.plugin_base.lm.get("actions.deafen.label_choice.title")) - for k in ['Top', 'Center', 'Bottom', 'None']: - self.label_model.append(k) - if self.label_location == k: - found = index - index += 1 - self.label_row.set_selected(found) - self.label_row.connect("notify::selected", self.on_change_label_row) - - super_rows.append(self.mode_row) - super_rows.append(self.label_row) - return super_rows - - def on_change_mode(self, *_): - settings = self.get_settings() - selected_index = self.mode_row.get_selected() - settings['mode'] = self.action_model[selected_index].get_string() - self.mode = settings['mode'] - self.set_settings(settings) - - def on_change_label_row(self, *_): - self.set_label('', position=self.label_location.lower()) - settings = self.get_settings() - selected_index = self.label_row.get_selected() - settings['label_location'] = self.label_model[selected_index].get_string() - self.label_location = settings['label_location'] - self.set_settings(settings) - - def on_key_down(self): - match self.mode: - case "Deafen": - if not self.plugin_base.backend.set_deafen(True): - self.show_error(5) - case "Undeafen": - if not self.plugin_base.backend.set_deafen(False): - self.show_error(5) - case "Toggle": - if not self.plugin_base.backend.set_deafen(not self.deafened): - self.show_error(5) diff --git a/actions/DiscordCore.py b/actions/DiscordCore.py new file mode 100644 index 0000000..076464a --- /dev/null +++ b/actions/DiscordCore.py @@ -0,0 +1,77 @@ +from loguru import logger as log +from src.backend.PluginManager.ActionCore import ActionCore +from src.backend.DeckManagement.InputIdentifier import InputEvent, Input +from src.backend.PluginManager.PluginSettings.Asset import Color, Icon + +from gi.repository import Gtk, Adw +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + + +class DiscordCore(ActionCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Setup AssetManager values + self.icon_keys: list[str] = [] + self.color_keys: list[str] = [] + self.current_icon: Icon = None + self.current_color: Color = None + self.icon_name: str = "" + self.color_name: str = "" + self.backend: 'Backend' = self.plugin_base.backend + + self.plugin_base.asset_manager.icons.add_listener(self._icon_changed) + self.plugin_base.asset_manager.colors.add_listener(self._color_changed) + + self.create_generative_ui() + self.create_event_assigners() + + def on_ready(self): + super().on_ready() + self.display_icon() + self.display_color() + + def create_generative_ui(self): + pass + + def create_event_assigners(self): + pass + + def display_icon(self): + if not self.current_icon: + return + _, rendered = self.current_icon.get_values() + if rendered: + self.set_media(image=rendered) + + async def _icon_changed(self, event: str, key: str, asset: Icon): + if not key in self.icon_keys: + return + if key != self.icon_name: + return + self.current_icon = asset + self.icon_name = key + self.display_icon() + + def display_color(self): + if not self.current_color: + return + color = self.current_color.get_values() + try: + self.set_background_color(color) + except: + # Sometimes we try to call this too early, and it leads to + # console errors, but no real impact. Ignoring this for now + pass + + async def _color_changed(self, event: str, key: str, asset: Color): + if not key in self.color_keys: + return + if key != self.color_name: + return + self.current_color = asset + self.color_name = key + self.display_color() diff --git a/actions/Mute.py b/actions/Mute.py new file mode 100644 index 0000000..c2ec7be --- /dev/null +++ b/actions/Mute.py @@ -0,0 +1,62 @@ +from enum import StrEnum + +from loguru import logger as log + +from .DiscordCore import DiscordCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input + +from GtkHelper.GenerativeUI.EntryRow import EntryRow + +from ..discordrpc.commands import VOICE_SETTINGS_UPDATE + + +class Icons(StrEnum): + MUTE = "mute" + UNMUTE = "unmute" + + +class Mute(DiscordCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.has_configuration = False + self._muted: bool = False + self.icon_keys = [Icons.MUTE, Icons.UNMUTE] + self.current_icon = self.get_icon(Icons.MUTE) + self.icon_name = Icons.MUTE + + def on_ready(self): + super().on_ready() + self.plugin_base.add_callback( + VOICE_SETTINGS_UPDATE, self._update_display) + self.backend.register_callback( + VOICE_SETTINGS_UPDATE, self._update_display) + + def create_event_assigners(self): + self.event_manager.add_event_assigner( + EventAssigner( + id="toggle-mute", + ui_label="toggle-mute", + default_event=Input.Key.Events.DOWN, + callback=self._on_toggle + ) + ) + + def _on_toggle(self, _): + try: + self.backend.set_mute(not self._muted) + except Exception as ex: + log.error(ex) + self.show_error(3) + + def _update_display(self, value: dict): + if not self.backend: + self.show_error() + return + else: + self.hide_error() + self._muted = value["mute"] + icon = Icons.MUTE if not self._muted else Icons.UNMUTE + self.icon_name = Icons(icon) + self.current_icon = self.get_icon(self.icon_name) + self.display_icon() diff --git a/actions/MuteAction.py b/actions/MuteAction.py deleted file mode 100644 index 9e46fd4..0000000 --- a/actions/MuteAction.py +++ /dev/null @@ -1,133 +0,0 @@ -import os - -from gi.repository import Gtk, Adw, Gio, GObject - -from src.backend.PluginManager.ActionBase import ActionBase -from ..discordrpc.commands import VOICE_SETTINGS_UPDATE - -from loguru import logger as log - - -class MuteAction(ActionBase): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.mode: str = 'Toggle' - self.muted: bool = False - self.label_location: str = 'Bottom' - - def on_ready(self): - self.load_config() - self.plugin_base.add_callback( - VOICE_SETTINGS_UPDATE, self.update_display) - self.plugin_base.backend.register_callback( - VOICE_SETTINGS_UPDATE, self.update_display) - - def update_display(self, value: dict): - if not self.plugin_base.backend: - self.show_error() - return - else: - self.hide_error() - self.muted = value['mute'] - image = "unmute.png" - if self.muted: - image = "mute.png" - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", image), size=0.85) - - def on_tick(self): - # We need to do this to help make sure icons are set on - # page transition. I don't like it, but it works for now - self.update_display({'mute': self.muted}) - if self.muted: - self.set_label("Muted", position=self.label_location.lower()) - else: - self.set_label("Unmuted", position=self.label_location.lower()) - - def load_config(self): - settings = self.get_settings() - self.mode = settings.get('mode') - if not self.mode: - self.mode = 'Toggle' - self.label_location = settings.get('label_location') - if not self.label_location: - self.label_location = 'Bottom' - - def get_config_rows(self): - super_rows = super().get_config_rows() - - self.action_model = Gtk.StringList() - self.mode_row = Adw.ComboRow( - model=self.action_model, title=self.plugin_base.lm.get("actions.mute.choice.title")) - - index = 0 - found = 0 - for k in ['Mute', 'Unmute', 'Toggle']: - self.action_model.append(k) - if self.mode == k: - found = index - index += 1 - self.mode_row.set_selected(found) - self.mode_row.connect("notify::selected", self.on_change_mode) - - found = 0 - index = 0 - self.label_model = Gtk.StringList() - self.label_row = Adw.ComboRow( - model=self.label_model, title=self.plugin_base.lm.get("actions.mute.label_choice.title")) - for k in ['Top', 'Center', 'Bottom', 'None']: - self.label_model.append(k) - if self.label_location == k: - found = index - index += 1 - self.label_row.set_selected(found) - self.label_row.connect("notify::selected", self.on_change_label_row) - - # icon_box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 1) - # icon_box.set_spacing(18) - # icon_box.set_halign(Gtk.Align.CENTER) - # for title, icon in {"Mute": "Discord_Mic_-_Off.png", "Unmute": "Discord_Mic_-_On.png"}.items(): - # box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 1) - # box.append(Gtk.Label.new(f"{title} Icon")) - # image = Gtk.Image.new_from_file(os.path.join( - # self.plugin_base.PATH, "assets", icon)) - # image.set_pixel_size(32) - # box.append(image) - # button = Gtk.Button.new() - # button.set_child(box) - # icon_box.append(button) - - # icon_row = Adw.PreferencesRow() - # icon_row.set_child(icon_box) - - super_rows.append(self.mode_row) - super_rows.append(self.label_row) - # super_rows.append(icon_row) - return super_rows - - def on_change_mode(self, *_): - settings = self.get_settings() - selected_index = self.mode_row.get_selected() - settings['mode'] = self.action_model[selected_index].get_string() - self.mode = settings['mode'] - self.set_settings(settings) - - def on_change_label_row(self, *_): - self.set_label('', position=self.label_location.lower()) - settings = self.get_settings() - selected_index = self.label_row.get_selected() - settings['label_location'] = self.label_model[selected_index].get_string() - self.label_location = settings['label_location'] - self.set_settings(settings) - - def on_key_down(self): - match self.mode: - case "Mute": - if not self.plugin_base.backend.set_mute(True): - self.show_error(5) - case "Unmute": - if not self.plugin_base.backend.set_mute(False): - self.show_error(5) - case "Toggle": - if not self.plugin_base.backend.set_mute(not self.muted): - self.show_error(5) diff --git a/actions/TogglePTT.py b/actions/TogglePTT.py new file mode 100644 index 0000000..2403d8c --- /dev/null +++ b/actions/TogglePTT.py @@ -0,0 +1,66 @@ +from enum import StrEnum + +from loguru import logger as log + +from .DiscordCore import DiscordCore +from src.backend.PluginManager.EventAssigner import EventAssigner +from src.backend.PluginManager.InputBases import Input + +from ..discordrpc.commands import VOICE_SETTINGS_UPDATE + + +class ActivityMethod(StrEnum): + VA = "VOICE_ACTIVITY" + PTT = "PUSH_TO_TALK" + + +class Icons(StrEnum): + VOICE = "voice" + PTT = "ptt" + + +class TogglePTT(DiscordCore): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.has_configuration = False + self._mode: str = ActivityMethod.PTT + self.icon_keys = [Icons.VOICE, Icons.PTT] + self.current_icon = self.get_icon(Icons.VOICE) + self.icon_name = Icons.VOICE + + def on_ready(self): + super().on_ready() + self.plugin_base.add_callback( + VOICE_SETTINGS_UPDATE, self._update_display) + self.backend.register_callback( + VOICE_SETTINGS_UPDATE, self._update_display) + + def create_event_assigners(self): + self.event_manager.add_event_assigner( + EventAssigner( + id="toggle-ptt", + ui_label="toggle-ptt", + default_event=Input.Key.Events.DOWN, + callback=self._on_toggle + ) + ) + + def _on_toggle(self, _): + new = ActivityMethod.PTT if self._mode == ActivityMethod.VA else ActivityMethod.VA + try: + self.backend.set_push_to_talk(str(new)) + except Exception as ex: + log.error(ex) + self.show_error(3) + + def _update_display(self, value: dict): + if not self.backend: + self.show_error() + return + else: + self.hide_error() + self._mode = value["mode"]["type"] + icon = Icons.VOICE if self._mode == ActivityMethod.PTT else Icons.PTT + self.icon_name = Icons(icon) + self.current_icon = self.get_icon(self.icon_name) + self.display_icon() diff --git a/actions/TogglePushToTalkAction.py b/actions/TogglePushToTalkAction.py deleted file mode 100644 index a742853..0000000 --- a/actions/TogglePushToTalkAction.py +++ /dev/null @@ -1,123 +0,0 @@ -import os - -from gi.repository import Gtk, Adw - -from src.backend.PluginManager.ActionBase import ActionBase -from ..discordrpc.commands import VOICE_SETTINGS_UPDATE - -from loguru import logger as log - - -class TogglePushToTalkAction(ActionBase): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.mode: str = 'Toggle' - self.ptt: bool = False - self.ptt_string: str = "" - self.label_location: str = 'Bottom' - self.ptt_lookup = { - "PUSH_TO_TALK": True, - "VOICE_ACTIVITY": False - } - self.has_configuration = True - - def on_ready(self): - self.load_config() - self.plugin_base.add_callback( - VOICE_SETTINGS_UPDATE, self.update_display) - self.plugin_base.backend.register_callback( - VOICE_SETTINGS_UPDATE, self.update_display) - - def update_display(self, value: dict): - if not self.plugin_base.backend: - self.show_error() - return - else: - self.hide_error() - self.ptt = self.ptt_lookup.get(value["mode"]["type"]) - self.ptt_string = value["mode"]["type"] - image = "voice_act.png" - if self.ptt: - image = "ptt.png" - self.set_media(media_path=os.path.join( - self.plugin_base.PATH, "assets", image), size=0.85) - - def on_tick(self): - # We need to do this to help make sure icons are set on - # page transition. I don't like it, but it works for now - self.update_display({'mode': {"type": self.ptt_string}}) - if self.ptt: - self.set_label("Push to\ntalk", - position=self.label_location.lower()) - else: - self.set_label( - "Voice\nActivity", position=self.label_location.lower()) - - def load_config(self): - settings = self.get_settings() - self.mode = settings.get('mode') - if not self.mode: - self.mode = 'Toggle' - self.label_location = settings.get('label_location') - if not self.label_location: - self.label_location = 'Bottom' - - def get_config_rows(self): - super_rows = super().get_config_rows() - self.action_model = Gtk.StringList() - self.mode_row = Adw.ComboRow( - model=self.action_model, title=self.plugin_base.lm.get("actions.ptt.choice.title")) - - index = 0 - found = 0 - for k in ['Enable', 'Disable', 'Toggle']: - self.action_model.append(k) - if self.mode == k: - found = index - index += 1 - self.mode_row.set_selected(found) - self.mode_row.connect("notify::selected", self.on_change_mode) - - index = 0 - found = 0 - self.label_model = Gtk.StringList() - self.label_row = Adw.ComboRow( - model=self.label_model, title=self.plugin_base.lm.get("actions.ptt.label_choice.title")) - for k in ['Top', 'Center', 'Bottom', 'None']: - self.label_model.append(k) - if self.label_location == k: - found = index - index += 1 - self.label_row.set_selected(found) - self.label_row.connect("notify::selected", self.on_change_label_row) - - super_rows.append(self.mode_row) - super_rows.append(self.label_row) - return super_rows - - def on_change_mode(self, *_): - settings = self.get_settings() - selected_index = self.mode_row.get_selected() - settings['mode'] = self.action_model[selected_index].get_string() - self.mode = settings['mode'] - self.set_settings(settings) - - def on_change_label_row(self, *_): - self.set_label('', position=self.label_location.lower()) - settings = self.get_settings() - selected_index = self.label_row.get_selected() - settings['label_location'] = self.label_model[selected_index].get_string() - self.label_location = settings['label_location'] - self.set_settings(settings) - - def on_key_down(self): - match self.mode: - case "Emable": - if not self.plugin_base.backend.set_push_to_talk("PUSH_TO_TALK"): - self.show_error(5) - case "Disable": - if not self.plugin_base.backend.set_push_to_talk("VOICE_ACTIVITY"): - self.show_error(5) - case "Toggle": - if not self.plugin_base.backend.set_push_to_talk("VOICE_ACTIVITY" if self.ptt == True else "PUSH_TO_TALK"): - self.show_error(5) diff --git a/assets/requirements.txt b/assets/requirements.txt index b95b9f2..b6b9589 100644 --- a/assets/requirements.txt +++ b/assets/requirements.txt @@ -5,5 +5,5 @@ loguru==0.7.2 plumbum==1.8.3 requests==2.32.3 rpyc==6.0.0 -streamcontroller-plugin-tools==2.0.1 +streamcontroller-plugin-tools==2.0.2 urllib3==2.2.2 diff --git a/backend.py b/backend.py index e1338c4..aea18f1 100644 --- a/backend.py +++ b/backend.py @@ -80,6 +80,9 @@ def setup_client(self): except Exception as ex: self.frontend.on_auth_callback(False, str(ex)) log.error("failed to setup discord client: {0}", ex) + if self.discord_client: + self.discord_client.disconnect() + self.discord_client = None def update_client_credentials(self, client_id: str, client_secret: str, access_token: str = "", refresh_token: str = ""): if None in (client_id, client_secret) or "" in (client_id, client_secret): @@ -102,57 +105,30 @@ def register_callback(self, key: str, callback: callable): if self._is_authed: self.discord_client.subscribe(key) - def set_mute(self, muted: bool) -> bool: + def set_mute(self, muted: bool): if self.discord_client is None or not self.discord_client.is_connected(): self.setup_client() - try: - self.discord_client.set_voice_settings({'mute': muted}) - except Exception as ex: - log.error("failed to set mute {0}", ex) - return False - return True + self.discord_client.set_voice_settings({'mute': muted}) - def set_deafen(self, muted: bool) -> bool: + def set_deafen(self, muted: bool): if self.discord_client is None or not self.discord_client.is_connected(): self.setup_client() - try: - self.discord_client.set_voice_settings({'deaf': muted}) - except Exception as ex: - log.error("failed to set deaf {0}", ex) - return False - return True + self.discord_client.set_voice_settings({'deaf': muted}) def change_voice_channel(self, channel_id: str = None) -> bool: if self.discord_client is None or not self.discord_client.is_connected(): self.setup_client() - try: - self.discord_client.select_voice_channel(channel_id, True) - except Exception as ex: - log.error( - "failed to change voice channel {0}. {1}", channel_id, ex) - return False - return True + self.discord_client.select_voice_channel(channel_id, True) def change_text_channel(self, channel_id: str) -> bool: if self.discord_client is None or not self.discord_client.is_connected(): self.setup_client() - try: - self.discord_client.select_text_channel(channel_id) - except Exception as ex: - log.error( - "failed to change text channel {0}. {1}", channel_id, ex) - return False - return True + self.discord_client.select_text_channel(channel_id) def set_push_to_talk(self, ptt: str) -> bool: if self.discord_client is None or not self.discord_client.is_connected(): self.setup_client() - try: - self.discord_client.set_voice_settings({'mode': {"type": ptt}}) - except Exception as ex: - log.error("failed to set push to talk {0}", ex) - return False - return True + self.discord_client.set_voice_settings({'mode': {"type": ptt}}) backend = Backend() diff --git a/discordrpc/asyncdiscord.py b/discordrpc/asyncdiscord.py index 8cb6251..570320e 100644 --- a/discordrpc/asyncdiscord.py +++ b/discordrpc/asyncdiscord.py @@ -41,7 +41,11 @@ def connect(self, callback: callable): self.rpc.connect() self.rpc.send({'v': 1, 'client_id': self.client_id}, OP_HANDSHAKE) _, resp = self.rpc.receive() - data = json.loads(resp) + try: + data = json.loads(resp) + except Exception as ex: + log.error(f"invalid response. {ex}") + raise RPCException if data.get('code') == 4000: raise InvalidID if data.get('cmd') != 'DISPATCH' or data.get('evt') != 'READY': @@ -56,6 +60,8 @@ def disconnect(self): def poll_callback(self, callback: callable): while self.polling: val = self.rpc.receive() + if val[0] == -1: + self.disconnect() callback(val[0], val[1]) def authorize(self): diff --git a/discordrpc/sockets.py b/discordrpc/sockets.py index 1e5c77f..432b682 100644 --- a/discordrpc/sockets.py +++ b/discordrpc/sockets.py @@ -3,19 +3,24 @@ import struct import json import re +import select from loguru import logger as log from .exceptions import DiscordNotOpened +SOCKET_DISCONNECTED: int = -1 + class UnixPipe: + def __init__(self): self.socket: socket.socket = None def connect(self): if self.socket is None: self.socket = socket.socket(socket.AF_UNIX) + self.socket.setblocking(False) base_path = path = os.environ.get('XDG_RUNTIME_DIR') or os.environ.get( 'TMPDIR') or os.environ.get('TMP') or os.environ.get('TEMP') or '/tmp' base_path = re.sub(r'\/$', '', path) + '/discord-ipc-{0}' @@ -26,6 +31,11 @@ def connect(self): break except FileNotFoundError: pass + except Exception as ex: + log.error( + f"failed to connect to socket {path}, trying next socket. {ex}") + # Skip all errors to try all sockets + pass else: raise DiscordNotOpened @@ -41,14 +51,18 @@ def send(self, payload, op): self.socket.send(payload) def receive(self) -> (int, str): + ready = select.select([self.socket], [], [], 1) + if not ready[0]: + return 0, {} data = self.socket.recv(1024) + if len(data) == 0: + return SOCKET_DISCONNECTED, {} header = data[:8] code = int.from_bytes(header[:4], "little") length = int.from_bytes(header[4:], "little") all_data = data[8:] buffer_size = length - len(all_data) if buffer_size < 0: - log.debug("buffer size too small") return 0, {} data = self.socket.recv(length-len(all_data)) all_data += data diff --git a/locales.csv b/locales.csv new file mode 100644 index 0000000..8484cb6 --- /dev/null +++ b/locales.csv @@ -0,0 +1,7 @@ +key;en_US +change-channel-text;Channel ID +change-channel-voice;Channel ID +change-channel;Change Channel +toggle-mute;Toggle Mute +toggle-deafen;Toggle Deafen +toggle-ptt;Toggle Push to Talk diff --git a/main.py b/main.py index ca09bc6..d9358c5 100644 --- a/main.py +++ b/main.py @@ -10,112 +10,124 @@ # Import actions from .settings import PluginSettings -from .actions.MuteAction import MuteAction -from .actions.DeafenAction import DeafenAction -from .actions.ChangeVoiceChannelAction import ChangeVoiceChannelAction +from .actions.Mute import Mute +from .actions.Deafen import Deafen +from .actions.ChangeVoiceChannel import ChangeVoiceChannel from .actions.ChangeTextChannel import ChangeTextChannel -from .actions.TogglePushToTalkAction import TogglePushToTalkAction +from .actions.TogglePTT import TogglePTT from loguru import logger as log class PluginTemplate(PluginBase): def __init__(self): - super().__init__() - + super().__init__(use_legacy_locale=False) self.callbacks = {} - self.auth_callback_fn: callable = None - self.lm = self.locale_manager self.lm.set_to_os_default() - self._settings_manager = PluginSettings(self) self.has_plugin_settings = True - self.message_mute_action_holder = ActionHolder( + self._add_icons() + self._setup_backend() + self._register_actions() + + try: + with open(os.path.join(self.PATH, "manifest.json"), "r", encoding="UTF-8") as f: + data = json.load(f) + except Exception as ex: + log.error(ex) + data = {} + app_manifest = { + "plugin_version": data.get("version", "0.0.0"), + "app_version": data.get("app-version", "0.0.0") + } + + self.register( + plugin_name="Discord", + github_repo="https://github.com/imdevinc/StreamControllerDiscordPlugin", + plugin_version=app_manifest.get("plugin_version"), + app_version=app_manifest.get("app_version") + ) + + self.add_css_stylesheet(os.path.join(self.PATH, "style.css")) + + def _add_icons(self): + self.add_icon("deafen", self.get_asset_path("deafen.png")) + self.add_icon("undeafen", self.get_asset_path("undeafen.png")) + self.add_icon("mute", self.get_asset_path("mute.png")) + self.add_icon("unmute", self.get_asset_path("unmute.png")) + self.add_icon("ptt", self.get_asset_path("ptt.png")) + self.add_icon("voice", self.get_asset_path("voice_act.png")) + + def _register_actions(self): + change_text = ActionHolder( plugin_base=self, - action_base=MuteAction, - action_id="com_imdevinc_StreamControllerDiscordPlugin::Mute", - action_name="Mute", + action_base=ChangeTextChannel, + action_id_suffix="ChangeText", + action_name="Change Text Channel", action_support={ Input.Key: ActionInputSupport.SUPPORTED, Input.Dial: ActionInputSupport.UNTESTED, Input.Touchscreen: ActionInputSupport.UNTESTED, } ) - self.add_action_holder(self.message_mute_action_holder) + self.add_action_holder(change_text) - self.message_deafen_action_holder = ActionHolder( + change_voice = ActionHolder( plugin_base=self, - action_base=DeafenAction, - action_id="com_imdevinc_StreamControllerDiscordPlugin::Deafen", - action_name="Deafen", + action_base=ChangeVoiceChannel, + action_id_suffix="ChangeVoice", + action_name="Change Voice Channel", action_support={ Input.Key: ActionInputSupport.SUPPORTED, Input.Dial: ActionInputSupport.UNTESTED, Input.Touchscreen: ActionInputSupport.UNTESTED, } ) - self.add_action_holder(self.message_deafen_action_holder) + self.add_action_holder(change_voice) - self.change_voice_channel_action = ActionHolder( + deafen = ActionHolder( plugin_base=self, - action_base=ChangeVoiceChannelAction, - action_id="com_imdevinc_StreamControllerDiscordPlugin::ChangeVoiceChannel", - action_name="Change Voice Channel", + action_base=Deafen, + action_id_suffix="Deafen", + action_name="Toggle Deafen", action_support={ Input.Key: ActionInputSupport.SUPPORTED, Input.Dial: ActionInputSupport.UNTESTED, Input.Touchscreen: ActionInputSupport.UNTESTED, } ) - self.add_action_holder(self.change_voice_channel_action) + self.add_action_holder(deafen) - self.change_text_channel_action = ActionHolder( + mute = ActionHolder( plugin_base=self, - action_base=ChangeTextChannel, - action_id="com_imdevinc_StreamControllerDiscordPlugin::ChangeTextChannel", - action_name="Change Text Channel", + action_base=Mute, + action_id_suffix="Mute", + action_name="Toggle Mute", action_support={ Input.Key: ActionInputSupport.SUPPORTED, Input.Dial: ActionInputSupport.UNTESTED, Input.Touchscreen: ActionInputSupport.UNTESTED, } ) - self.add_action_holder(self.change_text_channel_action) + self.add_action_holder(mute) - self.message_ptt_action_holder = ActionHolder( + toggle_ptt = ActionHolder( plugin_base=self, - action_base=TogglePushToTalkAction, - action_id="com_imdevinc_StreamControllerDiscordPlugin::Push_To_Talk", - action_name="Toggle push to talk", + action_base=TogglePTT, + action_id_suffix="TogglePTT", + action_name="Toggle PTT", action_support={ Input.Key: ActionInputSupport.SUPPORTED, Input.Dial: ActionInputSupport.UNTESTED, Input.Touchscreen: ActionInputSupport.UNTESTED, } ) - self.add_action_holder(self.message_ptt_action_holder) - - try: - with open(os.path.join(self.PATH, "manifest.json"), "r", encoding="UTF-8") as f: - data = json.load(f) - except Exception as ex: - log.error(ex) - data = {} - app_manifest = { - "plugin_version": data.get("version", "0.0.0"), - "app_version": data.get("app-version", "0.0.0") - } - - self.register( - plugin_name="Discord", - github_repo="https://github.com/imdevinc/StreamControllerDiscordPlugin", - plugin_version=app_manifest.get("plugin_version"), - app_version=app_manifest.get("app_version") - ) + self.add_action_holder(toggle_ptt) + def _setup_backend(self): settings = self.get_settings() client_id = settings.get('client_id', '') client_secret = settings.get('client_secret', '') @@ -125,12 +137,9 @@ def __init__(self): backend_path = os.path.join(self.PATH, 'backend.py') self.launch_backend(backend_path=backend_path, open_in_terminal=False, venv_path=os.path.join(self.PATH, '.venv')) - threading.Thread(target=self.backend.update_client_credentials, daemon=True, args=[ client_id, client_secret, access_token, refresh_token]).start() - self.add_css_stylesheet(os.path.join(self.PATH, "style.css")) - def save_access_token(self, access_token: str): settings = self.get_settings() settings['access_token'] = access_token diff --git a/manifest.json b/manifest.json index 5e1ed7f..8e2355c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "version": "1.4.2", + "version": "1.5.0", "thumbnail": "store/thumbnail.png", "id": "com_imdevinc_StreamControllerDiscordPlugin", "name": "Discord", @@ -8,8 +8,8 @@ "tags": [ "discord" ], - "minimum-app-version": "1.5.0-beta.8", - "app-version": "1.5.0-beta.8", + "minimum-app-version": "1.5.0-beta.9", + "app-version": "1.5.0-beta.9", "description": "Adds controls for Discord including mute, deafen, and change channel", "short-description": "Control Discord" }