Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Треды Threads — ветки</title>
<style>
body { font-family: sans-serif; max-width: 780px; margin: 20px auto; padding: 0 10px; background:#f7f7f8; color:#222; }
h2, h3 { text-align:center; }
.status { text-align:center; color:#1faa59; font-size:13px; min-height:18px; margin-bottom:12px; }
.hint { background:#fff; border:1px solid #e2e2e2; border-radius:8px; padding:12px 14px; font-size:13px; color:#555; margin-bottom:14px; line-height:1.5; }
.hint code { background:#eee; padding:1px 5px; border-radius:4px; }
#input { width:100%; min-height:160px; box-sizing:border-box; font-size:14px; padding:10px; border-radius:8px; border:1px solid #ccc; }
.controls { margin:10px 0; display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
input[type=text] { padding:9px 10px; font-size:14px; border:1px solid #ccc; border-radius:6px; flex:1; min-width:180px; }
button { padding:9px 14px; font-size:14px; cursor:pointer; border:none; border-radius:6px; background:#2d7ff9; color:#fff; }
button.done { background:#1faa59; }
button.clear, button.del { background:#999; }
button.collect { background:#7a52e0; }
button.del { background:#e23; }
button:disabled { background:#ccc; cursor:not-allowed; }
.block { margin-bottom:12px; border:1px solid #e2e2e2; border-radius:8px; padding:10px; background:#fff; }
.block-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:4px; }
.block-head h4 { margin:0; color:#444; }
.count { font-size:13px; color:#1faa59; font-weight:bold; }
.count.over { color:#e23; }
.block textarea { width:100%; min-height:80px; box-sizing:border-box; font-size:14px; padding:8px; border-radius:6px; border:1px solid #ddd; }
hr { margin:26px 0; border:none; border-top:1px solid #ddd; }
.saved { background:#fff; border:1px solid #e2e2e2; border-radius:8px; padding:12px 14px; margin-bottom:10px; }
.saved-head { display:flex; justify-content:space-between; align-items:center; gap:8px; flex-wrap:wrap; }
.saved-head b { font-size:15px; }
.saved-meta { font-size:12px; color:#888; }
.saved-btns { display:flex; gap:6px; flex-wrap:wrap; }
.saved-btns button { padding:6px 10px; font-size:13px; }
#savedList:empty::after { content:"Пока нет сохранённых веток."; color:#aaa; font-size:13px; }
.counter-top { text-align:center; font-size:13px; color:#666; margin-bottom:10px; }
</style>
</head>
<body>
<h2>Треды Threads</h2>
<div class="status" id="status">Готово к работе</div>
<div class="hint">
1. Вставь весь тред в поле. Между подпостами ставь <code>---</code> (три дефиса на отдельной строке).<br>
2. Нажми «Разбить» — увидишь подпосты со счётчиком символов.<br>
3. <b>Правь текст прямо в окошках подпостов.</b> Перед сохранением жми «Собрать изменения в поле» (или просто «Сохранить» — правки подтянутся автоматически).<br>
4. Впиши название и «Сохранить в ветку». Максимум 10 веток.<br>
<b>Счётчик краснеет, если подпост длиннее 500 символов.</b>
</div>
<textarea id="input" placeholder="Вставь сюда весь тред.
Пример:
Пост первый...
---
Пост второй...
---
Пост третий..."></textarea>
<div class="controls">
<button onclick="split()">Разбить на посты</button>
<button class="collect" onclick="collectToInput(true)">Собрать изменения в поле</button>
<button class="clear" onclick="clearInput()">Очистить поле</button>
</div>
<div id="output"></div>
<div class="controls">
<input type="text" id="branchName" placeholder="Название ветки (напр. История про Диму)">
<button id="saveBtn" onclick="saveBranch()">Сохранить в ветку</button>
</div>
<hr>
<h3>Сохранённые ветки</h3>
<div class="counter-top" id="counterTop"></div>
<div id="savedList"></div>
<script>
const SEPARATOR_RE = /^\s*---\s*$/m;
const LIMIT = 500;
const MAX_BRANCHES = 10;
const STORE_KEY = 'threads_branches';
const DRAFT_KEY = 'threads_draft';
const input = document.getElementById('input');
let editingId = null; // id ветки, которую редактируем (null = новая)
// --- черновик текста ---
const draft = localStorage.getItem(DRAFT_KEY);
if (draft) input.value = draft;
input.addEventListener('input', () => localStorage.setItem(DRAFT_KEY, input.value));
// --- разбивка на подпосты ---
function getParts() {
return input.value
.split(SEPARATOR_RE)
.map(p => p.trim())
.filter(p => p.length > 0);
}
// Есть ли сейчас разбитые подпосты на экране
function hasPostBlocks() {
return document.querySelectorAll('#output .block textarea').length > 0;
}
// Собрать текст из окошек подпостов обратно в большое поле.
// showStatus=true — показать сообщение и перерисовать счётчики.
function collectToInput(showStatus) {
const areas = document.querySelectorAll('#output .block textarea');
if (areas.length === 0) {
if (showStatus) setStatus('Нет подпостов — сначала «Разбить на посты»');
return false;
}
const parts = Array.from(areas)
.map(a => a.value.trim())
.filter(p => p.length > 0);
const joined = parts.join('\n---\n');
input.value = joined;
localStorage.setItem(DRAFT_KEY, joined);
if (showStatus) {
// перерисуем блоки, чтобы номера/счётчики совпали с новым содержимым
split();
setStatus('Изменения из подпостов собраны в поле ✅');
}
return true;
}
function split() {
const parts = getParts();
const out = document.getElementById('output');
out.innerHTML = '';
if (parts.length === 0) { setStatus('Пусто — вставь текст с разделителями ---'); return; }
parts.forEach((text, i) => {
const id = 'post' + i;
const over = text.length > LIMIT;
const block = document.createElement('div');
block.className = 'block';
block.innerHTML = `
<div class="block-head">
<h4>Пост ${i + 1} / ${parts.length}</h4>
<span class="count ${over ? 'over' : ''}" id="c${i}">${text.length} / ${LIMIT}</span>
</div>
<textarea id="${id}">${escapeHtml(text)}</textarea>
<button onclick="copyText(document.getElementById('${id}').value, this)">Копировать</button>
`;
out.appendChild(block);
const ta = block.querySelector('textarea');
ta.addEventListener('input', () => {
const c = document.getElementById('c' + i);
c.textContent = ta.value.length + ' / ' + LIMIT;
c.classList.toggle('over', ta.value.length > LIMIT);
});
});
setStatus('Разбито на ' + parts.length + ' подпостов ✅');
}
function clearInput() {
input.value = '';
localStorage.removeItem(DRAFT_KEY);
document.getElementById('output').innerHTML = '';
document.getElementById('branchName').value = '';
editingId = null;
document.getElementById('saveBtn').textContent = 'Сохранить в ветку';
setStatus('Поле очищено');
}
// --- хранилище веток ---
function loadBranches() {
try { return JSON.parse(localStorage.getItem(STORE_KEY)) || []; }
catch(e) { return []; }
}
function storeBranches(arr) {
localStorage.setItem(STORE_KEY, JSON.stringify(arr));
}
function saveBranch() {
// ГЛАВНОЕ ИСПРАВЛЕНИЕ: если есть разбитые подпосты — сперва собираем
// правки из окошек обратно в большое поле, потом сохраняем.
if (hasPostBlocks()) {
collectToInput(false);
}
const name = document.getElementById('branchName').value.trim();
const parts = getParts();
if (!name) { setStatus('Впиши название ветки'); return; }
if (parts.length === 0) { setStatus('Нечего сохранять — поле пустое'); return; }
let branches = loadBranches();
if (editingId !== null) {
// обновляем существующую
const idx = branches.findIndex(b => b.id === editingId);
if (idx !== -1) {
branches[idx].name = name;
branches[idx].text = input.value;
storeBranches(branches);
setStatus('Ветка обновлена ✅ (правки из подпостов сохранены)');
editingId = null;
document.getElementById('saveBtn').textContent = 'Сохранить в ветку';
renderBranches();
return;
}
}
if (branches.length >= MAX_BRANCHES) {
setStatus('Достигнут лимит 10 веток — удали лишнее');
return;
}
branches.push({ id: Date.now(), name, text: input.value });
storeBranches(branches);
setStatus('Ветка «' + name + '» сохранена ✅');
document.getElementById('branchName').value = '';
renderBranches();
}
function openBranch(id) {
const b = loadBranches().find(x => x.id === id);
if (!b) return;
input.value = b.text;
localStorage.setItem(DRAFT_KEY, b.text);
document.getElementById('branchName').value = b.name;
editingId = id;
document.getElementById('saveBtn').textContent = 'Обновить ветку';
split();
setStatus('Загружена ветка «' + b.name + '» — правь прямо в подпостах и жми «Обновить ветку»');
window.scrollTo({top:0, behavior:'smooth'});
}
function deleteBranch(id) {
if (!confirm('Удалить эту ветку?')) return;
storeBranches(loadBranches().filter(b => b.id !== id));
if (editingId === id) { editingId = null; document.getElementById('saveBtn').textContent = 'Сохранить в ветку'; }
renderBranches();
setStatus('Ветка удалена');
}
function copyAll(id) {
const b = loadBranches().find(x => x.id === id);
if (!b) return;
const parts = b.text.split(SEPARATOR_RE).map(p => p.trim()).filter(Boolean);
const total = parts.length;
const numbered = parts.map((p, i) => `${i+1}/${total}\n${p}`).join('\n\n———\n\n');
copyText(numbered, event.target);
}
function renderBranches() {
const branches = loadBranches();
const list = document.getElementById('savedList');
list.innerHTML = '';
document.getElementById('counterTop').textContent = branches.length + ' / ' + MAX_BRANCHES + ' веток';
document.getElementById('saveBtn').disabled = (branches.length >= MAX_BRANCHES && editingId === null);
branches.forEach(b => {
const count = b.text.split(SEPARATOR_RE).map(p=>p.trim()).filter(Boolean).length;
const div = document.createElement('div');
div.className = 'saved';
div.innerHTML = `
<div class="saved-head">
<div>
<b>${escapeHtml(b.name)}</b>
<div class="saved-meta">${count} подпостов</div>
</div>
<div class="saved-btns">
<button onclick="openBranch(${b.id})">Открыть</button>
<button onclick="copyAll(${b.id})">Копировать всё</button>
<button class="del" onclick="deleteBranch(${b.id})">Удалить</button>
</div>
</div>
`;
list.appendChild(div);
});
}
// --- утилиты ---
function copyText(text, btn) {
navigator.clipboard.writeText(text).then(() => {
const old = btn.textContent;
btn.textContent = '✅';
btn.classList.add('done');
setTimeout(() => { btn.textContent = old; btn.classList.remove('done'); }, 1400);
});
}
function setStatus(t) { document.getElementById('status').textContent = t; }
function escapeHtml(s) {
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
// старт
renderBranches();
if (input.value.trim()) split();
</script>
</body>
</html>