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.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): self.controls_win.withdraw() self.hide_mouse() def schedule_hide_controls(self): 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) def on_activity(self, event=None): 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() self.root.after(100, self.next_media_loop) def show_video(self, file_path, on_end=None, duration=None): 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() self.vlc_instance = vlc.Instance('--vout=x11') self.vlc_player = self.vlc_instance.media_player_new() self.vlc_player.set_mrl(file_path) self.vlc_player.set_fullscreen(True) self.vlc_player.set_xwindow(self.video_canvas.winfo_id()) self.vlc_player.play() # Force video to play for the specified duration def finish_video(): 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(): if self.vlc_player.get_state() == vlc.State.Ended: finish_video() else: self.root.after(200, check_end) check_end() def show_current_media(self): self.root.attributes('-fullscreen', True) self.root.update_idletasks() if not self.playlist: self.label.config(text="No media available", fg='white', font=('Arial', 32)) 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 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')): self.show_image_via_vlc(file_path, duration if duration is not None else 10, on_end=self.next_media) else: self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow') def show_image_via_vlc(self, file_path, duration, on_end=None): # Use VLC to show image for a set duration 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() self.vlc_instance = vlc.Instance('--vout=x11') self.vlc_player = self.vlc_instance.media_player_new() self.vlc_player.set_mrl(file_path) self.vlc_player.set_fullscreen(True) self.vlc_player.set_xwindow(self.video_canvas.winfo_id()) self.vlc_player.play() # Schedule stop and next after duration def finish_image(): self.vlc_player.stop() self.video_canvas.pack_forget() self.label.pack(fill=tk.BOTH, expand=True) if on_end: on_end() self.root.after(int(duration * 1000), finish_image) def next_media(self): self.current_index = (self.current_index + 1) % len(self.playlist) self.show_current_media() def next_media_loop(self): if not self.playlist or self.paused: self.root.after(1000, self.next_media_loop) return self.show_current_media() def exit_app(self): # 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 # Unbind all events to prevent callbacks after destroy try: self.root.unbind('') self.root.unbind('') self.root.unbind('') except Exception: pass # Attempt to destroy all Toplevel windows before root try: # Withdraw controls_win if it exists if hasattr(self, 'controls_win') and self.controls_win: if self.controls_win.winfo_exists(): self.controls_win.withdraw() # Destroy controls_win if it exists (this will also destroy controls_frame) if hasattr(self, 'controls_win') and self.controls_win: if self.controls_win.winfo_exists(): self.controls_win.destroy() self.controls_win = None # Fallback: destroy controls_frame if it somehow still exists if hasattr(self, 'controls_frame') and self.controls_frame: if self.controls_frame.winfo_exists(): self.controls_frame.destroy() self.controls_frame = None # Fallback: destroy any remaining Toplevels in the app for widget in self.root.winfo_children(): if isinstance(widget, tk.Toplevel): try: widget.destroy() except Exception: pass except Exception as e: print(f"[EXIT] Error destroying controls_win/frame/toplevels: {e}") # Destroy any other Toplevels if needed (add here if you have more) try: if self.root.winfo_exists(): self.root.destroy() except Exception as e: print(f"[EXIT] Error destroying root: {e}") def open_settings(self): if self.paused is not True: 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() except Exception: pass # Destroy controls overlay so settings window is always interactive if hasattr(self, 'controls_win') and self.controls_win: self.controls_win.destroy() self.controls_win = None settings_path = os.path.join(os.path.dirname(__file__), 'appsettings.py') # Open settings in a new process so it doesn't block the main player proc = subprocess.Popen([sys.executable, settings_path], close_fds=True) # Give the window manager a moment to focus the new window self.root.after(300, lambda: self.root.focus_force()) # Wait for the settings window to close, then resume self.root.after(1000, lambda: self.check_settings_closed(proc)) def check_settings_closed(self, proc): if proc.poll() is not None: # Resume playback and unpause VLC if needed self.resume_play() # Restore and recreate controls overlay self.root.deiconify() self.create_controls() # Re-bind mouse and button events to new controls self.root.bind('', self.on_activity) self.root.bind('', self.on_activity) self.show_controls() if hasattr(self, 'vlc_player') and self.vlc_player: try: # Only resume if it was paused by us if self.vlc_player.get_state() == vlc.State.Paused: self.vlc_player.play() except Exception: pass else: self.root.after(1000, lambda: self.check_settings_closed(proc)) 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) return data.get('playlist', [])