Illustrations/web/templates/entry_view.html

417 lines
16 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>
<!-- TTS toast (one-time, tiny helper UI) -->
<div id="tts-toast"
style="position:fixed;bottom:84px;left:50%;transform:translateX(-50%);
background:#111827;color:#fff;padding:10px 14px;border-radius:8px;
font-size:14px;display:none;z-index:9999;box-shadow:0 6px 18px rgba(0,0,0,.25);">
</div>
<script>
(function(){
const LS_KEY = "tts.voiceName";
const speakBtn = document.querySelector('#ttsBtn, #btn-speak, .btn-speak');
// --- Toast ---
function showTTSToast(msg, ms=2200){
const el = document.getElementById('tts-toast');
if (!el) return;
el.textContent = msg;
el.style.display = 'block';
clearTimeout(showTTSToast._t);
showTTSToast._t = setTimeout(()=>{ el.style.display = 'none'; }, ms);
}
// --- Voice helpers ---
function listVoices(){ return window.speechSynthesis ? speechSynthesis.getVoices() : []; }
const PREFERRED_ORDER = [
/Google .*English/i, // Chrome voices
/Microsoft .*Natural/i, // Edge neural voices
/Siri/i, // Safari voices
/Alex/i, // macOS Alex
/English/i
];
function pickBestVoice(lang="en"){
const vs = listVoices();
if (!vs.length) return null;
const saved = localStorage.getItem(LS_KEY);
if (saved){
const found = vs.find(v => v.name === saved);
if (found) return found;
}
for (const rx of PREFERRED_ORDER){
const m = vs.find(v => rx.test(v.name) && v.lang?.toLowerCase().startsWith(lang));
if (m) return m;
}
return vs.find(v => v.lang?.toLowerCase().startsWith(lang)) || vs[0];
}
function voiceServiceLabel(v){
if (!v) return "Browser TTS";
const n = (v.name || "").toLowerCase();
if (/google/.test(n)) return "Google (Chrome) voice: " + v.name;
if (/microsoft|aria|guy|jessa|zira|david|mark|neural/.test(n)) return "Microsoft (Edge) voice: " + v.name;
if (/siri/.test(n)) return "Apple Siri voice: " + v.name;
if (/alex/.test(n)) return "Apple Alex voice";
return "Browser TTS voice: " + v.name + (v.lang ? ` [${v.lang}]` : "");
}
// --- Build the text (ensure punctuation + two spaces) ---
function ensureSentenceEnd(s){
if (!s) return "";
return /[.!?…]$/.test(s) ? s : (s + ".");
}
function buildCombinedText(){
const ill = (document.getElementById("illustration-text")?.innerText || "").trim();
const app = (document.getElementById("application-text")?.innerText || "").trim();
const illP = ensureSentenceEnd(ill);
return [illP, app].filter(Boolean).join(" ") || "No text available for this illustration.";
}
function isSpeaking(){
return !!(window.speechSynthesis && (speechSynthesis.speaking || speechSynthesis.pending));
}
function startSpeaking(){
const text = buildCombinedText();
if (!("speechSynthesis" in window) || !("SpeechSynthesisUtterance" in window)){
alert("Text-to-Speech not supported in this browser.");
return;
}
speechSynthesis.cancel();
const begin = () => {
const u = new SpeechSynthesisUtterance(text);
const v = pickBestVoice("en");
if (v) u.voice = v;
u.rate = 1.0;
u.pitch = 1.0;
u.volume = 1.0;
// UI feedback
speakBtn?.setAttribute('aria-pressed', 'true');
speakBtn?.classList.add('active');
u.onend = u.onerror = () => {
speakBtn?.setAttribute('aria-pressed', 'false');
speakBtn?.classList.remove('active');
};
speechSynthesis.speak(u);
showTTSToast(voiceServiceLabel(v));
};
if (!listVoices().length){
speechSynthesis.onvoiceschanged = () => { speechSynthesis.onvoiceschanged = null; begin(); };
speechSynthesis.getVoices(); // nudge
setTimeout(() => { if (!speechSynthesis.speaking) begin(); }, 600);
} else {
begin();
}
}
function stopSpeaking(){
if (!window.speechSynthesis) return;
speechSynthesis.cancel();
speakBtn?.setAttribute('aria-pressed', 'false');
speakBtn?.classList.remove('active');
showTTSToast("Playback stopped");
}
if (speakBtn){
// Toggle behavior
speakBtn.addEventListener('click', () => {
if (isSpeaking()){
stopSpeaking();
} else {
startSpeaking();
}
});
} else {
console.warn('TTS: speak button not found (looked for #ttsBtn, #btn-speak, .btn-speak).');
}
})();
</script>
{% endblock %}