Gmeek/templates/post.html

540 lines
17 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 head %}
<meta name="description" content="{{ blogBase['description'] }}">
<meta property="og:title" content="{{ blogBase['postTitle'] }}">
<meta property="og:description" content="{{ blogBase['description'] }}">
<meta property="og:type" content="article">
<meta property="og:url" content="{{ blogBase['postUrl'] }}">
<meta property="og:image" content="{{ blogBase['ogImage'] }}">
<title>{{ blogBase['postTitle'] }}</title>
{% if blogBase['highlight']==1 %}<link href="//unpkg.com/@wooorm/starry-night@2.1.1/style/both.css" rel="stylesheet" />{% endif %}
{{ blogBase['head'] }}
{% endblock %}
{% block style %}
<style>
.postTitle{margin: auto 0;font-size:40px;font-weight:bold;}
.title-right{display:flex;margin:auto 0 0 auto;}
.title-right .circle{padding: 14px 16px;margin-right:8px;}
#postBody{border-bottom: 1px solid var(--color-border-default);padding-bottom:36px;}
#postBody hr{height:2px;}
#postBody .paragraph {
display: block;
position: relative;
padding-right: 30px;
cursor: pointer;
user-select: text;
}
#postBody .paragraph.highlighted {
background-color: #ffffcc;
}
#postBody .paragraph .note-text {
display: block;
margin-top: 4px;
font-size: 0.9em;
color: #333;
background: #fffae6;
border-left: 3px solid #f0c000;
padding: 2px 6px;
user-select: text;
}
#cmButton{height:48px;margin-top:48px;}
#comments{margin-top:64px;}
.g-emoji{font-size:24px;}
@media (max-width: 600px) {
body {padding: 8px;}
.postTitle{font-size:24px;}
}
{% if blogBase['highlight']!=0 -%}
.copy-feedback {
display: none;
position: absolute;
top: 10px;
right: 50px;
color: var(--color-fg-on-emphasis);
background-color: var(--color-fg-muted);
border-radius: 3px;
padding: 5px 8px;
font-size: 12px;
}{% endif %}
.highlighted-sentence {
background: yellow;
transition: background 0.3s ease;
}
#backToTopBtn {
position: fixed;
bottom: 40px;
right: 30px;
z-index: 999;
background-color: #007bff;
color: white;
border: none;
outline: none;
padding: 10px 14px;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
transition: opacity 0.3s ease;
}
#backToTopBtn:hover {
background-color: #0056b3;
}
</style>
{{ blogBase['style'] }}
{% endblock %}
{% block header %}
<h1 class="postTitle">{{ blogBase['postTitle'] }}</h1>
<div class="title-right">
<a href="{{ blogBase['homeUrl'] }}" id="buttonHome" class="btn btn-invisible circle" title="{{ i18n['home'] }}">
<svg class="octicon" width="16" height="16">
<path id="pathHome" fill-rule="evenodd"></path>
</svg>
</a>
{% if blogBase['showPostSource']==1 %}
<a href="{{ blogBase['postSourceUrl'] }}" target="_blank" class="btn btn-invisible circle" title="Issue">
<svg class="octicon" width="16" height="16">
<path id="pathIssue" fill-rule="evenodd"></path>
</svg>
</a>
{% endif %}
<a class="btn btn-invisible circle" onclick="modeSwitch();" title="{{ i18n['switchTheme'] }}" {%- if blogBase['themeMode']=='fix' -%}style="display:none;"{%- endif -%}>
<svg class="octicon" width="16" height="16" >
<path id="themeSwitch" fill-rule="evenodd"></path>
</svg>
</a>
<a id="ttsBtn" class="btn btn-invisible circle" title="Read">
<svg class="octicon" width="16" height="16" >
<path d="M4 4 v8 l6 -4 -6 -4 z" fill-rule="evenodd"></path>
</svg>
</a>
<a id="exportNotesBtn" class="btn btn-invisible circle" title="导出笔记" href="javascript:void(0)">
<svg width="16" height="16" class="octicon">
<path d="M5 3v2h6V3h2v2h1a1 1 0 011 1v2H2V6a1 1 0 011-1h1V3h2zm-3 6h14v2a1 1 0 01-1 1H3a1 1 0 01-1-1V9zm0 4h14v2H2v-2z"></path>
</svg>
</a>
</div>
{% endblock %}
{% block content %}
<div class="markdown-body" id="postBody">{{ blogBase['postBody'] }}</div>
<div style="font-size:small;margin-top:8px;float:right;">{{ blogBase['bottomText'] }}</div>
{% if blogBase['needComment']==1 %}
<button class="btn btn-block" type="button" onclick="openComments()" id="cmButton">{{ i18n['comments'] }}</button>
<div class="comments" id="comments"></div>
{% endif %}
<button id="backToTopBtn" title="返回顶部" style="display: none;">
<svg width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 8l6 6H6z"/>
</svg>
</button>
{% endblock %}
{% block script %}
<script>
document.getElementById("pathHome").setAttribute("d",IconList["home"]);
//document.getElementById("read").setAttribute("d",IconList["startread"]);
console.log(IconList["startread"]);
{% if blogBase['showPostSource']==1 %}document.getElementById("pathIssue").setAttribute("d",IconList["github"]);{% endif %}
{% if blogBase['commentNum']>0 -%}
cmButton=document.getElementById("cmButton");
span=document.createElement("span");
span.setAttribute("class","Counter");
span.innerHTML="{{ blogBase['commentNum'] }}";
cmButton.appendChild(span);
{%- endif %}
{% if blogBase['needComment']==1 %}
function openComments(){
cm=document.getElementById("comments");
cmButton=document.getElementById("cmButton");
cmButton.disabled=true;
cmButton.innerHTML="loading";
span=document.createElement("span");
span.setAttribute("class","AnimatedEllipsis");
cmButton.appendChild(span);
script=document.createElement("script");
script.setAttribute("src","https://utteranc.es/client.js");
script.setAttribute("repo","{{ blogBase['repoName'] }}");
script.setAttribute("issue-term","title");
{% if blogBase['themeMode']=='manual' %}
if(localStorage.getItem("meek_theme")=="dark"){script.setAttribute("theme","dark-blue");}
else if(localStorage.getItem("meek_theme")=="light") {script.setAttribute("theme","github-light");}
else{script.setAttribute("theme","preferred-color-scheme");}
{% else %}
script.setAttribute("theme","{{ blogBase['nightTheme'] }}");
{% endif %}
script.setAttribute("crossorigin","anonymous");
script.setAttribute("async","");
cm.appendChild(script);
int=self.setInterval("iFrameLoading()",200);
}
function iFrameLoading(){
var utterances=document.getElementsByClassName('utterances');
if(utterances.length==1){
if(utterances[0].style.height!=""){
utterancesLoad=1;
int=window.clearInterval(int);
document.getElementById("cmButton").style.display="none";
console.log("utterances Load OK");
}
}
}
{%- endif %}
document.getElementById('exportNotesBtn')?.addEventListener('click', () => {
const notesData = JSON.parse(localStorage.getItem('myParagraphNotes') || '{}');
if (!Object.keys(notesData).length) {
alert('你还没有收藏任何段落');
return;
}
let exportText = '我的段落笔记:\n\n';
for (const [pid, note] of Object.entries(notesData)) {
const paraElem = document.getElementById(pid);
if (!paraElem) continue;
const paraText = paraElem.cloneNode(true);
paraText.querySelectorAll('.note-text').forEach(n => n.remove());
const pureText = paraText.innerText.replace(/\n/g, ' ');
exportText += `段落 ${pid.replace('para-', '')}\n${pureText}\n笔记:${note}\n\n`;
}
const blob = new Blob([exportText], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '我的段落笔记.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
(function() {
const STORAGE_KEY = 'postScrollPosition';
window.addEventListener('load', () => {
const savedPos = localStorage.getItem(STORAGE_KEY);
if (savedPos) {
window.scrollTo(0, parseInt(savedPos, 10));
}
});
let saveTimeout = null;
window.addEventListener('scroll', () => {
if (saveTimeout) return;
saveTimeout = setTimeout(() => {
localStorage.setItem(STORAGE_KEY, window.scrollY);
saveTimeout = null;
}, 200);
});
})();
let originalHTML = "";
(() => {
const ttsBtn = document.getElementById('ttsBtn');
const ttsRateSelect = document.getElementById('ttsRate');
const postBody = document.getElementById('postBody');
let utterance = null;
let isSpeaking = false;
let sentenceSpans = [];
let currentIndex = 0;
function resetTTSButton() {
ttsBtn.title = "朗读/暂停";
ttsBtn.querySelector('path').setAttribute('d', "M4 4v8l6-4-6-4z"); // 播放图标
}
function getCleanTextFromPostBody() {
const postBody = document.getElementById('postBody');
const clone = postBody.cloneNode(true);
clone.querySelectorAll('mark, .highlight, [data-note="true"]').forEach(el => {
const textNode = document.createTextNode(el.innerText || el.textContent || '');
el.replaceWith(textNode);
});
return clone.innerText.trim();
}
function clearHighlights() {
sentenceSpans.forEach(span => span.classList.remove("highlighted-sentence"));
}
function scrollIntoViewSmoothly(el) {
el.scrollIntoView({
behavior: "smooth",
block: "center"
});
}
function splitTextIntoSentences(text) {
return text.match(/[^。!?]+[。!?]?/g) || [text];
}
function prepareSentences() {
const postBody = document.getElementById('postBody');
const text = Array.from(document.querySelectorAll('#postBody .paragraph'))
.map(p => {
// 拿掉 .note-text 的内容
const clone = p.cloneNode(true);
clone.querySelectorAll('.note-text').forEach(n => n.remove());
return clone.innerText.trim();
})
.filter(Boolean)
.join('\n');
const sentences = splitTextIntoSentences(text);
originalHTML = postBody.innerHTML;
postBody.innerHTML = ""; // 清空内容
sentenceSpans = sentences.map(s => {
const span = document.createElement("span");
span.textContent = s;
span.style.display = "inline";
postBody.appendChild(span);
postBody.appendChild(document.createTextNode(" ")); // 保持间距
return span;
});
return sentences;
}
function speakSentences(sentences) {
if (!window.speechSynthesis) return alert("不支持语音合成");
if (!sentences.length) return;
currentIndex = 0;
isSpeaking = true;
function speakNext() {
if (currentIndex >= sentences.length) {
isSpeaking = false;
resetTTSButton();
postBody.innerHTML = originalHTML;
return;
}
const text = sentences[currentIndex];
utterance = new SpeechSynthesisUtterance(text);
utterance.lang = "zh-CN";
utterance.rate = parseFloat(ttsRateSelect?.value || 1);
utterance.onstart = () => {
clearHighlights();
const span = sentenceSpans[currentIndex];
if (span) {
span.classList.add("highlighted-sentence");
scrollIntoViewSmoothly(span);
}
};
utterance.onend = () => {
currentIndex++;
speakNext();
};
utterance.onerror = () => {
isSpeaking = false;
resetTTSButton();
alert("朗读出错");
};
speechSynthesis.speak(utterance);
}
speakNext();
}
ttsBtn.addEventListener("click", () => {
if (!window.speechSynthesis) {
alert('你的浏览器不支持语音合成功能');
return;
}
if (isSpeaking) {
if (speechSynthesis.paused) {
speechSynthesis.resume();
ttsBtn.title = "暂停朗读";
ttsBtn.querySelector('path').setAttribute('d', "M6 4h2v8H6zM10 4h2v8h-2z");
} else {
speechSynthesis.pause();
ttsBtn.title = "继续朗读";
ttsBtn.querySelector('path').setAttribute('d', "M4 4v8l6-4-6-4z");
}
} else {
speechSynthesis.cancel();
clearHighlights();
const sentences = prepareSentences();
speakSentences(sentences);
ttsBtn.title = "暂停朗读";
ttsBtn.querySelector('path').setAttribute('d', "M6 4h2v8H6zM10 4h2v8h-2z");
}
});
})();
(function() {
const STORAGE_KEY = 'myParagraphNotes';
const postBody = document.getElementById('postBody');
if (!postBody) return;
const text = postBody.innerText || postBody.textContent;
const lines = text.split(/\n+/).filter(line => line.trim().length > 0);
postBody.innerHTML = '';
lines.forEach((line, index) => {
const span = document.createElement('span');
span.className = 'paragraph';
span.id = 'para-' + index;
span.textContent = line.trim();
postBody.appendChild(span);
});
let notesData = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
function saveNotes() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(notesData));
}
function renderParagraph(p) {
const pid = p.id;
const oldNote = p.querySelector('.note-text');
if (oldNote) oldNote.remove();
if (notesData[pid]) {
p.classList.add('highlighted');
const noteElem = document.createElement('div');
noteElem.className = 'note-text';
noteElem.textContent = notesData[pid];
noteElem.style.fontSize = '0.9em';
noteElem.style.color = '#888';
noteElem.style.marginTop = '4px';
noteElem.setAttribute('data-note', 'true');
p.appendChild(noteElem);
} else {
p.classList.remove('highlighted');
}
}
function setupParagraph(p) {
p.addEventListener('click', () => {
const pid = p.id;
const currentNote = notesData[pid] || '';
const userInput = prompt('编辑笔记(留空将删除):', currentNote);
if (userInput === null) return; // 取消
if (userInput.trim() === '') {
delete notesData[pid];
} else {
notesData[pid] = userInput.trim();
}
renderParagraph(p);
saveNotes();
});
}
document.querySelectorAll('#postBody .paragraph').forEach(p => {
renderParagraph(p);
setupParagraph(p);
});
})();
window.addEventListener('scroll', () => {
const btn = document.getElementById('backToTopBtn');
if (document.documentElement.scrollTop > 300 || document.body.scrollTop > 300) {
btn.style.display = 'block';
} else {
btn.style.display = 'none';
}
});
document.getElementById('backToTopBtn').addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
{% if blogBase['highlight']!=0 -%}
document.addEventListener('DOMContentLoaded', () => {
const createClipboardHTML = (codeContent, additionalClasses = '') => `
<pre class="notranslate"><code class="notranslate">${codeContent}</code></pre>
<div class="clipboard-container position-absolute right-0 top-0 ${additionalClasses}">
<clipboard-copy class="ClipboardButton btn m-2 p-0" role="button" style="display: inherit;">
<svg height="16" width="16" class="octicon octicon-copy m-2"><path d="${IconList["copy"]}"></path></svg>
<svg height="16" width="16" class="octicon octicon-check color-fg-success m-2 d-none"><path d="${IconList["check"]}"></path></svg>
</clipboard-copy>
<div class="copy-feedback">Copied!</div>
</div>
`;
const handleCodeElements = (selector = '') => {
document.querySelectorAll(selector).forEach(codeElement => {
const codeContent = codeElement.innerHTML;
const newStructure = document.createElement('div');
newStructure.className = 'snippet-clipboard-content position-relative overflow-auto';
newStructure.innerHTML = createClipboardHTML(codeContent);
const parentElement = codeElement.parentElement;
if (selector.includes('highlight')) {
parentElement.insertBefore(newStructure, codeElement.nextSibling);
parentElement.removeChild(codeElement);
} else {
parentElement.parentElement.replaceChild(newStructure, parentElement);
}
});
};
handleCodeElements('pre.notranslate > code.notranslate');
handleCodeElements('div.highlight > pre.notranslate');
let currentFeedback = null;
document.querySelectorAll('clipboard-copy').forEach(copyButton => {
copyButton.addEventListener('click', () => {
const codeContent = copyButton.closest('.snippet-clipboard-content').innerText;
const tempTextArea = document.createElement('textarea');
tempTextArea.value = codeContent;
document.body.appendChild(tempTextArea);
tempTextArea.select();
document.execCommand('copy');
document.body.removeChild(tempTextArea);
const copyIcon = copyButton.querySelector('.octicon-copy');
const checkIcon = copyButton.querySelector('.octicon-check');
const copyFeedback = copyButton.nextElementSibling;
if (currentFeedback && currentFeedback !== copyFeedback) {currentFeedback.style.display = 'none';}
currentFeedback = copyFeedback;
copyIcon.classList.add('d-none');
checkIcon.classList.remove('d-none');
copyFeedback.style.display = 'block';
copyButton.style.borderColor = 'var(--color-success-fg)';
setTimeout(() => {
copyIcon.classList.remove('d-none');
checkIcon.classList.add('d-none');
copyFeedback.style.display = 'none';
copyButton.style.borderColor = '';
}, 2000);
});
});
});
{%- endif %}
</script>
{{ blogBase['script'] }}
{% endblock %}