<?php
// ============================================================
// merk.php – Notizen mit Themen
// Dateiformat topics.dat: ID$#$Name$#$deleted(0/1)\n
// Dateiformat merk.dat: ID$#$TopicID$#$Text$#$deleted(0/1)[$#$deleted_at(unix)]\n
// ============================================================
define('ITEMS_FILE', __DIR__ . '/merk.dat');
define('TOPICS_FILE', __DIR__ . '/topics.dat');
define('HISTORY_FILE', __DIR__ . '/items-history.dat');
// ── Hilfsfunktionen ─────────────────────────────────────────
function generateId(): string {
return str_pad((string)rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
function readTopics(): array {
if (!file_exists(TOPICS_FILE)) return [];
$lines = file(TOPICS_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$topics = [];
foreach ($lines as $line) {
$parts = explode('$#$', $line);
if (count($parts) === 3) {
$topics[] = [
'id' => $parts[0],
'name' => $parts[1],
'deleted' => $parts[2] === '1',
];
}
}
return $topics;
}
function writeTopics(array $topics): void {
$lines = array_map(fn($t) =>
$t['id'] . '$#$' . $t['name'] . '$#$' . ($t['deleted'] ? '1' : '0'),
$topics
);
file_put_contents(TOPICS_FILE, implode("\n", $lines) . (count($lines) ? "\n" : ''));
}
function readItems(): array {
if (!file_exists(ITEMS_FILE)) return [];
$lines = file(ITEMS_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$items = [];
foreach ($lines as $line) {
$parts = explode('$#$', $line);
if (count($parts) >= 4) {
$items[] = [
'id' => $parts[0],
'topic_id' => $parts[1],
'text' => $parts[2],
'deleted' => $parts[3] === '1',
'deleted_at' => isset($parts[4]) ? (int)$parts[4] : 0,
];
}
}
return $items;
}
function writeItems(array $items): void {
$lines = array_map(fn($i) =>
$i['id'] . '$#$' . $i['topic_id'] . '$#$' . $i['text'] . '$#$' . ($i['deleted'] ? '1' : '0') .
(!empty($i['deleted_at']) ? '$#$' . $i['deleted_at'] : ''),
$items
);
file_put_contents(ITEMS_FILE, implode("\n", $lines) . (count($lines) ? "\n" : ''));
}
// Historisierung: Eintrag in items-history.dat schreiben
// Format: Datum$#$Typ(topic|item)$#$ID$#$TopicName$#$Text
function writeHistory(string $type, string $id, string $topicName, string $text): void {
$date = date('Y-m-d H:i:s');
$line = $date . '$#$' . $type . '$#$' . $id . '$#$' . $topicName . '$#$' . $text . "\n";
file_put_contents(HISTORY_FILE, $line, FILE_APPEND);
}
function autoDeleteEmptyTopics(): void {
$topics = readTopics();
$items = readItems();
foreach ($topics as &$topic) {
if ($topic['deleted']) continue;
$activeItems = array_filter($items, fn($i) =>
$i['topic_id'] === $topic['id'] && !$i['deleted']
);
// Thema hat noch nie Items gehabt ODER alle gelöscht → soft-delete
$anyItem = array_filter($items, fn($i) => $i['topic_id'] === $topic['id']);
if (count($anyItem) > 0 && count($activeItems) === 0) {
$topic['deleted'] = true;
}
}
unset($topic);
writeTopics($topics);
}
// ── Aktionen ────────────────────────────────────────────────
$action = $_POST['action'] ?? $_GET['action'] ?? '';
$topicId = $_POST['topic_id'] ?? $_GET['topic_id'] ?? '';
$view = $_GET['view'] ?? 'topics'; // topics | trash
$redirect = 'merk.php';
switch ($action) {
case 'add_topic':
$name = trim($_POST['name'] ?? '');
if ($name !== '') {
$topics = readTopics();
$topics[] = ['id' => generateId(), 'name' => htmlspecialchars($name, ENT_QUOTES), 'deleted' => false];
writeTopics($topics);
}
header("Location: $redirect");
exit;
case 'delete_topic':
// Nur löschen, wenn das Thema noch nie Items hatte (frisch angelegt)
$topics = readTopics();
$items = readItems();
$anyItem = array_filter($items, fn($i) => $i['topic_id'] === $topicId);
if (count($anyItem) === 0) {
foreach ($topics as &$t) {
if ($t['id'] === $topicId) { $t['deleted'] = true; break; }
}
unset($t);
writeTopics($topics);
}
header("Location: $redirect");
exit;
case 'restore_topic':
$topics = readTopics();
foreach ($topics as &$t) {
if ($t['id'] === $topicId) { $t['deleted'] = false; break; }
}
unset($t);
// Items dieses Themas auch wiederherstellen
$items = readItems();
foreach ($items as &$i) {
if ($i['topic_id'] === $topicId) { $i['deleted'] = false; }
}
unset($i);
writeItems($items);
writeTopics($topics);
header("Location: $redirect?view=trash");
exit;
case 'purge_topic':
// Thema + alle Items endgültig löschen → History schreiben
$topics = readTopics();
$topicRec = array_values(array_filter($topics, fn($t) => $t['id'] === $topicId));
$tName = $topicRec[0]['name'] ?? $topicId;
$topics = array_filter($topics, fn($t) => $t['id'] !== $topicId);
writeTopics(array_values($topics));
$items = readItems();
$purged = array_filter($items, fn($i) => $i['topic_id'] === $topicId);
foreach ($purged as $pi) {
writeHistory('item', $pi['id'], $tName, $pi['text']);
}
writeHistory('topic', $topicId, $tName, '— Thema endgültig gelöscht —');
$items = array_filter($items, fn($i) => $i['topic_id'] !== $topicId);
writeItems(array_values($items));
header("Location: $redirect?view=trash");
exit;
case 'add_item':
$text = trim($_POST['text'] ?? '');
if ($text !== '' && $topicId !== '') {
$items = readItems();
$items[] = [
'id' => generateId(),
'topic_id' => $topicId,
'text' => htmlspecialchars($text, ENT_QUOTES),
'deleted' => false,
];
writeItems($items);
autoDeleteEmptyTopics();
}
header("Location: $redirect?view=topic&topic_id=$topicId");
exit;
case 'delete_item':
$itemId = $_POST['item_id'] ?? $_GET['item_id'] ?? '';
$items = readItems();
foreach ($items as &$i) {
if ($i['id'] === $itemId) { $i['deleted'] = true; $i['deleted_at'] = time(); break; }
}
unset($i);
writeItems($items);
autoDeleteEmptyTopics();
// Weiterleitung: zurück zum Thema oder Papierkorb
$dest = ($view === 'trash') ? "?view=trash" : "?view=topic&topic_id=$topicId";
header("Location: $redirect$dest");
exit;
case 'restore_item':
$itemId = $_POST['item_id'] ?? $_GET['item_id'] ?? '';
$items = readItems();
foreach ($items as &$i) {
if ($i['id'] === $itemId) { $i['deleted'] = false; $i['deleted_at'] = 0; break; }
}
unset($i);
writeItems($items);
header("Location: $redirect?view=trash");
exit;
case 'purge_item':
$itemId = $_POST['item_id'] ?? $_GET['item_id'] ?? '';
$items = readItems();
$purgedIt = array_values(array_filter($items, fn($i) => $i['id'] === $itemId));
if (!empty($purgedIt)) {
$pi = $purgedIt[0];
$topics = readTopics();
$tRec = array_values(array_filter($topics, fn($t) => $t['id'] === $pi['topic_id']));
$tName = $tRec[0]['name'] ?? $pi['topic_id'];
writeHistory('item', $pi['id'], $tName, $pi['text']);
}
$items = array_filter($items, fn($i) => $i['id'] !== $itemId);
writeItems(array_values($items));
header("Location: $redirect?view=trash");
exit;
}
// ── Daten für Anzeige laden ──────────────────────────────────
$allTopics = readTopics();
$allItems = readItems();
// Thema-Map für schnellen Zugriff
$topicMap = [];
foreach ($allTopics as $t) $topicMap[$t['id']] = $t;
// Aktuelles Thema (Topic-View)
$currentTopic = null;
if ($view === 'topic' && $topicId !== '' && isset($topicMap[$topicId])) {
$currentTopic = $topicMap[$topicId];
}
// Linkify
function linkify(string $text): string {
return preg_replace(
'/(https?:\/\/[^\s<>"\']+)/i',
'<a href="$1" target="_blank" rel="noopener">$1</a>',
htmlspecialchars_decode($text)
);
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mein Gehirn</title>
<style>
:root {
--bg: #f5f2eb;
--card: #fffef9;
--border: #d6cfc0;
--accent: #3d6b4f;
--accent2: #7a3d2e;
--text: #2c2416;
--muted: #8a7f70;
--trash: #b84a2e;
--shadow: 0 1px 4px rgba(0,0,0,.10);
--radius: 6px;
--font: 'Georgia', serif;
--mono: 'Courier New', monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font);
font-size: 16px;
line-height: 1.6;
min-height: 100vh;
}
/* ── Header ── */
header {
background: var(--accent);
color: #fff;
padding: 12px 20px;
display: flex;
align-items: center;
gap: 12px;
position: sticky;
top: 0;
z-index: 10;
box-shadow: 0 2px 8px rgba(0,0,0,.18);
}
header h1 {
font-size: 1.15rem;
font-weight: normal;
letter-spacing: .04em;
flex: 1;
}
.header-badge {
font-size: .75rem;
background: rgba(255,255,255,.18);
border-radius: 20px;
padding: 2px 9px;
font-family: var(--mono);
}
/* ── Nav-Tabs ── */
.tabs {
display: flex;
border-bottom: 2px solid var(--border);
background: var(--card);
padding: 0 16px;
}
.tabs a {
display: inline-block;
padding: 10px 18px;
color: var(--muted);
text-decoration: none;
font-size: .9rem;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: color .15s, border-color .15s;
}
.tabs a.active {
color: var(--accent);
border-bottom-color: var(--accent);
font-weight: bold;
}
.tabs a:hover:not(.active) { color: var(--text); }
/* ── Layout ── */
.container { max-width: 620px; margin: 0 auto; padding: 20px 16px 60px; }
/* ── Themen-Kacheln ── */
.topic-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 28px;
}
.topic-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
transition: box-shadow .15s, transform .1s;
display: flex;
flex-direction: column;
}
.topic-card:hover { box-shadow: 0 3px 10px rgba(0,0,0,.14); transform: translateY(-1px); }
.topic-card a.topic-link {
display: block;
padding: 14px 14px 10px;
text-decoration: none;
color: var(--text);
flex: 1;
}
.topic-name { font-size: 1rem; font-weight: bold; }
.topic-count {
font-size: .78rem;
color: var(--text);
margin-top: 3px;
font-family: var(--mono);
}
.topic-card-footer {
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
padding: 4px 8px;
}
/* ── Topic-View Header ── */
.topic-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.topic-header h2 {
flex: 1;
font-size: 1.25rem;
}
/* ── Items ── */
.item-list { list-style: none; margin-bottom: 24px; }
.item-list li {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 10px 12px;
margin-bottom: 8px;
display: flex;
align-items: flex-start;
gap: 10px;
transition: opacity .2s;
}
.item-list li.deleted { opacity: .55; }
.item-text { flex: 1; word-break: break-word; }
.item-text a { color: var(--accent); }
/* ── Eingabefeld ── */
.add-form {
display: flex;
gap: 8px;
margin-top: 8px;
}
.add-form input[type=text] {
flex: 1;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 9px 12px;
font-family: var(--font);
font-size: .95rem;
background: var(--card);
color: var(--text);
outline: none;
transition: border-color .15s;
}
.add-form input[type=text]:focus { border-color: var(--accent); }
/* ── Buttons ── */
button, .btn {
cursor: pointer;
border: none;
border-radius: var(--radius);
font-family: var(--font);
font-size: .85rem;
padding: 7px 13px;
transition: background .15s, color .15s;
text-decoration: none;
display: inline-block;
}
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { background: #2e5239; }
.btn-back { background: #e8e2d8; color: var(--text); }
.btn-back:hover { background: #d6cfc0; }
.btn-danger { background: transparent; color: var(--trash); border: 1px solid #e0b8b0; }
.btn-danger:hover { background: #fdecea; }
.btn-restore { background: transparent; color: var(--accent); border: 1px solid #b0cebc; }
.btn-restore:hover { background: #eaf3ee; }
.btn-purge { background: transparent; color: #888; border: 1px solid #ccc; font-size: .8rem; }
.btn-purge:hover { background: #f5f5f5; color: #555; }
.btn-sm { padding: 4px 9px; font-size: 1rem; }
/* ── Papierkorb ── */
.trash-section { margin-bottom: 28px; }
.trash-section h3 {
font-size: .8rem;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--muted);
border-bottom: 1px solid var(--border);
padding-bottom: 5px;
margin-bottom: 10px;
}
.trash-topic-block {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 10px;
overflow: hidden;
}
.trash-topic-title {
padding: 10px 14px;
font-weight: bold;
background: #f0ebe1;
display: flex;
align-items: center;
gap: 8px;
font-size: .95rem;
}
.trash-topic-title span { flex: 1; }
.trash-item-row {
padding: 7px 14px;
border-top: 1px solid var(--border);
display: flex;
align-items: flex-start;
gap: 8px;
font-size: .9rem;
}
.trash-item-row .item-text { flex: 1; color: var(--muted); }
.deleted-date {
font-size: .75rem;
color: var(--muted);
font-family: var(--mono);
white-space: nowrap;
align-self: center;
}
/* ── Empty State ── */
.empty {
text-align: center;
color: var(--muted);
padding: 40px 20px;
font-size: .95rem;
}
.empty span { display: block; font-size: 2rem; margin-bottom: 8px; }
/* ── Responsive ── */
@media (max-width: 420px) {
.topic-grid { grid-template-columns: 1fr 1fr; }
}
</style>
</head>
<body>
<header>
<?php if ($view === 'topic' && $currentTopic): ?>
<h1><?= htmlspecialchars_decode($currentTopic['name']) ?></h1>
<a href="merk.php" class="btn btn-back btn-sm" style="padding:4px 10px;">Alle Themen</a>
<?php else: ?>
<h1>Mein Gehirn</h1>
<?php endif; ?>
<a href="merk.php?view=trash" class="header-badge" title="Papierkorb">🗑</a>
</header>
<?php if ($view !== 'topic'): ?>
<nav class="tabs">
<a href="merk.php" class="<?= $view === 'topics' ? 'active' : '' ?>">Themen</a>
<a href="merk.php?view=trash" class="<?= $view === 'trash' ? 'active' : '' ?>">Papierkorb</a>
</nav>
<?php endif; ?>
<div class="container">
<?php
// ════════════════════════════════════════════════════════════
// VIEW: THEMEN-ÜBERSICHT
// ════════════════════════════════════════════════════════════
if ($view === 'topics'):
$activeTopics = array_filter($allTopics, fn($t) => !$t['deleted']);
?>
<?php if (empty($activeTopics)): ?>
<div class="empty"><span>📋</span>Noch keine Themen. Lege unten eines an!</div>
<?php else: ?>
<div class="topic-grid">
<?php foreach ($activeTopics as $t):
$cnt = count(array_filter($allItems, fn($i) => $i['topic_id'] === $t['id'] && !$i['deleted']));
?>
<div class="topic-card">
<a href="merk.php?view=topic&topic_id=<?= $t['id'] ?>" class="topic-link">
<div class="topic-name"><?= htmlspecialchars_decode($t['name']) ?></div>
<div class="topic-count"><?= $cnt ?> Eintr<?= $cnt !== 1 ? 'äge' : 'ag' ?></div>
</a>
<?php
$anyItem = count(array_filter($allItems, fn($i) => $i['topic_id'] === $t['id']));
if ($anyItem === 0):
?>
<div class="topic-card-footer">
<form method="post" style="display:inline">
<input type="hidden" name="action" value="delete_topic">
<input type="hidden" name="topic_id" value="<?= $t['id'] ?>">
<button type="submit" class="btn btn-danger btn-sm" title="Leeres Thema löschen">✕</button>
</form>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form method="post" class="add-form">
<input type="hidden" name="action" value="add_topic">
<input type="text" name="name" placeholder="Neues Thema…" autocomplete="off" required>
<button type="submit" class="btn btn-primary">+ Thema</button>
</form>
<?php
// ════════════════════════════════════════════════════════════
// VIEW: EINZELNES THEMA
// ════════════════════════════════════════════════════════════
elseif ($view === 'topic' && $currentTopic):
$topicItems = array_filter($allItems, fn($i) =>
$i['topic_id'] === $topicId && !$i['deleted']
);
?>
<?php if (empty($topicItems)): ?>
<div class="empty"><span>✏️</span>Noch keine Einträge in diesem Thema.</div>
<?php else: ?>
<ul class="item-list">
<?php foreach ($topicItems as $item): ?>
<li>
<span class="item-text"><?= linkify($item['text']) ?></span>
<form method="post">
<input type="hidden" name="action" value="delete_item">
<input type="hidden" name="item_id" value="<?= $item['id'] ?>">
<input type="hidden" name="topic_id" value="<?= $topicId ?>">
<button type="submit" class="btn btn-danger btn-sm" title="Löschen">✕</button>
</form>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<form method="post" class="add-form">
<input type="hidden" name="action" value="add_item">
<input type="hidden" name="topic_id" value="<?= $topicId ?>">
<input type="text" name="text" placeholder="Neuer Eintrag…" autocomplete="off" required>
<button type="submit" class="btn btn-primary">+</button>
</form>
<?php
// ════════════════════════════════════════════════════════════
// VIEW: PAPIERKORB
// ════════════════════════════════════════════════════════════
elseif ($view === 'trash'):
// Gelöschte Themen (inkl. ihrer Items)
$deletedTopics = array_filter($allTopics, fn($t) => $t['deleted']);
// Gelöschte Items aus AKTIVEN Themen
$deletedItems = array_values(array_filter($allItems, fn($i) =>
$i['deleted'] && isset($topicMap[$i['topic_id']]) && !$topicMap[$i['topic_id']]['deleted']
));
usort($deletedItems, fn($a, $b) => $b['deleted_at'] - $a['deleted_at']);
$hasAnything = count($deletedTopics) > 0 || count($deletedItems) > 0;
?>
<?php if (!$hasAnything): ?>
<div class="empty"><span>🗑</span>Papierkorb ist leer.</div>
<?php else: ?>
<?php if (!empty($deletedTopics)): ?>
<div class="trash-section">
<h3>Gelöschte Themen</h3>
<?php foreach ($deletedTopics as $t):
$tItems = array_filter($allItems, fn($i) => $i['topic_id'] === $t['id']);
?>
<div class="trash-topic-block">
<div class="trash-topic-title">
<span><?= htmlspecialchars_decode($t['name']) ?></span>
<form method="post" style="display:inline">
<input type="hidden" name="action" value="restore_topic">
<input type="hidden" name="topic_id" value="<?= $t['id'] ?>">
<button type="submit" class="btn btn-restore btn-sm">↩ Wiederherstellen</button>
</form>
<form method="post" style="display:inline" onsubmit="return confirm('Endgültig löschen?')">
<input type="hidden" name="action" value="purge_topic">
<input type="hidden" name="topic_id" value="<?= $t['id'] ?>">
<button type="submit" class="btn btn-purge btn-sm">✕ Löschen</button>
</form>
</div>
<?php foreach ($tItems as $item): ?>
<div class="trash-item-row">
<span class="item-text"><?= linkify($item['text']) ?></span>
</div>
<?php endforeach; ?>
<?php if (empty($tItems)): ?>
<div class="trash-item-row"><span class="item-text" style="font-style:italic">Keine Einträge</span></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($deletedItems)): ?>
<div class="trash-section">
<h3>Gelöschte Einträge (Themen noch aktiv)</h3>
<?php
// Gruppieren nach Thema
$byTopic = [];
foreach ($deletedItems as $item) {
$byTopic[$item['topic_id']][] = $item;
}
foreach ($byTopic as $tid => $items):
$tName = htmlspecialchars_decode($topicMap[$tid]['name'] ?? 'Unbekannt');
?>
<div class="trash-topic-block">
<div class="trash-topic-title">
<span>📌 <?= $tName ?></span>
</div>
<?php foreach ($items as $item): ?>
<div class="trash-item-row">
<span class="item-text"><?= linkify($item['text']) ?></span>
<?php if (!empty($item['deleted_at'])): ?>
<span class="deleted-date" title="Gelöscht am <?= date('d.m.Y H:i', $item['deleted_at']) ?>">
<?= date('d.m.', $item['deleted_at']) ?>
</span>
<?php endif; ?>
<form method="post" style="display:inline">
<input type="hidden" name="action" value="restore_item">
<input type="hidden" name="item_id" value="<?= $item['id'] ?>">
<button type="submit" class="btn btn-restore btn-sm">↩</button>
</form>
<form method="post" style="display:inline" onsubmit="return confirm('Endgültig löschen?')">
<input type="hidden" name="action" value="purge_item">
<input type="hidden" name="item_id" value="<?= $item['id'] ?>">
<button type="submit" class="btn btn-purge btn-sm">✕</button>
</form>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php
// ════════════════════════════════════════════════════════════
// Fallback: Topic nicht gefunden
// ════════════════════════════════════════════════════════════
else:
?>
<div class="empty"><span>🔍</span>Thema nicht gefunden. <a href="merk.php">Zurück</a></div>
<?php endif; ?>
</div><!-- .container -->
</body>
</html>