@@ -399,7 +399,7 @@ def _build_ui(self):
399399
400400 self ._build_cameras_section (left )
401401 self ._build_session_section (left )
402- self ._build_profiles_panel (right )
402+ self ._build_right_panel (right )
403403
404404 # ── status bar ───────────────────────────────────────────────────────
405405 tk .Frame (root , bg = BORDER , height = 1 ).pack (fill = tk .X )
@@ -594,17 +594,20 @@ def _browse_dir(self, var: tk.StringVar):
594594
595595 # ── profiles panel ───────────────────────────────────────────────────────
596596
597- def _build_profiles_panel (self , parent ):
597+ def _build_right_panel (self , parent ):
598598 tk .Frame (parent , bg = BORDER , width = 1 ).pack (side = tk .LEFT , fill = tk .Y )
599599
600- inner = tk . Frame (parent , bg = BG2 , padx = 14 )
601- inner .pack (fill = tk .BOTH , expand = True )
600+ notebook = ttk . Notebook (parent )
601+ notebook .pack (fill = tk .BOTH , expand = True )
602602
603- tk .Label (inner , text = "📁 Profiles" ,
604- bg = BG2 , fg = FG , font = FONT_HEAD ).pack (anchor = tk .W , pady = (16 , 10 ))
603+ # ── Tab 1: Profiles ──────────────────────────────────────────────────
604+ tab_profiles = tk .Frame (notebook , bg = BG2 , padx = 14 )
605+ notebook .add (tab_profiles , text = " Profiles " )
605606
606- # profile listbox
607- lb_frame = tk .Frame (inner , bg = BG2 )
607+ tk .Label (tab_profiles , text = "📁 Profiles" ,
608+ bg = BG2 , fg = FG , font = FONT_HEAD ).pack (anchor = tk .W , pady = (12 , 8 ))
609+
610+ lb_frame = tk .Frame (tab_profiles , bg = BG2 )
608611 lb_frame .pack (fill = tk .BOTH , expand = True )
609612
610613 sb = ttk .Scrollbar (lb_frame , orient = tk .VERTICAL )
@@ -614,8 +617,7 @@ def _build_profiles_panel(self, parent):
614617 relief = tk .FLAT , font = FONT_BODY ,
615618 activestyle = tk .NONE ,
616619 yscrollcommand = sb .set ,
617- highlightthickness = 1 ,
618- highlightcolor = BORDER ,
620+ highlightthickness = 1 , highlightcolor = BORDER ,
619621 highlightbackground = BORDER ,
620622 )
621623 sb .config (command = self ._profile_lb .yview )
@@ -624,58 +626,125 @@ def _build_profiles_panel(self, parent):
624626 self ._profile_lb .bind ("<<ListboxSelect>>" , self ._on_profile_select )
625627 self ._profile_lb .bind ("<Double-Button-1>" , lambda e : self ._load_profile ())
626628
627- # profile buttons
628629 btn_cfg = {"bg" : BG3 , "fg" : FG , "activebackground" : BORDER ,
629630 "activeforeground" : FG , "relief" : tk .FLAT ,
630631 "cursor" : "hand2" , "font" : FONT_SMALL }
631- btn_row1 = tk .Frame (inner , bg = BG2 )
632- btn_row1 .pack (fill = tk .X , pady = (8 , 0 ))
632+ btn_row1 = tk .Frame (tab_profiles , bg = BG2 )
633+ btn_row1 .pack (fill = tk .X , pady = (6 , 0 ))
633634 tk .Button (btn_row1 , text = "Load" , ** btn_cfg ,
634635 command = self ._load_profile ).pack (side = tk .LEFT , fill = tk .X ,
635- expand = True , padx = (0 , 2 ), ipady = 4 )
636+ expand = True , padx = (0 , 2 ), ipady = 3 )
636637 tk .Button (btn_row1 , text = "Save As…" , ** btn_cfg ,
637638 command = self ._save_profile ).pack (side = tk .LEFT , fill = tk .X ,
638- expand = True , padx = (2 , 0 ), ipady = 4 )
639-
640- btn_row2 = tk .Frame (inner , bg = BG2 )
641- btn_row2 .pack (fill = tk .X , pady = (4 , 0 ))
639+ expand = True , padx = (2 , 0 ), ipady = 3 )
640+ btn_row2 = tk .Frame (tab_profiles , bg = BG2 )
641+ btn_row2 .pack (fill = tk .X , pady = (3 , 0 ))
642642 tk .Button (btn_row2 , text = "Rename…" , ** btn_cfg ,
643643 command = self ._rename_profile ).pack (side = tk .LEFT , fill = tk .X ,
644- expand = True , padx = (0 , 2 ), ipady = 4 )
644+ expand = True , padx = (0 , 2 ), ipady = 3 )
645645 tk .Button (btn_row2 , text = "Delete" , bg = BG3 , fg = DANGER ,
646646 activebackground = BORDER , activeforeground = DANGER ,
647647 relief = tk .FLAT , cursor = "hand2" , font = FONT_SMALL ,
648648 command = self ._delete_profile ).pack (side = tk .LEFT , fill = tk .X ,
649- expand = True , padx = (2 , 0 ), ipady = 4 )
650-
651- # quick-save current button
652- tk .Button (inner , text = "💾 Quick Save Current Profile" ,
653- bg = BG3 , fg = ACCENT ,
649+ expand = True , padx = (2 , 0 ), ipady = 3 )
650+ tk .Button (tab_profiles , text = "💾 Quick Save" , bg = BG3 , fg = ACCENT ,
654651 activebackground = BORDER , activeforeground = ACCENT ,
655652 relief = tk .FLAT , cursor = "hand2" , font = FONT_SMALL ,
656- command = self ._quick_save
657- ).pack (fill = tk .X , pady = (8 , 0 ), ipady = 5 )
653+ command = self ._quick_save ).pack (fill = tk .X , pady = (6 , 0 ), ipady = 4 )
658654
659- # separator
660- tk .Frame (inner , bg = BORDER , height = 1 ).pack (fill = tk .X , pady = 14 )
655+ self ._refresh_profile_list ()
656+
657+ # ── Tab 2: Addons ────────────────────────────────────────────────────
658+ tab_addons = tk .Frame (notebook , bg = BG2 , padx = 14 )
659+ notebook .add (tab_addons , text = " Addons " )
660+ self ._build_addons_panel (tab_addons )
661+
662+ def _build_addons_panel (self , parent ):
663+ tk .Label (parent , text = "🧩 Addons" ,
664+ bg = BG2 , fg = FG , font = FONT_HEAD ).pack (anchor = tk .W , pady = (12 , 8 ))
665+
666+ # scan addons directory
667+ addons_root = Path (__file__ ).resolve ().parent .parent / "addons"
668+ found = []
669+ if addons_root .exists ():
670+ for d in sorted (addons_root .iterdir ()):
671+ if d .is_dir ():
672+ manifest_path = d / "addon.json"
673+ if manifest_path .exists ():
674+ try :
675+ manifest = json .loads (manifest_path .read_text ())
676+ found .append (manifest )
677+ except Exception :
678+ found .append ({"id" : d .name , "name" : d .name ,
679+ "version" : "?" , "description" : "Invalid manifest" })
680+
681+ if not found :
682+ tk .Label (parent , text = "No addons found" , bg = BG2 , fg = FG3 ,
683+ font = FONT_SMALL ).pack (anchor = tk .W , pady = 10 )
684+ tk .Label (parent , text = "Add addon directories to:" , bg = BG2 ,
685+ fg = FG3 , font = FONT_SMALL ).pack (anchor = tk .W )
686+ tk .Label (parent , text = str (addons_root ), bg = BG2 ,
687+ fg = ACCENT , font = FONT_SMALL ).pack (anchor = tk .W )
688+ return
689+
690+ # addon list
691+ lb_frame = tk .Frame (parent , bg = BG2 )
692+ lb_frame .pack (fill = tk .BOTH , expand = True )
693+
694+ self ._addon_lb = tk .Listbox (
695+ lb_frame , bg = BG3 , fg = FG ,
696+ selectbackground = ACCENT , selectforeground = "#fff" ,
697+ relief = tk .FLAT , font = FONT_BODY ,
698+ activestyle = tk .NONE ,
699+ highlightthickness = 1 , highlightcolor = BORDER ,
700+ highlightbackground = BORDER ,
701+ )
702+ self ._addon_lb .pack (side = tk .LEFT , fill = tk .BOTH , expand = True )
703+
704+ self ._addon_data = found
705+ for a in found :
706+ desc = a .get ("description" , "" )[:40 ]
707+ label = f" { a ['name' ]} v{ a .get ('version' ,'?' )} "
708+ self ._addon_lb .insert (tk .END , label )
709+
710+ # addon detail area
711+ self ._addon_detail_var = tk .StringVar (value = "Select an addon for details" )
712+ tk .Label (parent , textvariable = self ._addon_detail_var ,
713+ bg = BG2 , fg = FG2 , font = FONT_SMALL ,
714+ wraplength = 200 , justify = tk .LEFT , anchor = tk .W ).pack (
715+ anchor = tk .W , fill = tk .X , pady = (6 , 0 ))
716+
717+ self ._addon_lb .bind ("<<ListboxSelect>>" , self ._on_addon_select )
718+
719+ # footer
720+ tk .Frame (parent , bg = BORDER , height = 1 ).pack (fill = tk .X , pady = 8 )
661721
662- # help text
663722 help_lines = [
664- "Quick tips:" ,
665- "• Double-click profile to load" ,
666- "• Press Test to check cameras" ,
667- "• Press ▶ Launch to start" ,
668- "" ,
669- "Profiles saved to:" ,
670- "~/.droidgrid/profiles.json" ,
723+ "Addons extend DroidGrid with" ,
724+ "custom capabilities." ,
725+ "Manage addons in the Pro" ,
726+ "web UI (Extensions tab)." ,
671727 ]
672728 for line in help_lines :
673- tk .Label (inner , text = line , bg = BG2 ,
674- fg = FG3 if not line .startswith ("~" ) else ACCENT ,
675- font = FONT_SMALL , anchor = tk .W ,
676- justify = tk .LEFT ).pack (anchor = tk .W )
729+ tk .Label (parent , text = line , bg = BG2 ,
730+ fg = FG3 , font = FONT_SMALL ,
731+ anchor = tk .W ).pack (anchor = tk .W )
677732
678- self ._refresh_profile_list ()
733+ def _on_addon_select (self , event = None ):
734+ sel = self ._addon_lb .curselection ()
735+ if not sel :
736+ return
737+ idx = sel [0 ]
738+ if idx < len (self ._addon_data ):
739+ a = self ._addon_data [idx ]
740+ desc = a .get ("description" , "No description available." )
741+ lines = [f"Name: { a ['name' ]} " ,
742+ f"ID: { a .get ('id' , '?' )} " ,
743+ f"Ver: { a .get ('version' , '?' )} " ,
744+ f"Auth: { a .get ('author' , '?' )} " ,
745+ f"" ,
746+ f"{ desc } " ]
747+ self ._addon_detail_var .set ("\n " .join (lines ))
679748
680749 def _refresh_profile_list (self ):
681750 self ._profile_lb .delete (0 , tk .END )
0 commit comments