Skip to content

Commit e25ea5a

Browse files
authored
Merge pull request #41 from oparada1988/main
Update to Plugin
2 parents 3ad49f5 + 7b4e5e4 commit e25ea5a

27 files changed

Lines changed: 916 additions & 374 deletions

File tree

OBSActionBase.py

Lines changed: 116 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import threading
1515
import os
16+
import time
1617
from loguru import logger as log
1718

1819
CLASS_ICON_MAP = {
@@ -90,12 +91,31 @@
9091
}
9192

9293

94+
class BackendProxy:
95+
def __init__(self, plugin_base, action):
96+
self._plugin_base = plugin_base
97+
self._action = action
98+
99+
def __getattr__(self, name):
100+
backend = self._plugin_base.backend
101+
if backend is None:
102+
raise AttributeError("Backend is not initialized")
103+
104+
attr = getattr(backend, name)
105+
if callable(attr):
106+
def wrapper(*args, **kwargs):
107+
return attr(*args, connection_id=self._action.connection_id, **kwargs)
108+
return wrapper
109+
return attr
110+
111+
93112
class OBSActionBase(ActionBase):
94113
def __init__(self, *args, **kwargs):
95114
super().__init__(*args, **kwargs)
96115

97116
self.has_configuration = True
98117
self.custom_icon_entries = {}
118+
self.custom_icon_clear_buttons = {}
99119
self.validated_custom_icons = {}
100120
self.custom_icon_cache_initialized = False
101121

@@ -105,8 +125,23 @@ def __init__(self, *args, **kwargs):
105125

106126
self.update_custom_icon_cache()
107127

108-
if not self.plugin_base.backend.get_connected():
109-
self.reconnect_obs()
128+
if self.plugin_base.backend is not None:
129+
if not self.backend.get_connected():
130+
self.reconnect_obs()
131+
132+
@property
133+
def connection_id(self):
134+
return self.get_settings().get("connection_id", "default")
135+
136+
@property
137+
def backend(self):
138+
if not hasattr(self, "_backend_proxy"):
139+
self._backend_proxy = BackendProxy(self.plugin_base, self)
140+
return self._backend_proxy
141+
142+
@backend.setter
143+
def backend(self, value):
144+
pass
110145

111146
def update_custom_icon_cache(self):
112147
self.validated_custom_icons = {}
@@ -121,36 +156,46 @@ def update_custom_icon_cache(self):
121156
self.custom_icon_cache_initialized = True
122157

123158
def get_config_rows(self) -> list:
124-
self.ip_entry = Adw.EntryRow(title=self.plugin_base.lm.get("actions.base.ip.label"))
125-
self.port_spinner = Adw.SpinRow.new_with_range(0, 65535, 1)
126-
self.port_spinner.set_title(self.plugin_base.lm.get("actions.base.port.label"))
127-
self.password_entry = Adw.PasswordEntryRow(title=self.plugin_base.lm.get("actions.base.password.label"))
159+
self.connection_model = Gtk.StringList()
160+
self.connection_row = Adw.ComboRow(
161+
model=self.connection_model,
162+
title="OBS Connection Profile"
163+
)
128164

129165
self.load_config_defaults()
130166

131167
# Connect signals
132-
self.ip_entry.connect("notify::text", self.on_change_ip)
133-
self.port_spinner.connect("notify::value", self.on_change_port)
134-
self.password_entry.connect("notify::text", self.on_change_password)
168+
self.connection_row.connect("notify::selected", self.on_change_connection)
135169

136-
rows = [self.ip_entry, self.port_spinner, self.password_entry]
170+
rows = [self.connection_row]
137171

138172
# Add custom icon rows
139173
custom_icons = CLASS_ICON_MAP.get(self.__class__.__name__, [])
140174
for default_filename, label_text in custom_icons:
141-
entry = Adw.EntryRow(title=label_text)
175+
entry = Adw.ActionRow(title=label_text)
142176
current_val = self.get_settings().get(f"custom_icon_{default_filename}", "")
143-
entry.set_text(current_val)
177+
178+
# Set subtitle to filename without extension
179+
if current_val:
180+
filename = os.path.basename(current_val)
181+
name_without_ext = os.path.splitext(filename)[0]
182+
entry.set_subtitle(name_without_ext)
183+
else:
184+
entry.set_subtitle("Default Icon")
144185

145186
# Create choose button
146187
btn = Gtk.Button.new_from_icon_name("document-open-symbolic")
147188
btn.connect("clicked", lambda button, df=default_filename: self.on_select_custom_icon(df))
148189
entry.add_suffix(btn)
149190

150-
# Connect text change signal
151-
entry.connect("notify::text", lambda widget, pspec, df=default_filename: self.on_change_custom_icon_path(df))
191+
# Create clear button
192+
clear_btn = Gtk.Button.new_from_icon_name("edit-clear-symbolic")
193+
clear_btn.set_sensitive(bool(current_val))
194+
clear_btn.connect("clicked", lambda button, df=default_filename: self.on_clear_custom_icon(df))
195+
entry.add_suffix(clear_btn)
152196

153197
self.custom_icon_entries[default_filename] = entry
198+
self.custom_icon_clear_buttons[default_filename] = clear_btn
154199
rows.append(entry)
155200

156201
return rows
@@ -161,15 +206,23 @@ def on_select_custom_icon(self, default_filename):
161206
def on_select_callback(path):
162207
if not path:
163208
return
164-
# Update the text entry
165-
if default_filename in self.custom_icon_entries:
166-
self.custom_icon_entries[default_filename].set_text(path)
167209
# Save setting
168210
settings = self.get_settings()
169211
if settings.get(f"custom_icon_{default_filename}") != path:
170212
settings[f"custom_icon_{default_filename}"] = path
171213
self.set_settings(settings)
172214
self.update_custom_icon_cache()
215+
216+
# Update subtitle
217+
if default_filename in self.custom_icon_entries:
218+
filename = os.path.basename(path)
219+
name_without_ext = os.path.splitext(filename)[0]
220+
self.custom_icon_entries[default_filename].set_subtitle(name_without_ext)
221+
222+
# Enable clear button
223+
if default_filename in self.custom_icon_clear_buttons:
224+
self.custom_icon_clear_buttons[default_filename].set_sensitive(True)
225+
173226
# Trigger update immediately
174227
if hasattr(self, "_current_state"):
175228
self._current_state = None
@@ -182,16 +235,18 @@ def on_select_callback(path):
182235

183236
GLib.idle_add(gl.app.let_user_select_asset, current_val, on_select_callback)
184237

185-
def on_change_custom_icon_path(self, default_filename):
186-
if default_filename not in self.custom_icon_entries:
187-
return
188-
path = self.custom_icon_entries[default_filename].get_text().strip()
238+
def on_clear_custom_icon(self, default_filename):
189239
settings = self.get_settings()
190-
# Save setting only if changed to avoid loop
191-
if settings.get(f"custom_icon_{default_filename}") != path:
192-
settings[f"custom_icon_{default_filename}"] = path
240+
if settings.get(f"custom_icon_{default_filename}"):
241+
settings[f"custom_icon_{default_filename}"] = ""
193242
self.set_settings(settings)
194243
self.update_custom_icon_cache()
244+
245+
if default_filename in self.custom_icon_entries:
246+
self.custom_icon_entries[default_filename].set_subtitle("Default Icon")
247+
if default_filename in self.custom_icon_clear_buttons:
248+
self.custom_icon_clear_buttons[default_filename].set_sensitive(False)
249+
195250
# Trigger update immediately
196251
if hasattr(self, "_current_state"):
197252
self._current_state = None
@@ -213,76 +268,58 @@ def set_media(self, media_path=None, *args, **kwargs):
213268
super().set_media(media_path=media_path, *args, **kwargs)
214269

215270
def load_config_defaults(self):
216-
settings = self.plugin_base.get_settings()
217-
ip = settings.setdefault("ip", "localhost")
218-
port = settings.setdefault("port", 4455)
219-
password = settings.setdefault("password", "")
220-
221-
# Update ui
222-
self.ip_entry.set_text(ip)
223-
self.port_spinner.set_value(port)
224-
self.password_entry.set_text(password)
225-
self.update_ip_warning_status()
271+
self.connections_list = self.plugin_base.get_settings().get("connections", [])
272+
273+
while self.connection_model.get_n_items() > 0:
274+
self.connection_model.remove(0)
275+
276+
selected_idx = 0
277+
active_id = self.connection_id
278+
279+
for i, conn in enumerate(self.connections_list):
280+
self.connection_model.append(conn.get("name", "Unnamed"))
281+
if conn.get("id") == active_id:
282+
selected_idx = i
283+
284+
self.connection_row.set_selected(selected_idx)
226285
self.update_status_label()
227286

228-
def on_change_ip(self, entry, *args):
229-
settings = self.plugin_base.get_settings()
230-
settings["ip"] = self.ip_entry.get_text().strip()
231-
self.plugin_base.set_settings(settings)
232-
233-
self.update_ip_warning_status()
234-
self.reconnect_obs()
235-
236-
def update_ip_warning_status(self):
237-
valid = self.plugin_base.backend.OBSController.validate_ip(self.ip_entry.get_text().strip())
238-
if valid:
239-
self.ip_entry.remove_css_class("error")
240-
else:
241-
self.ip_entry.add_css_class("error")
242-
243-
def on_change_port(self, spinner, *args):
244-
settings = self.plugin_base.get_settings()
245-
settings["port"] = int(spinner.get_value())
246-
self.plugin_base.set_settings(settings)
247-
248-
self.reconnect_obs()
249-
250-
def on_change_password(self, entry, *args):
251-
settings = self.plugin_base.get_settings()
252-
settings["password"] = entry.get_text()
253-
self.plugin_base.set_settings(settings)
287+
def on_change_connection(self, combo, *args):
288+
selected_idx = combo.get_selected()
289+
if 0 <= selected_idx < len(self.connections_list):
290+
settings = self.get_settings()
291+
settings["connection_id"] = self.connections_list[selected_idx]["id"]
292+
self.set_settings(settings)
293+
294+
self.reconnect_obs()
295+
self.on_connection_changed()
254296

255-
self.reconnect_obs()
297+
def on_connection_changed(self):
298+
# Virtual method to be overridden by subclasses
299+
pass
256300

257301
def reconnect_obs(self):
258302
threading.Thread(target=self._reconnect_obs, daemon=True, name="reconnect_obs").start()
259303

260304
def _reconnect_obs(self):
261-
try:
262-
self.plugin_base.backend.connect_to(
263-
host=self.plugin_base.get_settings().get("ip", "localhost"),
264-
port=self.plugin_base.get_settings().get("port", 4455),
265-
password=self.plugin_base.get_settings().get("password") or "",
266-
timeout=3,
267-
legacy=False,
268-
)
269-
except Exception as e:
270-
log.error(e)
271-
272-
if hasattr(self, "status_label"):
273-
self.update_status_label()
305+
# Let backend process sync
306+
time.sleep(0.5)
307+
self.update_status_label()
274308

275309
def update_status_label(self) -> None:
276-
threading.Thread(target=self._update_status_label, daemon=True, name="update_status_label").start()
310+
if not hasattr(self, "status_label") or self.status_label is None:
311+
return
312+
connected = self.backend.get_connected()
313+
GLib.idle_add(self._update_status_label, connected)
277314

278-
def _update_status_label(self):
279-
if self.plugin_base.backend.get_connected():
280-
print("connected - label")
315+
def _update_status_label(self, connected):
316+
if not hasattr(self, "status_label") or self.status_label is None:
317+
return
318+
if connected:
281319
self.status_label.set_label(self.plugin_base.lm.get("actions.base.status.connected"))
282320
self.status_label.remove_css_class("red")
283321
self.status_label.add_css_class("green")
284322
else:
285-
print("not connected - label")
286323
self.status_label.set_label(self.plugin_base.lm.get("actions.base.status.no-connection"))
287324
self.status_label.remove_css_class("green")
288325
self.status_label.add_css_class("red")

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
<img width="1000" height="360" alt="obsplus" src="store/Thumbnail.png" />
1+
<img width="1000" height="360" alt="obsplus" src="store/Thumbnail-official.png" />
22

33
Official OBS Plugin for StreamController
44

55
Current features:
66
- Custom action icons
77
- show action icon instead of error when OBS is not running
88
- OBS Stats action: Monitor CPU, FPS, and Bandwidth bitrate
9+
- OBS Connection profile
10+
- OBS Remote connections
911

1012
Notice: Plugin was written/updated with assistance of Google Antigravity

actions/Filter/FilterBase.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from plugins.com_oparada1988_OBS_Plus.OBSActionBase import OBSActionBase
2-
from plugins.com_oparada1988_OBS_Plus.actions.mixins import State, MixinBase
1+
from OBSActionBase import OBSActionBase
2+
from actions.mixins import State, MixinBase
33
from src.backend.DeckManagement.DeckController import DeckController
44
from src.backend.PageManagement.Page import Page
55
from src.backend.PluginManager.PluginBase import PluginBase
@@ -23,7 +23,7 @@ def __init__(self, *args, **kwargs):
2323
def on_ready(self):
2424
self.current_state = State.UNKNOWN
2525
# Connect to obs if not connected
26-
if self.plugin_base.backend is not None:
26+
if self.backend is not None:
2727
if not self.plugin_base.get_connected():
2828
self.reconnect_obs()
2929

@@ -41,7 +41,7 @@ def show_current_filter_status(self):
4141
self.show_error()
4242
return
4343

44-
status = self.plugin_base.backend.get_source_filter(self.get_settings().get("scene"), self.get_settings().get("filter"))
44+
status = self.backend.get_source_filter(self.get_settings().get("scene"), self.get_settings().get("filter"))
4545
if status is None:
4646
self.current_state = State.UNKNOWN
4747
self.hide_error()
@@ -112,8 +112,8 @@ def load_filter_model(self):
112112
self.filter_model.remove(0)
113113

114114
# Load model
115-
if self.plugin_base.backend.get_connected():
116-
scenes = self.plugin_base.backend.get_scene_names()
115+
if self.backend.get_connected():
116+
scenes = self.backend.get_scene_names()
117117
if scenes is None:
118118
return
119119
for scene in scenes:
@@ -131,8 +131,8 @@ def load_filters_for_scene(self, scene_name):
131131
while self.filter_model.get_n_items() > 0:
132132
self.filter_model.remove(0)
133133

134-
if self.plugin_base.backend.get_connected():
135-
filters = self.plugin_base.backend.get_source_filters(scene_name)
134+
if self.backend.get_connected():
135+
filters = self.backend.get_source_filters(scene_name)
136136
if filters is None:
137137
self.show_error()
138138
return
@@ -194,7 +194,7 @@ def on_key_down(self):
194194
return
195195

196196
next_state = bool(self.next_state().value)
197-
self.plugin_base.backend.set_source_filter_enabled(scene_name, filter_name, next_state)
197+
self.backend.set_source_filter_enabled(scene_name, filter_name, next_state)
198198
self.on_tick()
199199

200200
def on_tick(self):

actions/Filter/SetFilter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from plugins.com_oparada1988_OBS_Plus.actions.mixins import SetMixin
1+
from actions.mixins import SetMixin
22
from .FilterBase import FilterBase
33

44

actions/Filter/ToggleFilter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from plugins.com_oparada1988_OBS_Plus.actions.mixins import ToggleMixin
1+
from actions.mixins import ToggleMixin
22
from .FilterBase import FilterBase
33

44

0 commit comments

Comments
 (0)