tyosuke20xx.com/nijiura.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>二次裏クローン(ローカルHTML版・強化)</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* ------------------------------
全体レイアウト / ベーススタイル
------------------------------ */
body {
background: #f2f2e9;
color: #333;
font-family: "YuGothic", "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
font-size: 14px;
margin: 0;
padding: 0;
}
header {
background: #d8d8c0;
padding: 10px;
border-bottom: 1px solid #b0b08f;
}
header h1 {
margin: 0;
font-size: 18px;
}
header small {
display: block;
font-size: 11px;
color: #555;
}
.container {
width: 95%;
max-width: 900px;
margin: 10px auto 40px auto;
background: #fff;
border: 1px solid #ccc;
padding: 10px 15px 20px 15px;
box-sizing: border-box;
}
a {
color: #0044cc;
text-decoration: none;
cursor: pointer;
}
a:hover {
text-decoration: underline;
}
.hidden {
display: none;
}
/* ------------------------------
上部ナビ・ステータス
------------------------------ */
.board-nav {
font-size: 12px;
padding: 5px 0 8px 0;
border-bottom: 1px solid #ddd;
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.board-nav .nav-left {
flex: 1 1 auto;
}
.board-nav .nav-right {
flex: 1 1 auto;
text-align: right;
}
.board-nav label {
font-size: 12px;
margin-left: 6px;
}
.board-nav select,
.board-nav input[type="text"] {
font-size: 12px;
padding: 2px 4px;
}
.board-nav input[type="checkbox"] {
vertical-align: middle;
}
.stats {
font-size: 11px;
color: #555;
}
/* ------------------------------
スレッド一覧
------------------------------ */
.thread-list {
margin-bottom: 20px;
}
.thread-item {
border-bottom: 1px dotted #ccc;
padding: 5px 0;
display: flex;
align-items: flex-start;
gap: 4px;
}
.thread-main {
flex: 1 1 auto;
}
.thread-item:last-child {
border-bottom: none;
}
.thread-title {
font-weight: bold;
}
.thread-category {
font-size: 11px;
color: #444;
background: #f0f0d8;
padding: 1px 4px;
border-radius: 3px;
margin-right: 4px;
}
.meta {
font-size: 11px;
color: #666;
}
/* お気に入りスター */
.fav-toggle {
font-size: 16px;
cursor: pointer;
user-select: none;
padding: 0 2px;
}
.fav-true {
color: #d89a00;
}
.fav-false {
color: #ccc;
}
/* ------------------------------
フォーム共通
------------------------------ */
.form-block {
margin: 20px 0;
padding: 10px;
background: #f7f7ee;
border: 1px solid #ddd;
}
.form-block h2 {
margin: 0 0 5px 0;
font-size: 14px;
}
.form-row {
margin-bottom: 5px;
}
label {
font-size: 12px;
display: inline-block;
width: 70px;
}
input[type="text"],
textarea,
select {
width: 90%;
max-width: 500px;
box-sizing: border-box;
font-size: 13px;
}
textarea {
height: 80px;
}
input[type="submit"],
button {
font-size: 12px;
padding: 3px 10px;
margin-right: 4px;
}
.notice {
font-size: 11px;
color: #999;
margin-top: 5px;
}
/* ------------------------------
レス表示
------------------------------ */
.posts {
margin-top: 10px;
}
.post {
border-top: 1px dotted #ccc;
padding: 5px 0;
}
.post:first-child {
border-top: none;
}
.post-header {
font-size: 12px;
margin-bottom: 3px;
}
.post-body {
font-size: 13px;
white-space: pre-wrap;
word-wrap: break-word;
}
.post-id {
font-family: "Consolas", "Menlo", monospace;
font-size: 11px;
color: #666;
}
.post-no-link {
font-weight: bold;
}
/* ------------------------------
スレッドビューのナビ
------------------------------ */
.nav-top {
margin-bottom: 10px;
font-size: 12px;
}
/* ------------------------------
データツール用エリア
------------------------------ */
#backup-text {
width: 100%;
max-width: 100%;
box-sizing: border-box;
font-size: 12px;
}
/* ------------------------------
設定ブロック
------------------------------ */
#settings-block label {
width: auto;
}
#settings-block input[type="checkbox"] {
width: auto;
}
#setting-last-name {
width: 200px;
}
/* ------------------------------
レスポンシブ調整
------------------------------ */
@media (max-width: 600px) {
label {
display: block;
width: auto;
margin-bottom: 2px;
}
input[type="text"],
textarea,
select {
width: 100%;
}
.board-nav {
flex-direction: column;
align-items: flex-start;
}
.board-nav .nav-right {
text-align: left;
}
.thread-item {
flex-direction: row;
}
}
</style>
</head>
<body>
<header>
<h1>二次裏クローン(ローカルHTML版・強化)</h1>
<small>※ブラウザの localStorage に保存 / 本家とは一切関係ありません</small>
</header>
<div class="container" id="top">
<!-- スレ一覧ビュー -->
<div id="view-list">
<div class="board-nav">
<div class="nav-left">
<span class="stats">
スレ数:<span id="stat-threads">0</span> /
総レス数:<span id="stat-posts">0</span>
</span>
<label>
カテゴリ
<select id="category-filter">
<option value="">全カテゴリ</option>
<option value="雑談">雑談</option>
<option value="ゲーム">ゲーム</option>
<option value="アニメ">アニメ</option>
<option value="ニュース">ニュース</option>
<option value="その他">その他</option>
</select>
</label>
<label>
<input type="checkbox" id="fav-only">
お気に入りのみ
</label>
</div>
<div class="nav-right">
<label>
並び替え
<select id="sort-select">
<option value="updated">更新順</option>
<option value="created">作成順</option>
<option value="id">スレ番号順</option>
</select>
</label>
<label>
検索
<input type="text" id="search-input" placeholder="タイトル・本文から検索">
</label>
</div>
</div>
<h2>スレッド一覧</h2>
<div id="thread-list" class="thread-list">
<!-- JSで一覧を描画 -->
</div>
<div class="form-block">
<h2>新規スレッド作成</h2>
<form id="new-thread-form">
<div class="form-row">
<label>名前</label>
<input type="text" name="name" id="new-name" placeholder="名無し">
</div>
<div class="form-row">
<label>カテゴリ</label>
<select name="category" id="new-category">
<option value="雑談">雑談</option>
<option value="ゲーム">ゲーム</option>
<option value="アニメ">アニメ</option>
<option value="ニュース">ニュース</option>
<option value="その他">その他</option>
</select>
</div>
<div class="form-row">
<label>タイトル</label>
<input type="text" name="title" required>
</div>
<div class="form-row">
<label>本文</label>
<textarea name="body" required></textarea>
</div>
<div class="form-row">
<input type="submit" value="スレ立て">
</div>
<div class="notice">
※超シンプルローカル実装。データはこのブラウザ内だけに保存されます。<br>
※別ブラウザ・シークレットモードでは共有されません。
</div>
</form>
</div>
<!-- ローカルデータ管理ツール -->
<div class="form-block" id="data-tools">
<h2>ローカルデータ管理</h2>
<p class="notice">
このPC / ブラウザだけで使う簡易ツールです。<br>
別環境へ移したい場合は JSON をエクスポートして保存してください。
</p>
<div class="form-row">
<button type="button" id="btn-export">エクスポート</button>
<button type="button" id="btn-import">インポート</button>
<button type="button" id="btn-clear">全消去</button>
</div>
<div class="form-row">
<textarea id="backup-text" rows="4" placeholder="エクスポートしたJSONがここに出力されます。ここに貼り付けてインポートもできます。"></textarea>
</div>
</div>
<!-- 簡易設定 -->
<div class="form-block" id="settings-block">
<h2>ミニ設定</h2>
<div class="form-row">
<label>
<input type="checkbox" id="opt-autoname">
名前を記憶して自動で入れる
</label>
</div>
<div class="form-row">
<label>前回の名前</label>
<input type="text" id="setting-last-name" placeholder="名無し">
</div>
<div class="form-row">
<button type="button" id="btn-save-settings">設定を保存</button>
</div>
<div class="notice">
※名前を記憶しておくと、新規スレ・レス投稿時の名前欄に自動で反映されます。<br>
※これらも localStorage に保存されます。
</div>
</div>
</div>
<!-- 個別スレッドビュー -->
<div id="view-thread" class="hidden">
<div class="nav-top">
<a id="back-to-list"><< スレ一覧に戻る</a> |
<a href="#bottom">▼ 一番下へ</a> |
<a href="#top">▲ ページ先頭へ</a>
</div>
<h2 id="thread-title"></h2>
<div class="meta" id="thread-meta"></div>
<div id="posts" class="posts">
<!-- JSでレスを描画 -->
</div>
<div class="form-block">
<h2>レスを書く</h2>
<form id="reply-form">
<div class="form-row">
<label>名前</label>
<input type="text" name="name" id="reply-name" placeholder="名無し">
</div>
<div class="form-row">
<label>本文</label>
<textarea name="body" id="reply-body" required></textarea>
</div>
<div class="form-row">
<input type="submit" value="レス投稿">
</div>
<div class="notice">
※レス番号をクリックすると本文に「>>番号」が入ります。<br>
※画像アップロード機能はこのHTML版には入れていません。<br>
※本格運用したい場合はPHPやDB版で実装してください。
</div>
</form>
</div>
</div>
</div>
<div id="bottom"></div>
<script>
(function() {
"use strict";
const STORAGE_KEY = "nijiura_clone_threads_v1";
const SETTINGS_KEY = "nijiura_clone_settings_v1";
// --------------------
// データ構造
// --------------------
// threads = [
// {
// id: number,
// title: string,
// category: string,
// favorite: boolean,
// createdAt: string,
// updatedAt: string,
// posts: [
// { id, name, body, createdAt, uid }
// ]
// }, ...
// ]
//
// settings = {
// autoName: boolean,
// lastName: string
// }
let threads = loadThreads();
let settings = loadSettings();
let currentThreadId = null;
let currentSort = "updated"; // updated / created / id
const viewList = document.getElementById("view-list");
const viewThread = document.getElementById("view-thread");
const threadListEl = document.getElementById("thread-list");
const threadTitleEl = document.getElementById("thread-title");
const threadMetaEl = document.getElementById("thread-meta");
const postsEl = document.getElementById("posts");
const newThreadForm = document.getElementById("new-thread-form");
const replyForm = document.getElementById("reply-form");
const backToListBtn = document.getElementById("back-to-list");
const searchInput = document.getElementById("search-input");
const sortSelect = document.getElementById("sort-select");
const categoryFilter = document.getElementById("category-filter");
const favOnlyCheckbox = document.getElementById("fav-only");
const statThreadsEl = document.getElementById("stat-threads");
const statPostsEl = document.getElementById("stat-posts");
const btnExport = document.getElementById("btn-export");
const btnImport = document.getElementById("btn-import");
const btnClear = document.getElementById("btn-clear");
const backupText = document.getElementById("backup-text");
const optAutoname = document.getElementById("opt-autoname");
const settingLastName = document.getElementById("setting-last-name");
const btnSaveSettings = document.getElementById("btn-save-settings");
const newNameInput = document.getElementById("new-name");
const newCategorySelect = document.getElementById("new-category");
const replyNameInput = document.getElementById("reply-name");
const replyBodyTextarea = document.getElementById("reply-body");
// 既存データにカテゴリ・お気に入り・UIDがなければ補完
migrateThreads();
// 初期ソート&描画
sortThreadsByUpdated();
applySettingsToUI();
renderThreadList();
showListView();
// --------------------
// イベント: スレ立て
// --------------------
newThreadForm.addEventListener("submit", function(e) {
e.preventDefault();
const formData = new FormData(newThreadForm);
let name = (formData.get("name") || "").toString().trim();
const category = (formData.get("category") || "雑談").toString();
const title = (formData.get("title") || "").toString().trim();
const body = (formData.get("body") || "").toString().trim();
if (!name) {
name = "名無し";
}
if (!title || !body) {
alert("タイトルと本文は必須です。");
return;
}
const now = getNowStr();
const newId = getNewThreadId();
const newThread = {
id: newId,
title: title,
category: category || "雑談",
favorite: false,
createdAt: now,
updatedAt: now,
posts: [
{
id: 1,
name: name,
body: body,
createdAt: now,
uid: generateUid(newId, 1, name, body, now)
}
]
};
threads.push(newThread);
sortThreadsByUpdated();
saveThreads(threads);
// 名前を設定に反映
updateLastNameSetting(name);
newThreadForm.reset();
currentSort = "updated";
sortSelect.value = "updated";
openThread(newId);
});
// --------------------
// イベント: レス投稿
// --------------------
replyForm.addEventListener("submit", function(e) {
e.preventDefault();
if (currentThreadId === null) return;
const formData = new FormData(replyForm);
let name = (formData.get("name") || "").toString().trim();
const body = (formData.get("body") || "").toString().trim();
if (!name) {
name = "名無し";
}
if (!body) {
alert("本文は必須です。");
return;
}
const thread = threads.find(t => t.id === currentThreadId);
if (!thread) {
alert("スレッドが見つかりません。");
return;
}
const now = getNowStr();
const newPostId = getNewPostId(thread);
thread.posts.push({
id: newPostId,
name: name,
body: body,
createdAt: now,
uid: generateUid(thread.id, newPostId, name, body, now)
});
thread.updatedAt = now;
sortThreadsByUpdated();
saveThreads(threads);
// 名前を設定に反映
updateLastNameSetting(name);
replyForm.reset();
renderThread(thread);
renderThreadList(); // 更新日時が変わるので一覧も更新
});
// --------------------
// イベント: 一覧に戻る
// --------------------
backToListBtn.addEventListener("click", function() {
currentThreadId = null;
showListView();
});
// --------------------
// イベント: 並び替え&検索&フィルタ
// --------------------
sortSelect.addEventListener("change", function() {
currentSort = this.value;
renderThreadList();
});
searchInput.addEventListener("input", function() {
renderThreadList();
});
categoryFilter.addEventListener("change", function() {
renderThreadList();
});
favOnlyCheckbox.addEventListener("change", function() {
renderThreadList();
});
// --------------------
// イベント: データツール
// --------------------
btnExport.addEventListener("click", function() {
try {
const json = JSON.stringify(threads, null, 2);
backupText.value = json;
alert("現在のデータをJSONとして出力しました。必要ならコピーして保存してください。");
} catch (e) {
console.warn("export failed:", e);
alert("エクスポートに失敗しました。");
}
});
btnImport.addEventListener("click", function() {
const text = backupText.value.trim();
if (!text) {
alert("インポートするJSONが入力されていません。");
return;
}
if (!confirm("テキストエリアのJSONで現在のデータを上書きします。よろしいですか?")) {
return;
}
try {
const data = JSON.parse(text);
if (!Array.isArray(data)) {
alert("JSONの形式が不正です。(配列ではありません)");
return;
}
threads = data;
migrateThreads(); // 新フィールドを補完
sortThreadsByUpdated();
saveThreads(threads);
currentThreadId = null;
showListView();
alert("インポートに成功しました。");
} catch (e) {
console.warn("import failed:", e);
alert("JSONの解析に失敗しました。形式が正しいか確認してください。");
}
});
btnClear.addEventListener("click", function() {
if (!confirm("本当に全データを削除しますか?(取り消しできません)")) {
return;
}
try {
localStorage.removeItem(STORAGE_KEY);
} catch (e) {
console.warn("clear failed:", e);
}
threads = [];
currentThreadId = null;
renderThreadList();
showListView();
alert("全データを削除しました。");
});
// --------------------
// イベント: 設定
// --------------------
btnSaveSettings.addEventListener("click", function() {
const autoName = !!optAutoname.checked;
const lastName = (settingLastName.value || "").toString().trim() || "名無し";
settings.autoName = autoName;
settings.lastName = lastName;
saveSettings(settings);
applySettingsToUI();
alert("設定を保存しました。");
});
// --------------------
// イベント: レス番クリック(>>アンカー挿入)
// --------------------
postsEl.addEventListener("click", function(e) {
const target = e.target;
if (target && target.classList.contains("post-no-link")) {
e.preventDefault();
const no = target.getAttribute("data-no");
if (!no) return;
insertAnchorToReply(">>" + no + "\n");
}
});
// --------------------
// ビュー切替
// --------------------
function showListView() {
viewList.classList.remove("hidden");
viewThread.classList.add("hidden");
renderThreadList();
}
function showThreadView() {
viewList.classList.add("hidden");
viewThread.classList.remove("hidden");
}
// --------------------
// レンダリング: 一覧
// --------------------
function renderThreadList() {
updateStats();
if (!threads.length) {
threadListEl.innerHTML = "<p>まだスレッドはありません。</p>";
return;
}
const query = (searchInput.value || "").toString().trim();
const filterCategory = (categoryFilter.value || "").toString();
const favOnly = !!favOnlyCheckbox.checked;
let list = threads.slice();
// 検索
if (query) {
list = list.filter(function(t) {
const q = query;
if (t.title && t.title.indexOf(q) !== -1) return true;
if (Array.isArray(t.posts)) {
return t.posts.some(function(p) {
return p.body && p.body.indexOf(q) !== -1;
});
}
return false;
});
}
// カテゴリフィルタ
if (filterCategory) {
list = list.filter(function(t) {
return (t.category || "雑談") === filterCategory;
});
}
// お気に入りのみ
if (favOnly) {
list = list.filter(function(t) {
return !!t.favorite;
});
}
// 並び替え
if (currentSort === "created") {
list.sort(function(a, b) {
if (a.createdAt < b.createdAt) return 1;
if (a.createdAt > b.createdAt) return -1;
return 0;
});
} else if (currentSort === "id") {
list.sort(function(a, b) {
return b.id - a.id; // 新しい番号が上
});
} else {
// 更新順は threads 自体を sortThreadsByUpdated で管理しているのでそのまま
// ただしフィルタ・検索後も順序を維持するだけ
list.sort(function(a, b) {
if (a.updatedAt < b.updatedAt) return 1;
if (a.updatedAt > b.updatedAt) return -1;
return 0;
});
}
if (!list.length) {
threadListEl.innerHTML = "<p>条件に一致するスレッドはありません。</p>";
return;
}
let html = "";
list.forEach(function(t) {
const cat = t.category || "雑談";
const fav = !!t.favorite;
const favClass = fav ? "fav-true" : "fav-false";
const favSymbol = fav ? "★" : "☆";
html += `
<div class="thread-item">
<span class="fav-toggle ${favClass}" data-thread-id="${t.id}" title="お気に入り切り替え">${favSymbol}</span>
<div class="thread-main">
<span class="thread-title">
<span class="thread-category">[${escapeHtml(cat)}]</span>
<a data-thread-id="${t.id}" class="thread-link">
${escapeHtml(t.title)}
</a>
</span><br>
<span class="meta">
No.${t.id} / 作成:${escapeHtml(t.createdAt)} /
最終更新:${escapeHtml(t.updatedAt)} /
レス:${t.posts ? t.posts.length : 0}
</span>
</div>
</div>
`;
});
threadListEl.innerHTML = html;
// スレッドリンクイベント
const links = threadListEl.querySelectorAll(".thread-link");
links.forEach(function(link) {
link.addEventListener("click", function() {
const id = parseInt(this.getAttribute("data-thread-id"), 10);
openThread(id);
});
});
// お気に入り切り替えイベント
const favToggles = threadListEl.querySelectorAll(".fav-toggle");
favToggles.forEach(function(btn) {
btn.addEventListener("click", function() {
const id = parseInt(this.getAttribute("data-thread-id"), 10);
toggleFavorite(id, this);
});
});
}
// --------------------
// レンダリング: 個別スレ
// --------------------
function renderThread(thread) {
currentThreadId = thread.id;
const cat = thread.category || "雑談";
threadTitleEl.textContent = "[" + cat + "] " + thread.title;
threadMetaEl.textContent =
"スレ番号:" + thread.id +
" 作成:" + thread.createdAt +
" 最終更新:" + thread.updatedAt +
" レス数:" + (thread.posts ? thread.posts.length : 0);
if (!thread.posts || !thread.posts.length) {
postsEl.innerHTML = "<p>まだレスはありません。</p>";
return;
}
let html = "";
thread.posts.forEach(function(p) {
const uid = p.uid || generateUid(thread.id, p.id, p.name, p.body, p.createdAt);
html += `
<div class="post">
<div class="post-header">
<a href="#" class="post-no-link" data-no="${p.id}">No.${p.id}</a>
名前:${escapeHtml(p.name)}
投稿日:${escapeHtml(p.createdAt)}
ID:<span class="post-id">${escapeHtml(uid)}</span>
</div>
<div class="post-body">
${escapeHtml(p.body).replace(/\n/g, "<br>")}
</div>
</div>
`;
});
postsEl.innerHTML = html;
// 自動名前反映
applyAutoNameToReply();
}
function openThread(id) {
const thread = threads.find(t => t.id === id);
if (!thread) {
alert("スレッドが見つかりません。");
return;
}
renderThread(thread);
showThreadView();
}
// --------------------
// お気に入り切り替え
// --------------------
function toggleFavorite(threadId, element) {
const thread = threads.find(t => t.id === threadId);
if (!thread) return;
thread.favorite = !thread.favorite;
saveThreads(threads);
const fav = !!thread.favorite;
element.textContent = fav ? "★" : "☆";
element.classList.toggle("fav-true", fav);
element.classList.toggle("fav-false", !fav);
}
// --------------------
// ストレージ操作
// --------------------
function loadThreads() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const data = JSON.parse(raw);
if (!Array.isArray(data)) return [];
return data;
} catch (e) {
console.warn("failed to load threads:", e);
return [];
}
}
function saveThreads(data) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch (e) {
console.warn("failed to save threads:", e);
alert("保存に失敗しました。localStorage容量オーバーの可能性があります。");
}
}
function loadSettings() {
try {
const raw = localStorage.getItem(SETTINGS_KEY);
if (!raw) {
return {
autoName: false,
lastName: "名無し"
};
}
const data = JSON.parse(raw);
if (!data || typeof data !== "object") {
return {
autoName: false,
lastName: "名無し"
};
}
if (typeof data.autoName !== "boolean") {
data.autoName = false;
}
if (typeof data.lastName !== "string") {
data.lastName = "名無し";
}
return data;
} catch (e) {
console.warn("failed to load settings:", e);
return {
autoName: false,
lastName: "名無し"
};
}
}
function saveSettings(data) {
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(data));
} catch (e) {
console.warn("failed to save settings:", e);
}
}
// --------------------
// 統計更新
// --------------------
function updateStats() {
statThreadsEl.textContent = threads.length;
let totalPosts = 0;
threads.forEach(function(t) {
if (Array.isArray(t.posts)) {
totalPosts += t.posts.length;
}
});
statPostsEl.textContent = totalPosts;
}
// --------------------
// ID・日付・エスケープ
// --------------------
function getNewThreadId() {
if (!threads.length) return 1;
const ids = threads.map(function(t) { return t.id; });
return Math.max.apply(null, ids) + 1;
}
function getNewPostId(thread) {
if (!thread.posts || !thread.posts.length) return 1;
const ids = thread.posts.map(function(p) { return p.id; });
return Math.max.apply(null, ids) + 1;
}
function getNowStr() {
const d = new Date();
const y = d.getFullYear();
const m = ("0" + (d.getMonth() + 1)).slice(-2);
const day = ("0" + d.getDate()).slice(-2);
const h = ("0" + d.getHours()).slice(-2);
const min = ("0" + d.getMinutes()).slice(-2);
const s = ("0" + d.getSeconds()).slice(-2);
return y + "/" + m + "/" + day + " " + h + ":" + min + ":" + s;
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// 簡易ハッシュでUIDを生成(8桁16進)
function generateUid(threadId, postId, name, body, createdAt) {
const src = String(threadId) + ":" + String(postId) + ":" +
String(name) + ":" + String(body) + ":" + String(createdAt);
let hash = 0;
for (let i = 0; i < src.length; i++) {
hash = ((hash << 5) - hash) + src.charCodeAt(i);
hash |= 0; // 32bit
}
// 符号を外す
if (hash < 0) {
hash = ~hash + 1;
}
let hex = hash.toString(16);
if (hex.length < 8) {
hex = ("00000000" + hex).slice(-8);
} else if (hex.length > 8) {
hex = hex.slice(-8);
}
return hex;
}
// --------------------
// ソート
// --------------------
function sortThreadsByUpdated() {
threads.sort(function(a, b) {
if (a.updatedAt < b.updatedAt) return 1;
if (a.updatedAt > b.updatedAt) return -1;
return 0;
});
}
// --------------------
// 設定適用
// --------------------
function applySettingsToUI() {
optAutoname.checked = !!settings.autoName;
settingLastName.value = settings.lastName || "名無し";
if (settings.autoName) {
if (newNameInput) newNameInput.value = settings.lastName || "名無し";
if (replyNameInput) replyNameInput.value = settings.lastName || "名無し";
}
}
function applyAutoNameToReply() {
if (settings.autoName) {
replyNameInput.value = settings.lastName || "名無し";
}
}
function updateLastNameSetting(name) {
settings.lastName = name || "名無し";
if (settings.autoName) {
// UIへ即時反映
settingLastName.value = settings.lastName;
if (newNameInput) newNameInput.value = settings.lastName;
if (replyNameInput) replyNameInput.value = settings.lastName;
}
saveSettings(settings);
}
// --------------------
// レス番アンカー挿入
// --------------------
function insertAnchorToReply(text) {
replyBodyTextarea.focus();
const start = replyBodyTextarea.selectionStart;
const end = replyBodyTextarea.selectionEnd;
const value = replyBodyTextarea.value;
replyBodyTextarea.value = value.slice(0, start) + text + value.slice(end);
// キャレット位置を挿入したテキストの後ろに
const pos = start + text.length;
replyBodyTextarea.selectionStart = replyBodyTextarea.selectionEnd = pos;
}
// --------------------
// 既存データのマイグレーション
// --------------------
function migrateThreads() {
threads.forEach(function(t) {
if (!t.category) {
t.category = "雑談";
}
if (typeof t.favorite !== "boolean") {
t.favorite = false;
}
if (!Array.isArray(t.posts)) {
t.posts = [];
}
t.posts.forEach(function(p) {
if (!p.uid) {
p.uid = generateUid(t.id, p.id, p.name || "名無し", p.body || "", p.createdAt || "");
}
});
});
saveThreads(threads);
}
})();
</script>
</body>
</html>
