<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Tsumugi</title>
<!-- Favicon -->
<link rel="shortcut icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='%23667eea' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/></svg>"/>
<!-- Tailwind CSS v2 -->
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"/>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css"/>
<style>
:root {
--grad-a: #667eea;
--grad-b: #764ba2;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: radial-gradient(circle at top left, #1f2937 0%, #111827 40%, #020617 100%);
min-height: 100vh;
color: #111827;
}
.glass-effect {
background: radial-gradient(circle at top left, rgba(255,255,255,0.15), rgba(255,255,255,0.03));
backdrop-filter: blur(18px);
border-radius: 18px;
border: 1px solid rgba(255,255,255,0.16);
}
.card-hover { transition: all 0.25s ease; }
.card-hover:hover { transform: translateY(-3px); box-shadow: 0 20px 30px -12px rgba(0,0,0,0.45); }
.gradient-text {
background: linear-gradient(90deg, var(--grad-a), var(--grad-b));
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.timeline-post {
background: rgba(15,23,42,0.96);
border-radius: 18px;
box-shadow: 0 14px 30px -16px rgba(0,0,0,0.7);
transition: all 0.25s ease;
border-left: 4px solid var(--grad-a);
}
.timeline-post:hover { transform: translateX(4px); }
.profile-avatar {
width: 100px; height: 100px; border-radius: 50%;
object-fit: cover; border: 3px solid rgba(255,255,255,0.9);
box-shadow: 0 10px 24px rgba(0,0,0,0.35);
}
.mini-avatar {
width: 46px; height: 46px; border-radius: 50%; object-fit: cover;
border: 2px solid rgba(255,255,255,0.9);
}
.btn-primary {
background: linear-gradient(135deg, var(--grad-a), var(--grad-b));
border: none; transition: all 0.2s ease;
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 10px 18px rgba(0,0,0,0.4); }
.section-divider {
height: 3px; background: linear-gradient(90deg, var(--grad-a), var(--grad-b));
border-radius: 999px; margin: 2rem 0;
}
.username-badge {
background: radial-gradient(circle at top left, var(--grad-a), var(--grad-b));
color: white; padding: 0.2rem 0.6rem; border-radius: 999px;
font-size: 0.75rem; font-weight: 600; display: inline-flex;
align-items: center; margin-left: 0.5rem;
}
.username-badge i { margin-right: 4px; }
.share-menu { position: absolute; z-index: 50; min-width: 180px; right: 0; top: 110%; background: #020617; border-radius: 12px; box-shadow: 0 18px 45px rgba(0,0,0,0.75); border: 1px solid rgba(148,163,184,0.5); }
.share-menu button { width: 100%; text-align: left; padding: 10px 20px; border: none; background: none; cursor: pointer; font-size: 0.95rem; color: #e5e7eb; }
.share-menu button:hover { background: rgba(51,65,85,0.9); }
.status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
.status-active { background-color: #10b981; animation: pulse 1.5s infinite; }
.status-inactive { background-color: #6b7280; }
.log-container {
max-height: 180px; overflow-y: auto; background: rgba(15,23,42,0.85);
border-radius: 10px; padding: 10px; margin-top: 10px; font-size: 0.8rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
border: 1px solid rgba(148,163,184,0.5); color: #e5e7eb;
}
.error-message { color: #fecaca; background: rgba(127,29,29,0.6); padding: 8px; border-radius: 8px; margin: 5px 0; }
.success-message { color: #bbf7d0; background: rgba(6,95,70,0.6); padding: 8px; border-radius: 8px; margin: 5px 0; }
@keyframes pulse { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(1.1); } }
.dark .glass-effect { background: rgba(15,23,42,0.92); border: 1px solid rgba(148,163,184,0.4); }
.dark .timeline-post { background: #020617; color: #f9fafb; border-left-color: #4f46e5; }
.dark .share-menu { background: #020617; color: #e5e7eb; }
.dark .success-message { background: rgba(6,95,70,0.7); }
.dark .error-message { background: rgba(127,29,29,0.7); }
.icon-label {
font-size: 0.8rem; color: #e5e7eb; text-transform: uppercase; letter-spacing: 0.05em;
}
@media print {
body { background: white !important; -webkit-print-color-adjust: exact; }
.glass-effect { background: white !important; backdrop-filter: none !important; border: 1px solid #e5e7eb !important; }
.timeline-post { background: white !important; color: #111827 !important; }
}
</style>
</head>
<body class="dark text-gray-100">
<!-- ログイン/登録モーダル -->
<div id="auth-modal" class="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50" style="display:none">
<div class="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-sm p-6 border border-gray-700">
<h2 class="text-2xl font-bold mb-4 text-center gradient-text" id="auth-title">ログイン</h2>
<div id="auth-error" class="error-message mb-2" style="display:none"></div>
<form id="auth-form" autocomplete="off">
<div class="mb-3">
<label class="block mb-1 text-xs font-semibold text-gray-300">メールアドレス</label>
<input type="email" id="auth-email" class="w-full border border-gray-700 bg-gray-800 rounded px-3 py-2 text-sm text-gray-100" required>
</div>
<div class="mb-3">
<label class="block mb-1 text-xs font-semibold text-gray-300">パスワード</label>
<input type="password" id="auth-password" class="w-full border border-gray-700 bg-gray-800 rounded px-3 py-2 text-sm text-gray-100" required>
</div>
<button type="submit" class="btn-primary w-full py-2 rounded-lg text-white font-semibold text-sm mt-2">
<i class="fas fa-sign-in-alt mr-2"></i>ログイン
</button>
</form>
<div class="mt-4 text-center">
<button id="toggle-auth-mode" class="text-indigo-400 underline text-xs">新規登録はこちら</button>
</div>
</div>
</div>
<!-- ヘッダー -->
<header class="glass-effect mx-4 mt-4 p-6 border border-indigo-500/40">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
<div class="text-left">
<h1 class="text-3xl md:text-4xl font-extrabold text-white mb-1 tracking-tight">
<i class="fas fa-comments mr-3 text-indigo-300"></i>
<span class="gradient-text">Tsumugi</span>
<span class="ml-2 text-xs px-2 py-1 rounded-full bg-indigo-500/20 border border-indigo-400/60 align-middle">Verse Core v3.0</span>
</h1>
<p class="text-indigo-100 text-sm md:text-base opacity-90">
次世代ソーシャルネットワーク • RSS / BOT 専用エディション(AI機能なし)
</p>
</div>
<div class="flex flex-wrap items-center justify-end gap-3">
<div class="flex items-center bg-slate-900/70 rounded-2xl px-3 py-2 shadow-inner border border-slate-700">
<img id="header-profile-icon" class="mini-avatar" src="https://via.placeholder.com/80" alt="プロフィール">
<div class="ml-3 text-left">
<div class="font-semibold text-sm" id="header-username">未設定</div>
<div class="text-xs text-slate-300 opacity-75" id="header-user-email"></div>
</div>
</div>
<button onclick="toggleDarkMode()" class="btn-primary px-4 py-2 rounded-full text-white text-xs flex items-center">
<i class="fas fa-moon mr-2"></i><span>テーマ切替</span>
</button>
<button onclick="showSystemStatus()" class="bg-slate-900 hover:bg-slate-800 px-4 py-2 rounded-full text-white text-xs border border-slate-600 flex items-center">
<i class="fas fa-info-circle mr-2"></i>ステータス
</button>
<button onclick="clearVerseCache()" class="bg-yellow-400 hover:bg-yellow-500 px-4 py-2 rounded-full text-black text-xs flex items-center">
<i class="fas fa-broom mr-2"></i>キャッシュクリア
</button>
<button id="logout-btn" onclick="logout()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded-full text-white text-xs flex items-center hidden">
<i class="fas fa-sign-out-alt mr-2"></i>ログアウト
</button>
</div>
</div>
</header>
<!-- メイン -->
<div class="max-w-6xl mx-auto px-4 py-6" id="main-content" style="display:none">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 左カラム:プロフィール/BOT/RSS -->
<div class="lg:col-span-1 space-y-6">
<!-- プロフィール -->
<div class="glass-effect p-6 card-hover border border-slate-600">
<h3 class="text-2xl font-bold gradient-text mb-4 flex items-center">
<i class="fas fa-user-circle mr-2 text-indigo-300"></i>プロフィール
</h3>
<div class="text-center mb-6">
<img id="profile-icon" class="profile-avatar mx-auto mb-4" src="https://via.placeholder.com/100" alt="プロフィール">
<input type="file" id="profile-upload" accept="image/*" onchange="uploadProfileIcon(event)" class="hidden">
<button onclick="document.getElementById('profile-upload').click()" class="btn-primary px-4 py-2 rounded-full text-white text-sm">
<i class="fas fa-camera mr-2"></i>プロフィール画像
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-gray-200 font-semibold mb-1 text-xs">ユーザー名</label>
<input type="text" id="username" class="w-full p-3 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" placeholder="ユーザー名を入力" maxlength="20">
</div>
<div>
<label class="block text-gray-200 font-semibold mb-1 text-xs">自己紹介</label>
<textarea id="self-intro" class="w-full p-3 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" rows="4" placeholder="自己紹介を入力"></textarea>
</div>
<button onclick="saveProfile()" class="btn-primary w-full py-2 rounded-lg text-white text-sm font-semibold">
<i class="fas fa-save mr-2"></i>プロフィール保存
</button>
<div class="p-3 bg-slate-900/80 rounded-lg border border-slate-700">
<h5 class="font-semibold text-gray-200 mb-2 text-xs">プレビュー</h5>
<div class="text-gray-300 text-sm">
<div class="font-semibold mb-1" id="username-preview">未設定</div>
<div id="self-intro-preview" class="text-xs whitespace-pre-line min-h-8">まだ自己紹介がありません</div>
</div>
</div>
</div>
</div>
<!-- BOT/Feed アイコン設定 -->
<div class="glass-effect p-6 card-hover border border-indigo-500/40">
<h3 class="text-xl font-bold text-indigo-100 mb-4 flex items-center">
<i class="fas fa-icons mr-2 text-indigo-300"></i>BOT / Feed アイコン設定
</h3>
<div class="space-y-4 text-xs">
<div class="flex items-center space-x-3">
<img id="bot-icon-preview" class="mini-avatar" src="https://cdn-icons-png.flaticon.com/512/4712/4712109.png" alt="BOT">
<div class="flex-1">
<div class="icon-label">BOT / Markov BOT</div>
<input type="file" id="bot-icon-upload" accept="image/*" class="hidden" onchange="uploadIcon('bot', event)">
<button onclick="document.getElementById('bot-icon-upload').click()" class="bg-slate-900 hover:bg-slate-800 px-3 py-1 rounded-full text-gray-100 text-[11px] border border-slate-600 mt-1">
<i class="fas fa-robot mr-1"></i>BOTアイコン変更
</button>
</div>
</div>
<div class="flex items-center space-x-3">
<img id="feed-icon-preview" class="mini-avatar" src="https://cdn-icons-png.flaticon.com/512/3416/3416046.png" alt="Feed">
<div class="flex-1">
<div class="icon-label">RSS FEED BOT</div>
<input type="file" id="feed-icon-upload" accept="image/*" class="hidden" onchange="uploadIcon('feed', event)">
<button onclick="document.getElementById('feed-icon-upload').click()" class="bg-slate-900 hover:bg-slate-800 px-3 py-1 rounded-full text-gray-100 text-[11px] border border-slate-600 mt-1">
<i class="fas fa-rss mr-1"></i>Feedアイコン変更
</button>
</div>
</div>
</div>
</div>
<!-- RSS自動投稿機能 -->
<div class="glass-effect p-6 card-hover border border-amber-500/40">
<h3 class="text-xl font-bold text-amber-100 mb-4">
<i class="fas fa-rss mr-2 text-amber-300"></i>RSS自動投稿(全体共有)
<span class="status-indicator" id="rss-status"></span>
<span id="rss-status-text" class="text-xs opacity-75">停止中</span>
</h3>
<div>
<input id="rss-url" type="text" class="w-full p-2 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-xs mb-2" placeholder="RSSフィードURLを入力">
<button onclick="addRssFeed()" class="btn-primary w-full py-2 rounded-lg text-white mb-2 text-xs">
<i class="fas fa-plus mr-2"></i>追加
</button>
<div id="rss-list" class="mb-3 text-xs"></div>
<div class="flex items-center space-x-2 mb-2">
<input type="number" id="rss-interval" class="w-1/2 p-2 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-xs" min="10" max="3600" value="300" placeholder="間隔(秒)">
<button onclick="setRssInterval()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-clock mr-2"></i>間隔設定
</button>
</div>
<div class="flex items-center space-x-2 mb-2">
<button onclick="fetchRssNow()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-sync mr-2"></i>今すぐ取得
</button>
<button onclick="stopRssAuto()" class="bg-red-600 hover:bg-red-700 flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-stop mr-2"></i>自動停止
</button>
</div>
<div class="flex items-center space-x-2 text-xs">
<button onclick="setAllRssEnabled(true)" class="btn-primary flex-1 py-2 rounded-lg text-white text-[11px]">
<i class="fas fa-toggle-on mr-1"></i>すべてON
</button>
<button onclick="setAllRssEnabled(false)" class="bg-gray-600 hover:bg-gray-700 flex-1 py-2 rounded-lg text-white text-[11px]">
<i class="fas fa-toggle-off mr-1"></i>すべてOFF
</button>
</div>
<div id="rss-log" class="log-container text-xs mt-3"></div>
</div>
</div>
<!-- BOT機能 -->
<div class="glass-effect p-6 card-hover border border-emerald-500/40">
<h3 class="text-xl font-bold text-emerald-100 mb-4">
<i class="fas fa-robot mr-2 text-emerald-300"></i>BOT機能
<span class="status-indicator" id="bot-status"></span>
<span id="bot-status-text" class="text-xs opacity-75">停止中</span>
</h3>
<div class="space-y-4">
<div>
<textarea id="botContent" class="w-full p-3 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" rows="3" placeholder="BOT投稿内容"></textarea>
<button onclick="postBotMessage()" class="btn-primary w-full mt-2 py-2 rounded-lg text-white text-xs">
<i class="fas fa-robot mr-2"></i>BOT投稿
</button>
</div>
<div>
<input type="number" id="botIntervalSec" class="w-full p-2 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-xs" placeholder="マルコフ自動投稿間隔(秒)" min="10" max="3600" value="60">
<div class="flex space-x-2 mt-2">
<button onclick="postMarkovBot()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-dice mr-2"></i>マルコフ生成
</button>
<button onclick="startBotAutoPost()" class="btn-primary flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-play mr-2"></i>自動開始
</button>
<button onclick="stopBotAutoPost()" class="bg-red-600 hover:bg-red-700 flex-1 py-2 rounded-lg text-white text-xs">
<i class="fas fa-stop mr-2"></i>停止
</button>
</div>
</div>
<div class="text-emerald-100 text-xs opacity-80">
<i class="fas fa-info-circle mr-1"></i>
マルコフ連鎖ではユーザー/BOT投稿のみを学習し、RSS記事本文は学習対象から除外します。
</div>
<div id="bot-log" class="log-container text-xs"></div>
</div>
</div>
</div>
<!-- 右カラム:投稿&タイムライン -->
<div class="lg:col-span-2 space-y-6">
<!-- 新規投稿 -->
<div class="glass-effect p-6 card-hover border border-slate-600">
<h3 class="text-2xl font-bold gradient-text mb-4 flex items-center">
<i class="fas fa-edit mr-2 text-indigo-300"></i>新規投稿
</h3>
<div>
<textarea id="postContent" class="w-full p-4 border border-slate-600 rounded-lg bg-slate-900 text-gray-100 text-sm" rows="4" placeholder="今何を考えていますか?(Ctrl+Enter で投稿)" maxlength="500"></textarea>
<div class="mt-4 flex flex-col md:flex-row md:items-center md:justify-between space-y-3 md:space-y-0">
<div class="text-indigo-100 text-xs opacity-90 flex items-center space-x-2">
<i class="fas fa-info-circle"></i>
<span>あなたの思いを共有しましょう</span>
<span id="char-count" class="ml-2 px-2 py-1 rounded-full bg-slate-900 border border-slate-600">(0/500)</span>
</div>
<div class="flex space-x-2">
<button onclick="createUserPost()" class="btn-primary px-5 py-2 rounded-lg text-white text-xs font-semibold">
<i class="fas fa-paper-plane mr-2"></i>投稿する
</button>
</div>
</div>
</div>
</div>
<div class="section-divider"></div>
<!-- タイムライン -->
<div class="glass-effect p-6 border border-slate-600">
<div class="flex flex-col md:flex-row md:justify-between md:items-center mb-4 space-y-3 md:space-y-0">
<h3 class="text-2xl font-bold text-indigo-100 flex items-center">
<i class="fas fa-stream mr-2 text-indigo-300"></i>タイムライン
<span id="post-count" class="text-sm font-normal opacity-75 ml-2">(0件の投稿)</span>
</h3>
<div class="flex space-x-2">
<button onclick="clearAllPosts()" class="bg-red-600 hover:bg-red-700 px-3 py-1 rounded text-white text-xs flex items-center">
<i class="fas fa-trash mr-1"></i>全削除
</button>
<button onclick="exportData()" class="bg-emerald-600 hover:bg-emerald-700 px-3 py-1 rounded text-white text-xs flex items-center">
<i class="fas fa-download mr-1"></i>エクスポート
</button>
</div>
</div>
<!-- フィルター&検索 -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-4 space-y-3 md:space-y-0">
<div class="flex flex-wrap gap-2 text-xs">
<button id="filter-all" onclick="setFilter('all')" class="px-3 py-1 rounded-full border border-slate-600 bg-indigo-600 text-white flex items-center">
<i class="fas fa-globe mr-1"></i>すべて
</button>
<button id="filter-user" onclick="setFilter('user')" class="px-3 py-1 rounded-full border border-slate-600 text-slate-200 flex items-center">
<i class="fas fa-user mr-1"></i>ユーザー
</button>
<button id="filter-bot" onclick="setFilter('bot')" class="px-3 py-1 rounded-full border border-slate-600 text-slate-200 flex items-center">
<i class="fas fa-robot mr-1"></i>BOT
</button>
<button id="filter-feed" onclick="setFilter('feed')" class="px-3 py-1 rounded-full border border-slate-600 text-slate-200 flex items-center">
<i class="fas fa-rss mr-1"></i>Feed
</button>
</div>
<div class="relative w-full md:w-64">
<input id="timeline-search" type="text" class="w-full pl-8 pr-3 py-2 rounded-full bg-slate-900 border border-slate-600 text-xs text-slate-100" placeholder="キーワード検索(本文・ユーザー名)">
<i class="fas fa-search text-slate-400 text-xs absolute left-2.5 top-1/2 transform -translate-y-1/2"></i>
</div>
</div>
<div id="timeline" class="space-y-4"></div>
<div id="empty-timeline" class="text-center py-12 text-slate-200 opacity-80">
<i class="fas fa-comments text-4xl mb-4 text-indigo-300"></i>
<p class="text-lg">まだ投稿がありません</p>
<p class="text-xs text-slate-300">最初の投稿をして、タイムラインを始めましょう!</p>
</div>
</div>
</div>
</div>
</div>
<footer class="glass-effect mx-4 mb-4 p-4 text-center border border-slate-700">
<p class="text-slate-200 opacity-80 text-xs">
<i class="fas fa-copyright mr-1"></i>
2025 Verse – 次世代ソーシャルネットワーク v3.0
<span class="ml-4 inline-flex items-center">
<i class="fas fa-rss mr-1 text-amber-300"></i>共有RSS / 個別ON/OFF / BOT・マルコフ自動投稿
</span>
</p>
</footer>
<script>
// ==== 初期RSS ====
const PRESET_RSS = [
"http://2ch-2.net/rss/all.xml",
"http://2ch-ranking.net/rss/livemarket1.rdf",
"http://2ch-ranking.net/rss/livemarket2.rdf",
"http://kabumatome.doorblog.jp/index.rdf",
"http://momoniji.com/feed",
"http://oekakigakusyuu.blog97.fc2.com/?xml",
"http://otanews.livedoor.biz/atom.xml",
"http://otanews.livedoor.biz/index.rdf",
"http://news4vip.livedoor.biz/index.rdf",
"http://news.kakaku.com/prdnews/rss.asp",
"http://www.jma-net.go.jp/rss/jma.rss",
"http://rss.asahi.com/rss/asahi/newsheadlines.rdf",
"https://uploadvr.com/feed/",
"http://www.atmarkit.co.jp/rss/rss2dc.xml",
"http://liginc.co.jp/feed",
"http://liginc.co.jp/feed/",
"http://blog.livedoor.jp/shachiani/index.rdf",
"http://manga.lemon-s.com/atom.xml",
"http://b.hatena.ne.jp/search/text?safe=on&q=%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3&users=500&mode=rss",
"http://creator-life.net/feed/",
"http://feedblog.ameba.jp/rss/ameblo/ca-1pixel/rss20.xml",
"http://rssblog.ameba.jp/ca-1pixel/rss20.xml",
"http://weekly.ascii.jp/cate/1/rss.xml",
"http://blog.livedoor.jp/coleblog/atom.xml",
"http://2chantena.antenam.biz/rss1s.rss",
"http://www.4gamer.net/rss/index.xml",
"http://www.4gamer.net/rss/news_topics.xml",
"http://nyan.eggtree.net/feed.xml",
"http://nunnnunn.hatenablog.com/rss",
"http://www.nikkansports.com/general/atom.xml",
"http://feeds.afpbb.com/rss/afpbb/access_ranking",
"http://akiba-pc.watch.impress.co.jp/cda/rss/akiba-pc.rdf",
"https://area.autodesk.jp/rss.xml",
"http://av.watch.impress.co.jp/sublink/av.rdf",
"http://rss.allabout.co.jp/aa/latest/ch/netpc/",
"http://www.ar-ch.org/atom.xml",
"http://feeds.arstechnica.com/arstechnica/BAaf",
"https://feeds.feedburner.com/awwwards-sites-of-the-day",
"http://news.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss091.xml",
"http://www.criteo.com/blog/rss/",
"https://blueskyweb.xyz/rss.xml",
"http://boingboing.net/rss.xml",
"http://www.cc2.co.jp/blog/?feed=rss2",
"http://cgarena.com/cgarena.xml",
"http://cgtracking.net/feed",
"http://japan.cnet.com/rss/index.rdf",
"http://newclassic.jp/feed",
"https://www.cssmania.com/feed/",
"http://ceron.jp/top/?type=rss",
"http://blog.btrax.com/jp/comments/feed/",
"http://2ch.logpo.jp/1hour.xml",
"http://menthas.com/javascript/rss",
"http://www.nhk.or.jp/rss/news/cat0.xml",
"http://ozpa-h4.com/feed/",
"https://www.youtube.com/feeds/videos.xml?channel_id=UC1DCedRgGHBdm81E1llLhOQ",
"http://rass.blog43.fc2.com/?xml",
"http://stackoverflow.com/feeds",
"http://www.slideshare.net/rss/latest",
"http://www.jp.square-enix.com/whatsnew2/whatsnew.rdf",
"http://www.ituore.com/feed",
"http://synodos.jp/comments/feed",
"http://www.shinkigensha.co.jp/feed/",
"http://e-shuushuu.net/index.rss",
"http://slashdot.org/index.rss",
"http://feeds.feedburner.com/TheHackersNews?format=xml",
"http://googleblog.blogspot.com/atom.xml",
"http://www.theregister.co.uk/tonys/slashdot.rdf",
"http://thinkit.co.jp/rss.xml",
"http://blog.livedoor.jp/news23vip/atom.xml",
"http://blog.livedoor.jp/news23vip/index.rdf",
"http://www.webcreatorbox.com/feed/",
"http://web-d.navigater.info/atom.xml",
"http://2ch-c.net/?xml_all",
"http://smhn.info/feed",
"http://feeds.japan.zdnet.com/rss/zdnet/all.rdf",
"http://20kaido.com/index.rdf",
"http://2chnode.com/rss/feed/all",
"http://akiba-souken.com/feed/all/",
"http://amaebi.net/index.rdf",
"http://amakakeru.blog59.fc2.com/?xml",
"http://artskype.com/rss/feed.xml",
"http://asitagamienai.blog118.fc2.com/?xml",
"http://beta.egmnow.com/feed/",
"http://blog.livedoor.jp/ogenre/index.rdf",
"http://blog.nicovideo.jp/atom.xml",
"http://blog.tsubuani.com/feed",
"http://blogs.adobe.com/flex/atom.xml",
"http://blogs.adobe.com/index.xml",
"http://bm.s5-style.com/feed",
"http://business.nikkeibp.co.jp/rss/all_nbo.rdf",
"http://createlier.sitemix.jp/feed/",
"http://crocro.com/news/nc.cgi?action=search&skin=rdf_srch_xml",
"http://d.hatena.ne.jp/thk/rss",
"http://damage0.blomaga.jp/index.rdf",
"http://danbooru.donmai.us/posts.atom",
"http://danbooru.donmai.us/posts.atom?tags=rss",
"http://dengekionline.com/cate/11/rss.xml",
"http://dictionary.reference.com/wordoftheday/wotd.rss",
"http://doujin-games88.net/feed",
"http://doujin.sekurosu.com/rss",
"http://dousyoko.blog.fc2.com/?xml",
"http://eroaniblog.blog.fc2.com/?xml",
"http://eroanimedougakan.blog.fc2.com/?xml",
"http://erogetrailers.com/api?md=latest",
"http://eronizimage.blog.fc2.com/?xml",
"http://erosanime.blog121.fc2.com/?xml",
"http://erotaganime.blog.fc2.com/?xml",
"http://feed.nikkeibp.co.jp/rss/nikkeibp/index.rdf",
"http://feed.rssad.jp/rss/gigazine/rss_2.0",
"http://feed.rssad.jp/rss/jcast/index.xml",
"http://feed.rssad.jp/rss/klug/fxnews/rss5.xml",
"http://feedblog.ameba.jp/rss/ameblo/yusayusa0211/rss20.xml",
"http://feeds.adobe.com/xml/rss.cfm?query=byMostRecent&languages=1",
"http://feeds.builder.japan.zdnet.com/rss/builder/all.rdf",
"http://feeds.fc2.com/fc2/xml?host=anrism.blog&format=xml",
"http://feeds.fc2.com/fc2/xml?host=kahouha2jigen.blog&format=xml",
"http://feeds.feedburner.com/gekiura",
"http://feeds.journal.mycom.co.jp/rss/mycom/index",
"http://feeds.reuters.com/reuters/JPTopNews?format=xml",
"http://galten705.blog.fc2.com/?xml",
"http://gamanjiru.net/feed",
"http://gamanjiru.net/feed/atom",
"http://gamebiz.jp/?feed=rss",
"http://gamenode.jp/rss/feed/all",
"http://ggsoku.com/feed/atom/",
"http://girlcelly.blog.fc2.com/?xml&trackback",
"http://hairana.blog.fc2.com/?xml",
"http://haruka-yumenoato.net/static/rss/index.rss",
"http://headline.harikonotora.net/rss2.xml",
"http://hentaidoujinanime.com/?xml",
"http://homepage1.nifty.com/maname/index.rdf",
"http://horiemon.com/feed/",
"http://ideahacker.net/feed/",
"http://itpro.nikkeibp.co.jp/rss/develop.rdf",
"http://itpro.nikkeibp.co.jp/rss/news.rss",
"http://itpro.nikkeibp.co.jp/rss/oss.rdf",
"http://itpro.nikkeibp.co.jp/rss/win.rdf",
"http://japan.internet.com/rss/rdf/index.rdf",
"http://jp.leopard-raws.org/rss.php",
"http://jp.techcrunch.com/feed/",
"http://kakaku.com/trendnews/rss.xml",
"http://kamisoku.blog47.fc2.com/?xml",
"http://kanesoku.com/index.rdf",
"http://kibougamotenai.blog.fc2.com/?xml",
"http://kiisu.jpn.org/rss/now.xml",
"http://konachan.com/post/piclens?page=1&tags=loli",
"http://labo.tv/2chnews/index.xml",
"http://lineblog.me/yamamotoichiro/atom.xml",
"http://majimougen.blog.fc2.com/?xml",
"http://mantan-web.jp/rss/mantan.xml",
"http://matome.naver.jp/feed/hot",
"http://matome.naver.jp/feed/tech",
"http://matome.sekurosu.com/rss",
"http://mizuhonokuni2ch.com/?xml",
"http://momoiroanime.blog.fc2.com/?xml",
"http://moroahedoujin.com/?xml",
"http://nesingazou.blog.fc2.com/?xml",
"http://newnews-moe.com/index.rdf",
"http://news.ameba.jp/index.xml",
"http://news.com.com/2547-1_3-0-5.xml",
"http://news.nicovideo.jp/?rss=2.0",
"http://news.nicovideo.jp/ranking/hot?rss=2.0",
"http://newsbiz.yahoo.co.jp/topnews.rss",
"http://nijitora.blog.fc2.com/?xml",
"http://nodvd21ver2.blog.fc2.com/?xml",
"http://orebibou.com/feed/",
"http://osu.ppy.sh/feed/ranked/",
"http://otakomu.jp/feed",
"http://pcgameconquest.blog.fc2.com/?xml",
"http://picks.dir.yahoo.co.jp/dailypicks/rss/",
"http://piknik2ch.blog76.fc2.com/?xml",
"http://plus.appgiga.jp/feed/user",
"http://purisoku.com/index.rdf",
"http://rdsig.yahoo.co.jp/RV=1/RU=aHR0cDovL3NlYXJjaHJhbmtpbmcueWFob28uY28uanAvcnNzL2J1cnN0X3JhbmtpbmctcnNzLnhtbA--;_ylt=A2RhjFhfAi9XEi0A6Glhdu57",
"http://read2ch.net/rss/",
"http://rss.dailynews.yahoo.co.jp/fc/computer/rss.xml",
"http://rss.rssad.jp/rss/akibapc/akiba-pc.rdf",
"http://rss.rssad.jp/rss/ascii/biz/rss.xml",
"http://rss.rssad.jp/rss/ascii/hobby/rss.xml",
"http://rss.rssad.jp/rss/ascii/it/rss.xml",
"http://rss.rssad.jp/rss/ascii/mac/rss.xml",
"http://rss.rssad.jp/rss/ascii/pc/rss.xml",
"http://rss.rssad.jp/rss/ascii/rss.xml",
"http://rss.rssad.jp/rss/codezine/new/20/index.xml",
"http://rss.rssad.jp/rss/forest/rss.xml",
"http://rss.rssad.jp/rss/gihyo/feed/atom",
"http://rss.rssad.jp/rss/headline/headline.rdf",
"http://rss.rssad.jp/rss/impresswatch/pcwatch.rdf",
"http://rss.rssad.jp/rss/itm/1.0/makoto.xml",
"http://rss.rssad.jp/rss/itm/1.0/netlab.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_akiba.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_android_appli.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_apple.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_facebook.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_google.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_ipad.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_iphone.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_iphone_appli.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_mixi.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_smartphone.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_twitter.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_ustream.xml",
"http://rss.rssad.jp/rss/itm/2.0/kw_youtube.xml",
"http://rss.rssad.jp/rss/itmbizid/1.0/bizid.xml",
"http://rss.rssad.jp/rss/itmnews/2.0/news_bursts.xml",
"http://rss.rssad.jp/rss/japaninternetcom/index.rdf",
"http://rss.rssad.jp/rss/oshietekun/atom.xml",
"http://rss.rssad.jp/rss/slashdot/slashdot.rss",
"http://rss.rssad.jp/rss/zaikeishimbun/main.xml",
"http://rssc.dokoda.jp/r/8a1dd8f128047929ba4390dab3c8065e/http/searchranking.yahoo.co.jp/realtime_buzz/",
"http://sakurabaryo.com/feed/",
"http://sankei.jp.msn.com/rss/news/points.xml",
"http://sankei.jp.msn.com/rss/news/west_points.xml",
"http://search.goo.ne.jp/rss/newkw.rdf",
"http://sekurosu.com/rss",
"http://streaming.yahoo.co.jp/rss/newly/anime/",
"http://sub0000528116.hmk-temp.com/wordpress/?feed=rss2",
"http://sukebei.nyaa.se/?page=rss&sort=2",
"http://tenshoku.mynavi.jp/knowhow/rss.xml",
"http://tensinyakimeshi.blog98.fc2.com/?xml",
"http://thefreedom12.blog41.fc2.com/?xml",
"http://togetter.com/rss/hot/culture/62",
"http://togetter.com/rss/hot/culture/63",
"http://torimatome.main.jp/blogs/comments/feed",
"http://torimatome.main.jp/blogs/feed",
"http://toshinokyouko.com/rss.php",
"http://tvanimedouga.blog93.fc2.com/?xml",
"http://uranourainformation.blog21.fc2.com/?xml",
"http://video.fc2.com/a/feed_popular.php?m=week",
"http://weather.livedoor.com/forecast/rss/area/400010.xml",
"http://wotopi.jp/feed",
"http://www.100shiki.com/feed",
"http://www.alistapart.com/rss.xml",
"http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=36",
"http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=38",
"http://www.anime-sharing.com/forum/external.php?type=RSS2&forumids=47",
"http://www.blosxom.com/?feed=rss2",
"http://www.britannica.com/eb/dailycontent/rss",
"http://www.csmonitor.com/rss/top.rss",
"http://www.ehackingnews.com/feeds/posts/default",
"http://www.falcom.co.jp/new.xml",
"http://www.famitsu.com/rss/category/fcom_game.rdf",
"http://www.famitsu.com/rss/fcom_all.rdf",
"http://www.ganganonline.com/rss/index.xml",
"http://www.ideaxidea.com/feed",
"http://www.itnews711.com/index.rdf",
"http://www.jp.playstation.com/whatsnew/whatsnew.rdf",
"http://www.keyman.or.jp/rss/v1/?rss_type=all",
"http://www.koubo.co.jp/rss.xml",
"http://www.nyaa.se/?page=rss&sort=2",
"http://www.nyaa.se/?page=rss&user=118009",
"http://www.nytimes.com/services/xml/rss/userland/HomePage.xml",
"http://www.phianime.tv/feed/",
"http://www.rebootdevelop.hr/feed/",
"http://www.rictus.com/muchado/feed/",
"http://www.sbcr.jp/atom.xml",
"http://www.slashgear.com/comments/feed/",
"http://www.torrent-anime.com/feed",
"http://www.torrent-anime.com/feed/",
"http://www.webimemo.com/feed/",
"http://www.wired.com/news_drop/netcenter/netcenter.rdf",
"http://www.xvideos.com/rss/rss.xml",
"http://www.youtube.com/rss/user/KADOKAWAanime/videos.rss",
"http://www.youtube.com/rss/user/demosouko/videos.rss",
"http://www.yukawanet.com/index.rdf",
"http://www.zou3.net/php/rss/nikkei2rss.php?head=main",
"http://xml.ehgt.org/ehtracker.xml",
"http://xml.metafilter.com/rss.xml",
"http://xvideos.2jiero.info/feed",
"http://yaraon.blog109.fc2.com/?xml",
"http://yusaani.com/home/feed/",
"http://zipdeyaruo.blog42.fc2.com/?xml",
"http://www.portalgraphics.net/rss/latest_image_list.xml",
"http://api.syosetu.com/writernovel/430380.Atom",
"http://creive.me/feed/",
"http://gihyo.jp/dev/feed/atom",
"http://gihyo.jp/feed/rss1",
"http://hakase255.blog135.fc2.com/?xml",
"http://2ch-ranking.net/rss/zenban.rdf",
"http://www.isus.jp/feed/",
"http://www.jiji.com/rss/ranking.rdf",
"http://jp.gamesindustry.biz/rss/index.xml",
"https://www.youtube.com/feeds/videos.xml?channel_id=UCx1nAvtVDIsaGmCMSe8ofsQ",
"http://zakuzaku911.com/index.rdf",
"http://ke-tai.org/blog/feed/",
"http://data.newantenna.net/ero/rss/all.xml",
"http://developer.mixi.co.jp/feed/atom",
"http://neoneetch.blog.fc2.com/?xml",
"http://rss.itmedia.co.jp/rss/1.0/netlab.xml",
"http://netgeek.biz/feed",
"http://blog.esuteru.com/index.rdf",
"http://b.hatena.ne.jp/hotentry/game.rss",
"http://b.hatena.ne.jp/hotentry.rss",
"http://mobile.seisyun.net/rss/hot.rdf",
"http://yomi.mobi/rss/hot.rdf",
"http://saymygame.com/feed/",
"http://blog.webcreativepark.net/atom.xml",
"http://buhidoh.net/?xml",
"http://www.webcyou.com/?feed=rss2",
"http://withnews.jp/rss/consumer/new.rdf",
"https://yande.re/post/atom?tags=loli",
"http://blog.livedoor.jp/nizigami/atom.xml",
"http://nvmzaq.blog.fc2.com/?xml",
"http://keieimanga.net/index.rdf",
"http://megumi.ldblog.jp/atom.xml",
"http://kirik.tea-nifty.com/diary/index.rdf",
"http://sinri.net/comments/feed",
"http://himasoku.com/atom.xml",
"http://himasoku.com/index.rdf",
"http://20kaido.com/index.rdf",
"http://h723.blog.fc2.com/?xml",
"http://onecall2ch.com/index.rdf",
"http://www.forest.impress.co.jp/rss.xml",
"http://www.zaikei.co.jp/rss/sections/it.xml",
"http://akiba.keizai.biz/rss.xml",
"http://agag.tw/feed/2d-popular.rss",
"http://adult-vr.jp/feed/",
"http://www.anige-sokuhouvip.com/?xml",
"http://animeanime.jp/rss/index.rdf",
"http://alfalfalfa.com/index.rdf",
"http://feeds.feedburner.com/fc2/GhfA",
"http://erogetaiken072.blog.fc2.com/?xml",
"http://otanew.jp/atom.xml",
"http://jin115.com/index.rdf",
"http://www.onlinegamer.jp/rss/news.rdf",
"http://karapaia.livedoor.biz/index.rdf",
"http://getnews.jp/feed/ext/orig",
"http://www.gungho.co.jp/news/xml/rss.xml",
"http://blog.livedoor.jp/kinisoku/index.rdf",
"http://feeds.gizmodo.jp/rss/gizmodo/index.xml",
"http://himado.in/?sort=movie_id&rss=1",
"http://k-tai.impress.co.jp/cda/rss/ktai.rdf",
"http://gehasoku.com/atom.xml",
"http://feedblog.ameba.jp/rss/ameblo/principia-ca/rss20.xml",
"http://zai.diamond.jp/list/feed/rssfxnews",
"http://capacitor.blog.fc2.com/?xml",
"http://blog.livedoor.jp/vipsister23/index.rdf",
"http://vipsister23.com/atom.xml",
"http://b.hatena.ne.jp/search/tag?safe=on&q=2ch&users=500&mode=rss",
"http://b.hatena.ne.jp/search/tag?safe=on&q=%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF&users=500&mode=rss",
"http://b.hatena.ne.jp/search/tag?safe=off&q=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0&users=500&mode=rss",
"http://shikaku2ch.doorblog.jp/atom.xml",
"http://dotinstall.com/lessons.rss",
"http://2ch-ranking.net/rss/news4vip.rdf",
"http://blog.livedoor.jp/insidears/index.rdf",
"http://2ch-ranking.net/rss/newsplus.rdf",
"http://2ch-ranking.net/rss/news.rdf",
"http://nullpoantenna.com/game.rdf",
"http://workingnews.blog117.fc2.com/?xml",
"http://bm.s5-style.com/feed",
"http://2ch-ranking.net/rss/ghard.rdf",
"http://www.724685.com/blog/rss.xml",
"http://www.yukawanet.com/index.rdf",
"http://2ch-ranking.net/rss/bizplus.rdf",
"http://www.nicovideo.jp/ranking/fav/daily/all?rss=2.0&lang=ja-jp",
"http://www.tarikin.net/rss0.rdf",
"http://blog.livedoor.jp/dqnplus/index.rdf",
"http://www.seojapan.com/blog/feed",
"http://2ch-ranking.net/rss/morningcoffee.rdf",
"http://2ch-ranking.net/mt50k.rdf",
"http://rssblog.ameba.jp/yandereotto/rss20.xml",
"https://business.nikkei.com/rss/sns/nb.rdf",
"http://daredemopc.blog51.fc2.com/?xml",
"http://erogetaikenban.blog65.fc2.com/?xml",
"http://news.goo.ne.jp/rss/topstories/gootop/index.rdf",
"http://lanovelien.blog121.fc2.com/?xml",
"http://news.livedoor.com/topics/rss/eco.xml",
"http://ragnarokonline.gungho.jp/index.rdf",
"http://rocketnews24.com/feed/",
"https://news.denfaminicogamer.jp/feed",
"http://www.igda.jp/?feed=rss2",
"http://feeds.cnn.co.jp/cnn/rss"
];
// ==== アプリ状態 ====
if (!localStorage.getItem('verse_shared_rssFeeds')) {
localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(PRESET_RSS));
}
let users = JSON.parse(localStorage.getItem('verse_users') || '[]');
let currentUser = JSON.parse(localStorage.getItem('verse_currentUser') || 'null');
let posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
let isDarkMode = localStorage.getItem('verse_darkMode') === 'true';
let isInitialized = false;
// BOT / RSS 状態
let botInterval = null;
let rssInterval = null;
// タイムラインフィルタ&検索
let currentFilter = 'all';
let currentSearch = '';
// 共有RSS設定
let sharedRssFeeds = JSON.parse(localStorage.getItem('verse_shared_rssFeeds') || '[]');
let sharedRssInterval = Number(localStorage.getItem('verse_shared_rssInterval')) || 300;
let sharedRssLastIds = JSON.parse(localStorage.getItem('verse_shared_rssLastIds') || '{}');
let sharedRssEnabled = JSON.parse(localStorage.getItem('verse_shared_rssEnabled') || '{}');
// アイコン設定(BOT, Feed)
let verseIcons = JSON.parse(localStorage.getItem('verse_icons') || 'null');
if (!verseIcons) {
verseIcons = {
bot: 'https://cdn-icons-png.flaticon.com/512/4712/4712109.png',
feed: 'https://cdn-icons-png.flaticon.com/512/3416/3416046.png'
};
localStorage.setItem('verse_icons', JSON.stringify(verseIcons));
}
function saveIcons() {
localStorage.setItem('verse_icons', JSON.stringify(verseIcons));
updateAllUI();
}
function uploadIcon(type, e) {
const f = e.target.files[0];
if (!f) return;
if (f.size > 5 * 1024 * 1024) { alert('5MB以下にしてください。'); return; }
const r = new FileReader();
r.onload = () => {
verseIcons[type] = r.result;
saveIcons();
alert(type.toUpperCase() + ' アイコンを更新しました');
};
r.readAsDataURL(f);
}
// ==== キャッシュクリア ====
function clearVerseCache() {
if (!confirm('Tsumugi / Verse のローカルキャッシュ(ユーザー, 投稿, RSS設定など)をすべて削除します。よろしいですか?')) return;
Object.keys(localStorage).forEach(k => {
if (k.startsWith('verse_')) localStorage.removeItem(k);
});
alert('ローカルキャッシュを削除しました。ページを再読み込みします。');
location.reload();
}
// ===== 認証UI =====
function showAuthModal(mode = 'login', errorMsg = '') {
document.getElementById('auth-title').textContent = (mode === 'register') ? '新規登録' : 'ログイン';
document.getElementById('auth-form').authMode = mode;
document.getElementById('auth-email').value = '';
document.getElementById('auth-password').value = '';
document.getElementById('auth-modal').style.display = '';
document.getElementById('main-content').style.display = 'none';
document.getElementById('auth-error').textContent = errorMsg || '';
document.getElementById('auth-error').style.display = errorMsg ? '' : 'none';
document.getElementById('toggle-auth-mode').textContent = (mode === 'register') ? 'ログインはこちら' : '新規登録はこちら';
}
function hideAuthModal() {
document.getElementById('auth-modal').style.display = 'none';
document.getElementById('main-content').style.display = '';
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('auth-form').onsubmit = function(e) {
e.preventDefault();
const email = document.getElementById('auth-email').value.trim().toLowerCase();
const password = document.getElementById('auth-password').value;
if (!email || !password) {
showAuthModal(this.authMode, 'メールアドレスとパスワードを入力してください');
return;
}
if (this.authMode === 'register') {
if (users.find(u => u.email === email)) {
showAuthModal('register', 'このメールアドレスは既に登録されています');
return;
}
const newUser = {
email,
password,
profile: { icon: 'https://via.placeholder.com/100', username: email.split('@')[0], selfIntro: '' }
};
users.push(newUser);
localStorage.setItem('verse_users', JSON.stringify(users));
currentUser = { email };
localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
showAuthModal('login', '登録完了!ログインしてください');
} else {
const user = users.find(u => u.email === email && u.password === password);
if (!user) { showAuthModal('login', 'メールアドレスまたはパスワードが違います'); return; }
currentUser = { email };
localStorage.setItem('verse_currentUser', JSON.stringify(currentUser));
hideAuthModal();
initializeApp();
}
};
document.getElementById('toggle-auth-mode').onclick = function() {
const mode = (document.getElementById('auth-title').textContent === '新規登録') ? 'login' : 'register';
showAuthModal(mode);
};
if (!currentUser) showAuthModal('login'); else { hideAuthModal(); initializeApp(); }
});
function logout() {
localStorage.removeItem('verse_currentUser');
currentUser = null;
stopRssAuto();
stopBotAutoPost();
showAuthModal('login');
}
// ===== 初期化 =====
function initializeApp() {
if (isInitialized) return;
if (!currentUser) { showAuthModal('login'); return; }
users = JSON.parse(localStorage.getItem('verse_users') || '[]');
posts = JSON.parse(localStorage.getItem('verse_posts') || '[]');
isDarkMode = localStorage.getItem('verse_darkMode') === 'true';
const user = users.find(u => u.email === currentUser.email);
window.profile = user ? user.profile : { icon: 'https://via.placeholder.com/100', username: 'ゲストユーザー', selfIntro: '' };
updateAllUI();
updateStatusIndicators();
updateRssUI();
if (isDarkMode) {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
document.getElementById('main-content').style.display = '';
document.getElementById('logout-btn').classList.remove('hidden');
isInitialized = true;
startRssAuto();
addLog('bot-log', 'BOT機能初期化完了', 'success');
addLog('rss-log', 'RSS自動投稿(全体共有)初期化完了', 'success');
}
// ===== 投稿(ユーザー/BOT/Feed/Markov) =====
function createUserPost() {
const ta = document.getElementById('postContent');
const txt = ta.value.trim();
if (!txt) return alert('投稿内容を入力してください。');
if (!currentUser) return alert('ログインが必要です。');
createPost(txt, 'user', profile.username, profile.icon);
ta.value = '';
updateCharCount();
}
function createPost(content, type = 'user', username = null, icon = null, extra = {}) {
if (!content || !content.trim()) return false;
let finalIcon = icon;
if (!finalIcon) {
if (type === 'bot' || type === 'markov') finalIcon = verseIcons.bot;
else if (type === 'feed') finalIcon = verseIcons.feed;
else finalIcon = profile.icon;
}
const post = {
id: Date.now() + Math.random(),
content: content.trim(),
likes: 0,
timestamp: new Date().toLocaleString('ja-JP'),
type,
username: username || profile.username,
icon: finalIcon,
userEmail: currentUser ? currentUser.email : '',
...extra
};
posts.unshift(post);
saveData();
renderTimeline();
return true;
}
function likePost(id) {
const idx = posts.findIndex(p => p.id === id);
if (idx >= 0) {
posts[idx].likes++;
saveData();
renderTimeline();
}
}
function deletePost(id) {
if (!confirm('この投稿を削除しますか?')) return;
posts = posts.filter(p => p.id !== id);
saveData();
renderTimeline();
}
function clearAllPosts() {
if (!confirm('全ての投稿を削除しますか?')) return;
posts = [];
saveData();
renderTimeline();
}
function exportData() {
const blob = new Blob([JSON.stringify(posts, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'verse_posts.json';
a.click();
URL.revokeObjectURL(url);
}
// ===== タイムライン描画 =====
function escapeHtml(s) {
return (s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[m]));
}
function renderTimeline() {
const tl = document.getElementById('timeline');
const emp = document.getElementById('empty-timeline');
const cnt = document.getElementById('post-count');
if (!tl || !emp || !cnt) return;
let displayPosts = posts.slice();
// フィルター
displayPosts = displayPosts.filter(p => {
if (currentFilter === 'user' && p.type !== 'user') return false;
if (currentFilter === 'bot' && !['bot','markov'].includes(p.type)) return false;
if (currentFilter === 'feed' && p.type !== 'feed') return false;
return true;
});
// 検索
if (currentSearch && currentSearch.trim() !== '') {
const q = currentSearch.trim().toLowerCase();
displayPosts = displayPosts.filter(p => {
const text = (p.content || '') + ' ' + (p.username || '');
return text.toLowerCase().includes(q);
});
}
if (displayPosts.length === 0) {
tl.innerHTML = '';
emp.style.display = 'block';
cnt.textContent = '(0件の投稿)';
return;
}
emp.style.display = 'none';
cnt.textContent = `(${displayPosts.length}件の投稿)`;
tl.innerHTML = displayPosts.map(p => {
const info = {
bot: '<i class="fas fa-robot mr-1"></i>BOT',
markov: '<i class="fas fa-dice mr-1"></i>MarkovBOT',
user: '<i class="fas fa-user mr-1"></i>ユーザー',
feed: '<i class="fas fa-rss mr-1"></i>FEEDBOT'
}[p.type] || '<i class="fas fa-user mr-1"></i>';
const main = p.link
? `<a href="${p.link}" target="_blank" class="text-sky-400 underline">${escapeHtml(p.content)}</a>`
: `${escapeHtml(p.content)}`;
return `
<div class="timeline-post p-6">
<div class="flex justify-between items-start mb-4">
<div class="flex items-center space-x-3">
<img src="${p.icon}" class="w-10 h-10 rounded-full object-cover border border-slate-500" onerror="this.src='https://via.placeholder.com/40'">
<div>
<div class="flex items-center">
<span class="font-semibold text-slate-100">${escapeHtml(p.username)}</span>
<span class="username-badge text-[10px]">${info}</span>
</div>
<div class="text-[11px] text-slate-400">${p.timestamp}</div>
</div>
</div>
</div>
<div class="text-slate-100 mb-4 leading-relaxed text-sm">${main}</div>
<div class="flex items-center space-x-4 pt-4 border-t border-slate-700">
<button onclick="likePost(${p.id})" class="flex items-center space-x-2 text-slate-300 hover:text-red-400 text-xs">
<i class="fas fa-heart"></i><span>${p.likes}</span>
</button>
<div class="relative">
<button onclick="toggleShareMenu(${p.id})" class="flex items-center space-x-2 text-slate-300 hover:text-sky-400 text-xs">
<i class="fas fa-share"></i><span>シェア</span>
</button>
<div id="share-menu-${p.id}" class="share-menu hidden">
<button onclick="shareToX(${p.id})"><i class="fab fa-x-twitter text-sky-400 mr-2"></i>Xでシェア</button>
<button onclick="shareToLine(${p.id})"><i class="fab fa-line text-green-400 mr-2"></i>LINEでシェア</button>
<button onclick="copyPost(${p.id})"><i class="fas fa-copy mr-2"></i>コピー</button>
</div>
</div>
<button onclick="deletePost(${p.id})" class="flex items-center space-x-2 text-slate-400 hover:text-red-400 ml-auto text-xs">
<i class="fas fa-trash"></i><span>削除</span>
</button>
</div>
</div>
`;
}).join('');
}
function getPostContentText(id) {
const p = posts.find(x => x.id === id);
if (!p) return '';
const tmp = document.createElement('div');
tmp.innerHTML = p.content;
return tmp.textContent || tmp.innerText || '';
}
function toggleShareMenu(id) {
document.querySelectorAll('[id^="share-menu-"]').forEach(el => el.classList.add('hidden'));
const m = document.getElementById('share-menu-' + id);
if (m) m.classList.toggle('hidden');
}
function shareToX(id) {
const t = encodeURIComponent(getPostContentText(id));
const u = encodeURIComponent(location.href);
window.open(`https://twitter.com/intent/tweet?text=${t}&url=${u}`, '_blank');
}
function shareToLine(id) {
const u = encodeURIComponent(location.href);
window.open(`https://social-plugins.line.me/lineit/share?url=${u}`, '_blank');
}
function copyPost(id) {
const t = getPostContentText(id);
if (navigator.clipboard) {
navigator.clipboard.writeText(t).then(() => alert('コピーしました')).catch(() => fallbackCopy(t));
} else fallbackCopy(t);
}
function fallbackCopy(t) {
const ta = document.createElement('textarea');
ta.value = t; document.body.appendChild(ta);
ta.select(); document.execCommand('copy');
document.body.removeChild(ta);
alert('コピーしました');
}
// ===== タイムライン フィルタ&検索 =====
function setFilter(f) {
currentFilter = f;
['all','user','bot','feed'].forEach(k => {
const btn = document.getElementById('filter-' + k);
if (!btn) return;
if (k === f) {
btn.classList.add('bg-indigo-600','text-white');
} else {
btn.classList.remove('bg-indigo-600','text-white');
}
});
renderTimeline();
}
// ===== RSS UI(個別ON/OFF + 一括ON/OFF) =====
function updateRssUI() {
const listDiv = document.getElementById('rss-list');
if (!listDiv) return;
if (!sharedRssFeeds || sharedRssFeeds.length === 0) {
listDiv.innerHTML = '<div class="text-slate-200 text-[11px] opacity-80">RSSフィード未登録</div>';
} else {
listDiv.innerHTML = sharedRssFeeds.map((url, i) => {
const enabled = sharedRssEnabled[url] !== false;
const enc = encodeURIComponent(url);
return `
<div class="flex items-center space-x-2 bg-slate-900 rounded px-2 py-2 mb-1 border border-slate-700 text-[11px]">
<input type="checkbox" ${enabled ? 'checked' : ''} onchange="toggleRssEnabled('${enc}', this.checked)" title="ON/OFF">
<div class="truncate flex-1 text-slate-100" title="${escapeHtml(url)}">${escapeHtml(url)}</div>
<button onclick="delRssFeed(${i})" class="text-red-400 hover:text-red-500" title="削除"><i class="fas fa-trash"></i></button>
</div>
`;
}).join('');
}
const iv = document.getElementById('rss-interval');
if (iv) iv.value = sharedRssInterval || 300;
}
function toggleRssEnabled(encUrl, on) {
const url = decodeURIComponent(encUrl);
sharedRssEnabled[url] = !!on;
saveSharedRss();
addLog('rss-log', `FEED ${on ? 'ON' : 'OFF'}: ${url}`, 'info');
}
function setAllRssEnabled(on) {
(sharedRssFeeds || []).forEach(u => sharedRssEnabled[u] = !!on);
saveSharedRss();
updateRssUI();
addLog('rss-log', `全フィードを${on ? 'ON' : 'OFF'}にしました`, 'success');
}
function saveSharedRss() {
localStorage.setItem('verse_shared_rssFeeds', JSON.stringify(sharedRssFeeds));
localStorage.setItem('verse_shared_rssInterval', String(sharedRssInterval));
localStorage.setItem('verse_shared_rssLastIds', JSON.stringify(sharedRssLastIds));
localStorage.setItem('verse_shared_rssEnabled', JSON.stringify(sharedRssEnabled));
}
function addRssFeed() {
const url = document.getElementById('rss-url').value.trim();
if (!/^https?:\/\/.+/.test(url)) { addLog('rss-log', '正しいRSSフィードURLを入力してください', 'error'); return; }
if (!sharedRssFeeds) sharedRssFeeds = [];
if (sharedRssFeeds.includes(url)) { addLog('rss-log', 'すでに登録済みです', 'error'); return; }
sharedRssFeeds.push(url);
sharedRssEnabled[url] = true;
saveSharedRss();
updateRssUI();
addLog('rss-log', `RSS追加: ${url}`, 'success');
document.getElementById('rss-url').value = '';
}
function delRssFeed(i) {
if (!sharedRssFeeds[i]) return;
if (!confirm('このフィードを削除しますか?')) return;
const url = sharedRssFeeds[i];
sharedRssFeeds.splice(i, 1);
delete sharedRssEnabled[url];
delete sharedRssLastIds[url];
saveSharedRss();
updateRssUI();
addLog('rss-log', 'RSS削除', 'info');
}
function setRssInterval() {
const iv = document.getElementById('rss-interval').valueAsNumber || 300;
if (iv < 10) return alert('間隔は10秒以上で設定してください。');
sharedRssInterval = iv;
saveSharedRss();
updateRssUI();
startRssAuto();
addLog('rss-log', `自動投稿間隔を${iv}秒に設定`, 'success');
}
function fetchRssNow() { fetchRssFeeds(); }
function fetchRssFeeds() {
if (!sharedRssFeeds || sharedRssFeeds.length === 0) return;
sharedRssFeeds.forEach(feedUrl => {
if (sharedRssEnabled[feedUrl] === false) {
addLog('rss-log', `OFFのため取得スキップ: ${feedUrl}`, 'info');
return;
}
fetch('https://api.rss2json.com/v1/api.json?rss_url=' + encodeURIComponent(feedUrl))
.then(resp => resp.json())
.then(data => {
if (!data.items || !data.items.length) return;
let lastId = sharedRssLastIds[feedUrl] || '';
let newItems = [];
for (const item of data.items) {
const guid = item.guid || item.link || item.pubDate || item.title;
if (!lastId || String(guid) > String(lastId)) newItems.push(item);
}
if (newItems.length === 0) return;
newItems.reverse().forEach(item => {
const guid = item.guid || item.link || item.pubDate || item.title;
if (!posts.some(p => p.type === 'feed' && p.link === item.link)) {
createPost(item.title, 'feed', 'FEEDBOT', verseIcons.feed, { link: item.link });
addLog('rss-log', `新しい記事: ${item.title}`, 'success');
}
sharedRssLastIds[feedUrl] = guid;
});
saveSharedRss();
})
.catch(() => addLog('rss-log', 'RSS取得エラー: ' + feedUrl, 'error'));
});
}
function startRssAuto() {
stopRssAuto();
fetchRssFeeds();
rssInterval = setInterval(fetchRssFeeds, (sharedRssInterval || 300) * 1000);
updateStatusIndicators();
addLog('rss-log', `RSS自動投稿を開始 (${sharedRssInterval}秒間隔)`, 'success');
}
function stopRssAuto() {
if (rssInterval) clearInterval(rssInterval);
rssInterval = null;
updateStatusIndicators();
addLog('rss-log', 'RSS自動投稿を停止しました', 'info');
}
// ===== BOT =====
function postBotMessage() {
const ta = document.getElementById('botContent');
const txt = ta.value.trim();
if (!txt) return alert('BOT投稿内容を入力してください。');
if (!currentUser) return alert('ログインが必要です。');
if (createPost(txt, 'bot', 'BOT', verseIcons.bot)) {
ta.value = '';
addLog('bot-log', `BOT投稿: "${txt.substring(0, 30)}..."`, 'success');
}
}
function generateMarkovText() {
let text = posts
.filter(p => ['user','bot','markov'].includes(p.type))
.map(p => {
const d = document.createElement('div');
d.innerHTML = p.content;
return (d.textContent || d.innerText || '')
.replace(/\s+/g, ' ').replace(/https?:\/\/\S+/g, '').trim();
}).join(' ');
if (text.length < 20) {
const fallbacks = [
"今日はいい天気ですね!",
"最近面白いニュースありましたか?",
"新しいアイデアが浮かんできました。",
"みんなはどう思いますか?"
];
return fallbacks[Math.floor(Math.random() * fallbacks.length)];
}
const tokens = text.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\w]+|[。、!?\r\n]/g) || [];
if (tokens.length < 2) return tokens.join('');
const markov = {};
for (let i = 0; i < tokens.length - 2; i++) {
const key = tokens[i] + '|' + tokens[i+1];
if (!markov[key]) markov[key] = [];
markov[key].push(tokens[i+2]);
}
let idx = Math.floor(Math.random() * (tokens.length - 2));
let key = tokens[idx] + '|' + tokens[idx+1];
let result = [tokens[idx], tokens[idx+1]];
let maxLen = 60 + Math.floor(Math.random() * 40);
for (let i = 0; i < maxLen; i++) {
const nexts = markov[key];
if (!nexts || nexts.length === 0) break;
const next = nexts[Math.floor(Math.random() * nexts.length)];
result.push(next);
if (/[。!?\n]/.test(next)) break;
key = result[result.length - 2] + '|' + result[result.length - 1];
}
return result.join('').replace(/\n/g, '');
}
function postMarkovBot() {
const txt = generateMarkovText();
if (createPost(txt, 'markov', 'MarkovBOT', verseIcons.bot)) {
addLog('bot-log', `マルコフ投稿: "${txt.substring(0, 40)}..."`, 'success');
}
}
function startBotAutoPost() {
const iv = document.getElementById('botIntervalSec').valueAsNumber || 60;
if (iv < 10) { alert('間隔は10秒以上で設定してください。'); return; }
stopBotAutoPost();
setTimeout(postMarkovBot, 3000);
botInterval = setInterval(postMarkovBot, iv * 1000);
updateStatusIndicators();
addLog('bot-log', `マルコフBOT自動投稿開始 (${iv}秒間隔)`, 'success');
}
function stopBotAutoPost() {
if (botInterval) {
clearInterval(botInterval);
botInterval = null;
updateStatusIndicators();
addLog('bot-log', 'マルコフBOT自動投稿を停止しました', 'info');
}
}
// ===== 共通UI/保存 =====
function addLog(id, msg, type = 'info') {
const el = document.getElementById(id);
const ts = new Date().toLocaleTimeString('ja-JP');
const div = document.createElement('div');
const cls = { error: 'error-message', success: 'success-message', info: 'text-slate-100 text-[11px]' }[type] || 'text-slate-100 text-[11px]';
div.className = cls;
div.innerHTML = `<span class="opacity-70">[${ts}]</span> ${escapeHtml(msg)}`;
if (el) {
el.appendChild(div);
el.scrollTop = el.scrollHeight;
while (el.children.length > 100) el.removeChild(el.firstChild);
}
try { console.log(`[${ts}] ${msg}`); } catch(_) {}
}
function updateStatusIndicators() {
const botI = document.getElementById('bot-status');
const botT = document.getElementById('bot-status-text');
if (botI && botT) {
const active = botInterval !== null;
botI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
botT.textContent = active ? '動作中' : '停止中';
}
const rssI = document.getElementById('rss-status');
const rssT = document.getElementById('rss-status-text');
if (rssI && rssT) {
const active = rssInterval !== null;
rssI.className = `status-indicator ${active ? 'status-active' : 'status-inactive'}`;
rssT.textContent = active ? '動作中' : '停止中';
}
}
function showSystemStatus() {
alert(
`=== Verse システムステータス ===
全体投稿数: ${posts.length}
RSS登録数: ${sharedRssFeeds.length}
BOT投稿数: ${posts.filter(p => ['bot', 'markov'].includes(p.type)).length}
BOT自動投稿: ${botInterval ? '動作中' : '停止中'}
RSS自動投稿: ${rssInterval ? '動作中' : '停止中'}`
);
}
function uploadProfileIcon(e) {
const f = e.target.files[0];
if (!f) return;
if (f.size > 5 * 1024 * 1024) { alert('5MB以下にしてください。'); return; }
const r = new FileReader();
r.onload = () => {
profile.icon = r.result;
saveProfileNoAlert();
updateAllUI();
alert('プロフィール画像更新!');
};
r.readAsDataURL(f);
}
function saveProfile() {
const un = document.getElementById('username').value.trim();
const si = document.getElementById('self-intro').value.trim();
if (un.length > 20) { alert('ユーザー名は20文字以内で。'); return; }
profile.username = un || 'ゲストユーザー';
profile.selfIntro = si;
saveProfileNoAlert();
updateAllUI();
alert('プロフィール保存!');
}
function saveProfileNoAlert() {
users = JSON.parse(localStorage.getItem('verse_users') || '[]');
const idx = users.findIndex(u => u.email === (currentUser && currentUser.email));
if (idx >= 0) {
users[idx].profile = profile;
localStorage.setItem('verse_users', JSON.stringify(users));
}
}
function updateAllUI() {
const pi = document.getElementById('profile-icon');
const hi = document.getElementById('header-profile-icon');
if (pi) pi.src = profile.icon;
if (hi) hi.src = profile.icon;
['username-preview','header-username'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = profile.username;
});
const sip = document.getElementById('self-intro-preview');
if (sip) sip.textContent = profile.selfIntro || 'まだ自己紹介がありません';
const emailEl = document.getElementById('header-user-email');
if (emailEl && currentUser) emailEl.textContent = currentUser.email;
const bi = document.getElementById('bot-icon-preview');
const fi = document.getElementById('feed-icon-preview');
if (bi) bi.src = verseIcons.bot;
if (fi) fi.src = verseIcons.feed;
renderTimeline();
}
function updateCharCount() {
const pc = document.getElementById('postContent');
const cc = document.getElementById('char-count');
if (pc && cc) {
const l = pc.value.length;
cc.textContent = `(${l}/500)`;
cc.style.color = l > 450 ? '#fca5a5' : '';
}
}
function toggleDarkMode() {
isDarkMode = !isDarkMode;
if (isDarkMode) {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
localStorage.setItem('verse_darkMode', isDarkMode.toString());
}
function saveData() { localStorage.setItem('verse_posts', JSON.stringify(posts)); }
// 入力UIフック
document.addEventListener('DOMContentLoaded', () => {
const pc = document.getElementById('postContent');
if (pc) {
pc.addEventListener('input', updateCharCount);
pc.addEventListener('keydown', e => { if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); createUserPost(); } });
}
const ui = document.getElementById('username');
if (ui) ui.addEventListener('input', () => {
const v = ui.value.trim() || 'ゲストユーザー';
['username-preview','header-username'].forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = v;
});
});
const si = document.getElementById('self-intro');
if (si) si.addEventListener('input', () => {
const v = si.value.trim() || 'まだ自己紹介がありません';
const el = document.getElementById('self-intro-preview');
if (el) el.textContent = v;
});
const ts = document.getElementById('timeline-search');
if (ts) ts.addEventListener('input', () => {
currentSearch = ts.value || '';
renderTimeline();
});
setFilter('all');
});
window.addEventListener('beforeunload', () => { stopBotAutoPost(); stopRssAuto(); saveData(); });
window.addEventListener('error', e => { addLog('bot-log', `システムエラー: ${e.message}`, 'error'); });
</script>
</body>
</html>