diff --git a/terminalTv.py b/terminalTv.py index 6af99ab..23c8420 100644 --- a/terminalTv.py +++ b/terminalTv.py @@ -96,6 +96,154 @@ class FocusAwareListBox(urwid.ListBox): self.on_focus_change(current_focus) self._last_focus = current_focus +class SettingsMenu: + """Settings configuration menu""" + def __init__(self, config, on_save_callback, on_cancel_callback): + self.config = config.copy() + self.on_save_callback = on_save_callback + self.on_cancel_callback = on_cancel_callback + self.setup_ui() + + def setup_ui(self): + # Create styled form fields with consistent colors + self.m3u_edit = urwid.Edit(("title", "M3U URL: "), self.config.get('m3u_url', '')) + self.xmltv_edit = urwid.Edit(("title", "XMLTV URL: "), self.config.get('xmltv_url', '')) + self.user_agent_edit = urwid.Edit(("title", "User Agent: "), self.config.get('user_agent', '')) + self.update_interval_edit = urwid.IntEdit(("title", "Update Interval (seconds): "), self.config.get('update_interval', 900)) + + # Monitor selection - simplified as a numeric selection + monitor_value = self.config.get('monitor', None) + + # Create a radio button group for monitor selection + self.monitor_group = [] + rb_none = urwid.RadioButton(self.monitor_group, "Default (No specific monitor)", state=(monitor_value is None)) + rb_1 = urwid.RadioButton(self.monitor_group, "Monitor 1", state=(monitor_value == 1)) + rb_2 = urwid.RadioButton(self.monitor_group, "Monitor 2", state=(monitor_value == 2)) + rb_3 = urwid.RadioButton(self.monitor_group, "Monitor 3", state=(monitor_value == 3)) + + # Create styled form sections + form_items = [ + # Header section + urwid.AttrMap(urwid.Text("Settings Configuration", align='center'), 'header'), + urwid.AttrMap(urwid.Divider("─"), 'divider'), + urwid.Divider(), + + # Connection settings section + urwid.AttrMap(urwid.Text("CONNECTION SETTINGS", align='left'), 'program_now'), + urwid.Divider(), + urwid.AttrMap(self.m3u_edit, None, 'channel_focus'), + urwid.Divider(), + urwid.AttrMap(self.xmltv_edit, None, 'channel_focus'), + urwid.Divider(), + urwid.AttrMap(self.user_agent_edit, None, 'channel_focus'), + urwid.Divider(), + + # Application settings section + urwid.AttrMap(urwid.Text("APPLICATION SETTINGS", align='left'), 'program_now'), + urwid.Divider(), + urwid.AttrMap(self.update_interval_edit, None, 'channel_focus'), + urwid.Divider(), + + # Display settings section + urwid.AttrMap(urwid.Text("DISPLAY SETTINGS", align='left'), 'program_now'), + urwid.Divider(), + urwid.AttrMap(urwid.Text("Fullscreen Monitor Selection:", align='left'), 'program_desc'), + urwid.AttrMap(rb_none, None, 'channel_focus'), + urwid.AttrMap(rb_1, None, 'channel_focus'), + urwid.AttrMap(rb_2, None, 'channel_focus'), + urwid.AttrMap(rb_3, None, 'channel_focus'), + urwid.Divider(), + + # Action buttons + urwid.AttrMap(urwid.Divider("─"), 'divider'), + urwid.Divider(), + urwid.Columns([ + ('weight', 1, urwid.AttrMap(urwid.Button("Save Settings", on_press=self.save_settings), 'program_now', 'program_desc')), + ('weight', 1, urwid.AttrMap(urwid.Button("Cancel", on_press=self.cancel_settings), 'error', 'program_desc')) + ], dividechars=2) + ] + + listwalker = urwid.SimpleFocusListWalker(form_items) + self.listbox = urwid.ListBox(listwalker) + + # Create the main widget with styled frame + self.widget = urwid.Frame( + urwid.AttrMap(urwid.LineBox(self.listbox, title=" Settings ", title_align='center'), 'channel'), + footer=urwid.AttrMap( + urwid.Text("Tab/Shift+Tab: Navigate | Enter: Select | Esc: Cancel", align='center'), + 'footer' + ) + ) + + def save_settings(self, button=None): + """Validate and save settings""" + try: + # Validate and collect settings + new_config = { + 'm3u_url': self.m3u_edit.edit_text.strip(), + 'xmltv_url': self.xmltv_edit.edit_text.strip(), + 'user_agent': self.user_agent_edit.edit_text.strip(), + 'update_interval': self.update_interval_edit.value() + } + + # Handle monitor setting from radio buttons + # Find which radio button is selected + monitor_value = None + for i, rb in enumerate(self.monitor_group): + if rb.state: + if i == 0: # "Default" option + monitor_value = None + else: + monitor_value = i # Monitor number (1, 2, 3) + break + + new_config['monitor'] = monitor_value + + # Set default MPV options without exposing them in the UI + standard_mpv_options = [ + "--really-quiet", + "--no-terminal", + "--force-window=immediate" + ] + + # Add fullscreen options if monitor is selected + if monitor_value is not None: + fs_options = [ + "--fs", + f"--fs-screen={monitor_value}" + ] + new_config['mpv_options'] = fs_options + standard_mpv_options + else: + new_config['mpv_options'] = standard_mpv_options + + # Validate required fields + if not new_config['m3u_url']: + self.show_error("M3U URL is required") + return + if not new_config['xmltv_url']: + self.show_error("XMLTV URL is required") + return + if not new_config['user_agent']: + self.show_error("User Agent is required") + return + if new_config['update_interval'] <= 0: + self.show_error("Update interval must be positive") + return + + self.on_save_callback(new_config) + except Exception as e: + self.show_error(f"Error saving settings: {str(e)}") + + def cancel_settings(self, button=None): + """Cancel settings changes""" + self.on_cancel_callback() + + def show_error(self, message): + """Show error message in footer""" + error_text = urwid.AttrMap(urwid.Text(f"Error: {message}", align='center'), 'error') + self.widget.footer = error_text + + class IPTVPlayer: def __init__(self): self.channels = [] @@ -106,6 +254,8 @@ class IPTVPlayer: self.last_update = datetime.now() self.update_thread = None self.update_running = True + self.settings_menu = None + self.main_widget = None self.load_data() self.setup_ui() @@ -263,7 +413,7 @@ class IPTVPlayer: # Add update time to footer monitor_info = f" | Monitor: {MONITOR}" if MONITOR is not None else "" - self.footer_text = urwid.Text(f"Q: Quit | ↑↓: Navigate | Enter: Play Channel | L: Reload | Last update: Loading...{monitor_info}", align='center') + self.footer_text = urwid.Text(f"Q: Quit | ↑↓: Navigate | Enter: Play | L: Reload | S: Settings | Last update: Loading...{monitor_info}", align='center') self.footer = urwid.AttrMap(self.footer_text, 'footer') program_frame = urwid.Frame( @@ -288,9 +438,12 @@ class IPTVPlayer: columns ]) + # Store the main layout + self.main_widget = urwid.LineBox(layout, title="") + # Overlay for centering self.top = urwid.Overlay( - urwid.LineBox(layout, title=""), + self.main_widget, urwid.SolidFill(' '), align='center', width=('relative', 85), @@ -305,7 +458,87 @@ class IPTVPlayer: """Update footer with last update time""" time_str = self.last_update.strftime("%H:%M:%S") monitor_info = f" | Monitor: {MONITOR}" if MONITOR is not None else "" - self.footer_text.set_text(f"Q: Quit | ↑↓: Navigate | Enter: Play Channel | L: Reload | Last update: {time_str}{monitor_info}") + self.footer_text.set_text(f"Q: Quit | ↑↓: Navigate | Enter: Play | L: Reload | S: Settings | Last update: {time_str}{monitor_info}") + + def show_settings_menu(self): + """Show settings configuration menu""" + # Get current config + current_config = { + 'm3u_url': M3U_URL, + 'xmltv_url': XMLTV_URL, + 'user_agent': USER_AGENT, + 'update_interval': UPDATE_INTERVAL, + 'monitor': MONITOR, + 'mpv_options': MPV_BASE[1:] if MPV_BASE[0] == 'mpv' else MPV_BASE # Remove 'mpv' command + } + + # Create settings menu + self.settings_menu = SettingsMenu( + current_config, + self.on_settings_save, + self.on_settings_cancel + ) + + # Create settings overlay properly + self.top = urwid.Overlay( + self.settings_menu.widget, + self.top, + align='center', + width=('relative', 80), + valign='middle', + height=('relative', 90) + ) + + # Update the MainLoop's widget + if self.loop: + self.loop.widget = self.top + + def on_settings_save(self, new_config): + """Handle settings save""" + try: + # Save to config.json + with open('config.json', 'w') as config_file: + json.dump(new_config, config_file, indent=4) + + # Update global variables + global M3U_URL, XMLTV_URL, USER_AGENT, UPDATE_INTERVAL, MONITOR, MPV_BASE + M3U_URL = new_config['m3u_url'] + XMLTV_URL = new_config['xmltv_url'] + USER_AGENT = new_config['user_agent'] + UPDATE_INTERVAL = new_config['update_interval'] + MONITOR = new_config['monitor'] + + # Update MPV base command directly from new_config + MPV_BASE = ["mpv"] + MPV_BASE.extend(new_config['mpv_options']) + + # Close settings menu + self.on_settings_cancel() + + # Reload data with new settings + self.reload_data() + + except Exception as e: + # Show error in settings menu + if self.settings_menu: + self.settings_menu.show_error(f"Failed to save config: {str(e)}") + + def on_settings_cancel(self): + """Close settings menu""" + self.settings_menu = None + # Restore the original top widget structure + self.top = urwid.Overlay( + self.main_widget, + urwid.SolidFill(' '), + align='center', + width=('relative', 85), + valign='middle', + height=('relative', 85) + ) + + # Update the MainLoop's widget + if self.loop: + self.loop.widget = self.top def start_update_thread(self): """Start background thread for periodic updates""" @@ -557,10 +790,18 @@ class IPTVPlayer: self.update_thread.join(timeout=1.0) def handle_input(self, key): + # Only handle main UI input if settings menu is not open + if self.settings_menu: + if key == 'esc': + self.on_settings_cancel() + return + if key in ('q', 'Q'): self.quit_player() elif key in ('l', 'L'): self.reload_data() + elif key in ('s', 'S'): + self.show_settings_menu() elif key == 'enter': if self.current_channel and self.current_channel.get('url'): self.play_channel(self.current_channel['url'])