Add Next/Previous clip buttons

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) <noreply@anthropic.com>
This commit is contained in:
Jean-Michel Tremblay 2026-07-08 19:53:35 -04:00
parent 99b5c27e65
commit a6400d56f5
5 changed files with 57 additions and 2 deletions

View file

@ -194,6 +194,25 @@ class PlayerStateManager:
if pid is not None: if pid is not None:
self._kodi.call("Player.PlayPause", playerid=pid) 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): def seek(self, offset_seconds):
pid = self._active_player_id() pid = self._active_player_id()
if pid is not None: if pid is not None:

View file

@ -57,6 +57,24 @@ def playpause():
return jsonify({"ok": True}) 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"]) @bp.route("/control/seek", methods=["POST"])
def seek(): def seek():
body = request.get_json(force=True, silent=True) or {} body = request.get_json(force=True, silent=True) or {}

View file

@ -135,18 +135,26 @@ html, body {
.transport-row { .transport-row {
display: flex; display: flex;
gap: 0.5rem; gap: 0.4rem;
margin: 1rem 0; margin: 1rem 0;
} }
.transport-row button { .transport-row button {
flex: 1; flex: 1;
padding: 0.8rem; padding: 0.8rem 0.3rem;
border-radius: 6px; border-radius: 6px;
border: 1px solid #444; border: 1px solid #444;
background: #1c1c1c; background: #1c1c1c;
color: #eee; color: #eee;
font-size: 1rem; 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 { .volume-row {

View file

@ -120,6 +120,14 @@
api("/api/control/playpause", { method: "POST" }); 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", () => { el("btn-back30").addEventListener("click", () => {
api("/api/control/seek", { method: "POST", body: JSON.stringify({ offset: -30 }) }); api("/api/control/seek", { method: "POST", body: JSON.stringify({ offset: -30 }) });
}); });

View file

@ -31,9 +31,11 @@
<span id="time-dur">0:00</span> <span id="time-dur">0:00</span>
</div> </div>
<div class="transport-row"> <div class="transport-row">
<button id="btn-prev" title="Previous clip">&#9198;</button>
<button id="btn-back30">&laquo; 30</button> <button id="btn-back30">&laquo; 30</button>
<button id="btn-playpause">Play/Pause</button> <button id="btn-playpause">Play/Pause</button>
<button id="btn-fwd30">30 &raquo;</button> <button id="btn-fwd30">30 &raquo;</button>
<button id="btn-next" title="Next clip">&#9197;</button>
</div> </div>
<div class="volume-row"> <div class="volume-row">
<span>Vol</span> <span>Vol</span>