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:
parent
99b5c27e65
commit
a6400d56f5
5 changed files with 57 additions and 2 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 }) });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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">⏮</button>
|
||||||
<button id="btn-back30">« 30</button>
|
<button id="btn-back30">« 30</button>
|
||||||
<button id="btn-playpause">Play/Pause</button>
|
<button id="btn-playpause">Play/Pause</button>
|
||||||
<button id="btn-fwd30">30 »</button>
|
<button id="btn-fwd30">30 »</button>
|
||||||
|
<button id="btn-next" title="Next clip">⏭</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="volume-row">
|
<div class="volume-row">
|
||||||
<span>Vol</span>
|
<span>Vol</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue