#!/usr/bin/env python3
# Govibe Nexus Video Player
# Debian/Ubuntu GTK front end for mpv with video stats, audio EQ, video filters, rendering presets, color control, OSD banner, and tray icon.

import os
os.environ.setdefault("GDK_BACKEND", "x11")

import sys
import json
import time
import math
import signal
import socket
import shutil
import subprocess
import threading
from pathlib import Path

import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
try:
    gi.require_version("GdkX11", "3.0")
except Exception:
    pass
from gi.repository import Gtk, Gdk, GLib, Gio
try:
    from gi.repository import GdkX11  # noqa: F401
except Exception:
    GdkX11 = None

APP_ID = "org.govibe.NexusVideoPlayer"
APP_NAME = "Govibe Nexus Video Player"
DESKTOP_ID = "govibe-nexus-video-player.desktop"
MPV_LOG_PATH = "/tmp/govibe-nexus-video-player-mpv.log"
VERSION = "1.0.25"
VIDEO_MIME_TYPES = [
    "video/mp4", "video/x-matroska", "video/webm", "video/quicktime", "video/x-msvideo",
    "video/x-ms-wmv", "video/mpeg", "video/ogg", "video/3gpp", "video/3gpp2", "video/x-flv",
    "video/mp2t", "application/ogg"
]

THEMES = {
    "Nexus Green": {
        "bg": "#030805", "panel": "#07170a", "text": "#e8ffe8", "muted": "#7efc8d", "accent": "#00ff66", "accent2": "#8cff32",
    },
    "Classic Blue": {
        "bg": "#050812", "panel": "#09152c", "text": "#edf4ff", "muted": "#7bbcff", "accent": "#3ca1ff", "accent2": "#72e8ff",
    },
    "Windows Classic Silver": {
        "bg": "#101216", "panel": "#2b3038", "text": "#f2f2f2", "muted": "#c7d7ff", "accent": "#9bb7ff", "accent2": "#ffffff",
    },
    "Winamp Dark": {
        "bg": "#030303", "panel": "#111111", "text": "#f7f7f7", "muted": "#c6c6c6", "accent": "#ffb000", "accent2": "#fff05a",
    },
    "Mortal Red": {
        "bg": "#090101", "panel": "#180404", "text": "#fff4f4", "muted": "#ff8a8a", "accent": "#ff1f2d", "accent2": "#ffcc33",
    },
    "Crimson Skin": {
        "bg": "#0e0204", "panel": "#1c060a", "text": "#fff1f1", "muted": "#ff8080", "accent": "#ff243d", "accent2": "#ff9e3d",
    },
    "Purple Neon": {
        "bg": "#090413", "panel": "#160a2b", "text": "#fbf2ff", "muted": "#cb95ff", "accent": "#a100ff", "accent2": "#00d5ff",
    },
    "Cyber Cyan": {
        "bg": "#02090d", "panel": "#061821", "text": "#effcff", "muted": "#85ecff", "accent": "#00d9ff", "accent2": "#7cfffa",
    },
    "Ocean Blue": {
        "bg": "#020615", "panel": "#061a33", "text": "#f1f8ff", "muted": "#8ec7ff", "accent": "#0078ff", "accent2": "#57f2ff",
    },
    "Amber Studio": {
        "bg": "#100903", "panel": "#211305", "text": "#fff8e8", "muted": "#ffd17a", "accent": "#ffb000", "accent2": "#fff04c",
    },
    "Matrix Terminal": {
        "bg": "#000000", "panel": "#021006", "text": "#d6ffd6", "muted": "#66ff88", "accent": "#00ff41", "accent2": "#b9ff00",
    },
    "Arctic White": {
        "bg": "#f3f6fb", "panel": "#e4ebf5", "text": "#061018", "muted": "#305f8f", "accent": "#1f77d0", "accent2": "#003b73",
    },
    "Graphite Steel": {
        "bg": "#080a0d", "panel": "#171b21", "text": "#f0f3f7", "muted": "#aab4c0", "accent": "#8fa3b8", "accent2": "#ffffff",
    },
    "Royal Violet": {
        "bg": "#07020f", "panel": "#160628", "text": "#f8f0ff", "muted": "#c8a2ff", "accent": "#7d35ff", "accent2": "#e1c4ff",
    },
    "Magenta Laser": {
        "bg": "#100016", "panel": "#23002f", "text": "#fff0ff", "muted": "#ff9df2", "accent": "#ff2bd6", "accent2": "#80f7ff",
    },
    "Emerald Glass": {
        "bg": "#00110b", "panel": "#052319", "text": "#eafff7", "muted": "#7fffd4", "accent": "#00c983", "accent2": "#b8ffe7",
    },
    "Lime Black": {
        "bg": "#050800", "panel": "#101900", "text": "#f5ffe6", "muted": "#baff5a", "accent": "#99ff00", "accent2": "#e6ff55",
    },
    "Deep Teal": {
        "bg": "#001015", "panel": "#032a32", "text": "#eaffff", "muted": "#78e8e8", "accent": "#00b8b8", "accent2": "#a5ffff",
    },
    "Ice Blue": {
        "bg": "#06111f", "panel": "#0c2742", "text": "#eef8ff", "muted": "#a8dbff", "accent": "#69c9ff", "accent2": "#ffffff",
    },
    "Gold Premium": {
        "bg": "#0c0902", "panel": "#1b1505", "text": "#fff9e6", "muted": "#d8c36a", "accent": "#d6b43f", "accent2": "#fff0a3",
    },
    "Ruby Cinema": {
        "bg": "#100004", "panel": "#23000a", "text": "#fff3f6", "muted": "#ff94ad", "accent": "#c90036", "accent2": "#ffd0da",
    },
    "Midnight Indigo": {
        "bg": "#030417", "panel": "#0b0d2e", "text": "#f0f3ff", "muted": "#9ca8ff", "accent": "#4f63ff", "accent2": "#dbe0ff",
    },
    "White Cinema": {
        "bg": "#f7f7f2", "panel": "#e8e8df", "text": "#111111", "muted": "#555555", "accent": "#2f2f2f", "accent2": "#000000",
    },
}

TEMPLATES = {
    "Wide Theater": {"width": 1340, "height": 790, "paned": 1000, "side": 320, "video_w": 800, "video_h": 450},
}



EQ_PRESETS = {
    "Flat": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    "Bass Boost": [8, 7, 5, 3, 1, 0, 0, 0, 0, 0],
    "Deep Bass": [12, 10, 8, 4, 1, 0, -1, -1, -2, -2],
    "Loudness": [7, 6, 4, 2, 0, 1, 3, 5, 6, 7],
    "Cinema": [5, 4, 2, 1, 0, 1, 3, 4, 4, 5],
    "Movie Night": [4, 3, 2, 1, -1, 0, 2, 3, 4, 4],
    "Voice Clear": [-3, -2, -1, 2, 5, 6, 4, 2, 1, 0],
    "Podcast": [-4, -3, -1, 3, 6, 7, 4, 1, -1, -2],
    "Rock": [5, 4, 2, -1, -2, 1, 4, 5, 5, 4],
    "Metal": [6, 5, 3, -2, -3, 2, 5, 6, 6, 5],
    "Pop": [3, 5, 4, 1, 0, 2, 4, 5, 4, 3],
    "Dance": [7, 6, 3, 0, -1, 1, 3, 5, 6, 6],
    "Electronic": [6, 5, 3, 0, -2, 1, 4, 6, 7, 7],
    "Hip Hop": [8, 7, 4, 1, -1, 0, 2, 3, 4, 5],
    "Jazz": [3, 2, 1, 2, 3, 3, 2, 2, 1, 0],
    "Classical": [2, 1, 0, 1, 2, 2, 3, 4, 4, 3],
    "Acoustic": [2, 2, 1, 2, 3, 4, 3, 2, 1, 0],
    "Warm Movie": [3, 2, 1, 0, 1, 2, 2, 1, 0, -1],
    "Treble Boost": [-2, -2, -1, 0, 1, 2, 4, 5, 6, 7],
    "Headphones": [4, 3, 2, 0, -1, 1, 3, 4, 4, 3],
    "Laptop Speakers": [2, 3, 4, 4, 2, 1, 2, 3, 3, 2],
    "Small Speakers": [1, 2, 4, 5, 3, 2, 2, 2, 1, 0],
    "Night Mode": [-5, -4, -3, -1, 0, 1, 1, 0, -1, -2],
    "Soft Background": [-3, -2, -1, 0, 1, 1, 0, -1, -2, -3],
}

EQ_FREQS = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000]

VIDEO_FILTERS = {
    "None": "",
    "Sharper Detail": "lavfi=[unsharp=5:5:0.75:3:3:0.35]",
    "Ultra Sharp": "lavfi=[unsharp=7:7:1.00:5:5:0.50]",
    "Soft Clean": "lavfi=[hqdn3d=1.5:1.5:4:4]",
    "Strong Noise Clean": "lavfi=[hqdn3d=4:3:8:6]",
    "Anime Clean": "lavfi=[hqdn3d=1.0:1.0:3:3,unsharp=3:3:0.45]",
    "Film Contrast": "lavfi=[eq=contrast=1.10:saturation=1.08:gamma=0.98]",
    "Vivid Colour": "lavfi=[eq=contrast=1.12:saturation=1.25:gamma=0.98]",
    "Bright Room": "lavfi=[eq=brightness=0.04:contrast=1.08:saturation=1.06]",
    "Low Light Boost": "lavfi=[eq=brightness=0.07:contrast=1.15:saturation=1.10]",
    "Dark Scene Rescue": "lavfi=[eq=brightness=0.10:contrast=1.22:saturation=1.12:gamma=0.92]",
    "Warm Cinema": "lavfi=[eq=contrast=1.08:saturation=1.12:gamma=0.94]",
    "Cool Clean": "lavfi=[eq=contrast=1.05:saturation=1.06:gamma=1.05]",
    "Old Film": "lavfi=[eq=contrast=1.15:saturation=0.75:gamma=0.92,noise=alls=8:allf=t+u]",
    "Black And White": "lavfi=[format=gray]",
    "Sepia Look": "lavfi=[colorchannelmixer=.393:.769:.189:0:.349:.686:.168:0:.272:.534:.131]",
    "Deinterlace": "lavfi=[yadif]",
    "Fast Sharpen": "lavfi=[unsharp=3:3:0.35]",
    "Edge Enhance": "lavfi=[edgedetect=low=0.05:high=0.20]",
}

RENDERING_SETTINGS = {
    "Balanced": {"deband": "no", "interpolation": "no", "scale": "bilinear", "cscale": "bilinear", "dscale": "bilinear"},
    "High Quality": {"deband": "yes", "interpolation": "yes", "scale": "ewa_lanczossharp", "cscale": "ewa_lanczossharp", "dscale": "mitchell", "correct-downscaling": "yes", "linear-downscaling": "yes"},
    "GPU Next HQ": {"deband": "yes", "interpolation": "yes", "scale": "ewa_lanczossharp", "cscale": "ewa_lanczossharp", "dscale": "ewa_lanczossoft", "correct-downscaling": "yes", "sigmoid-upscaling": "yes"},
    "Sharp Upscale": {"deband": "yes", "interpolation": "no", "scale": "spline36", "cscale": "spline36", "dscale": "spline36", "scale-antiring": "0.7", "cscale-antiring": "0.7"},
    "Anime Upscale": {"deband": "yes", "interpolation": "no", "scale": "ewa_lanczossharp", "cscale": "ewa_lanczossharp", "dscale": "mitchell", "scale-antiring": "0.8"},
    "Smooth Motion": {"deband": "yes", "interpolation": "yes", "video-sync": "display-resample", "scale": "ewa_lanczos", "cscale": "ewa_lanczos", "tscale": "oversample"},
    "Film Quality": {"deband": "yes", "interpolation": "yes", "scale": "ewa_lanczos", "cscale": "ewa_lanczos", "dscale": "mitchell", "dither-depth": "auto"},
    "Low Latency": {"deband": "no", "interpolation": "no", "video-sync": "audio", "framedrop": "vo", "scale": "bilinear", "cscale": "bilinear"},
    "Low CPU": {"deband": "no", "interpolation": "no", "scale": "bilinear", "cscale": "bilinear", "dscale": "bilinear"},
    "Old GPU Safe": {"deband": "no", "interpolation": "no", "scale": "bilinear", "cscale": "bilinear", "dscale": "bilinear", "correct-downscaling": "no"},
    "Battery Saver": {"deband": "no", "interpolation": "no", "framedrop": "decoder+vo", "scale": "bilinear", "cscale": "bilinear"},
}


RENDERING_PRESETS = list(RENDERING_SETTINGS.keys())


def safe_str(value):
    if value is None:
        return "unknown"
    return str(value)


def clamp(value, low, high):
    return max(low, min(high, value))


class MPVController:
    def __init__(self, window_id, initial_file=None):
        self.window_id = window_id
        self.socket_path = f"/tmp/govibe-nexus-video-player-{os.getpid()}.sock"
        self.input_conf_path = f"/tmp/govibe-nexus-video-player-input-{os.getpid()}.conf"
        self.event_file_path = f"/tmp/govibe-nexus-video-player-events-{os.getpid()}.log"
        self.event_helper_path = f"/tmp/govibe-nexus-video-player-event-{os.getpid()}.sh"
        self.event_callback = None
        self.event_thread = None
        self.event_file_thread = None
        self.event_file_pos = 0
        self.event_stop = False
        self.proc = None
        self.req_id = 0
        self.ready = False
        self.start(initial_file=initial_file)

    def start(self, initial_file=None):
        if not shutil.which("mpv"):
            raise RuntimeError("mpv is not installed. Install it with: sudo apt install mpv")
        try:
            if os.path.exists(self.socket_path):
                os.unlink(self.socket_path)
        except Exception:
            pass
        for tmp_path in [self.input_conf_path, self.event_file_path, self.event_helper_path]:
            try:
                if os.path.exists(tmp_path):
                    os.unlink(tmp_path)
            except Exception:
                pass
        try:
            Path(self.event_file_path).write_text("", encoding="utf-8")
            helper = "#!/bin/sh\n"
            helper += "event=\"$1\"\n"
            helper += "[ -n \"$event\" ] || exit 0\n"
            helper += f"printf '%s\n' \"$event\" >> '{self.event_file_path}'\n"
            helper += "exit 0\n"
            Path(self.event_helper_path).write_text(helper, encoding="utf-8")
            os.chmod(self.event_helper_path, 0o700)
            input_conf = (
                f'MBTN_LEFT_DBL run "{self.event_helper_path}" "toggle-fullscreen"\n'
                f'MOUSE_BTN0_DBL run "{self.event_helper_path}" "toggle-fullscreen"\n'
                f'MBTN_RIGHT run "{self.event_helper_path}" "right-click"\n'
                f'MOUSE_BTN2 run "{self.event_helper_path}" "right-click"\n'
                f'ESC run "{self.event_helper_path}" "exit-fullscreen"\n'
                'MOUSE_MOVE script-message govibe-mouse-move\n'
            )
            Path(self.input_conf_path).write_text(input_conf, encoding="utf-8")
        except Exception:
            pass

        base_args = [
            "mpv",
            "--no-config",
            "--idle=yes",
            "--force-window=yes",
            "--keep-open=yes",
            "--no-terminal",
            "--input-default-bindings=yes",
            "--input-vo-keyboard=yes",
            f"--input-conf={self.input_conf_path}",
            "--osc=no",
            "--cursor-autohide=4000",
            "--cursor-autohide-fs-only=yes",
            "--osd-level=1",
            "--volume-max=300",
            f"--wid={self.window_id}",
            f"--input-ipc-server={self.socket_path}",
            "--audio-display=no",
        ]
        video_backends = [
            ["--vo=gpu", "--gpu-context=x11egl"],
            ["--vo=gpu", "--gpu-context=x11"],
            ["--vo=gpu"],
            ["--vo=x11"],
        ]
        last_error = None
        for backend in video_backends:
            try:
                if os.path.exists(self.socket_path):
                    os.unlink(self.socket_path)
            except Exception:
                pass
            args = list(base_args) + list(backend) + ["--border=no", "--msg-level=all=warn"]
            if initial_file:
                args.append(str(initial_file))
            try:
                log = open(MPV_LOG_PATH, "ab", buffering=0)
                log.write(("\n--- Govibe Nexus mpv start " + time.strftime("%Y-%m-%d %H:%M:%S") + " backend=" + " ".join(backend) + " ---\n").encode("utf-8", "replace"))
            except Exception:
                log = subprocess.DEVNULL
            try:
                self.proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=log, start_new_session=True)
                self.ready = self.wait_until_ready(4.0 if initial_file else 6.0)
                if self.process_alive():
                    return
            except Exception as exc:
                last_error = exc
            try:
                if self.proc and self.proc.poll() is None:
                    self.proc.terminate()
                    self.proc.wait(timeout=0.8)
            except Exception:
                try:
                    if self.proc:
                        self.proc.kill()
                except Exception:
                    pass
            self.proc = None
        if last_error:
            raise last_error

    def wait_until_ready(self, timeout=6.0):
        deadline = time.time() + float(timeout)
        while time.time() < deadline:
            if self.proc and self.proc.poll() is not None:
                return False
            if os.path.exists(self.socket_path):
                resp = self._send({"command": ["get_property", "mpv-version"], "request_id": 1}, wait=True, timeout=0.55, attempts=1)
                if resp and resp.get("error") == "success":
                    return True
            time.sleep(0.08)
        return os.path.exists(self.socket_path)

    def _send(self, payload, wait=False, timeout=0.35, attempts=3):
        expected_id = payload.get("request_id") if isinstance(payload, dict) else None
        for _attempt in range(max(1, int(attempts))):
            if self.proc and self.proc.poll() is not None:
                return None
            if not os.path.exists(self.socket_path):
                time.sleep(0.08)
                continue
            try:
                with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
                    client.settimeout(timeout)
                    client.connect(self.socket_path)
                    client.sendall((json.dumps(payload) + "\n").encode("utf-8"))
                    if not wait:
                        return True
                    deadline = time.time() + float(timeout)
                    data = b""
                    last_response = None
                    while time.time() < deadline:
                        try:
                            chunk = client.recv(65536)
                        except socket.timeout:
                            break
                        if not chunk:
                            break
                        data += chunk
                        while b"\n" in data:
                            line, data = data.split(b"\n", 1)
                            if not line:
                                continue
                            try:
                                decoded = json.loads(line.decode("utf-8", "replace"))
                            except Exception:
                                continue
                            if expected_id is not None:
                                if decoded.get("request_id") == expected_id:
                                    return decoded
                                continue
                            if "error" in decoded:
                                return decoded
                            last_response = decoded
                    return last_response
            except Exception:
                time.sleep(0.10)
        return None

    def command(self, *args):
        return bool(self._send({"command": list(args)}, wait=False, attempts=4))

    def command_wait(self, *args, timeout=1.4):
        self.req_id += 1
        resp = self._send({"command": list(args), "request_id": self.req_id}, wait=True, timeout=timeout, attempts=5)
        return bool(resp and resp.get("error") == "success")

    def process_alive(self):
        return bool(self.proc and self.proc.poll() is None)

    def is_alive(self):
        return self.process_alive()

    def get_property(self, prop):
        self.req_id += 1
        resp = self._send({"command": ["get_property", prop], "request_id": self.req_id}, wait=True, attempts=3)
        if not resp or resp.get("error") != "success":
            return None
        return resp.get("data")

    def set_property(self, prop, value):
        return self.command("set_property", prop, value)

    def load_file(self, path):
        if not self.process_alive():
            return False
        if not self.ready:
            self.ready = self.wait_until_ready(8.0)
        if not self.ready:
            return False
        return self.command_wait("loadfile", path, "replace", timeout=5.0)

    def set_event_callback(self, callback):
        self.event_callback = callback
        self.event_stop = False
        if not self.event_thread:
            self.event_thread = threading.Thread(target=self._event_loop, daemon=True)
            self.event_thread.start()
        if not self.event_file_thread:
            self.event_file_thread = threading.Thread(target=self._event_file_loop, daemon=True)
            self.event_file_thread.start()

    def _event_file_loop(self):
        # Robust fallback for mouse events inside mpv's embedded child window.
        # mpv runs a tiny helper that appends event names to this file; this thread forwards them to GTK.
        while not self.event_stop:
            try:
                if os.path.exists(self.event_file_path):
                    with open(self.event_file_path, "r", encoding="utf-8", errors="replace") as fh:
                        fh.seek(self.event_file_pos)
                        lines = fh.readlines()
                        self.event_file_pos = fh.tell()
                    for line in lines:
                        msg = line.strip()
                        if msg and self.event_callback:
                            GLib.idle_add(self.event_callback, msg)
                time.sleep(0.08)
            except Exception:
                time.sleep(0.20)

    def _event_loop(self):
        deadline = time.time() + 4
        while not os.path.exists(self.socket_path) and time.time() < deadline and not self.event_stop:
            time.sleep(0.05)
        if self.event_stop or not os.path.exists(self.socket_path):
            return
        try:
            with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
                client.settimeout(0.5)
                client.connect(self.socket_path)
                buffer = b""
                while not self.event_stop:
                    try:
                        chunk = client.recv(65536)
                    except socket.timeout:
                        continue
                    if not chunk:
                        break
                    buffer += chunk
                    while b"\n" in buffer:
                        line, buffer = buffer.split(b"\n", 1)
                        if not line:
                            continue
                        try:
                            event = json.loads(line.decode("utf-8", "replace"))
                        except Exception:
                            continue
                        if event.get("event") == "client-message":
                            args = event.get("args") or []
                            if args and args[0] == "govibe-toggle-fullscreen" and self.event_callback:
                                GLib.idle_add(self.event_callback, "toggle-fullscreen")
                            elif args and args[0] == "govibe-right-click" and self.event_callback:
                                GLib.idle_add(self.event_callback, "right-click")
                            elif args and args[0] == "govibe-exit-fullscreen" and self.event_callback:
                                GLib.idle_add(self.event_callback, "exit-fullscreen")
                            elif args and args[0] == "govibe-mouse-move" and self.event_callback:
                                GLib.idle_add(self.event_callback, "mouse-move")
        except Exception:
            pass

    def terminate(self):
        self.event_stop = True
        try:
            self.command("quit")
        except Exception:
            pass
        try:
            if self.proc and self.proc.poll() is None:
                self.proc.terminate()
                try:
                    self.proc.wait(timeout=1.5)
                except subprocess.TimeoutExpired:
                    self.proc.kill()
        except Exception:
            pass
        try:
            if os.path.exists(self.socket_path):
                os.unlink(self.socket_path)
        except Exception:
            pass
        for tmp_path in [self.input_conf_path, self.event_file_path, self.event_helper_path]:
            try:
                if os.path.exists(tmp_path):
                    os.unlink(tmp_path)
            except Exception:
                pass


class GovibeNexusVideoPlayer(Gtk.Application):
    def __init__(self):
        super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.HANDLES_OPEN)
        self.window = None
        self.mpv = None
        self.current_file = None
        self.current_theme = "Nexus Green"
        self.osd_enabled = True
        self.is_seeking = False
        self.tray = None
        self.status_icon = None
        self.eq_sliders = []
        self.last_stats = {}
        self.pending_open_files = []
        self.top_menu_visible = False
        self.stats_visible = True
        self.is_fullscreen_mode = False
        self.fullscreen_auto_hide_id = None
        self.fullscreen_pointer_poll_id = None
        self.last_pointer_xy = None
        self.controls_visible = True
        self.options_hidden = False
        self.always_on_top = False
        self.loop_enabled = False
        self.muted = False
        self.volume_level = 100
        self.volume_boost_percent = 100
        self.context_popup = None
        self.color_window = None
        self.last_mpv_mouse_event = 0
        self.last_context_menu_open_ts = 0
        self.color_values = {"brightness": 0, "contrast": 0, "saturation": 0, "gamma": 0, "hue": 0}
        self.default_paned_position = 1000
        self.did_initial_window_fit = False
        self.pending_open_attempts = 0
        self.pending_open_source_id = None
        self.connect("activate", self.on_activate)
        self.connect("open", self.on_open)

    def on_open(self, app, files, n_files, hint):
        self.pending_open_files = [f.get_path() for f in files if f.get_path()]
        self.pending_open_attempts = 0
        self.activate()
        if self.pending_open_files:
            self.schedule_pending_open(450)

    def on_activate(self, app):
        if self.window:
            self.force_launch_visible()
            return
        self.build_ui()
        self.window.show_all()
        self.apply_theme(self.current_theme)
        self.force_launch_visible()
        GLib.idle_add(self.launch_window_full_visible)
        GLib.timeout_add(250, self.launch_window_full_visible)
        GLib.timeout_add(700, self.force_launch_visible)
        self.setup_tray()
        # Do not start mpv on an empty window. Starting mpv too early can cover the GTK skin on some Ubuntu/GNOME sessions.
        # mpv starts only when a media file is opened, and embeds into the dedicated video host widget.
        GLib.timeout_add_seconds(1, self.refresh_stats)
        if self.pending_open_files:
            self.schedule_pending_open(900)

    def build_ui(self):
        self.window = Gtk.ApplicationWindow(application=self)
        self.window.set_title(APP_NAME)
        self.window.set_default_size(1360, 800)
        self.window.set_size_request(1060, 680)
        try:
            self.window.set_position(Gtk.WindowPosition.CENTER)
        except Exception:
            pass
        self.window.set_icon_name("govibe-nexus-video-player")
        try:
            self.window.set_wmclass("govibe-nexus-video-player", "Govibe Nexus Video Player")
        except Exception:
            pass
        self.window.connect("delete-event", self.on_window_close)
        self.window.connect("window-state-event", self.on_window_state_event)
        self.window.connect("configure-event", self.on_window_configure)
        self.window.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.KEY_PRESS_MASK)
        self.window.connect("button-press-event", self.on_right_click)
        self.window.connect("key-press-event", self.on_key_press)
        self.window.connect("motion-notify-event", self.on_mouse_motion)

        main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        self.main_box = main
        main.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
        main.connect("button-press-event", self.on_right_click)
        main.connect("motion-notify-event", self.on_mouse_motion)
        self.window.add(main)

        menubar = Gtk.MenuBar()
        self.menubar = menubar
        menubar.set_no_show_all(True)
        file_menu = Gtk.Menu()
        file_item = Gtk.MenuItem(label="File")
        file_item.set_submenu(file_menu)
        open_item = Gtk.MenuItem(label="Open Video or Audio")
        open_item.connect("activate", lambda *_: self.choose_file())
        default_item = Gtk.MenuItem(label="Set as the default video player")
        default_item.connect("activate", lambda *_: self.set_default_video_player())
        quit_item = Gtk.MenuItem(label="Exit")
        quit_item.connect("activate", lambda *_: self.quit_app())
        file_menu.append(open_item)
        file_menu.append(default_item)
        file_menu.append(Gtk.SeparatorMenuItem())
        file_menu.append(quit_item)

        view_menu = Gtk.Menu()
        view_item = Gtk.MenuItem(label="View")
        view_item.set_submenu(view_menu)
        fullscreen_item = Gtk.MenuItem(label="Full Screen")
        fullscreen_item.connect("activate", lambda *_: self.toggle_fullscreen())
        osd_item = Gtk.CheckMenuItem(label="Show title banner on start")
        osd_item.set_active(True)
        osd_item.connect("toggled", self.toggle_osd)
        view_menu.append(fullscreen_item)
        view_menu.append(osd_item)

        help_menu = Gtk.Menu()
        help_item = Gtk.MenuItem(label="Help")
        help_item.set_submenu(help_menu)
        govibe_item = Gtk.MenuItem(label="About Govibe.org")
        govibe_item.connect("activate", lambda *_: self.open_govibe_website())
        about_item = Gtk.MenuItem(label="About")
        about_item.connect("activate", lambda *_: self.about_dialog())
        help_menu.append(govibe_item)
        help_menu.append(about_item)

        menubar.append(file_item)
        menubar.append(view_item)
        menubar.append(help_item)
        main.pack_start(menubar, False, False, 0)
        menubar.hide()

        titlebar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        self.titlebar = titlebar
        titlebar.get_style_context().add_class("topbar")
        titlebar.set_border_width(8)
        main.pack_start(titlebar, False, False, 0)

        logo = Gtk.Label(label="NEXUS")
        logo.get_style_context().add_class("logo")
        titlebar.pack_start(logo, False, False, 6)

        self.file_label = Gtk.Label(label="No media loaded")
        self.file_label.set_xalign(0)
        self.file_label.get_style_context().add_class("filename")
        titlebar.pack_start(self.file_label, True, True, 4)

        self.time_label = Gtk.Label(label="00:00 / 00:00")
        self.time_label.get_style_context().add_class("timecode")
        titlebar.pack_start(self.time_label, False, False, 6)

        paned = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
        self.paned = paned
        main.pack_start(paned, True, True, 0)

        video_column = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        video_column.set_hexpand(True)
        video_column.set_vexpand(True)
        paned.pack1(video_column, True, True)

        self.overlay = Gtk.Overlay()
        self.overlay.set_hexpand(True)
        self.overlay.set_vexpand(True)
        self.overlay.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
        self.overlay.connect("button-press-event", self.on_right_click)
        self.overlay.connect("motion-notify-event", self.on_mouse_motion)
        self.overlay.get_style_context().add_class("videowrap")

        # Dedicated X11/XWayland host for mpv.
        # Gtk.Socket is made for embedding an external X11 client. This prevents mpv from opening as a separate movable window.
        self.video_host_kind = "socket"
        try:
            self.video_area = Gtk.Socket()
            self.video_area.set_can_focus(True)
        except Exception:
            self.video_host_kind = "eventbox"
            self.video_area = Gtk.EventBox()
            self.video_area.set_visible_window(True)
            self.video_area.set_above_child(False)
        self.video_area.set_size_request(640, 360)
        self.video_area.set_hexpand(True)
        self.video_area.set_vexpand(True)
        self.video_area.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
        self.video_area.connect("button-press-event", self.on_right_click)
        self.video_area.connect("motion-notify-event", self.on_mouse_motion)
        self.video_area.get_style_context().add_class("videohost")
        self.video_area.connect("realize", self.on_video_realize)
        self.overlay.add(self.video_area)

        # No transparent GTK shield is placed above mpv.
        # The v1.0.12 shield could cover the embedded X11 video surface and leave playback black on some Ubuntu sessions.
        # Right click and fullscreen video shortcuts are handled by mpv input bindings instead.

        self.placeholder = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        self.placeholder.set_halign(Gtk.Align.CENTER)
        self.placeholder.set_valign(Gtk.Align.CENTER)
        self.placeholder.get_style_context().add_class("placeholder")
        ph_title = Gtk.Label(label="GOVIBE NEXUS VIDEO PLAYER")
        ph_title.set_line_wrap(True)
        ph_title.set_max_width_chars(32)
        ph_title.get_style_context().add_class("placeholder_title")
        ph_sub = Gtk.Label(label="Open a video to start mpv powered playback")
        ph_sub.get_style_context().add_class("placeholder_sub")
        ph_btn = Gtk.Button(label="OPEN VIDEO OR AUDIO")
        ph_btn.get_style_context().add_class("nexusbtn")
        ph_btn.connect("clicked", lambda *_: self.choose_file())
        self.placeholder.pack_start(ph_title, False, False, 0)
        self.placeholder.pack_start(ph_sub, False, False, 0)
        self.placeholder.pack_start(ph_btn, False, False, 0)
        self.overlay.add_overlay(self.placeholder)

        self.banner = Gtk.Label(label="")
        self.banner.get_style_context().add_class("osdbanner")
        self.banner.set_halign(Gtk.Align.CENTER)
        self.banner.set_valign(Gtk.Align.START)
        self.banner.set_margin_top(12)
        self.banner.set_no_show_all(True)
        self.overlay.add_overlay(self.banner)

        self.fullscreen_overlay_controls = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        self.fullscreen_overlay_controls.get_style_context().add_class("overlaycontrols")
        self.fullscreen_overlay_controls.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
        self.fullscreen_overlay_controls.connect("button-press-event", self.on_right_click)
        self.fullscreen_overlay_controls.connect("motion-notify-event", self.on_mouse_motion)
        self.fullscreen_overlay_controls.set_halign(Gtk.Align.FILL)
        self.fullscreen_overlay_controls.set_valign(Gtk.Align.END)
        self.fullscreen_overlay_controls.set_margin_left(18)
        self.fullscreen_overlay_controls.set_margin_right(18)
        self.fullscreen_overlay_controls.set_margin_bottom(18)

        overlay_seek = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        self.overlay_seek_box = overlay_seek
        self.overlay_position = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 100, 0.1)
        self.overlay_position.set_draw_value(False)
        self.overlay_position.connect("button-press-event", self.seek_start)
        self.overlay_position.connect("button-release-event", self.seek_finish)
        overlay_seek.pack_start(self.overlay_position, True, True, 0)
        self.fullscreen_overlay_controls.pack_start(overlay_seek, False, False, 0)

        overlay_buttons = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=7)
        self.overlay_buttons_box = overlay_buttons
        overlay_buttons.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
        overlay_buttons.connect("button-press-event", self.on_right_click)
        overlay_buttons.connect("motion-notify-event", self.on_mouse_motion)
        self.add_button(overlay_buttons, "OPEN", self.choose_file)
        self.add_button(overlay_buttons, "PLAY", lambda *_: self.set_pause(False))
        self.add_button(overlay_buttons, "PAUSE", lambda *_: self.set_pause(True))
        self.add_button(overlay_buttons, "STOP", lambda *_: self.stop_media())
        self.overlay_mute_button = self.add_button(overlay_buttons, "MUTE", lambda *_: self.set_mute_enabled(not self.muted))
        self.overlay_volume_boost_button = self.add_button(overlay_buttons, "BOOST OFF", lambda btn: self.open_volume_boost_menu(btn))
        self.overlay_loop_button = self.add_button(overlay_buttons, "LOOP", lambda *_: self.set_loop_enabled(not self.loop_enabled))
        self.add_button(overlay_buttons, "FULL", lambda *_: self.toggle_fullscreen())
        self.add_button(overlay_buttons, "OPTIONS", lambda *_: self.open_options_panel())
        self.fullscreen_overlay_controls.pack_start(overlay_buttons, False, False, 0)
        self.fullscreen_overlay_controls.set_no_show_all(True)
        self.fullscreen_overlay_controls.hide()
        self.overlay.add_overlay(self.fullscreen_overlay_controls)
        try:
            self.overlay.set_overlay_pass_through(self.fullscreen_overlay_controls, False)
        except Exception:
            pass

        video_column.pack_start(self.overlay, True, True, 0)

        seek_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        self.seek_box = seek_box
        seek_box.get_style_context().add_class("controlbar")
        seek_box.set_border_width(8)
        video_column.pack_start(seek_box, False, False, 0)
        self.position = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 100, 0.1)
        self.position.set_draw_value(False)
        self.position.connect("button-press-event", self.seek_start)
        self.position.connect("button-release-event", self.seek_finish)
        seek_box.pack_start(self.position, True, True, 0)

        controls_scroll = Gtk.ScrolledWindow()
        self.controls_scroll = controls_scroll
        controls_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER)
        controls_scroll.set_min_content_height(62)
        controls_scroll.set_shadow_type(Gtk.ShadowType.NONE)
        controls_scroll.get_style_context().add_class("controlbar")
        video_column.pack_start(controls_scroll, False, False, 0)

        controls = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
        self.controls_box = controls
        controls.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
        controls.connect("button-press-event", self.on_right_click)
        controls.connect("motion-notify-event", self.on_mouse_motion)
        controls.get_style_context().add_class("controlbar")
        controls.set_border_width(6)
        controls_scroll.add(controls)
        self.add_button(controls, "OPEN", self.choose_file)
        self.add_button(controls, "PLAY", lambda *_: self.set_pause(False))
        self.add_button(controls, "PAUSE", lambda *_: self.set_pause(True))
        self.add_button(controls, "STOP", lambda *_: self.stop_media())
        self.mute_button = self.add_button(controls, "MUTE", lambda *_: self.set_mute_enabled(not self.muted))
        self.loop_button = self.add_button(controls, "LOOP", lambda *_: self.set_loop_enabled(not self.loop_enabled))
        self.add_button(controls, "FULL", lambda *_: self.toggle_fullscreen())
        self.focus_button = self.add_button(controls, "FOCUS VIDEO", lambda *_: self.toggle_focus_mode())
        self.options_button = self.add_button(controls, "OPTIONS", lambda *_: self.open_options_panel())
        self.menu_button = self.add_button(controls, "MENU", lambda *_: self.toggle_top_menu())
        self.stats_button = self.add_button(controls, "HIDE STATS", lambda *_: self.toggle_stats_details())
        volume_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
        volume_box.get_style_context().add_class("volumecluster")
        controls.pack_start(volume_box, False, False, 0)
        volume_label = Gtk.Label(label="VOL")
        volume_label.get_style_context().add_class("tiny")
        volume_box.pack_start(volume_label, False, False, 0)
        self.volume_slider = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 100, 1)
        self.volume_slider.set_value(self.volume_level)
        self.volume_slider.set_draw_value(True)
        self.volume_slider.set_size_request(185, -1)
        self.volume_slider.connect("value-changed", self.on_volume_slider_changed)
        volume_box.pack_start(self.volume_slider, False, False, 0)
        # Volume boost moved to the right side action panel so the bottom control row stays fully visible.

        self.stats_label = Gtk.Label(label="Stats will appear when media starts.")
        self.stats_label.set_xalign(0)
        self.stats_label.set_line_wrap(True)
        self.stats_label.get_style_context().add_class("stats")
        video_column.pack_start(self.stats_label, False, False, 8)

        side_scroll = Gtk.ScrolledWindow()
        self.side_scroll = side_scroll
        side_scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        side_scroll.set_size_request(320, -1)
        side_scroll.set_min_content_width(300)
        side_scroll.set_hexpand(False)
        paned.pack2(side_scroll, False, False)
        side = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        side.set_border_width(8)
        side.set_size_request(300, -1)
        side_scroll.add(side)

        self.build_theme_panel(side)
        self.build_eq_panel(side)
        self.build_video_filter_panel(side)
        self.build_govibe_bottom_link(side)

        self.default_paned_position = 1000
        paned.set_position(self.default_paned_position)
        GLib.idle_add(self.fit_layout_to_window)

    def add_button(self, box, label, callback):
        btn = Gtk.Button(label=label)
        btn.get_style_context().add_class("nexusbtn")
        btn.connect("clicked", callback)
        box.pack_start(btn, False, False, 0)
        return btn

    def build_theme_panel(self, side):
        frame = self.panel_frame("Skins and colour theme")
        side.pack_start(frame, False, False, 0)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        frame.add(box)
        self.theme_combo = Gtk.ComboBoxText()
        for name in THEMES:
            self.theme_combo.append_text(name)
        self.theme_combo.set_active(0)
        self.theme_combo.connect("changed", lambda combo: self.apply_theme(combo.get_active_text() or "Nexus Green"))
        box.pack_start(Gtk.Label(label="Colour skin"), False, False, 0)
        self.theme_combo.set_size_request(300, -1)
        box.pack_start(self.theme_combo, False, False, 0)
        hint = Gtk.Label(label="Default layout is Wide Theater. Theme, EQ, filter, and render controls stay clean in this skin.")
        hint.set_xalign(0)
        hint.set_line_wrap(True)
        hint.get_style_context().add_class("hint")
        box.pack_start(hint, False, False, 0)

    def build_eq_panel(self, side):
        frame = self.panel_frame("Video sound equalizer")
        side.pack_start(frame, False, False, 0)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        frame.add(box)
        preset_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        box.pack_start(preset_row, False, False, 0)
        self.eq_combo = Gtk.ComboBoxText()
        for name in EQ_PRESETS:
            self.eq_combo.append_text(name)
        self.eq_combo.set_active(0)
        self.eq_combo.connect("changed", self.eq_preset_changed)
        self.eq_combo.set_size_request(230, -1)
        preset_row.pack_start(self.eq_combo, True, True, 0)
        self.add_button(preset_row, "RESET EQ", lambda *_: self.apply_eq_preset("Flat"))

        grid = Gtk.Grid(column_spacing=6, row_spacing=4)
        box.pack_start(grid, False, False, 0)
        self.eq_sliders = []
        for i, freq in enumerate(EQ_FREQS):
            slider = Gtk.Scale.new_with_range(Gtk.Orientation.VERTICAL, -12, 12, 1)
            slider.set_value(0)
            slider.set_inverted(True)
            slider.set_size_request(28, 120)
            slider.set_draw_value(True)
            slider.connect("value-changed", lambda *_: self.apply_custom_eq())
            self.eq_sliders.append(slider)
            grid.attach(slider, i, 0, 1, 1)
            label = Gtk.Label(label=(str(freq) if freq < 1000 else f"{int(freq/1000)}K"))
            label.get_style_context().add_class("tiny")
            grid.attach(label, i, 1, 1, 1)

    def build_video_filter_panel(self, side):
        frame = self.panel_frame("Video filter and rendering")
        side.pack_start(frame, False, False, 0)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=7)
        frame.add(box)
        self.filter_combo = Gtk.ComboBoxText()
        for name in VIDEO_FILTERS:
            self.filter_combo.append_text(name)
        self.filter_combo.set_active(0)
        self.filter_combo.connect("changed", self.video_filter_changed)
        box.pack_start(Gtk.Label(label="Video filter preset"), False, False, 0)
        self.filter_combo.set_size_request(300, -1)
        box.pack_start(self.filter_combo, False, False, 0)

        self.render_combo = Gtk.ComboBoxText()
        for name in RENDERING_PRESETS:
            self.render_combo.append_text(name)
        self.render_combo.set_active(0)
        self.render_combo.connect("changed", self.rendering_changed)
        box.pack_start(Gtk.Label(label="Rendering preset"), False, False, 0)
        self.render_combo.set_size_request(300, -1)
        box.pack_start(self.render_combo, False, False, 0)

        row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        box.pack_start(row, False, False, 0)
        self.add_button(row, "DEFAULT FILTER", lambda *_: self.reset_video_filters())
        self.add_button(row, "DEFAULT RENDER", lambda *_: self.apply_rendering("Balanced"))

    def build_govibe_bottom_link(self, side):
        spacer = Gtk.Box()
        spacer.set_vexpand(True)
        side.pack_start(spacer, True, True, 0)
        link_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        link_box.get_style_context().add_class("panel")
        link_box.set_border_width(8)

        quick_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        link_box.pack_start(quick_row, False, False, 0)

        self.color_adjust_button = Gtk.Button(label="OPEN COLOUR ADJUSTMENT")
        self.color_adjust_button.get_style_context().add_class("nexusbtn")
        self.color_adjust_button.connect("clicked", lambda *_: self.toggle_color_adjustment_dialog())
        quick_row.pack_start(self.color_adjust_button, True, True, 0)

        self.volume_boost_button = Gtk.Button(label="BOOST VOLUME")
        self.volume_boost_button.get_style_context().add_class("nexusbtn")
        self.volume_boost_button.connect("clicked", lambda btn: self.open_volume_boost_menu(btn))
        quick_row.pack_start(self.volume_boost_button, True, True, 0)

        link = Gtk.Button(label="govibe.org")
        link.get_style_context().add_class("linkbtn")
        link.connect("clicked", lambda *_: self.open_govibe_website())
        link_box.pack_start(link, False, False, 0)
        side.pack_start(link_box, False, False, 0)

    def build_color_panel(self, side):
        frame = self.panel_frame("Colour adjustment")
        side.pack_start(frame, False, False, 0)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=7)
        frame.add(box)
        self.color_controls = {}
        for name, low, high, default in [
            ("brightness", -100, 100, 0),
            ("contrast", -100, 100, 0),
            ("saturation", -100, 100, 0),
            ("gamma", -100, 100, 0),
            ("hue", -100, 100, 0),
        ]:
            row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
            label = Gtk.Label(label=name.capitalize())
            label.set_size_request(90, -1)
            label.set_xalign(0)
            row.pack_start(label, False, False, 0)
            scale = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, low, high, 1)
            scale.set_value(default)
            scale.set_draw_value(True)
            scale.connect("value-changed", self.color_changed, name)
            row.pack_start(scale, True, True, 0)
            self.color_controls[name] = scale
            box.pack_start(row, False, False, 0)
        self.add_button(box, "SET COLOUR TO DEFAULT", lambda *_: self.reset_color())

    def build_settings_panel(self, side):
        frame = self.panel_frame("Settings")
        side.pack_start(frame, False, False, 0)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
        frame.add(box)
        self.osd_check = Gtk.CheckButton(label="Show title and resolution banner for 4 seconds")
        self.osd_check.set_active(True)
        self.osd_check.connect("toggled", self.toggle_osd)
        box.pack_start(self.osd_check, False, False, 0)
        self.keep_open_check = Gtk.CheckButton(label="Keep video open after ending")
        self.keep_open_check.set_active(True)
        self.keep_open_check.connect("toggled", lambda w: self.mpv and self.mpv.set_property("keep-open", "yes" if w.get_active() else "no"))
        box.pack_start(self.keep_open_check, False, False, 0)
        self.always_top_check = Gtk.CheckButton(label="Always on top")
        self.always_top_check.connect("toggled", self.toggle_always_on_top)
        box.pack_start(self.always_top_check, False, False, 0)
        self.loop_check = Gtk.CheckButton(label="Loop current media")
        self.loop_check.connect("toggled", self.toggle_loop)
        box.pack_start(self.loop_check, False, False, 0)
        self.mute_check = Gtk.CheckButton(label="Mute audio")
        self.mute_check.connect("toggled", self.toggle_mute)
        box.pack_start(self.mute_check, False, False, 0)
        self.add_button(box, "TAKE SCREENSHOT", lambda *_: self.take_screenshot())
        self.add_button(box, "ABOUT GOVIBE.ORG", lambda *_: self.open_govibe_website())
        info = Gtk.Label(label="Right click anywhere in the player for playback, fullscreen, focus video, themes, EQ presets, video filters, rendering presets, colour adjustment, screenshot, default video player, and Govibe.org.")
        info.set_xalign(0)
        info.set_line_wrap(True)
        info.get_style_context().add_class("hint")
        box.pack_start(info, False, False, 0)

    def apply_template(self, name):
        settings = TEMPLATES.get(name, TEMPLATES["Wide Theater"])
        try:
            if self.window:
                self.window.resize(settings["width"], settings["height"])
            self.default_paned_position = settings.get("paned", 860)
            if hasattr(self, "paned"):
                self.paned.set_position(self.default_paned_position)
            if hasattr(self, "side_scroll"):
                self.side_scroll.set_size_request(settings["side"], -1)
                self.side_scroll.set_min_content_width(max(300, settings["side"] - 20))
            if hasattr(self, "video_area"):
                self.video_area.set_size_request(settings["video_w"], settings["video_h"])
        except Exception:
            pass

    def panel_frame(self, title):
        frame = Gtk.Frame(label=title)
        frame.get_style_context().add_class("panel")
        frame.set_shadow_type(Gtk.ShadowType.NONE)
        return frame

    def on_video_realize(self, widget):
        # mpv is intentionally started only after the user opens media.
        return False

    def get_video_window_id(self):
        try:
            if not self.video_area.get_realized():
                self.video_area.realize()
            while Gtk.events_pending():
                Gtk.main_iteration_do(False)
            if self.video_host_kind == "socket" and hasattr(self.video_area, "get_id"):
                xid = int(self.video_area.get_id())
            else:
                gdk_window = self.video_area.get_window()
                if not gdk_window:
                    return None
                try:
                    gdk_window.ensure_native()
                except Exception:
                    pass
                xid = int(gdk_window.get_xid())
            if xid <= 0:
                return None
            return xid
        except Exception:
            return None

    def schedule_pending_open(self, delay_ms=800):
        if not self.pending_open_files:
            return False
        if self.pending_open_source_id:
            try:
                GLib.source_remove(self.pending_open_source_id)
            except Exception:
                pass
            self.pending_open_source_id = None
        self.pending_open_source_id = GLib.timeout_add(int(delay_ms), self.try_pending_open)
        return False

    def try_pending_open(self):
        self.pending_open_source_id = None
        if not self.pending_open_files:
            self.pending_open_attempts = 0
            return False
        path = self.pending_open_files[0]
        if not path or not os.path.exists(path):
            self.pending_open_files = []
            self.pending_open_attempts = 0
            return False
        if hasattr(self, "stats_label"):
            self.stats_label.set_text("Loading media engine...")
        ok = self.open_media(path)
        if ok:
            self.pending_open_files = []
            self.pending_open_attempts = 0
            return False
        self.pending_open_attempts += 1
        if self.pending_open_attempts < 20:
            delay = 450 if self.pending_open_attempts < 6 else 900
            self.pending_open_source_id = GLib.timeout_add(delay, self.try_pending_open)
            return False
        if hasattr(self, "stats_label"):
            self.stats_label.set_text("The media engine did not answer yet. Click Open Video or Audio again, or restart the player.")
        return False

    def ensure_mpv_started(self, initial_file=None):
        if self.mpv:
            try:
                if self.mpv.process_alive():
                    return True
                self.mpv.terminate()
            except Exception:
                pass
            self.mpv = None
        xid = self.get_video_window_id()
        if not xid:
            self.show_message("Could not create the internal video surface. On Ubuntu Wayland, use the Ubuntu on Xorg session for embedded mpv video.", Gtk.MessageType.WARNING)
            return False
        try:
            self.mpv = MPVController(xid, initial_file=initial_file)
            if not self.mpv.process_alive():
                try:
                    self.mpv.terminate()
                except Exception:
                    pass
                self.mpv = None
                return False
            try:
                self.mpv.set_event_callback(self.on_mpv_client_message)
            except Exception:
                pass
            self.apply_rendering("Balanced")
            self.apply_volume_settings()
            return True
        except Exception as exc:
            self.mpv = None
            self.show_message(str(exc), Gtk.MessageType.ERROR)
            return False

    def choose_file(self, *_):
        dialog = Gtk.FileChooserDialog(
            title="Open video or audio",
            transient_for=self.window,
            action=Gtk.FileChooserAction.OPEN,
        )
        dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
        filt = Gtk.FileFilter()
        filt.set_name("Video and audio files")
        for pat in ["*.mp4", "*.mkv", "*.webm", "*.mov", "*.avi", "*.wmv", "*.mpeg", "*.mpg", "*.m4v", "*.flv", "*.mp3", "*.wav", "*.flac", "*.ogg", "*.m4a"]:
            filt.add_pattern(pat)
        dialog.add_filter(filt)
        resp = dialog.run()
        if resp == Gtk.ResponseType.OK:
            selected = dialog.get_filename()
            if selected and not self.open_media(selected):
                self.pending_open_files = [selected]
                self.pending_open_attempts = 0
                self.schedule_pending_open(700)
        dialog.destroy()

    def open_media(self, path):
        if not path:
            return False
        first_start = not (self.mpv and self.mpv.process_alive())
        if first_start:
            if not self.ensure_mpv_started(initial_file=path):
                return False
            loaded = True
        else:
            loaded = self.mpv.load_file(path)
            if not loaded:
                try:
                    self.mpv.terminate()
                except Exception:
                    pass
                self.mpv = None
                if not self.ensure_mpv_started(initial_file=path):
                    return False
                loaded = True
        if not loaded:
            if hasattr(self, "stats_label"):
                self.stats_label.set_text("Media engine is starting. Retrying automatically...")
            return False
        self.current_file = path
        self.file_label.set_text(Path(path).name)
        if hasattr(self, "placeholder"):
            self.placeholder.hide()
        if hasattr(self, "stats_label"):
            self.stats_label.set_text("Media loaded. Stats will appear when playback starts.")
        self.post_open_setup_attempts = 0
        GLib.timeout_add(250, self.finish_media_runtime_setup)
        GLib.timeout_add(900, lambda: self.show_start_banner() or False)
        return True

    def finish_media_runtime_setup(self):
        if not self.mpv or not self.mpv.process_alive():
            return False
        if not self.mpv.ready:
            self.mpv.ready = self.mpv.wait_until_ready(0.5)
        if not self.mpv.ready:
            self.post_open_setup_attempts = getattr(self, "post_open_setup_attempts", 0) + 1
            return self.post_open_setup_attempts < 16
        try:
            self.mpv.set_event_callback(self.on_mpv_client_message)
        except Exception:
            pass
        try:
            self.mpv.set_property("keep-open", "yes")
        except Exception:
            pass
        self.set_loop_enabled(self.loop_enabled)
        self.set_mute_enabled(self.muted)
        self.apply_volume_settings()
        self.apply_color_values()
        self.set_pause(False)
        return False

    def stop_media(self):
        # Keep the loaded file available so PLAY can restart it.
        # This avoids the bug where Stop unloaded mpv and Play had nothing left to resume.
        if self.mpv and self.current_file:
            try:
                self.mpv.command("seek", 0, "absolute")
                self.mpv.set_property("pause", True)
            except Exception:
                pass
            if hasattr(self, "placeholder"):
                self.placeholder.hide()
            self.time_label.set_text("00:00 / " + self.format_time(self.last_stats.get("duration") or 0))
            self.stats_label.set_text("Stopped at beginning. Press PLAY to restart.")
            return
        if self.mpv:
            self.mpv.command("stop")
        self.file_label.set_text("No media loaded")
        if hasattr(self, "placeholder"):
            self.placeholder.show_all()
        self.stats_label.set_text("Stopped.")
        self.time_label.set_text("00:00 / 00:00")

    def set_pause(self, pause):
        if not self.mpv:
            return
        if not pause and self.current_file:
            try:
                idle = self.mpv.get_property("idle-active")
                eof = self.mpv.get_property("eof-reached")
                pos = self.mpv.get_property("time-pos") or 0
                dur = self.mpv.get_property("duration") or 0
                if idle:
                    self.mpv.load_file(self.current_file)
                elif eof or (dur and float(pos) >= max(0, float(dur) - 0.25)):
                    self.mpv.command("seek", 0, "absolute")
            except Exception:
                pass
        self.mpv.set_property("pause", bool(pause))

    def relative_seek(self, seconds):
        if self.mpv:
            self.mpv.command("seek", seconds, "relative")

    def seek_start(self, *_):
        self.is_seeking = True
        return False

    def seek_finish(self, slider=None, *_):
        if self.mpv:
            try:
                value = float(slider.get_value()) if slider is not None and hasattr(slider, "get_value") else float(self.position.get_value())
            except Exception:
                value = float(self.position.get_value())
            self.mpv.command("seek", value, "absolute")
            if hasattr(self, "position"):
                self.position.set_value(value)
            if hasattr(self, "overlay_position"):
                self.overlay_position.set_value(value)
        self.is_seeking = False
        return False

    def refresh_stats(self):
        if not self.mpv:
            return True
        props = {}
        for prop in [
            "time-pos", "duration", "pause", "width", "height", "container-fps", "estimated-vf-fps",
            "video-codec", "audio-codec", "audio-params/samplerate", "audio-params/channel-count",
            "video-bitrate", "audio-bitrate", "packet-video-bitrate", "packet-audio-bitrate", "media-title", "path",
        ]:
            props[prop] = self.mpv.get_property(prop)
        self.last_stats = props
        pos = props.get("time-pos") or 0
        dur = props.get("duration") or 0
        if dur and not self.is_seeking:
            self.position.set_range(0, float(dur))
            self.position.set_value(float(pos))
            if hasattr(self, "overlay_position"):
                self.overlay_position.set_range(0, float(dur))
                self.overlay_position.set_value(float(pos))
        self.time_label.set_text(f"{self.format_time(pos)} / {self.format_time(dur)}")
        w = props.get("width") or "?"
        h = props.get("height") or "?"
        fps = props.get("container-fps") or props.get("estimated-vf-fps") or "unknown"
        hz = props.get("audio-params/samplerate") or "unknown"
        vbit = props.get("video-bitrate") or props.get("packet-video-bitrate")
        abit = props.get("audio-bitrate") or props.get("packet-audio-bitrate")
        vbit_s = self.format_bitrate(vbit)
        abit_s = self.format_bitrate(abit)
        vcodec = props.get("video-codec") or "unknown"
        acodec = props.get("audio-codec") or "unknown"
        channels = props.get("audio-params/channel-count") or "unknown"
        title = props.get("media-title") or (Path(self.current_file).name if self.current_file else "unknown")
        title_res = f"{title} | {w}x{h}" if w != "?" and h != "?" else str(title)
        try:
            self.file_label.set_text(title_res)
            self.window.set_title(f"{APP_NAME} - {title_res}")
        except Exception:
            pass
        self.stats_label.set_text(
            f"Title: {title}\n"
            f"Video: {w}x{h} | FPS: {fps} | Codec: {vcodec} | Bitrate: {vbit_s}\n"
            f"Audio: {hz} Hz | Channels: {channels} | Codec: {acodec} | Bitrate: {abit_s}"
        )
        return True

    def format_time(self, value):
        try:
            value = int(float(value or 0))
        except Exception:
            value = 0
        h = value // 3600
        m = (value % 3600) // 60
        s = value % 60
        if h:
            return f"{h:02d}:{m:02d}:{s:02d}"
        return f"{m:02d}:{s:02d}"

    def format_bitrate(self, value):
        if value is None:
            return "unknown"
        try:
            value = float(value)
            if value <= 0:
                return "unknown"
            if value > 1000000:
                return f"{value / 1000000:.2f} Mbps"
            if value > 1000:
                return f"{value / 1000:.0f} kbps"
            return f"{value:.0f} bps"
        except Exception:
            return str(value)

    def show_start_banner(self):
        if not self.osd_enabled:
            return False
        title = self.last_stats.get("media-title") or (Path(self.current_file).name if self.current_file else "Media")
        w = self.last_stats.get("width") or "?"
        h = self.last_stats.get("height") or "?"
        fps = self.last_stats.get("container-fps") or self.last_stats.get("estimated-vf-fps") or ""
        suffix = f" | {w}x{h}" if w != "?" else ""
        if fps:
            suffix += f" | {fps} FPS"
        self.banner.set_text(f"{title}{suffix}")
        self.banner.show()
        GLib.timeout_add_seconds(4, self.hide_banner)
        return False

    def hide_banner(self):
        self.banner.hide()
        return False

    def toggle_osd(self, widget):
        self.osd_enabled = widget.get_active()
        if hasattr(self, "osd_check") and widget is not self.osd_check:
            self.osd_check.set_active(self.osd_enabled)

    def eq_preset_changed(self, combo):
        name = combo.get_active_text()
        if name:
            self.apply_eq_preset(name)

    def apply_eq_preset(self, name):
        gains = EQ_PRESETS.get(name, EQ_PRESETS["Flat"])
        for slider, gain in zip(self.eq_sliders, gains):
            slider.handler_block_by_func(self.apply_custom_eq) if False else None
            slider.set_value(gain)
        self.apply_custom_eq()
        if hasattr(self, "eq_combo"):
            for i, key in enumerate(EQ_PRESETS.keys()):
                if key == name:
                    self.eq_combo.set_active(i)
                    break

    def apply_custom_eq(self, *_):
        if not self.mpv or not self.eq_sliders:
            return
        gains = [int(slider.get_value()) for slider in self.eq_sliders]
        if all(g == 0 for g in gains):
            self.mpv.command("af", "clr", "")
            return
        filters = []
        for freq, gain in zip(EQ_FREQS, gains):
            if gain != 0:
                filters.append(f"equalizer=f={freq}:t=q:w=1:g={gain}")
        filter_chain = "lavfi=[" + ",".join(filters) + "]"
        self.mpv.command("af", "set", filter_chain)

    def video_filter_changed(self, combo):
        name = combo.get_active_text() or "None"
        self.apply_video_filter(name)

    def apply_video_filter(self, name):
        if not self.mpv:
            return
        filt = VIDEO_FILTERS.get(name, "")
        if not filt:
            self.mpv.command("vf", "clr", "")
        else:
            self.mpv.command("vf", "set", filt)

    def reset_video_filters(self):
        if hasattr(self, "filter_combo"):
            self.filter_combo.set_active(0)
        if self.mpv:
            self.mpv.command("vf", "clr", "")

    def rendering_changed(self, combo):
        self.apply_rendering(combo.get_active_text() or "Balanced")

    def apply_rendering(self, name):
        if hasattr(self, "render_combo"):
            for i, key in enumerate(RENDERING_PRESETS):
                if key == name:
                    self.render_combo.set_active(i)
                    break
        if not self.mpv:
            return
        settings = RENDERING_SETTINGS.get(name, RENDERING_SETTINGS.get("Balanced", {}))
        for prop, val in settings.items():
            self.mpv.set_property(prop, val)

    def color_changed(self, slider, name):
        try:
            self.color_values[name] = int(slider.get_value())
        except Exception:
            pass
        if self.mpv:
            self.mpv.set_property(name, int(self.color_values.get(name, 0)))

    def apply_color_values(self):
        if not self.mpv:
            return
        for name, value in self.color_values.items():
            try:
                self.mpv.set_property(name, int(value))
            except Exception:
                pass

    def adjust_color(self, name, delta):
        if name not in self.color_values:
            return
        self.color_values[name] = clamp(int(self.color_values.get(name, 0)) + int(delta), -100, 100)
        if self.mpv:
            self.mpv.set_property(name, int(self.color_values[name]))

    def reset_color(self):
        self.color_values = {"brightness": 0, "contrast": 0, "saturation": 0, "gamma": 0, "hue": 0}
        if hasattr(self, "color_controls"):
            for name, scale in self.color_controls.items():
                try:
                    scale.set_value(0)
                except Exception:
                    pass
        if self.mpv:
            for name in self.color_values:
                self.mpv.set_property(name, 0)

    def close_context_menu(self, *_):
        if self.context_popup:
            try:
                self.context_popup.popdown()
            except Exception:
                pass
            self.context_popup = None
        return False

    def open_options_panel(self, *_):
        self.open_context_menu()
        return False

    def make_menu_item(self, label, callback=None, check=False, active=False):
        item = Gtk.CheckMenuItem(label=label) if check else Gtk.MenuItem(label=label)
        if check:
            item.set_active(bool(active))
        if callback:
            def run(_item):
                callback()
            item.connect("activate", run)
        return item

    def append_menu_item(self, menu, label, callback=None, check=False, active=False):
        item = self.make_menu_item(label, callback, check, active)
        menu.append(item)
        return item

    def append_separator(self, menu):
        menu.append(Gtk.SeparatorMenuItem())

    def append_submenu(self, menu, label, names, callback, active_name=None):
        item = Gtk.MenuItem(label=label)
        sub = Gtk.Menu()
        item.set_submenu(sub)
        for name in names:
            sub.append(self.make_menu_item(str(name), lambda n=name: callback(n), check=(active_name is not None), active=(name == active_name)))
        menu.append(item)
        return item

    def append_volume_boost_submenu(self, menu):
        item = Gtk.MenuItem(label="Volume boost")
        sub = Gtk.Menu()
        item.set_submenu(sub)
        for label, percent in [
            ("Boost off", 100),
            ("Boost 125%", 125),
            ("Boost 150%", 150),
            ("Boost 175%", 175),
            ("Boost 200%", 200),
            ("Boost 250%", 250),
            ("Boost 300% maximum", 300),
        ]:
            sub.append(self.make_menu_item(label, lambda p=percent: self.set_volume_boost(p), check=True, active=(int(self.volume_boost_percent) == int(percent))))
        menu.append(item)
        return item

    def open_context_menu(self, event=None):
        # Prevent duplicate opens when both the transparent GTK shield and mpv fallback report the same right click.
        now = time.time()
        if now - getattr(self, "last_context_menu_open_ts", 0) < 0.18:
            return True
        self.last_context_menu_open_ts = now
        self.close_context_menu()
        menu = Gtk.Menu()
        self.context_popup = menu

        self.append_menu_item(menu, "Open video or audio", self.choose_file)
        self.append_menu_item(menu, "Play", lambda: self.set_pause(False))
        self.append_menu_item(menu, "Pause", lambda: self.set_pause(True))
        self.append_menu_item(menu, "Stop", self.stop_media)
        self.append_menu_item(menu, "Mute audio", lambda: self.set_mute_enabled(not self.muted), check=True, active=self.muted)
        self.append_volume_boost_submenu(menu)
        self.append_menu_item(menu, "Loop current media", lambda: self.set_loop_enabled(not self.loop_enabled), check=True, active=self.loop_enabled)
        self.append_menu_item(menu, "Fullscreen", self.toggle_fullscreen)
        self.append_menu_item(menu, "Focus video", self.toggle_focus_mode)
        self.append_menu_item(menu, "Show top menu" if not self.top_menu_visible else "Hide top menu", self.toggle_top_menu)
        self.append_menu_item(menu, "Hide stats details" if self.stats_visible else "Show stats details", self.toggle_stats_details)

        self.append_separator(menu)
        self.append_menu_item(menu, "Title banner on start", lambda: self.set_osd_enabled(not self.osd_enabled), check=True, active=self.osd_enabled)
        self.append_menu_item(menu, "Always on top", lambda: self.set_always_on_top(not self.always_on_top), check=True, active=self.always_on_top)
        speed_labels = [("Speed 0.50x", 0.5), ("Speed 0.75x", 0.75), ("Speed 1.00x normal", 1.0), ("Speed 1.25x", 1.25), ("Speed 1.50x", 1.5), ("Speed 2.00x", 2.0)]
        speed_item = Gtk.MenuItem(label="Playback speed")
        speed_sub = Gtk.Menu()
        speed_item.set_submenu(speed_sub)
        for label, value in speed_labels:
            speed_sub.append(self.make_menu_item(label, lambda v=value: self.set_playback_speed(v)))
        menu.append(speed_item)

        self.append_separator(menu)
        self.append_submenu(menu, "Theme", list(THEMES.keys()), self.select_theme_from_menu, self.current_theme)
        active_eq = self.eq_combo.get_active_text() if hasattr(self, "eq_combo") else None
        self.append_submenu(menu, "Sound EQ preset", list(EQ_PRESETS.keys()), self.apply_eq_preset, active_eq)
        active_filter = self.filter_combo.get_active_text() if hasattr(self, "filter_combo") else None
        self.append_submenu(menu, "Video filter", list(VIDEO_FILTERS.keys()), self.select_filter_from_menu, active_filter)
        active_render = self.render_combo.get_active_text() if hasattr(self, "render_combo") else None
        self.append_submenu(menu, "Rendering preset", list(RENDERING_PRESETS), self.apply_rendering, active_render)
        self.append_menu_item(menu, "Open or close colour adjustment", self.toggle_color_adjustment_dialog)

        self.append_separator(menu)
        self.append_menu_item(menu, "Take screenshot", self.take_screenshot)
        self.append_menu_item(menu, "Set as the default video player", self.set_default_video_player)
        self.append_menu_item(menu, "About Govibe.org", self.open_govibe_website)
        self.append_menu_item(menu, "About this player", self.about_dialog)
        self.append_separator(menu)
        self.append_menu_item(menu, "Exit", self.quit_app)

        menu.attach_to_widget(self.window, None)
        menu.show_all()
        if event is not None:
            menu.popup_at_pointer(event)
        else:
            try:
                xy = self.pointer_xy() or (20, 20)
                wx, wy = self.window.get_position()
                rect = Gdk.Rectangle()
                rect.x = max(0, int(xy[0] - wx))
                rect.y = max(0, int(xy[1] - wy))
                rect.width = 1
                rect.height = 1
                menu.popup_at_rect(self.window.get_window(), rect, Gdk.Gravity.NORTH_WEST, Gdk.Gravity.NORTH_WEST, None)
            except Exception:
                menu.popup(None, None, None, None, 0, Gtk.get_current_event_time())
        return True

    def toggle_color_adjustment_dialog(self):
        if self.color_window:
            try:
                self.color_window.destroy()
                self.color_window = None
                if hasattr(self, "color_adjust_button"):
                    self.color_adjust_button.set_label("OPEN COLOUR ADJUSTMENT")
                return
            except Exception:
                self.color_window = None
        self.open_color_adjustment_dialog()

    def open_color_adjustment_dialog(self):
        if self.color_window:
            try:
                self.color_window.present()
                return
            except Exception:
                self.color_window = None

        win = Gtk.Window(title="Colour adjustment")
        self.color_window = win
        win.set_default_size(540, 290)
        win.set_resizable(True)
        win.set_modal(False)
        win.set_destroy_with_parent(False)
        try:
            win.set_transient_for(self.window)
        except Exception:
            pass
        try:
            win.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
        except Exception:
            pass
        def color_window_destroyed(*_):
            self.color_window = None
            if hasattr(self, "color_adjust_button"):
                self.color_adjust_button.set_label("OPEN COLOUR ADJUSTMENT")
        win.connect("destroy", color_window_destroyed)
        if hasattr(self, "color_adjust_button"):
            self.color_adjust_button.set_label("CLOSE COLOUR ADJUSTMENT")
        win.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)

        outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        outer.set_border_width(10)
        outer.get_style_context().add_class("panel")
        win.add(outer)

        header = Gtk.Label(label="Colour adjustment")
        header.set_xalign(0)
        header.get_style_context().add_class("filename")
        outer.pack_start(header, False, False, 0)

        local_scales = {}
        for name, label_text in [("brightness", "Brightness"), ("contrast", "Contrast"), ("saturation", "Saturation"), ("gamma", "Gamma"), ("hue", "Hue")]:
            row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
            label = Gtk.Label(label=label_text)
            label.set_size_request(98, -1)
            label.set_xalign(0)
            row.pack_start(label, False, False, 0)
            scale = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, -100, 100, 1)
            scale.set_value(int(self.color_values.get(name, 0)))
            scale.set_draw_value(True)
            scale.set_size_request(350, -1)
            scale.connect("value-changed", self.color_changed, name)
            row.pack_start(scale, True, True, 0)
            outer.pack_start(row, False, False, 0)
            local_scales[name] = scale

        row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        def reset_window(*_):
            self.reset_color()
            for scale in local_scales.values():
                try:
                    scale.set_value(0)
                except Exception:
                    pass
        reset = Gtk.Button(label="SET COLOUR TO DEFAULT")
        reset.get_style_context().add_class("nexusbtn")
        reset.connect("clicked", reset_window)
        row.pack_start(reset, False, False, 0)
        close = Gtk.Button(label="CLOSE")
        close.get_style_context().add_class("nexusbtn")
        close.connect("clicked", lambda *_: win.destroy())
        row.pack_start(close, False, False, 0)
        outer.pack_start(row, False, False, 0)

        hint = Gtk.Label(label="This is a normal movable window, not a dark modal overlay.")
        hint.set_xalign(0)
        hint.get_style_context().add_class("hint")
        outer.pack_start(hint, False, False, 0)

        self.apply_theme(self.current_theme)
        win.show_all()
        win.present()

    def combo_set_text(self, combo, names, name):
        if not combo or not name:
            return
        for i, key in enumerate(list(names)):
            if key == name:
                combo.set_active(i)
                break

    def select_theme_from_menu(self, name):
        self.combo_set_text(getattr(self, "theme_combo", None), THEMES.keys(), name)
        self.apply_theme(name)

    def select_template_from_menu(self, name):
        self.combo_set_text(getattr(self, "template_combo", None), TEMPLATES.keys(), name)
        self.apply_template(name)

    def select_filter_from_menu(self, name):
        self.combo_set_text(getattr(self, "filter_combo", None), VIDEO_FILTERS.keys(), name)
        self.apply_video_filter(name)

    def toggle_focus_mode(self):
        self.options_hidden = not self.options_hidden
        if self.options_hidden:
            for w in [getattr(self, "side_scroll", None), getattr(self, "stats_label", None)]:
                if w:
                    w.hide()
            if hasattr(self, "paned"):
                try:
                    self.paned.set_position(self.window.get_allocated_width())
                except Exception:
                    pass
            if hasattr(self, "focus_button"):
                self.focus_button.set_label("SHOW OPTIONS")
        else:
            if hasattr(self, "side_scroll"):
                self.side_scroll.show_all()
            if hasattr(self, "paned"):
                self.paned.set_position(self.default_paned_position)
            self.sync_stats_visibility()
            if hasattr(self, "focus_button"):
                self.focus_button.set_label("FOCUS VIDEO")

    def set_osd_enabled(self, enabled):
        self.osd_enabled = bool(enabled)
        if hasattr(self, "osd_check") and self.osd_check.get_active() != self.osd_enabled:
            self.osd_check.set_active(self.osd_enabled)

    def set_always_on_top(self, enabled):
        self.always_on_top = bool(enabled)
        if self.window:
            self.window.set_keep_above(self.always_on_top)
        if hasattr(self, "always_top_check") and self.always_top_check.get_active() != self.always_on_top:
            self.always_top_check.set_active(self.always_on_top)

    def toggle_always_on_top(self, widget):
        self.set_always_on_top(widget.get_active())

    def set_loop_enabled(self, enabled):
        self.loop_enabled = bool(enabled)
        if self.mpv:
            self.mpv.set_property("loop-file", "inf" if self.loop_enabled else "no")
        if hasattr(self, "loop_check") and self.loop_check.get_active() != self.loop_enabled:
            self.loop_check.set_active(self.loop_enabled)
        for btn_name in ["loop_button", "overlay_loop_button"]:
            btn = getattr(self, btn_name, None)
            if btn:
                btn.set_label("LOOP ON" if self.loop_enabled else "LOOP")

    def toggle_loop(self, widget):
        self.set_loop_enabled(widget.get_active())

    def set_mute_enabled(self, enabled):
        self.muted = bool(enabled)
        if self.mpv:
            self.mpv.set_property("mute", self.muted)
        if hasattr(self, "mute_check") and self.mute_check.get_active() != self.muted:
            self.mute_check.set_active(self.muted)
        for btn_name in ["mute_button", "overlay_mute_button"]:
            btn = getattr(self, btn_name, None)
            if btn:
                btn.set_label("MUTED" if self.muted else "MUTE")

    def toggle_mute(self, widget):
        self.set_mute_enabled(widget.get_active())

    def on_volume_slider_changed(self, slider):
        try:
            self.volume_level = int(slider.get_value())
        except Exception:
            self.volume_level = 100
        self.apply_volume_settings()

    def get_effective_volume(self):
        try:
            base = float(self.volume_level)
            boost = float(self.volume_boost_percent)
            return clamp(base * boost / 100.0, 0, 300)
        except Exception:
            return 100

    def apply_volume_settings(self):
        effective_volume = self.get_effective_volume()
        if self.mpv:
            try:
                self.mpv.set_property("volume", float(effective_volume))
            except Exception:
                pass
        self.sync_volume_ui()

    def sync_volume_ui(self):
        label = "BOOST VOLUME" if int(self.volume_boost_percent) <= 100 else f"BOOST {int(self.volume_boost_percent)}%"
        for btn_name in ["volume_boost_button", "overlay_volume_boost_button"]:
            btn = getattr(self, btn_name, None)
            if btn:
                btn.set_label(label)
        slider = getattr(self, "volume_slider", None)
        if slider and int(slider.get_value()) != int(self.volume_level):
            try:
                slider.set_value(int(self.volume_level))
            except Exception:
                pass

    def set_volume_boost(self, percent):
        try:
            self.volume_boost_percent = int(percent)
        except Exception:
            self.volume_boost_percent = 100
        self.volume_boost_percent = int(clamp(self.volume_boost_percent, 100, 300))
        self.apply_volume_settings()

    def open_volume_boost_menu(self, button=None):
        menu = Gtk.Menu()
        for label, percent in [
            ("Boost off", 100),
            ("Boost 125%", 125),
            ("Boost 150%", 150),
            ("Boost 175%", 175),
            ("Boost 200%", 200),
            ("Boost 250%", 250),
            ("Boost 300% maximum", 300),
        ]:
            item = Gtk.CheckMenuItem(label=label)
            item.set_active(int(self.volume_boost_percent) == int(percent))
            item.connect("activate", lambda _item, p=percent: self.set_volume_boost(p))
            menu.append(item)
        menu.show_all()
        try:
            if button:
                menu.popup_at_widget(button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, None)
            else:
                menu.popup(None, None, None, None, 0, Gtk.get_current_event_time())
        except Exception:
            menu.popup(None, None, None, None, 0, Gtk.get_current_event_time())
        return True

    def set_playback_speed(self, value):
        if self.mpv:
            self.mpv.set_property("speed", float(value))

    def take_screenshot(self):
        if not self.mpv:
            self.show_message("Open a video first, then take a screenshot.", Gtk.MessageType.INFO)
            return
        pictures = Path.home() / "Pictures"
        try:
            pictures.mkdir(parents=True, exist_ok=True)
        except Exception:
            pictures = Path.home()
        filename = pictures / ("govibe-nexus-video-screenshot-" + time.strftime("%Y%m%d-%H%M%S") + ".png")
        self.mpv.command("screenshot-to-file", str(filename), "subtitles")
        self.show_message(f"Screenshot saved to:\n{filename}", Gtk.MessageType.INFO)

    def toggle_top_menu(self):
        self.top_menu_visible = not self.top_menu_visible
        self.sync_top_menu_visibility()

    def sync_top_menu_visibility(self):
        if hasattr(self, "menubar"):
            if self.top_menu_visible and not self.is_fullscreen_mode:
                self.menubar.set_no_show_all(False)
                self.menubar.show_all()
            else:
                self.menubar.set_no_show_all(True)
                self.menubar.hide()
        if hasattr(self, "menu_button"):
            self.menu_button.set_label("HIDE MENU" if self.top_menu_visible else "MENU")

    def toggle_stats_details(self):
        self.stats_visible = not self.stats_visible
        self.sync_stats_visibility()

    def sync_stats_visibility(self):
        if hasattr(self, "stats_label"):
            if self.stats_visible and not self.is_fullscreen_mode:
                self.stats_label.set_no_show_all(False)
                self.stats_label.show_all()
            else:
                self.stats_label.set_no_show_all(True)
                self.stats_label.hide()
        if hasattr(self, "stats_button"):
            self.stats_button.set_label("HIDE STATS" if self.stats_visible else "SHOW STATS")

    def is_double_click(self, event):
        try:
            nick = getattr(event.type, "value_nick", "") or str(event.type).lower()
            return getattr(event, "button", 0) == 1 and ("2button" in nick or "double" in nick)
        except Exception:
            return False

    def on_right_click(self, widget, event):
        if self.is_double_click(event):
            self.toggle_fullscreen()
            return True
        if getattr(event, "button", 0) == 3:
            self.open_context_menu(event)
            return True
        if self.is_fullscreen_mode:
            self.show_fullscreen_controls_temporarily()
        return False

    def on_key_press(self, widget, event):
        try:
            key = Gdk.keyval_name(event.keyval)
        except Exception:
            key = None
        if key == "Escape" and self.is_fullscreen_mode:
            self.window.unfullscreen()
            return True
        return False

    def on_mouse_motion(self, *_):
        if self.is_fullscreen_mode:
            self.show_fullscreen_controls_temporarily()
        return False

    def set_blank_cursor(self):
        if not self.window or not self.window.get_window():
            return
        try:
            display = Gdk.Display.get_default()
            cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.BLANK_CURSOR)
            self.window.get_window().set_cursor(cursor)
        except Exception:
            pass

    def set_normal_cursor(self):
        if not self.window or not self.window.get_window():
            return
        try:
            self.window.get_window().set_cursor(None)
        except Exception:
            pass

    def on_mpv_client_message(self, message):
        if message == "toggle-fullscreen":
            self.toggle_fullscreen()
        elif message == "right-click":
            self.open_context_menu()
        elif message == "exit-fullscreen":
            if self.is_fullscreen_mode and self.window:
                self.window.unfullscreen()
        elif message == "mouse-move":
            now = time.time()
            if self.is_fullscreen_mode and now - self.last_mpv_mouse_event > 0.15:
                self.last_mpv_mouse_event = now
                self.show_fullscreen_controls_temporarily()
        return False

    def on_window_configure(self, widget, event):
        if not self.is_fullscreen_mode and not self.options_hidden:
            GLib.idle_add(self.fit_layout_to_window)
        return False

    def fit_layout_to_window(self):
        try:
            if not hasattr(self, "paned") or not self.window:
                return False
            width = int(self.window.get_allocated_width() or 0)
            if width <= 0:
                return False
            side_width = 320
            if width < 1220:
                side_width = max(290, min(320, int(width * 0.28)))
            if hasattr(self, "side_scroll"):
                self.side_scroll.set_size_request(side_width, -1)
                self.side_scroll.set_min_content_width(max(280, side_width - 20))
            position = max(780, width - side_width - 22)
            max_position = max(700, width - 290)
            position = min(position, max_position)
            self.default_paned_position = position
            if not self.is_fullscreen_mode and not self.options_hidden:
                self.paned.set_position(position)
        except Exception:
            pass
        return False

    def on_window_state_event(self, widget, event):
        is_full = bool(event.new_window_state & Gdk.WindowState.FULLSCREEN)
        if is_full != self.is_fullscreen_mode:
            self.is_fullscreen_mode = is_full
            if is_full:
                self.enter_fullscreen_view()
            else:
                self.exit_fullscreen_view()
        return False

    def fullscreen_control_widgets(self):
        return [getattr(self, "fullscreen_overlay_controls", None)]

    def show_fullscreen_controls_temporarily(self):
        if not self.is_fullscreen_mode:
            return False
        self.set_normal_cursor()
        current_xy = self.pointer_xy()
        if current_xy is not None:
            self.last_pointer_xy = current_xy
        for w in self.fullscreen_control_widgets():
            if w:
                w.set_no_show_all(False)
                w.show_all()
        self.controls_visible = True
        if self.fullscreen_auto_hide_id:
            try:
                GLib.source_remove(self.fullscreen_auto_hide_id)
            except Exception:
                pass
        self.fullscreen_auto_hide_id = GLib.timeout_add_seconds(4, self.hide_fullscreen_controls)
        return False

    def hide_fullscreen_controls(self):
        if not self.is_fullscreen_mode:
            self.fullscreen_auto_hide_id = None
            return False
        for w in self.fullscreen_control_widgets():
            if w:
                w.hide()
        self.set_blank_cursor()
        self.controls_visible = False
        self.fullscreen_auto_hide_id = None
        return False

    def pointer_xy(self):
        try:
            display = Gdk.Display.get_default()
            seat = display.get_default_seat()
            pointer = seat.get_pointer()
            _screen, x, y = pointer.get_position()
            return int(x), int(y)
        except Exception:
            pass
        try:
            if self.window:
                _x, _y, mask = self.window.get_pointer()
                root = self.window.get_window()
                if root:
                    ox, oy = self.window.get_position()
                    return int(ox + _x), int(oy + _y)
        except Exception:
            pass
        return None

    def start_fullscreen_pointer_poll(self):
        if self.fullscreen_pointer_poll_id:
            return
        self.last_pointer_xy = self.pointer_xy()
        self.fullscreen_pointer_poll_id = GLib.timeout_add(150, self.poll_fullscreen_pointer)

    def stop_fullscreen_pointer_poll(self):
        if self.fullscreen_pointer_poll_id:
            try:
                GLib.source_remove(self.fullscreen_pointer_poll_id)
            except Exception:
                pass
        self.fullscreen_pointer_poll_id = None
        self.last_pointer_xy = None
        self.set_normal_cursor()

    def poll_fullscreen_pointer(self):
        if not self.is_fullscreen_mode:
            self.fullscreen_pointer_poll_id = None
            return False
        xy = self.pointer_xy()
        if xy is not None and xy != self.last_pointer_xy:
            self.last_pointer_xy = xy
            self.show_fullscreen_controls_temporarily()
        return True

    def enter_fullscreen_view(self):
        self.close_context_menu()
        if hasattr(self, "side_scroll"):
            self.side_scroll.hide()
        if hasattr(self, "menubar"):
            self.menubar.hide()
        if hasattr(self, "titlebar"):
            self.titlebar.hide()
        if hasattr(self, "stats_label"):
            self.stats_label.hide()
        if hasattr(self, "seek_box"):
            self.seek_box.hide()
        if hasattr(self, "controls_box"):
            self.controls_box.hide()
        if hasattr(self, "paned") and self.window:
            try:
                self.paned.set_position(self.window.get_allocated_width())
            except Exception:
                pass
        self.start_fullscreen_pointer_poll()
        self.show_fullscreen_controls_temporarily()

    def exit_fullscreen_view(self):
        self.stop_fullscreen_pointer_poll()
        if self.fullscreen_auto_hide_id:
            try:
                GLib.source_remove(self.fullscreen_auto_hide_id)
            except Exception:
                pass
            self.fullscreen_auto_hide_id = None
        for w in self.fullscreen_control_widgets():
            if w:
                w.set_no_show_all(True)
                w.hide()
        if hasattr(self, "seek_box"):
            self.seek_box.show_all()
        if hasattr(self, "controls_box"):
            self.controls_box.show_all()
        if hasattr(self, "side_scroll"):
            self.side_scroll.show_all()
        if hasattr(self, "paned"):
            self.paned.set_position(self.default_paned_position)
        if hasattr(self, "titlebar"):
            self.titlebar.show_all()
        self.sync_top_menu_visibility()
        self.sync_stats_visibility()

    def open_govibe_website(self, *_):
        try:
            Gio.AppInfo.launch_default_for_uri("https://govibe.org", None)
        except Exception:
            self.show_message("Could not open https://govibe.org", Gtk.MessageType.WARNING)

    def set_default_video_player(self):
        errors = []
        for mime in VIDEO_MIME_TYPES:
            try:
                subprocess.run(["xdg-mime", "default", DESKTOP_ID, mime], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
            except Exception as exc:
                errors.append(str(exc))
        if errors:
            self.show_message("Some MIME associations could not be changed. Make sure xdg-utils is installed.", Gtk.MessageType.WARNING)
        else:
            self.show_message("Govibe Nexus Video Player is now set as the default video player for common video formats.", Gtk.MessageType.INFO)

    def setup_tray(self):
        menu = Gtk.Menu()
        open_item = Gtk.MenuItem(label="Open Software")
        open_item.connect("activate", lambda *_: self.show_main_window())
        exit_item = Gtk.MenuItem(label="Exit")
        exit_item.connect("activate", lambda *_: self.quit_app())
        menu.append(open_item)
        menu.append(Gtk.SeparatorMenuItem())
        menu.append(exit_item)
        menu.show_all()

        # Prefer AppIndicator/Ayatana for Ubuntu. Fallback to Gtk.StatusIcon for older desktops.
        try:
            gi.require_version("AyatanaAppIndicator3", "0.1")
            from gi.repository import AyatanaAppIndicator3 as AppIndicator
            self.tray = AppIndicator.Indicator.new(APP_ID, "govibe-nexus-video-player", AppIndicator.IndicatorCategory.APPLICATION_STATUS)
            self.tray.set_status(AppIndicator.IndicatorStatus.ACTIVE)
            self.tray.set_menu(menu)
            return
        except Exception:
            pass
        try:
            gi.require_version("AppIndicator3", "0.1")
            from gi.repository import AppIndicator3 as AppIndicator
            self.tray = AppIndicator.Indicator.new(APP_ID, "govibe-nexus-video-player", AppIndicator.IndicatorCategory.APPLICATION_STATUS)
            self.tray.set_status(AppIndicator.IndicatorStatus.ACTIVE)
            self.tray.set_menu(menu)
            return
        except Exception:
            pass
        try:
            self.status_icon = Gtk.StatusIcon.new_from_icon_name("govibe-nexus-video-player")
            self.status_icon.set_tooltip_text(APP_NAME)
            self.status_icon.set_visible(True)
            self.status_icon.connect("activate", lambda *_: self.show_main_window())
            self.status_icon.connect("popup-menu", lambda icon, button, activate_time: menu.popup(None, None, None, None, button, activate_time))
        except Exception:
            pass

    def launch_window_full_visible(self):
        if not self.window:
            return False
        if self.did_initial_window_fit:
            return False
        self.did_initial_window_fit = True
        try:
            try:
                self.window.unmaximize()
            except Exception:
                pass
            gdk_window = self.window.get_window()
            display = Gdk.Display.get_default()
            monitor = None
            if display and gdk_window:
                try:
                    monitor = display.get_monitor_at_window(gdk_window)
                except Exception:
                    monitor = None
            if not monitor and display:
                try:
                    monitor = display.get_primary_monitor()
                except Exception:
                    monitor = None
            width = 1360
            height = 800
            if monitor:
                area = monitor.get_workarea()
                width = max(1060, min(1360, int(area.width) - 60))
                height = max(680, min(800, int(area.height) - 40))
                self.window.resize(width, height)
                try:
                    self.window.move(int(area.x + max(0, (area.width - width) // 2)), int(area.y + max(0, (area.height - height) // 2)))
                except Exception:
                    pass
            else:
                self.window.resize(width, height)
            if hasattr(self, "paned"):
                self.paned.set_position(min(self.default_paned_position, max(780, width - 340)))
            self.force_launch_visible()
        except Exception:
            try:
                self.window.resize(1360, 800)
                self.force_launch_visible()
            except Exception:
                pass
        return False

    def force_launch_visible(self):
        if not self.window:
            return False
        try:
            self.window.set_skip_taskbar_hint(False)
            self.window.set_skip_pager_hint(False)
            self.window.show()
            self.window.deiconify()
            self.window.present_with_time(Gtk.get_current_event_time())
            gdk_window = self.window.get_window()
            if gdk_window:
                gdk_window.show()
                gdk_window.raise_()
        except Exception:
            try:
                self.window.show()
                self.window.deiconify()
                self.window.present()
            except Exception:
                pass
        return False

    def show_main_window(self):
        if self.window:
            self.force_launch_visible()

    def on_window_close(self, *_):
        # The window X button exits the player. The normal minimize button is still available for reducing the window.
        self.quit_app()
        return True

    def quit_app(self):
        if self.mpv:
            self.mpv.terminate()
            self.mpv = None
        self.quit()

    def toggle_fullscreen(self):
        if not self.window:
            return
        state = self.window.get_window().get_state() if self.window.get_window() else 0
        if state & Gdk.WindowState.FULLSCREEN:
            self.window.unfullscreen()
        else:
            self.window.fullscreen()

    def apply_theme(self, name):
        if name not in THEMES:
            name = "Nexus Green"
        self.current_theme = name
        c = THEMES[name]
        css = f"""
        * {{ font-family: Sans, Arial; }}
        window {{ background: {c['bg']}; color: {c['text']}; }}
        menubar, menu, menuitem {{ background: {c['panel']}; color: {c['text']}; border-color: {c['accent']}; }}
        menuitem:hover {{ background: {c['accent']}; color: #000000; }}
        .topbar {{ background: linear-gradient(to bottom, {c['panel']}, {c['bg']}); border-bottom: 1px solid {c['accent']}; }}
        .controlbar {{ background: {c['panel']}; border-top: 1px solid {c['accent']}; }}
        .logo {{ color: {c['accent']}; font-size: 30px; font-weight: 900; letter-spacing: 2px; text-shadow: 0 0 8px {c['accent']}; }}
        .filename {{ color: {c['accent2']}; font-size: 16px; font-weight: 700; }}
        .timecode {{ color: {c['accent2']}; font-weight: 700; }}
        .stats {{ color: {c['muted']}; background: {c['panel']}; padding: 8px; border: 1px solid {c['accent']}; border-radius: 8px; }}
        .panel {{ color: {c['text']}; background: {c['panel']}; border: 1px solid {c['accent']}; border-radius: 10px; padding: 8px; }}
        frame > label {{ color: {c['accent']}; font-weight: 900; font-size: 14px; }}
        button.nexusbtn, button {{ background: {c['panel']}; color: {c['text']}; border: 1px solid {c['accent']}; border-radius: 7px; padding: 4px 7px; font-weight: 800; font-size: 12px; }}
        button.nexusbtn:hover, button:hover {{ background: {c['accent']}; color: #000; box-shadow: 0 0 10px {c['accent']}; }}
        button.linkbtn {{ background: {c['panel']}; color: {c['text']}; border: 1px solid {c['muted']}; border-radius: 7px; padding: 7px 10px; font-weight: 900; text-decoration: none; }}
        button.linkbtn:hover {{ background: {c['muted']}; color: #000000; box-shadow: 0 0 10px {c['muted']}; }}
        scale slider {{ background: {c['accent2']}; border: 1px solid {c['accent']}; }}
        scale trough {{ background: #001a08; border: 1px solid {c['accent']}; min-height: 6px; }}
        scale highlight {{ background: {c['accent']}; }}
        combobox, combobox box, entry {{ background: #000; color: {c['accent2']}; border: 1px solid {c['accent']}; }}
        .hint {{ color: {c['muted']}; font-size: 12px; }}
        .tiny {{ color: {c['muted']}; font-size: 10px; }}
        .osdbanner {{ color: #ffffff; background: rgba(0,0,0,0.72); padding: 8px 18px; border: 1px solid {c['accent']}; border-radius: 18px; font-size: 17px; font-weight: 900; text-shadow: 0 0 8px {c['accent']}; }}
        .overlaycontrols {{ background: rgba(0,0,0,0.70); border: 1px solid {c['accent']}; border-radius: 12px; padding: 8px; }}
        .contextmenu {{ background: {c['panel']}; color: {c['text']}; border: 1px solid {c['accent']}; border-radius: 8px; padding: 4px; }}
        .contextheader {{ color: {c['accent']}; background: {c['bg']}; padding: 6px 8px; font-weight: 900; font-size: 12px; border-top: 1px solid {c['accent']}; }}
        button.contextbtn {{ background: {c['panel']}; color: {c['text']}; border: 0; border-radius: 4px; padding: 6px 9px; font-weight: 700; }}
        button.contextbtn:hover {{ background: {c['accent']}; color: #000000; }}
        .videowrap {{ background: #000000; border: 1px solid {c['accent']}; }}
        .videohost {{ background: #000000; }}
        .placeholder {{ background: rgba(0,0,0,0.58); padding: 18px; border: 1px solid {c['accent']}; border-radius: 16px; }}
        .placeholder_title {{ color: {c['accent']}; font-size: 24px; font-weight: 900; letter-spacing: 1px; text-shadow: 0 0 12px {c['accent']}; }}
        .placeholder_sub {{ color: {c['muted']}; font-size: 15px; }}
        drawingarea {{ background: #000; }}
        """
        provider = Gtk.CssProvider()
        provider.load_from_data(css.encode("utf-8"))
        Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

    def show_message(self, message, msg_type):
        dialog = Gtk.MessageDialog(transient_for=self.window, flags=0, message_type=msg_type, buttons=Gtk.ButtonsType.OK, text=message)
        dialog.run()
        dialog.destroy()

    def about_dialog(self):
        dialog = Gtk.AboutDialog(transient_for=self.window, modal=True)
        dialog.set_program_name(APP_NAME)
        dialog.set_version(VERSION)
        dialog.set_comments("A Govibe Nexus style video player powered by mpv.")
        dialog.set_website("https://govibe.org")
        dialog.run()
        dialog.destroy()


def main():
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    app = GovibeNexusVideoPlayer()
    return app.run(sys.argv)


if __name__ == "__main__":
    sys.exit(main())
