|
|
|
@ -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']) |
|
|
|
|