Show 3 previous + current (highlighted) + 21 next items in ring sidebar

Ring sidebar now displays a window around the current position: 3 items
behind (dimmed), the current item highlighted in blue with a play marker,
and 21 items ahead. Each item shows its signed ring offset (-3 to +21).

get_ring_related_products returns dicts with product and offset. Views
pass offset through to JS. Server-rendered template handles both formats.
This commit is contained in:
russell@unturf.com 2026-02-09 11:02:16 -05:00
parent 98efbfe566
commit 7f8a5e631a
5 changed files with 82 additions and 23 deletions

View file

@ -697,38 +697,44 @@ def tokenize_and_stem(text):
return {stem_word(w) for w in words}
def get_ring_related_products(product, ring, limit=8):
"""Get related products by walking forward from current position in the ring.
def get_ring_related_products(product, ring, forward=21, backward=3):
"""Get products from the ring: backward items, then forward items.
Returns a list of Product objects in ring order starting after the current product.
Returns a list of dicts: {"product": Product, "offset": int}
where offset is negative for previous, positive for next.
Falls back to get_related_products if product not in ring.
"""
product_id_str = str(product.id)
if product_id_str not in ring:
return get_related_products(product, limit=limit)
fallback = get_related_products(product, limit=forward)
return [{"product": p, "offset": i + 1} for i, p in enumerate(fallback)]
idx = ring.index(product_id_str)
ring_len = len(ring)
# Walk forward from current position, wrapping around
related_ids = []
for i in range(1, ring_len):
# Collect IDs with their offsets: negative=previous, positive=next
items = []
for i in range(backward, 0, -1):
prev_idx = (idx - i + ring_len) % ring_len
items.append((ring[prev_idx], -i))
for i in range(1, min(forward + 1, ring_len)):
next_idx = (idx + i) % ring_len
related_ids.append(ring[next_idx])
if len(related_ids) >= limit:
break
items.append((ring[next_idx], i))
if not related_ids:
if not items:
return []
# Fetch products and preserve ring order
products = get_products_by_ids(product.dbsession, related_ids)
all_ids = [pid for pid, _ in items]
products = get_products_by_ids(product.dbsession, all_ids)
if not products:
return []
# Build lookup by ID string for ordering
product_map = {str(p.id): p for p in products}
return [product_map[pid] for pid in related_ids if pid in product_map]
return [
{"product": product_map[pid], "offset": offset}
for pid, offset in items
if pid in product_map
]
def get_related_products(product, limit=8):

View file

@ -2761,6 +2761,21 @@ textarea {
.related-content-row-watched {
opacity: 0.45;
}
.related-content-row-current {
background-color: var(--blue-color, #5871ad);
color: white;
border-radius: 4px;
padding: 4px 0;
}
.related-content-row-current .related-content-index {
color: white;
}
.related-content-now-playing {
font-weight: bold;
}
.related-content-row-prev {
opacity: 0.55;
}
.ring-progress {
font-size: 0.8em;

View file

@ -561,13 +561,28 @@
}
var html = headerHtml;
var visibleIndex = 0;
var currentTitle = document.title;
var insertedCurrent = false;
related.forEach(function(item) {
visibleIndex++;
var offset = item.offset || 1;
var watched = isWatched(item.id);
var dimClass = watched ? ' related-content-row-watched' : '';
html += '<div class="related-content-row' + dimClass + '">';
html += '<span class="related-content-index">' + visibleIndex + '</span>';
var prevClass = offset < 0 ? ' related-content-row-prev' : '';
// Insert "now playing" row before the first forward item
if (offset > 0 && !insertedCurrent) {
insertedCurrent = true;
html += '<div class="related-content-row related-content-row-current">';
html += '<span class="related-content-index">&#9654;</span>';
html += '<span class="related-content-item related-content-now-playing">';
html += '<span class="related-content-title">' + escapeHtml(currentTitle) + '</span>';
html += '</span>';
html += '</div>';
}
html += '<div class="related-content-row' + dimClass + prevClass + '">';
html += '<span class="related-content-index">' + offset + '</span>';
html += '<a href="' + item.url + '" class="related-content-item" data-watch-id="' + item.id + '">';
if (item.thumbnail_url) {
html += '<img src="' + item.thumbnail_url + '" />';

View file

@ -3,6 +3,7 @@
<div class="related-content-header">
<h3>Up Next</h3>
{% if request.shop.watch_mode_enabled %}
<span id="ring-progress" class="ring-progress"></span>
<label class="watch-autoplay-label js-only">
<span class="autoplay-label-text">Autoplay</span>
<input type="checkbox" id="watch-autoplay-toggle" checked />
@ -10,9 +11,24 @@
</label>
{% endif %}
</div>
{% for related in related_products %}
<div class="related-content-row">
<span class="related-content-index">{{ loop.index }}</span>
{% for item in related_products %}
{% if item is mapping %}
{% set related = item.product %}
{% set offset = item.offset %}
{% else %}
{% set related = item %}
{% set offset = loop.index %}
{% endif %}
{% if offset == 1 %}
<div class="related-content-row related-content-row-current">
<span class="related-content-index">&#9654;</span>
<span class="related-content-item related-content-now-playing">
<span class="related-content-title">{{ product.title }}</span>
</span>
</div>
{% endif %}
<div class="related-content-row{% if offset < 0 %} related-content-row-prev{% endif %}">
<span class="related-content-index">{{ offset }}</span>
<a href="{{ related.absolute_url(request) }}" class="related-content-item" data-watch-id="{{ related.id }}">
{% if "thumbnail1" in related.extensions %}
<img src="{{ request.app["bucket.secure_uploads.get_endpoint"] }}/{{ related.s3_path }}/thumbnail1?ts={{ related.updated_timestamp }}" />

View file

@ -90,7 +90,13 @@ def watch_json(request):
related = get_related_products(product)
related_data = []
for r in related:
for item in related:
if isinstance(item, dict):
r = item["product"]
offset = item["offset"]
else:
r = item
offset = 1
r_ext = r.extensions.get("product", "")
r_media = get_media_type(r_ext)
r_thumb = None
@ -107,6 +113,7 @@ def watch_json(request):
"thumbnail_url": r_thumb,
"url": f"/{r_p_or_c}/{r.id}/{r.slug}",
"media_type": r_media,
"offset": offset,
})
# Check if current user is a shop editor (mod)