Illustrations/web/templates/entry_view.html

406 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block body_class %}themed-bg{% endblock %}
{% load static %}
{% block content %}
<div class="result-wrap">
<!-- Top bar: back + counter + clear Prev/Next -->
<div class="result-toolbar">
<div class="rt-left">
<a class="btn btn-secondary" href="{% url 'search' %}">← Back to Search</a>
{% if count %}
<span class="rt-count">{{ position }} of {{ count }}</span>
{% endif %}
</div>
<div class="rt-right">
<form method="get" action="{% url 'nav_prev' %}" class="inline">
<!-- send current zero-based index (position-1); view will subtract 1 -->
<input type="hidden" name="i" value="{{ position|add:'-1' }}">
<button class="btn btn-lg" {% if position <= 1 %}disabled{% endif %}> Prev</button>
</form>
<form method="get" action="{% url 'nav_next' %}" class="inline">
<!-- send current zero-based index (position-1); view will add 1 -->
<input type="hidden" name="i" value="{{ position|add:'-1' }}">
<button class="btn btn-lg btn-primary" {% if position >= count %}disabled{% endif %}>Next </button>
</form>
<!-- Share button (copies Illustration + two spaces + Application) -->
<button id="share-btn" class="btn btn-lg btn-primary" type="button" title="Copy to clipboard" style="margin-left:6px;">
<span style="display:flex;align-items:center;gap:6px;">
<!-- iOS-like share icon (SVG) -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
viewBox="0 0 16 16" aria-hidden="true" focusable="false" fill="currentColor">
<path d="M8 1a.5.5 0 0 1 .5.5V9a.5.5 0 0 1-1 0V1.5A.5.5 0 0 1 8 1z"/>
<path d="M5.646 3.646a.5.5 0 0 1 .708 0L8 5.293l1.646-1.647a.5.5 0 0 1 .708.708L8.354 6.354a.5.5 0 0 1-.708 0L5.646 4.354a.5.5 0 0 1 0-.708z"/>
<path d="M4.5 6A1.5 1.5 0 0 0 3 7.5v5A1.5 1.5 0 0 0 4.5 14h7A1.5 1.5 0 0 0 13 12.5v-5A1.5 1.5 0 0 0 11.5 6H10a.5.5 0 0 0 0 1h1.5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5v-5a.5.5 0 0 1 .5-.5H6a.5.5 0 0 0 0-1H4.5z"/>
</svg>
Share
</span>
</button>
<!-- Share button ... -->
<button class="btn btn-secondary" id="ttsBtn"
title="Read aloud"
aria-label="Read illustration out loud"
data-illustration="{{ entry.illustration|default_if_none:''|escapejs }}"
data-application="{{ entry.application|default_if_none:''|escapejs }}">
<!-- Speaker icon (inline SVG) -->
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" style="vertical-align:middle;">
<path d="M3 10v4h4l5 4V6L7 10H3z"></path>
<path d="M16.5 12c0-1.77-1-3.29-2.5-4.03v8.06A4.49 4.49 0 0 0 16.5 12z"></path>
<path d="M14 3.23v2.06c3.39.49 6 3.39 6 6.71s-2.61 6.22-6 6.71v2.06c4.45-.52 8-4.3 8-8.77s-3.55-8.25-8-8.77z"></path>
</svg>
</button>
<!-- Edit button ... -->
{% if user.is_authenticated and user.is_staff %}
<!-- Just the label changed to 'Edit'; link unchanged -->
<a class="btn btn-outline" href="{% url 'entry_edit' entry.id %}">Edit</a>
<a class="btn btn-danger" href="{% url 'entry_delete' entry.id %}">Delete</a>
{% endif %}
</div>
</div>
<!-- Main card -->
<div class="result-card">
<!-- SUBJECT pills (WOL-linked) -->
<div class="subject-pills">
{% if subject_list %}
{% for s in subject_list %}
<a
class="chip chip-subject"
href="https://wol.jw.org/en/wol/s/r1/lp-e?q={{ s }}"
target="_blank" rel="noopener noreferrer"
title="Search WOL for {{ s }}"
>{{ s }}</a>
{% endfor %}
{% else %}
<span class="chip chip-muted">(no subject)</span>
{% endif %}
</div>
<!-- ILLUSTRATION -->
<div class="section">
<div class="section-label">Illustration</div>
<div class="section-body lead-text" id="illustration-text">
{{ entry.illustration|linebreaksbr|default:"—" }}
</div>
</div>
<!-- APPLICATION -->
<div class="section">
<div class="section-label">Application</div>
<div class="section-body lead-text" id="application-text">
{{ entry.application|linebreaksbr|default:"—" }}
</div>
</div>
<!-- Meta (smaller) -->
<div class="meta-grid">
<div class="meta-item">
<div class="meta-label">Source</div>
<div class="meta-value">
{% if entry.source %}
{% with s=entry.source %}
{% with sl=s|lower %}
{% if sl|slice:":2" == "wp" or sl|slice:":2" == "ws" or sl|slice:":2" == "yb" or sl|slice:":2" == "km" or sl|slice:":3" == "mwb" or sl|slice:":1" == "w" or sl|slice:":1" == "g" or sl|slice:":2" == "ap" or sl|slice:":3" == "apf" or sl|slice:":2" == "be" or sl|slice:":2" == "bh" or sl|slice:":2" == "br" or sl|slice:":2" == "bt" or sl|slice:":3" == "btg" or sl|slice:":2" == "cf" or sl|slice:":2" == "cl" or sl|slice:":2" == "ct" or sl|slice:":2" == "dp" or sl|slice:":2" == "fg" or sl|slice:":2" == "fy" or sl|slice:":2" == "gt" or sl|slice:":2" == "hb" or sl|slice:":2" == "im" or sl|slice:":2" == "ip" or sl|slice:":2" == "it" or sl|slice:":2" == "jv" or sl|slice:":2" == "ka" or sl|slice:":2" == "kj" or sl|slice:":2" == "kl" or sl|slice:":2" == "lf" or sl|slice:":3" == "lff" or sl|slice:":2" == "ll" or sl|slice:":2" == "ly" or sl|slice:":2" == "my" or sl|slice:":2" == "od" or sl|slice:":2" == "pe" or sl|slice:":2" == "po" or sl|slice:":2" == "pt" or sl|slice:":2" == "rr" or sl|slice:":2" == "rs" or sl|slice:":2" == "sg" or sl|slice:":2" == "sh" or sl|slice:":2" == "si" or sl|slice:":2" == "td" or sl|slice:":2" == "tp" or sl|slice:":2" == "tr" or sl|slice:":2" == "ts" or sl|slice:":2" == "un" %}
<a class="chip chip-link"
href="https://wol.jw.org/en/wol/l/r1/lp-e?q={{ s|urlencode }}"
target="_blank" rel="noopener noreferrer">{{ s }}</a>
{% else %}
<a class="chip chip-link"
href="https://www.google.com/search?q={{ s|urlencode }}"
target="_blank" rel="noopener noreferrer">{{ s }}</a>
{% endif %}
{% endwith %}
{% endwith %}
{% else %}—{% endif %}
</div>
</div>
<div class="meta-item">
<div class="meta-label">Scripture</div>
<div class="meta-value">
{% if scripture_list %}
{% for sc in scripture_list %}
<a class="chip chip-link"
href="https://wol.jw.org/en/wol/l/r1/lp-e?q={{ sc|urlencode }}"
target="_blank" rel="noopener noreferrer">{{ sc }}</a>
{% endfor %}
{% else %}—{% endif %}
</div>
</div>
<div class="meta-item">
<div class="meta-label">Code</div>
<div class="meta-value">{{ entry.entry_code|default:"—" }}</div>
</div>
<div class="meta-item">
<div class="meta-label">Talk</div>
<div class="meta-value">
{% if entry.talk_title %}{{ entry.talk_title }}{% else %}—{% endif %}
{# Talk Number pill (staff-only link) #}
{% if entry.talk_number %}
{% if user.is_authenticated and user.is_staff and talk_pdf_url %}
<a href="{{ talk_pdf_url }}" target="_blank" rel="noopener"
class="chip chip-link" title="Open talk PDF">
<span class="muted">#:</span>
<strong>{{ entry.talk_number }}</strong>
</a>
{% else %}
<span class="chip">
<span class="muted">#:</span>
<strong>{{ entry.talk_number }}</strong>
</span>
{% endif %}
{% endif %}
</div>
</div>
<div class="meta-item">
<div class="meta-label">Dates</div>
<div class="meta-value small">
{% if entry.date_added %}Added: {{ entry.date_added }}{% else %}Added: —{% endif %}
{% if entry.date_edited %} • Edited: {{ entry.date_edited }}{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Toast for copy confirmation -->
<div id="copy-toast"
style="position:fixed;bottom:20px;left:50%;transform:translateX(-50%);
background:#333;color:#fff;padding:10px 16px;border-radius:6px;
font-size:14px;display:none;z-index:9999;">
The Illustration was copied to the clipboard
</div>
<style>
.subject-pills{
display:flex;
flex-wrap:wrap;
gap:10px;
margin:0 0 10px;
}
.chip-subject{
font-weight:700;
font-size:16px;
padding:8px 14px;
border-radius:999px;
background:#eef5fc;
border:1px solid #d7e6f7;
color:#0f172a;
text-decoration:none;
text-transform: capitalize; /* Capitalize each word */
}
.chip-subject:hover{
background:#e2effc;
border-color:#c9def5;
}
</style>
<script>
// Robust copy to clipboard that works on HTTP and HTTPS
(function () {
function copyText(text) {
// Try modern API first
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
// Fallback: temporary textarea
return new Promise(function (resolve, reject) {
try {
var ta = document.createElement('textarea');
ta.value = text;
// Avoid scrolling to bottom
ta.style.position = 'fixed';
ta.style.top = '-1000px';
ta.style.left = '-1000px';
document.body.appendChild(ta);
ta.focus();
ta.select();
var ok = document.execCommand('copy');
document.body.removeChild(ta);
ok ? resolve() : reject(new Error('execCommand failed'));
} catch (e) {
reject(e);
}
});
}
function showToast() {
var toast = document.getElementById('copy-toast');
if (!toast) return;
toast.style.display = 'block';
clearTimeout(showToast._t);
showToast._t = setTimeout(function(){ toast.style.display = 'none'; }, 5000);
}
var btn = document.getElementById('share-btn');
if (!btn) return;
btn.addEventListener('click', function () {
var ill = (document.getElementById('illustration-text')?.innerText || '').trim();
var app = (document.getElementById('application-text')?.innerText || '').trim();
var text = ill + ' ' + app; // two spaces
copyText(text).then(showToast).catch(function (err) {
alert('Failed to copy: ' + err);
});
});
})();
</script>
<script>
(function(){
{% if user.is_authenticated %}
const entryId = {{ entry.id }};
let timer = null;
function visible(){ return document.visibilityState === 'visible'; }
function start(){
if (timer || !visible()) return;
timer = setTimeout(()=>{
const fd = new FormData();
fetch("{% url 'api_log_view' 0 %}".replace("0", entryId), {
method:'POST', body: fd,
headers: {'X-CSRFToken': (document.cookie.match(/(^|;)\s*csrftoken\s*=\s*([^;]+)/)||[]).pop() || '' }
}).catch(()=>{});
}, 10000); // 10 seconds
}
function stop(){ if (timer){ clearTimeout(timer); timer = null; } }
document.addEventListener('visibilitychange', ()=> visible() ? start() : stop());
window.addEventListener('pagehide', stop);
start();
{% endif %}
})();
</script>
<script>
(function(){
const btn = document.getElementById('ttsBtn');
if (!btn) return;
// Graceful feature check
const synth = window.speechSynthesis;
if (!('speechSynthesis' in window)) {
btn.disabled = true;
btn.title = 'Text-to-Speech not supported in this browser';
return;
}
// Build the combined text (Illustration + two spaces + Application).
function buildParagraph() {
const ill = (btn.dataset.illustration || '').trim();
const app = (btn.dataset.application || '').trim();
// Ensure Illustration ends with punctuation if it has text
let illFixed = ill;
if (illFixed && !/[.!?…]$/.test(illFixed)) {
illFixed += '.';
}
// Two spaces between illustration and application (if both exist)
const spacer = (illFixed && app) ? ' ' : '';
return (illFixed + spacer + app).replace(/\s+/g, ' ').trim();
}
// Try to pick a natural-sounding voice if available
let voices = [];
function loadVoices() {
voices = synth.getVoices();
}
loadVoices();
if (typeof speechSynthesis.onvoiceschanged !== 'undefined') {
speechSynthesis.onvoiceschanged = loadVoices;
}
function chooseVoice() {
if (!voices || !voices.length) return null;
// Prefer a Google or high-quality en voice if present
const prefs = [
/Google\s.+English/i,
/en-US/i,
/English/i
];
for (const rx of prefs) {
const v = voices.find(v => rx.test(v.name) || rx.test(v.lang));
if (v) return v;
}
return voices[0] || null;
}
let isSpeaking = false;
let currentUtterances = [];
function chunkBySentences(text, maxLen=300) {
// Split by sentence-ish boundaries, merge small pieces to respect character limits
const raw = text.split(/(?<=[.!?…])\s+/);
const chunks = [];
let buf = '';
raw.forEach(part => {
if ((buf + ' ' + part).trim().length <= maxLen) {
buf = (buf ? (buf + ' ' + part) : part);
} else {
if (buf) chunks.push(buf.trim());
buf = part;
}
});
if (buf) chunks.push(buf.trim());
return chunks.length ? chunks : [text];
}
function speak(text) {
stop(); // ensure we start fresh
const voice = chooseVoice();
const chunks = chunkBySentences(text);
chunks.forEach((chunk, i) => {
const u = new SpeechSynthesisUtterance(chunk);
if (voice) u.voice = voice;
u.rate = 1.0; // adjust to taste (0.5 - 2)
u.pitch = 1.0; // 0 - 2
u.onstart = () => { isSpeaking = true; btn.classList.add('speaking'); };
u.onend = () => {
// When the last utterance ends, clear state
if (i === chunks.length - 1) {
isSpeaking = false;
btn.classList.remove('speaking');
}
};
u.onerror = () => {
isSpeaking = false;
btn.classList.remove('speaking');
};
currentUtterances.push(u);
});
// Queue them in order
currentUtterances.forEach(u => synth.speak(u));
}
function stop() {
if (synth.speaking || synth.pending) synth.cancel();
currentUtterances = [];
isSpeaking = false;
btn.classList.remove('speaking');
}
// Click to toggle: play if idle; stop if already speaking
btn.addEventListener('click', function(){
if (isSpeaking || synth.speaking || synth.pending) {
stop();
return;
}
const text = buildParagraph();
if (text) speak(text);
});
// Optional: stop if navigating away
window.addEventListener('beforeunload', stop);
})();
</script>
{% endblock %}