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:
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:

View file

@ -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 {}

View file

@ -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 {

View file

@ -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 }) });
});

View file

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