1313
1414import threading
1515import os
16+ import time
1617from loguru import logger as log
1718
1819CLASS_ICON_MAP = {
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+
93112class 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" )
0 commit comments