417 lines
16 KiB
HTML
417 lines
16 KiB
HTML
{% 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 %} |