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