Add parent page navigation and PDF support

- PiP now controls parent page: clicking prev/next in PiP navigates the parent page URL
- Add 'Go to Page' button (G key) to open current product page
- Add PDF support: renders PDFs in iframe, opens in pop-out (not PiP)
- PDFs and images fall back to pop-out player window
- Pass product URLs through playerData for navigation
- Audio gets small floating player, video gets PiP, PDFs get window
This commit is contained in:
russell@unturf.com 2026-01-21 10:51:36 -05:00
parent 962a89c3a6
commit f0b23ec0ff
6 changed files with 83 additions and 9 deletions

View file

@ -62,10 +62,11 @@ DEFAULT_BUNDLE_METADATA = unicode(
VIDEO_EXTENSIONS = ["mp4", "webm", "mkv", "avi", "mov", "wmv", "flv", "m4v", "ogv"]
AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a", "flac", "aac", "wma", "opus"]
IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg"]
DOCUMENT_EXTENSIONS = ["pdf"]
def get_media_type(extension):
"""Returns 'video', 'audio', 'image', or None based on file extension."""
"""Returns 'video', 'audio', 'image', 'document', or None based on file extension."""
if not extension:
return None
ext = extension.lower()
@ -75,6 +76,8 @@ def get_media_type(extension):
return "audio"
elif ext in IMAGE_EXTENSIONS:
return "image"
elif ext in DOCUMENT_EXTENSIONS:
return "document"
return None

View file

@ -302,6 +302,20 @@
e.preventDefault();
toggleAutoResize();
break;
case 'g':
case 'G':
e.preventDefault();
if (typeof goToProductPage === 'function') {
goToProductPage();
}
break;
case 'p':
case 'P':
e.preventDefault();
if (typeof togglePip === 'function') {
togglePip();
}
break;
}
}

View file

@ -119,6 +119,11 @@ async function loadMedia(mediaElement, productId) {
const match = scriptContent.match(/window\.playerData\s*=\s*(\{[^}]+\})/);
if (match) {
currentPlayerData = eval('(' + match[1] + ')');
// Navigate parent page to show the new product
if (currentPlayerData.productUrl) {
window.history.pushState({}, '', currentPlayerData.productUrl);
}
}
}
}
@ -175,7 +180,7 @@ async function openMediaPlayer(productId) {
}
}
// Enter Picture-in-Picture mode
// Enter Picture-in-Picture mode (only for video)
if (document.pictureInPictureEnabled && media.tagName === 'VIDEO') {
await media.play();
await media.requestPictureInPicture();
@ -199,9 +204,13 @@ async function openMediaPlayer(productId) {
media.addEventListener('leavepictureinpicture', () => {
document.body.removeChild(media);
});
} else {
// Fallback: keep small floating player on page
} else if (media.tagName === 'AUDIO') {
// Audio: keep small floating player on page
media.play();
} else {
// For non-media (PDFs, images), open in pop-out player window
document.body.removeChild(media);
window.open(`/player/${productId}`, 'mediaPlayer', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no,scrollbars=no');
}
} catch (err) {
console.error('Error opening media player:', err);

View file

@ -40,6 +40,11 @@
display: block;
}
/* For documents (PDFs), fill the viewport */
iframe#media {
border: none;
}
/* Mobile optimizations */
@media (max-width: 600px) {
.controls-grid {
@ -186,6 +191,10 @@
</audio>
{% elif media_type == 'image' %}
<img id="media" src="{{ presigned_url }}" alt="{{ product.title }}" />
{% elif media_type == 'document' %}
<iframe id="media" src="{{ presigned_url }}" frameborder="0">
Your browser does not support PDF viewing.
</iframe>
{% endif %}
<div class="product-title">{{ product.title }}</div>
@ -214,8 +223,8 @@
<!-- Row 3: More options -->
<button id="autoResizeBtn" onclick="toggleAutoResize()" title="Auto-Resize Window (R)">↔ Resize</button>
<button onclick="goToProductPage()" title="Go to Product Page (G)">🔗 Page</button>
<button onclick="window.close()" title="Close Window (ESC)">✕ Close</button>
<div></div><!-- Empty cell for grid alignment -->
</div>
</div>
</div>
@ -226,8 +235,24 @@
window.playerData = {
prevProductId: {{ ('"%s"' % prev_product_id) if prev_product_id else 'null' }},
nextProductId: {{ ('"%s"' % next_product_id) if next_product_id else 'null' }},
mediaType: "{{ media_type }}"
mediaType: "{{ media_type }}",
productId: "{{ product.id }}",
productUrl: "{{ product_url }}",
prevProductUrl: {{ ('"%s"' % prev_product_url) if prev_product_url else 'null' }},
nextProductUrl: {{ ('"%s"' % next_product_url) if next_product_url else 'null' }}
};
// Go to the product page
function goToProductPage() {
if (window.opener && !window.opener.closed) {
// If opened from another window, navigate that window
window.opener.location.href = window.playerData.productUrl;
window.opener.focus();
} else {
// Otherwise open in new tab
window.open(window.playerData.productUrl, '_blank');
}
}
</script>
</body>
</html>

View file

@ -197,6 +197,11 @@ async function loadMedia(mediaElement, productId) {
const match = scriptContent.match(/window\.playerData\s*=\s*(\{[^}]+\})/);
if (match) {
currentPlayerData = eval('(' + match[1] + ')');
// Navigate parent page to show the new product
if (currentPlayerData.productUrl) {
window.history.pushState({}, '', currentPlayerData.productUrl);
}
}
}
}
@ -253,7 +258,7 @@ async function openMediaPlayer(productId) {
}
}
// Enter Picture-in-Picture mode
// Enter Picture-in-Picture mode (only for video)
if (document.pictureInPictureEnabled && media.tagName === 'VIDEO') {
await media.play();
await media.requestPictureInPicture();
@ -277,9 +282,13 @@ async function openMediaPlayer(productId) {
media.addEventListener('leavepictureinpicture', () => {
document.body.removeChild(media);
});
} else {
// Fallback: keep small floating player on page
} else if (media.tagName === 'AUDIO') {
// Audio: keep small floating player on page
media.play();
} else {
// For non-media (PDFs, images), open in pop-out player window
document.body.removeChild(media);
window.open(`/player/${productId}`, 'mediaPlayer', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no,scrollbars=no');
}
} catch (err) {
console.error('Error opening media player:', err);

View file

@ -105,6 +105,17 @@ def player(request):
if current_index < len(media_products) - 1:
next_product_id = str(media_products[current_index + 1].id)
# Get product URLs for navigation
product_url = product.absolute_url(request)
prev_product_url = None
next_product_url = None
if prev_product_id and current_index is not None and current_index > 0:
prev_product_url = media_products[current_index - 1].absolute_url(request)
if next_product_id and current_index is not None and current_index < len(media_products) - 1:
next_product_url = media_products[current_index + 1].absolute_url(request)
return {
"product": product,
"presigned_url": presigned_url,
@ -112,4 +123,7 @@ def player(request):
"extension": extension,
"prev_product_id": prev_product_id,
"next_product_id": next_product_id,
"product_url": product_url,
"prev_product_url": prev_product_url,
"next_product_url": next_product_url,
}