From 1290f784e717bb141f6d353528bb79bd415ffb64 Mon Sep 17 00:00:00 2001 From: Jean-Michel Tremblay Date: Wed, 8 Jul 2026 19:53:35 -0400 Subject: [PATCH] Add Next/Previous clip buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skip forward/back through the current folder queue -- the player already keeps the folder as an ordered queue with an index, so next/previous just move within it (no-op at the ends). New PlayerStateManager.next/previous/skip, the /api/control/next|previous routes, and compact ⏮/⏭ buttons in the transport row. Lets you skip clips completely without a resume/history feature. Co-Authored-By: Claude Opus 4.8 (1M context) --- mediapi/player.py | 19 +++++++++++++++++++ mediapi/routes/api_routes.py | 18 ++++++++++++++++++ mediapi/static/css/style.css | 12 ++++++++++-- mediapi/static/js/app.js | 8 ++++++++ mediapi/templates/index.html | 2 ++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/mediapi/player.py b/mediapi/player.py index e2585c6..c4bc287 100644 --- a/mediapi/player.py +++ b/mediapi/player.py @@ -194,6 +194,25 @@ class PlayerStateManager: if pid is not None: self._kodi.call("Player.PlayPause", playerid=pid) + def skip(self, delta): + """Jump to another clip in the current folder queue (+1 next, -1 + previous). No-op at the ends, or when nothing is queued.""" + with self._lock: + if not self._queue_files: + return + new_index = self._queue_index + delta + if new_index < 0 or new_index >= len(self._queue_files): + return + self._queue_index = new_index + target = self._queue_files[new_index] + self._open(target) + + def next(self): + self.skip(1) + + def previous(self): + self.skip(-1) + def seek(self, offset_seconds): pid = self._active_player_id() if pid is not None: diff --git a/mediapi/routes/api_routes.py b/mediapi/routes/api_routes.py index 73bae57..aff9b70 100644 --- a/mediapi/routes/api_routes.py +++ b/mediapi/routes/api_routes.py @@ -57,6 +57,24 @@ def playpause(): return jsonify({"ok": True}) +@bp.route("/control/next", methods=["POST"]) +def next_clip(): + try: + current_app.player.next() + except KodiError as exc: + return jsonify({"error": f"player unavailable: {exc}"}), 503 + return jsonify({"ok": True}) + + +@bp.route("/control/previous", methods=["POST"]) +def previous_clip(): + try: + current_app.player.previous() + except KodiError as exc: + return jsonify({"error": f"player unavailable: {exc}"}), 503 + return jsonify({"ok": True}) + + @bp.route("/control/seek", methods=["POST"]) def seek(): body = request.get_json(force=True, silent=True) or {} diff --git a/mediapi/static/css/style.css b/mediapi/static/css/style.css index 9d63471..7b2bff2 100644 --- a/mediapi/static/css/style.css +++ b/mediapi/static/css/style.css @@ -135,18 +135,26 @@ html, body { .transport-row { display: flex; - gap: 0.5rem; + gap: 0.4rem; margin: 1rem 0; } .transport-row button { flex: 1; - padding: 0.8rem; + padding: 0.8rem 0.3rem; border-radius: 6px; border: 1px solid #444; background: #1c1c1c; color: #eee; font-size: 1rem; + white-space: nowrap; +} + +/* Prev/Next are compact icon buttons; the middle three carry the labels. */ +#btn-prev, #btn-next { + flex: 0 0 auto; + min-width: 2.6rem; + font-size: 1.2rem; } .volume-row { diff --git a/mediapi/static/js/app.js b/mediapi/static/js/app.js index 86cc390..5a8780c 100644 --- a/mediapi/static/js/app.js +++ b/mediapi/static/js/app.js @@ -120,6 +120,14 @@ api("/api/control/playpause", { method: "POST" }); }); + el("btn-prev").addEventListener("click", () => { + api("/api/control/previous", { method: "POST" }); + }); + + el("btn-next").addEventListener("click", () => { + api("/api/control/next", { method: "POST" }); + }); + el("btn-back30").addEventListener("click", () => { api("/api/control/seek", { method: "POST", body: JSON.stringify({ offset: -30 }) }); }); diff --git a/mediapi/templates/index.html b/mediapi/templates/index.html index 06f9687..02e6941 100644 --- a/mediapi/templates/index.html +++ b/mediapi/templates/index.html @@ -31,9 +31,11 @@ 0:00
+ +
Vol