import os import json import tkinter as tk import vlc import subprocess import sys CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt') PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist') MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media') class SimpleTkPlayer: def __init__(self, root, playlist): self.root = root self.playlist = playlist self.current_index = 0 self.paused = False self.pause_timer = None self.is_transitioning = False # Flag to prevent rapid cycling self.is_exiting = False # Flag to prevent operations during exit # Initialize all timer variables to None self.hide_controls_timer = None self.video_watchdog = None self.image_watchdog = None self.image_timer = None self.label = tk.Label(root, bg='black') self.label.pack(fill=tk.BOTH, expand=True) self.create_controls() self.hide_controls() self.root.bind('', self.on_activity) self.root.bind('', self.on_activity) self.root.after(100, self.ensure_fullscreen) self.root.after(200, self.hide_mouse) self.root.after(300, self.move_mouse_to_corner) self.root.protocol('WM_DELETE_WINDOW', self.exit_app) def ensure_fullscreen(self): self.root.attributes('-fullscreen', True) self.root.update_idletasks() def create_controls(self): # Create a transparent, borderless top-level window for controls self.controls_win = tk.Toplevel(self.root) self.controls_win.overrideredirect(True) self.controls_win.attributes('-topmost', True) self.controls_win.attributes('-alpha', 0.92) self.controls_win.configure(bg='') # Place the window at the bottom right def place_controls(): self.controls_win.update_idletasks() w = self.controls_win.winfo_reqwidth() h = self.controls_win.winfo_reqheight() sw = self.root.winfo_screenwidth() sh = self.root.winfo_screenheight() x = sw - w - 30 y = sh - h - 30 self.controls_win.geometry(f'+{x}+{y}') self.controls_frame = tk.Frame(self.controls_win, bg='#222', bd=2, relief='ridge') self.controls_frame.pack() btn_style = { 'bg': '#333', 'fg': 'white', 'activebackground': '#555', 'activeforeground': '#00e6e6', 'font': ('Arial', 16, 'bold'), 'bd': 0, 'highlightthickness': 0, 'relief': 'flat', 'cursor': 'hand2', 'padx': 10, 'pady': 6 } self.prev_btn = tk.Button(self.controls_frame, text='⏮ Prev', command=self.prev_media, **btn_style) self.prev_btn.grid(row=0, column=0, padx=4) self.pause_btn = tk.Button(self.controls_frame, text='⏸ Pause', command=self.toggle_pause, **btn_style) self.pause_btn.grid(row=0, column=1, padx=4) self.next_btn = tk.Button(self.controls_frame, text='Next ⏭', command=self.next_media, **btn_style) self.next_btn.grid(row=0, column=2, padx=4) self.settings_btn = tk.Button(self.controls_frame, text='⚙ Settings', command=self.open_settings, **btn_style) self.settings_btn.grid(row=0, column=3, padx=4) self.exit_btn = tk.Button(self.controls_frame, text='⏻ Exit', command=self.exit_app, **btn_style) self.exit_btn.grid(row=0, column=4, padx=4) self.exit_btn.config(fg='#ff4d4d') self.controls_win.withdraw() self.controls_win.after(200, place_controls) self.root.bind('', lambda e: self.controls_win.after(200, place_controls)) def hide_mouse(self): self.root.config(cursor='none') if hasattr(self, 'controls_win'): self.controls_win.config(cursor='none') def show_mouse(self): self.root.config(cursor='arrow') if hasattr(self, 'controls_win'): self.controls_win.config(cursor='arrow') def move_mouse_to_corner(self): try: import pyautogui sw = self.root.winfo_screenwidth() sh = self.root.winfo_screenheight() pyautogui.moveTo(sw-2, sh-2) except Exception: pass def show_controls(self): if not hasattr(self, 'controls_win') or self.controls_win is None or not self.controls_win.winfo_exists(): self.create_controls() self.controls_win.deiconify() self.controls_win.lift() self.show_mouse() self.schedule_hide_controls() def hide_controls(self): try: if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists(): self.controls_win.withdraw() self.hide_mouse() except Exception as e: print(f"[CONTROLS] Error hiding controls: {e}") def schedule_hide_controls(self): try: if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer: self.root.after_cancel(self.hide_controls_timer) self.hide_controls_timer = self.root.after(5000, self.hide_controls) except Exception as e: print(f"[CONTROLS] Error scheduling hide controls: {e}") self.hide_controls_timer = None def on_activity(self, event=None): if not self.is_exiting: self.show_controls() def prev_media(self): self.current_index = (self.current_index - 1) % len(self.playlist) self.show_current_media() def next_media(self): # If at the last media, update playlist before looping if self.current_index == len(self.playlist) - 1: self.update_playlist_from_server() self.current_index = 0 self.show_current_media() else: self.current_index = (self.current_index + 1) % len(self.playlist) self.show_current_media() def update_playlist_from_server(self): # Dummy implementation: replace with your actual update logic # For example, call a function to fetch and reload the playlist print("[INFO] Updating playlist from server...") # You can import and call your real update function here # Example: self.playlist = get_latest_playlist() def toggle_pause(self): if not self.paused: self.paused = True self.pause_btn.config(text='▶ Resume') self.pause_timer = self.root.after(30000, self.resume_play) else: self.resume_play() def resume_play(self): self.paused = False self.pause_btn.config(text='⏸ Pause') if self.pause_timer: self.root.after_cancel(self.pause_timer) self.pause_timer = None def play_intro_video(self): intro_path = os.path.join(os.path.dirname(__file__), 'main_data', 'intro1.mp4') if os.path.exists(intro_path): self.show_video(intro_path, on_end=self.after_intro) else: self.after_intro() def after_intro(self): self.show_current_media() def show_video(self, file_path, on_end=None, duration=None): try: print(f"[PLAYER] Attempting to play video: {file_path}") if hasattr(self, 'vlc_player') and self.vlc_player: self.vlc_player.stop() if not hasattr(self, 'video_canvas'): self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0) self.video_canvas.pack(fill=tk.BOTH, expand=True) self.label.pack_forget() self.video_canvas.pack(fill=tk.BOTH, expand=True) self.root.attributes('-fullscreen', True) self.root.update_idletasks() # Configure VLC for Raspberry Pi with better compatibility options vlc_args = [ '--no-xlib', '--vout=gl', '--aout=alsa', '--intf=dummy', '--quiet', '--no-video-title-show', '--no-stats', '--disable-screensaver' ] self.vlc_instance = vlc.Instance(vlc_args) self.vlc_player = self.vlc_instance.media_player_new() self.vlc_player.set_mrl(file_path) self.vlc_player.set_xwindow(self.video_canvas.winfo_id()) self.vlc_player.play() # Watchdog timer: fallback if video doesn't end def watchdog(): print(f"[WATCHDOG] Video watchdog triggered for {file_path}") self.vlc_player.stop() self.video_canvas.pack_forget() self.label.pack(fill=tk.BOTH, expand=True) if on_end: on_end() max_duration = duration if duration is not None else 60 # fallback max 60s self.video_watchdog = self.root.after(int(max_duration * 1200), watchdog) def finish_video(): if hasattr(self, 'video_watchdog') and self.video_watchdog: self.root.after_cancel(self.video_watchdog) self.video_watchdog = None if hasattr(self, 'vlc_player') and self.vlc_player: self.vlc_player.stop() self.video_canvas.pack_forget() self.label.pack(fill=tk.BOTH, expand=True) if on_end: on_end() if duration is not None: self.root.after(int(duration * 1000), finish_video) else: def check_end(): try: if self.vlc_player.get_state() == vlc.State.Ended: finish_video() elif self.vlc_player.get_state() == vlc.State.Error: print(f"[VLC] Error state detected for {file_path}") finish_video() else: self.root.after(200, check_end) except Exception as e: print(f"[VLC] Exception in check_end: {e}") finish_video() check_end() except Exception as e: print(f"[VLC] Error playing video {file_path}: {e}") if on_end: on_end() def show_current_media(self): self.root.attributes('-fullscreen', True) self.root.update_idletasks() if not self.playlist: print("[PLAYER] Playlist is empty. No media to show.") self.label.config(text="No media available", fg='white', font=('Arial', 32)) # Try to reload playlist after 10 seconds self.root.after(10000, self.reload_playlist_and_continue) return media = self.playlist[self.current_index] file_path = os.path.join(MEDIA_DATA_PATH, media['file_name']) ext = file_path.lower() duration = media.get('duration', None) if not os.path.isfile(file_path): print(f"[PLAYER] File missing: {file_path}. Skipping to next.") self.next_media() return if ext.endswith(('.mp4', '.avi', '.mov', '.mkv')): self.show_video(file_path, on_end=self.next_media, duration=duration) elif ext.endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')): # Use PIL for images instead of VLC to avoid display issues self.show_image_via_pil(file_path, duration if duration is not None else 10, on_end=self.next_media) else: print(f"[PLAYER] Unsupported file type: {media['file_name']}") self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow') self.root.after(2000, self.next_media) def show_image_via_pil(self, file_path, duration, on_end=None): """Fallback image display using PIL and Tkinter""" try: print(f"[PLAYER] Displaying image via PIL: {file_path}") from PIL import Image, ImageTk # Stop any VLC player that might be running if hasattr(self, 'vlc_player') and self.vlc_player: self.vlc_player.stop() # Hide video canvas if exists and show label if hasattr(self, 'video_canvas'): self.video_canvas.pack_forget() self.label.pack(fill=tk.BOTH, expand=True) # Load and resize image img = Image.open(file_path) screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() # Calculate scaling to fit screen while maintaining aspect ratio img_ratio = img.width / img.height screen_ratio = screen_width / screen_height if img_ratio > screen_ratio: # Image is wider than screen ratio new_width = screen_width new_height = int(screen_width / img_ratio) else: # Image is taller than screen ratio new_height = screen_height new_width = int(screen_height * img_ratio) img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) self.photo = ImageTk.PhotoImage(img) self.label.config(image=self.photo, text="", bg='black') print(f"[PLAYER] Image displayed successfully, will show for {duration} seconds") # Schedule next media after duration def finish_image(): print(f"[PLAYER] Image duration completed for {os.path.basename(file_path)}") self.label.config(image="", text="") if hasattr(self, 'photo'): del self.photo # Free memory if hasattr(self, 'image_timer'): self.image_timer = None # Clear timer reference if on_end: on_end() self.image_timer = self.root.after(int(duration * 1000), finish_image) except Exception as e: print(f"[PLAYER] PIL image display failed: {e}. Skipping.") self.label.config(text=f"Image Error: {os.path.basename(file_path)}", fg='red', font=('Arial', 24)) self.root.after(2000, lambda: on_end() if on_end else None) def reload_playlist_and_continue(self): print("[PLAYER] Attempting to reload playlist...") new_playlist = load_latest_playlist() if new_playlist: self.playlist = new_playlist self.current_index = 0 print("[PLAYER] Playlist reloaded. Continuing playback.") self.show_current_media() else: print("[PLAYER] Still no playlist. Will retry.") self.root.after(10000, self.reload_playlist_and_continue) def validate_image_file(self, file_path): """Validate if image file is not corrupted""" try: import imghdr # Check if file has valid image header img_type = imghdr.what(file_path) if img_type is None: return False # Additional check for file size (empty files) file_size = os.path.getsize(file_path) if file_size < 100: # Too small to be a valid image return False return True except Exception as e: print(f"[PLAYER] Error validating image {file_path}: {e}") return False def show_image_via_vlc(self, file_path, duration, on_end=None): try: print(f"[PLAYER] Attempting to show image: {file_path}") # Validate file before attempting to display if not self.validate_image_file(file_path): print(f"[PLAYER] Invalid or corrupted image file: {file_path}. Skipping.") if on_end: on_end() return if hasattr(self, 'vlc_player') and self.vlc_player: self.vlc_player.stop() if not hasattr(self, 'video_canvas'): self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0) self.video_canvas.pack(fill=tk.BOTH, expand=True) self.label.pack_forget() self.video_canvas.pack(fill=tk.BOTH, expand=True) self.root.attributes('-fullscreen', True) self.root.update_idletasks() # Configure VLC for Raspberry Pi with better compatibility options vlc_args = [ '--no-xlib', '--vout=gl', '--aout=dummy', '--intf=dummy', '--quiet', '--no-video-title-show', '--no-stats', '--disable-screensaver' ] self.vlc_instance = vlc.Instance(vlc_args) self.vlc_player = self.vlc_instance.media_player_new() self.vlc_player.set_mrl(file_path) self.vlc_player.set_xwindow(self.video_canvas.winfo_id()) self.vlc_player.play() # Watchdog timer: fallback if image doesn't advance def watchdog(): print(f"[WATCHDOG] Image watchdog triggered for {file_path}") self.vlc_player.stop() self.video_canvas.pack_forget() self.label.pack(fill=tk.BOTH, expand=True) if on_end: on_end() self.image_watchdog = self.root.after(int(duration * 1200), watchdog) def finish_image(): try: if hasattr(self, 'image_watchdog') and self.image_watchdog: self.root.after_cancel(self.image_watchdog) self.image_watchdog = None except Exception as e: print(f"[VLC] Error canceling image_watchdog: {e}") try: if hasattr(self, 'vlc_player') and self.vlc_player: self.vlc_player.stop() self.video_canvas.pack_forget() self.label.pack(fill=tk.BOTH, expand=True) except Exception as e: print(f"[VLC] Error in finish_image cleanup: {e}") if on_end: on_end() self.root.after(int(duration * 1000), finish_image) except Exception as e: print(f"[VLC] Error showing image {file_path}: {e}") if on_end: on_end() def next_media(self): if self.is_transitioning: print("[PLAYER] Already transitioning, ignoring next_media call") return self.is_transitioning = True self.current_index = (self.current_index + 1) % len(self.playlist) print(f"[PLAYER] Moving to next media: index {self.current_index}") # Clear any existing timers safely timer_names = ['video_watchdog', 'image_watchdog', 'image_timer'] for timer_name in timer_names: try: if hasattr(self, timer_name): timer_id = getattr(self, timer_name) if timer_id is not None: self.root.after_cancel(timer_id) setattr(self, timer_name, None) except Exception as e: print(f"[PLAYER] Error canceling {timer_name} during transition: {e}") self.show_current_media() # Reset transition flag after a brief delay self.root.after(500, lambda: setattr(self, 'is_transitioning', False)) def next_media_loop(self): # This function is no longer needed as media transitions are handled by the callback system pass def exit_app(self): """Show exit confirmation dialog instead of immediate exit""" print("[EXIT] Exit button pressed, showing confirmation dialog...") self.show_exit_confirmation() def show_exit_confirmation(self): """Show exit confirmation popup with quickconnect code verification""" print("[EXIT] Opening exit confirmation window...") # Pause playback if not already paused if not self.paused: self.paused = True self.pause_btn.config(text='▶ Resume') # Explicitly pause VLC video if playing if hasattr(self, 'vlc_player') and self.vlc_player: try: self.vlc_player.pause() print("[EXIT] VLC player paused") except Exception as e: print(f"[EXIT] Error pausing VLC: {e}") # Hide controls immediately and prevent them from reappearing print("[EXIT] Hiding controls for confirmation dialog...") try: # Cancel any pending control show/hide timers if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer: self.root.after_cancel(self.hide_controls_timer) self.hide_controls_timer = None # Hide controls window if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists(): self.controls_win.withdraw() print("[EXIT] Controls hidden successfully") # Temporarily unbind mouse events to prevent controls from showing self.root.unbind('') self.root.unbind('') print("[EXIT] Mouse events unbound") except Exception as e: print(f"[EXIT] Error hiding controls: {e}") # Create exit confirmation window self.create_exit_confirmation_window() def create_exit_confirmation_window(self): """Create the exit confirmation popup window""" try: # Create confirmation window self.exit_window = tk.Toplevel(self.root) self.exit_window.title('Exit Confirmation') self.exit_window.geometry('400x250') self.exit_window.resizable(False, False) self.exit_window.config(bg='#2c3e50') # Make window modal and on top self.exit_window.attributes('-topmost', True) self.exit_window.lift() self.exit_window.focus_force() self.exit_window.grab_set() # Center the window self.center_exit_window() # Create content self.create_exit_window_content() # Bind window close event self.exit_window.protocol('WM_DELETE_WINDOW', self.cancel_exit) print("[EXIT] Exit confirmation window created") except Exception as e: print(f"[EXIT] Error creating confirmation window: {e}") # If window creation fails, restore normal functionality self.cancel_exit() def center_exit_window(self): """Center the exit confirmation window on screen""" self.exit_window.update_idletasks() width = self.exit_window.winfo_width() height = self.exit_window.winfo_height() x = (self.exit_window.winfo_screenwidth() // 2) - (width // 2) y = (self.exit_window.winfo_screenheight() // 2) - (height // 2) self.exit_window.geometry(f'{width}x{height}+{x}+{y}') def create_exit_window_content(self): """Create the content for the exit confirmation window""" # Title title_label = tk.Label( self.exit_window, text='⚠️ Exit Application', font=('Arial', 16, 'bold'), fg='#e74c3c', bg='#2c3e50' ) title_label.pack(pady=(20, 10)) # Instructions instruction_label = tk.Label( self.exit_window, text='Enter the quickconnect code to exit:', font=('Arial', 12), fg='#ecf0f1', bg='#2c3e50' ) instruction_label.pack(pady=(0, 15)) # Code entry self.code_var = tk.StringVar() self.code_entry = tk.Entry( self.exit_window, textvariable=self.code_var, font=('Arial', 14), width=15, justify='center', show='*' # Hide the input like a password ) self.code_entry.pack(pady=(0, 20)) self.code_entry.focus_set() # Bind Enter key to confirm self.code_entry.bind('', lambda e: self.verify_exit_code()) # Buttons frame buttons_frame = tk.Frame(self.exit_window, bg='#2c3e50') buttons_frame.pack(pady=(0, 20)) # Cancel button cancel_btn = tk.Button( buttons_frame, text='Cancel', font=('Arial', 12, 'bold'), bg='#95a5a6', fg='white', padx=20, pady=8, command=self.cancel_exit ) cancel_btn.pack(side=tk.LEFT, padx=(0, 10)) # Confirm button confirm_btn = tk.Button( buttons_frame, text='Exit App', font=('Arial', 12, 'bold'), bg='#e74c3c', fg='white', padx=20, pady=8, command=self.verify_exit_code ) confirm_btn.pack(side=tk.LEFT, padx=(10, 0)) def verify_exit_code(self): """Verify the entered code and proceed with exit if correct""" try: entered_code = self.code_var.get().strip() # Load the quickconnect code from config with open(CONFIG_PATH, 'r') as f: config = json.load(f) quickconnect_code = config.get('quickconnect_key', '') print(f"[EXIT] Verifying exit code...") if entered_code == quickconnect_code: print("[EXIT] Correct code entered, proceeding with exit...") # Close confirmation window self.exit_window.destroy() # Proceed with actual exit self.perform_actual_exit() else: print("[EXIT] Incorrect code entered") # Show error and clear entry self.show_exit_error() except Exception as e: print(f"[EXIT] Error verifying code: {e}") self.show_exit_error() def show_exit_error(self): """Show error message for incorrect code""" # Clear the entry self.code_var.set('') # Temporarily change entry background to indicate error original_bg = self.code_entry.cget('bg') self.code_entry.config(bg='#e74c3c') # Reset background after 1 second self.exit_window.after(1000, lambda: self.code_entry.config(bg=original_bg)) # Focus back to entry self.code_entry.focus_set() def cancel_exit(self): """Cancel exit and restore normal functionality""" print("[EXIT] Exit cancelled, restoring normal functionality...") try: # Close confirmation window if it exists if hasattr(self, 'exit_window') and self.exit_window: self.exit_window.destroy() del self.exit_window except Exception as e: print(f"[EXIT] Error closing confirmation window: {e}") # Restore normal functionality (similar to settings restoration) self.restore_after_exit_cancel() def restore_after_exit_cancel(self): """Restore normal player functionality after exit cancellation""" if self.is_exiting: return # Don't restore if we're actually exiting try: print("[EXIT] Restoring player functionality after exit cancellation...") # Resume playback if self.paused: self.resume_play() print("[EXIT] Playback resumed") # Re-bind mouse and button events self.root.bind('', self.on_activity) self.root.bind('', self.on_activity) print("[EXIT] Mouse events re-bound") # Restore controls if needed if not hasattr(self, 'controls_win') or self.controls_win is None or not self.controls_win.winfo_exists(): self.create_controls() print("[EXIT] Controls recreated") # Force focus back to main window self.root.focus_force() self.root.lift() print("[EXIT] Focus restored to main window") # Resume VLC if it was paused if hasattr(self, 'vlc_player') and self.vlc_player: try: if self.vlc_player.get_state() == vlc.State.Paused: self.vlc_player.play() print("[EXIT] VLC playback resumed") except Exception as e: print(f"[EXIT] Error resuming VLC: {e}") print("[EXIT] Player functionality fully restored after exit cancellation") except Exception as e: print(f"[EXIT] Error restoring functionality: {e}") def perform_actual_exit(self): """Perform the actual application exit after code verification""" print("[EXIT] Starting application exit...") self.is_exiting = True # Set exit flag to prevent further operations # Signal all threads and flags to stop if hasattr(self, 'stop_event') and self.stop_event: self.stop_event.set() if hasattr(self, 'app_running') and self.app_running: self.app_running[0] = False # Cancel all pending timers first timer_names = ['hide_controls_timer', 'video_watchdog', 'image_watchdog', 'image_timer', 'pause_timer'] for timer_name in timer_names: try: if hasattr(self, timer_name): timer_id = getattr(self, timer_name) if timer_id is not None: self.root.after_cancel(timer_id) print(f"[EXIT] Canceled {timer_name}") setattr(self, timer_name, None) except Exception as e: print(f"[EXIT] Error canceling {timer_name}: {e}") # Continue with other timers even if one fails # Stop VLC player try: if hasattr(self, 'vlc_player') and self.vlc_player: self.vlc_player.stop() self.vlc_player = None except Exception as e: print(f"[EXIT] Error stopping VLC: {e}") # Unbind all events to prevent callbacks after destroy try: self.root.unbind('') self.root.unbind('') self.root.unbind('') except Exception: pass # Force hide and destroy controls immediately try: print("[EXIT] Destroying controls...") # First, try to hide and destroy the main controls window if hasattr(self, 'controls_win') and self.controls_win: try: if self.controls_win.winfo_exists(): self.controls_win.withdraw() self.controls_win.destroy() self.controls_win = None print("[EXIT] Main controls window destroyed") except Exception as e: print(f"[EXIT] Error destroying main controls_win: {e}") # Destroy controls_frame separately if it exists if hasattr(self, 'controls_frame') and self.controls_frame: try: if self.controls_frame.winfo_exists(): self.controls_frame.destroy() self.controls_frame = None print("[EXIT] Controls frame destroyed") except Exception as e: print(f"[EXIT] Error destroying controls_frame: {e}") # Destroy all button references button_names = ['prev_btn', 'pause_btn', 'next_btn', 'settings_btn', 'exit_btn'] for btn_name in button_names: try: if hasattr(self, btn_name): btn = getattr(self, btn_name) if btn and btn.winfo_exists(): btn.destroy() setattr(self, btn_name, None) except Exception as e: print(f"[EXIT] Error destroying {btn_name}: {e}") # Destroy any remaining Toplevel windows (more aggressive search) try: # Get all toplevel windows including potential orphans all_toplevels = [] for widget in list(self.root.winfo_children()): if isinstance(widget, tk.Toplevel): all_toplevels.append(widget) # Also check if there are any floating Toplevel references if hasattr(tk, '_default_root') and tk._default_root: for child in tk._default_root.winfo_children(): if isinstance(child, tk.Toplevel) and child not in all_toplevels: all_toplevels.append(child) print(f"[EXIT] Found {len(all_toplevels)} toplevel windows to destroy") for widget in all_toplevels: try: if widget.winfo_exists(): widget.withdraw() widget.quit() if hasattr(widget, 'quit') else None widget.destroy() print(f"[EXIT] Destroyed toplevel widget: {type(widget).__name__}") except Exception as e: print(f"[EXIT] Error destroying toplevel {widget}: {e}") except Exception as e: print(f"[EXIT] Error in comprehensive toplevel cleanup: {e}") except Exception as e: print(f"[EXIT] Error in controls cleanup: {e}") # Final step: destroy root window try: print("[EXIT] Destroying main window...") # Force update to process any pending widget operations self.root.update_idletasks() # One final check for any remaining Toplevel windows try: remaining_toplevels = [w for w in self.root.winfo_children() if isinstance(w, tk.Toplevel)] if remaining_toplevels: print(f"[EXIT] Found {len(remaining_toplevels)} remaining toplevel windows, destroying...") for widget in remaining_toplevels: try: widget.withdraw() widget.destroy() print(f"[EXIT] Destroyed remaining toplevel: {widget}") except Exception as e: print(f"[EXIT] Error destroying remaining toplevel: {e}") except Exception as e: print(f"[EXIT] Error checking for remaining toplevels: {e}") # Force another update self.root.update_idletasks() # Exit the main loop and destroy self.root.quit() # Exit mainloop first self.root.update() # Process the quit self.root.destroy() print("[EXIT] Application exit complete") except Exception as e: print(f"[EXIT] Error destroying root: {e}") # Force exit using system methods try: import os os._exit(0) except Exception: import sys sys.exit(0) def open_settings(self): print("[SETTINGS] Opening settings window...") # Pause playback if not already paused if not self.paused: self.paused = True self.pause_btn.config(text='▶ Resume') # Explicitly pause VLC video if playing if hasattr(self, 'vlc_player') and self.vlc_player: try: self.vlc_player.pause() print("[SETTINGS] VLC player paused") except Exception as e: print(f"[SETTINGS] Error pausing VLC: {e}") # Hide controls immediately and prevent them from reappearing print("[SETTINGS] Hiding controls...") try: # Cancel any pending control show/hide timers if hasattr(self, 'hide_controls_timer'): self.root.after_cancel(self.hide_controls_timer) # Hide controls window if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists(): self.controls_win.withdraw() print("[SETTINGS] Controls hidden successfully") # Temporarily unbind mouse events to prevent controls from showing self.root.unbind('') self.root.unbind('') print("[SETTINGS] Mouse events unbound") except Exception as e: print(f"[SETTINGS] Error hiding controls: {e}") # Launch settings window settings_path = os.path.join(os.path.dirname(__file__), 'appsettings.py') try: # Open settings in a new process proc = subprocess.Popen([sys.executable, settings_path], close_fds=True) print(f"[SETTINGS] Settings process started with PID: {proc.pid}") # Start monitoring the settings window self.root.after(500, lambda: self.check_settings_closed(proc)) except Exception as e: print(f"[SETTINGS] Error opening settings: {e}") # Restore functionality if settings failed to open self.restore_after_settings() def check_settings_closed(self, proc): if self.is_exiting: print("[SETTINGS] Exit in progress, stopping settings monitoring") return if proc.poll() is not None: print("[SETTINGS] Settings window closed, restoring player functionality...") self.restore_after_settings() else: # Continue checking every second self.root.after(1000, lambda: self.check_settings_closed(proc)) def restore_after_settings(self): """Restore normal player functionality after settings window closes""" if self.is_exiting: print("[SETTINGS] Exit in progress, skipping settings restoration") return try: print("[SETTINGS] Restoring player functionality...") # Resume playback if self.paused: self.resume_play() print("[SETTINGS] Playback resumed") # Re-bind mouse and button events self.root.bind('', self.on_activity) self.root.bind('', self.on_activity) print("[SETTINGS] Mouse events re-bound") # Ensure controls are properly destroyed before recreating if hasattr(self, 'controls_win') and self.controls_win: try: if self.controls_win.winfo_exists(): self.controls_win.destroy() self.controls_win = None except Exception as e: print(f"[SETTINGS] Error destroying old controls: {e}") # Recreate controls self.create_controls() print("[SETTINGS] Controls recreated") # Ensure exit button is properly configured after recreation if hasattr(self, 'exit_btn') and self.exit_btn: self.exit_btn.config(command=self.exit_app) print("[SETTINGS] Exit button command restored") # Force focus back to main window self.root.focus_force() self.root.lift() print("[SETTINGS] Focus restored to main window") # Resume VLC if it was paused if hasattr(self, 'vlc_player') and self.vlc_player: try: if self.vlc_player.get_state() == vlc.State.Paused: self.vlc_player.play() print("[SETTINGS] VLC playback resumed") except Exception as e: print(f"[SETTINGS] Error resuming VLC: {e}") print("[SETTINGS] Player functionality fully restored") except Exception as e: print(f"[SETTINGS] Error restoring functionality: {e}") def main_start(self): self.play_intro_video() def load_latest_playlist(): files = [f for f in os.listdir(PLAYLIST_DIR) if f.startswith('server_playlist_v') and f.endswith('.json')] if not files: return [] # Sort by version number descending files.sort(key=lambda x: int(x.split('_v')[-1].split('.json')[0]), reverse=True) latest_file = files[0] with open(os.path.join(PLAYLIST_DIR, latest_file), 'r') as f: data = json.load(f) playlist = data.get('playlist', []) # Validate playlist: skip missing or unsupported files valid_exts = ('.mp4', '.avi', '.mov', '.mkv', '.jpg', '.jpeg', '.png', '.bmp', '.gif') valid_playlist = [] for item in playlist: file_path = os.path.join(MEDIA_DATA_PATH, item.get('file_name', '')) if os.path.isfile(file_path) and file_path.lower().endswith(valid_exts): valid_playlist.append(item) else: print(f"[PLAYLIST] Skipping missing or unsupported file: {item.get('file_name')}") return valid_playlist