add copy button

This commit is contained in:
thek4n 2026-02-20 03:12:25 +03:00
parent 40bf0f413b
commit d6a1a3e67b

View File

@ -91,7 +91,6 @@ async fn sse_handler(
match res { match res {
Ok(changed_path) => { Ok(changed_path) => {
if changed_path.contains(&req_path) { if changed_path.contains(&req_path) {
// Явно указываем типы: Ok<Event, Infallible>
Some(Ok::<axum::response::sse::Event, Infallible>( Some(Ok::<axum::response::sse::Event, Infallible>(
axum::response::sse::Event::default() axum::response::sse::Event::default()
.event("reload") .event("reload")
@ -115,6 +114,7 @@ async fn sse_handler(
(headers, sse) (headers, sse)
} }
async fn serve_file( async fn serve_file(
State(state): State<AppState>, State(state): State<AppState>,
Path(full_path): Path<String>, Path(full_path): Path<String>,
@ -156,18 +156,14 @@ async fn serve_file(
/// Запуск наблюдателя за файловой системой /// Запуск наблюдателя за файловой системой
async fn run_file_watcher(state: AppState) { async fn run_file_watcher(state: AppState) {
// Создаем канал MPSC для передачи путей из колбэка notify в основной цикл
let (tx_fs, mut rx_fs) = tokio::sync::mpsc::channel::<PathBuf>(100); let (tx_fs, mut rx_fs) = tokio::sync::mpsc::channel::<PathBuf>(100);
// Настраиваем notify watcher
// Перемещаем tx_fs внутрь замыкания через clone
let tx_fs_clone = tx_fs.clone(); let tx_fs_clone = tx_fs.clone();
let mut watcher = RecommendedWatcher::new( let mut watcher = RecommendedWatcher::new(
move |res: Result<notify::Event, notify::Error>| { move |res: Result<notify::Event, notify::Error>| {
if let Ok(event) = res { if let Ok(event) = res {
if matches!(event.kind, EventKind::Modify(_)) { if matches!(event.kind, EventKind::Modify(_)) {
for path in event.paths { for path in event.paths {
// Используем клонированный отправитель внутри замыкания
let _ = tx_fs_clone.blocking_send(path); let _ = tx_fs_clone.blocking_send(path);
} }
} }
@ -184,7 +180,6 @@ async fn run_file_watcher(state: AppState) {
println!("Watcher запущен для директории: {:?}", watch_path); println!("Watcher запущен для директории: {:?}", watch_path);
// Цикл обработки событий от файловой системы
while let Some(path) = rx_fs.recv().await { while let Some(path) = rx_fs.recv().await {
if let Some(path_str) = path.to_str() { if let Some(path_str) = path.to_str() {
let _ = state.tx.send(path_str.to_string()); let _ = state.tx.send(path_str.to_string());
@ -223,15 +218,19 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s
Event::End(Tag::CodeBlock(_)) => { Event::End(Tag::CodeBlock(_)) => {
in_code_block = false; in_code_block = false;
// Исправленная подсветка синтаксиса (построчно) // Определяем отображаемое имя языка
let lang_display = current_lang.as_deref().unwrap_or("text");
// Экранируем имя языка для HTML атрибута и текста
let lang_escaped = escape_html(lang_display);
// Подсветка синтаксиса (построчно)
let highlighted_html = if let Some(lang) = &current_lang { let highlighted_html = if let Some(lang) = &current_lang {
if let Some(syntax) = ss.find_syntax_by_token(lang) { if let Some(syntax) = ss.find_syntax_by_token(lang) {
let mut h = HighlightLines::new(syntax, theme); let mut h = HighlightLines::new(syntax, theme);
let mut result_html = String::new(); let mut result_html = String::new();
// Разбиваем код на строки и подсвечиваем каждую отдельно
for line in current_code.lines() { for line in current_code.lines() {
// Добавляем перевод строки обратно, так как lines() удаляет его
let line_with_newline = format!("{}\n", line); let line_with_newline = format!("{}\n", line);
match h.highlight_line(&line_with_newline, ss) { match h.highlight_line(&line_with_newline, ss) {
Ok(regions) => { Ok(regions) => {
@ -250,11 +249,21 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s
escape_html(&current_code) escape_html(&current_code)
}; };
let full_html = format!( // Формируем HTML с заголовком и кнопкой копирования
r#"<pre style="background-color: #2b303b; border-radius: 6px; padding: 15px; overflow-x: auto; margin: 1em 0; border: 1px solid #444;"><code>{}</code></pre>"#, // Мы экранируем current_code еще раз для data-атрибута, хотя для копирования будем брать текст из pre
let code_container = format!(
r#"<div class="code-block-wrapper">
<div class="code-header">
<span class="code-lang">{}</span>
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
</div>
<pre style="margin: 0; border-radius: 0 0 6px 6px;"><code>{}</code></pre>
</div>"#,
lang_escaped,
highlighted_html highlighted_html
); );
processed_events.push(Event::Html(full_html.into()));
processed_events.push(Event::Html(code_container.into()));
}, },
Event::Text(text) if in_code_block => { Event::Text(text) if in_code_block => {
current_code.push_str(&text); current_code.push_str(&text);
@ -272,7 +281,6 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s
let sse_url = format!("/events/{}", file_path); let sse_url = format!("/events/{}", file_path);
// Обратите внимание на двойные фигурные скобки {{ и }} для экранирования в format!
format!( format!(
r#"<!DOCTYPE html> r#"<!DOCTYPE html>
<html lang="ru"> <html lang="ru">
@ -292,8 +300,62 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s
th {{ background-color: #2c2c2c; }} th {{ background-color: #2c2c2c; }}
blockquote {{ border-left: 4px solid #bb86fc; margin: 1em 0; padding-left: 1em; color: #aaa; background: #252525; padding: 10px; }} blockquote {{ border-left: 4px solid #bb86fc; margin: 1em 0; padding-left: 1em; color: #aaa; background: #252525; padding: 10px; }}
code {{ font-family: 'Consolas', 'Monaco', monospace; }} code {{ font-family: 'Consolas', 'Monaco', monospace; }}
/* Стили для обычных инлайн кодов */
p > code, li > code {{ background-color: #2c2c2c; padding: 2px 6px; border-radius: 4px; color: #ff79c6; }} p > code, li > code {{ background-color: #2c2c2c; padding: 2px 6px; border-radius: 4px; color: #ff79c6; }}
/* Стили для блоков кода с подсветкой */
.code-block-wrapper {{
margin: 1em 0;
border: 1px solid #444;
border-radius: 6px;
overflow: hidden;
background-color: #2b303b;
}}
.code-header {{
display: flex;
justify-content: space-between;
align-items: center;
background-color: #232730;
padding: 6px 12px;
border-bottom: 1px solid #444;
font-size: 0.85em;
color: #a0a0a0;
}}
.code-lang {{
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
}}
.copy-btn {{
background: transparent;
border: 1px solid #555;
color: #ccc;
padding: 2px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
transition: all 0.2s;
}}
.copy-btn:hover {{
background-color: #444;
color: #fff;
border-color: #777;
}}
.copy-btn:active {{
transform: scale(0.95);
}}
pre {{
padding: 15px;
overflow-x: auto;
margin: 0;
}}
pre code {{
background: transparent;
padding: 0;
color: inherit;
}}
#status {{ position: fixed; top: 10px; right: 10px; padding: 5px 10px; border-radius: 4px; font-size: 12px; font-weight: bold; }} #status {{ position: fixed; top: 10px; right: 10px; padding: 5px 10px; border-radius: 4px; font-size: 12px; font-weight: bold; }}
.connected {{ background-color: #2ecc71; color: #000; }} .connected {{ background-color: #2ecc71; color: #000; }}
.disconnected {{ background-color: #e74c3c; color: #fff; }} .disconnected {{ background-color: #e74c3c; color: #fff; }}
@ -323,6 +385,34 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &s
}} }}
connect(); connect();
function copyCode(button) {{
// Находим обертку, затем pre внутри неё
const wrapper = button.closest('.code-block-wrapper');
if (!wrapper) return;
const pre = wrapper.querySelector('pre');
if (!pre) return;
// Получаем текстовое содержимое (без HTML тегов подсветки)
const codeText = pre.innerText;
navigator.clipboard.writeText(codeText).then(() => {{
const originalText = button.innerText;
button.innerText = 'Copied!';
button.style.borderColor = '#2ecc71';
button.style.color = '#2ecc71';
setTimeout(() => {{
button.innerText = originalText;
button.style.borderColor = '';
button.style.color = '';
}}, 2000);
}}).catch(err => {{
console.error('Ошибка копирования:', err);
button.innerText = 'Error';
}});
}}
</script> </script>
</body> </body>
</html>"#, </html>"#,