1
0
Files
PonyWave-Tools/favorites_viewer/index.html

1225 lines
49 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Favoriten-Viewer | PonyWave Tools</title>
<!-- Open Graph Tags -->
<meta property="og:title" content="Favoriten-Viewer | PonyWave Tools">
<meta property="og:description" content="Importiere und visualisiere deine Browser-Lesezeichen im HTML- oder JSON-Format">
<meta property="og:url" content="https://tools.ponywave.de/favorites_viewer">
<meta property="og:type" content="website">
<meta property="og:image" content="https://tools.ponywave.de/favicon.png">
<link rel="icon" href="https://tools.ponywave.de/favicon.png" type="image/png">
<!-- Analytics -->
<script defer src="https://stats.ponywave.de/script" data-website-id="9ef713d2-adb9-4906-9df5-708d8a8b9131" data-tag="favorites_viewer"></script>
<style>
/* System-Schriften statt Google Fonts */
@font-face {
font-family: 'LocalIcons';
src: url('data:application/font-woff;charset=utf-8;base64,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') format('woff');
font-weight: normal;
font-style: normal;
}
:root {
--primary-color: #3f51b5;
--primary-light: #757de8;
--primary-dark: #002984;
--secondary-color: #ff4081;
--secondary-light: #ff79b0;
--secondary-dark: #c60055;
--text-on-primary: #ffffff;
--text-on-secondary: #000000;
--background-color: #f5f5f5;
--surface-color: #ffffff;
--border-radius: 8px;
--box-shadow: 0 2px 10px rgba(0,0,0,0.1);
--transition-speed: 0.3s;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
background-color: var(--background-color);
color: #333;
min-height: 100vh;
padding: 20px;
}
header {
background-color: var(--primary-color);
color: var(--text-on-primary);
padding: 20px;
border-radius: var(--border-radius);
margin-bottom: 20px;
box-shadow: var(--box-shadow);
text-align: center;
}
h1 {
font-weight: 500;
margin-bottom: 10px;
}
.app-description {
font-weight: 400;
max-width: 800px;
margin: 0 auto;
margin-top: 10px;
background-color: rgba(0, 0, 0, 0.1);
padding: 8px 15px;
border-radius: 20px;
display: inline-block;
}
.local-data-box {
margin-top: 15px;
margin-bottom: 5px;
}
.local-data-notice {
font-weight: 400;
font-size: 0.9em;
background-color: rgba(0, 0, 0, 0.1);
padding: 8px 15px;
border-radius: 20px;
display: inline-block;
color: var(--text-on-primary);
margin: 0 auto;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.card {
background-color: var(--surface-color);
padding: 25px;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
margin-bottom: 20px;
}
.upload-card {
display: flex;
flex-direction: column;
align-items: center;
}
.file-upload {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 500px;
margin: 20px 0;
}
.file-upload-label {
width: 100%;
text-align: center;
margin-bottom: 15px;
font-weight: 500;
}
.file-format-select {
margin: 15px 0;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
width: 100%;
}
.format-option {
padding: 10px 15px;
border: 2px solid #ddd;
border-radius: var(--border-radius);
cursor: pointer;
transition: all var(--transition-speed);
font-weight: 500;
}
.format-option.active {
border-color: var(--primary-color);
background-color: var(--primary-light);
color: var(--text-on-primary);
}
.custom-file-input {
display: none;
}
.file-upload-btn {
display: inline-block;
padding: 12px 24px;
background-color: var(--primary-color);
color: var(--text-on-primary);
border-radius: var(--border-radius);
cursor: pointer;
transition: background-color var(--transition-speed);
text-align: center;
font-weight: 500;
}
.file-upload-btn::before {
content: "📂";
margin-right: 8px;
}
.file-upload-btn:hover {
background-color: var(--primary-dark);
}
.file-name {
margin-top: 10px;
font-style: italic;
color: #666;
}
.favorites-card {
position: relative;
min-height: 100px;
}
#favoritesOutput {
padding: 10px;
}
#favoritesOutput details {
margin: 5px 0;
border-radius: var(--border-radius);
overflow: hidden;
}
#favoritesOutput details + details,
#favoritesOutput a + details,
#favoritesOutput details + a,
#favoritesOutput a + a {
margin-top: 8px;
}
#favoritesOutput summary {
font-weight: 500;
cursor: pointer;
padding: 8px 12px;
border-radius: var(--border-radius);
background-color: #eaeaea;
display: flex;
align-items: center;
transition: background-color var(--transition-speed);
}
#favoritesOutput summary:hover {
background-color: #d5d5d5;
}
#favoritesOutput summary::before {
content: "📁";
margin-right: 10px;
color: var(--primary-color);
}
#favoritesOutput details[open] > summary::before {
content: "📂";
}
#favoritesOutput > details {
margin-left: 0;
}
#favoritesOutput details details {
margin-left: 20px;
}
#favoritesOutput a {
display: flex;
align-items: center;
color: #333;
text-decoration: none;
padding: 6px 12px;
margin: 4px 0;
margin-left: 20px;
border-radius: var(--border-radius);
transition: background-color var(--transition-speed);
}
#favoritesOutput > a {
margin-left: 0;
}
#favoritesOutput a:hover {
background-color: #eaeaea;
text-decoration: none;
}
#favoritesOutput details a {
margin-left: 20px;
}
#favoritesOutput a::before {
content: "🔗";
margin-right: 10px;
color: var(--secondary-color);
}
.bookmark-info {
margin-left: 10px;
font-size: 0.8em;
color: #888;
}
.bookmark-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 4px;
}
.bookmark-tag {
background-color: #eaeaea;
padding: 2px 6px;
border-radius: 10px;
font-size: 0.7em;
color: #666;
}
.bookmark-icon {
width: 16px;
height: 16px;
margin-right: 10px;
object-fit: contain;
}
.status-indicators {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-top: 15px;
}
#loadingIndicator, #errorIndicator {
padding: 10px 20px;
font-weight: 500;
border-radius: var(--border-radius);
display: none;
}
#loadingIndicator {
background-color: #e3f2fd;
color: #1976d2;
}
/* Icon für Loading Indicator */
#loadingIndicator::before {
content: "\e804";
font-family: 'LocalIcons';
margin-right: 8px;
}
#errorIndicator {
background-color: #ffebee;
color: #d32f2f;
}
#successIndicator {
padding: 10px 20px;
font-weight: 500;
border-radius: var(--border-radius);
display: none;
background-color: #e8f5e9;
color: #2e7d32;
}
/* Icon für Success Indicator */
#successIndicator::before {
content: "✅";
margin-right: 8px;
}
.stats-container {
margin-top: 20px;
padding: 15px;
background-color: #f0f0f0;
border-radius: var(--border-radius);
display: none;
}
.stats-title {
font-weight: 500;
margin-bottom: 10px;
color: var(--primary-color);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
background-color: white;
border-radius: var(--border-radius);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
overflow: hidden;
width: 100%;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: var(--primary-color);
word-break: break-all;
overflow-wrap: break-word;
max-width: 100%;
text-align: center;
white-space: normal;
}
.stat-label {
font-size: 14px;
color: #666;
margin-top: 8px;
}
.domain-text {
width: 100%;
display: block;
font-size: 18px;
}
.domain-count {
font-size: 14px;
display: block;
margin-top: 4px;
}
.search-container {
margin: 10px 0;
width: 100%;
}
.search-input {
width: 100%;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: var(--border-radius);
font-size: 16px;
transition: border-color var(--transition-speed);
}
.search-input:focus {
border-color: var(--primary-color);
outline: none;
}
@media (max-width: 768px) {
.file-format-select {
flex-direction: column;
}
.format-option {
width: 100%;
text-align: center;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
footer {
margin-top: 40px;
background-color: #f5f5f5;
padding: 15px;
text-align: center;
border-top: 1px solid #ddd;
color: #666;
}
.dark-mode footer {
background-color: #333;
color: #ccc;
border-top: 1px solid #444;
}
footer a {
color: var(--primary-color);
text-decoration: none;
transition: opacity 0.3s;
}
footer a:hover {
opacity: 0.8;
text-decoration: underline;
}
</style>
</head>
<body>
<header>
<h1>Favoriten-Viewer</h1>
<p class="app-description">Importiere und visualisiere deine Browser-Lesezeichen im HTML- oder JSON-Format</p>
<div class="local-data-box">
<span class="local-data-notice">🔒 Alle Dateien werden lokal in deinem Browser verarbeitet. Es werden keine Daten an Server gesendet.</span>
</div>
</header>
<div class="container">
<div class="card upload-card">
<div class="file-upload">
<div class="file-format-select">
<div class="format-option active" data-format="html">Chrome (HTML)</div>
<div class="format-option" data-format="json">Firefox (JSON/HTML)</div>
</div>
<label for="fileInput" class="file-upload-btn">
Lesezeichen-Datei auswählen
</label>
<input type="file" id="fileInput" class="custom-file-input" accept=".html,.json">
<div class="file-name" id="fileName">Keine Datei ausgewählt</div>
</div>
<div class="status-indicators">
<div id="loadingIndicator">
⏳ Verarbeite Datei...
</div>
<div id="errorIndicator"></div>
<div id="successIndicator"></div>
</div>
</div>
<div class="card">
<div class="search-container">
<input type="text" class="search-input" id="searchInput" placeholder="Lesezeichen durchsuchen...">
</div>
</div>
<div class="card favorites-card">
<div id="favoritesOutput">
<!-- Hier werden die Favoriten angezeigt -->
<div style="text-align: center; padding: 20px; color: #666;">
<div style="font-size: 48px; color: #ccc; margin-bottom: 10px;">📚</div>
<p>Wähle eine Datei aus, um deine Lesezeichen anzuzeigen.</p>
</div>
</div>
</div>
<div class="card stats-container" id="statsContainer">
<h3 class="stats-title">Lesezeichen-Statistiken</h3>
<div class="stats-grid" id="statsGrid">
<!-- Hier werden die Statistiken angezeigt -->
</div>
</div>
</div>
<footer>
<p><a href="https://tools.ponywave.de/">Zurück zur Startseite</a> | &copy; <span id="current-year">2023</span> Akamaru | Made with ❤️ by Claude</p>
</footer>
<script>
const fileInput = document.getElementById('fileInput');
const fileName = document.getElementById('fileName');
const outputDiv = document.getElementById('favoritesOutput');
const loadingIndicator = document.getElementById('loadingIndicator');
const errorIndicator = document.getElementById('errorIndicator');
const successIndicator = document.getElementById('successIndicator');
const searchInput = document.getElementById('searchInput');
const statsContainer = document.getElementById('statsContainer');
const statsGrid = document.getElementById('statsGrid');
const formatOptions = document.querySelectorAll('.format-option');
let selectedFormat = 'html';
let allBookmarks = [];
let statistics = {
totalBookmarks: 0,
totalFolders: 0,
domains: {},
oldestBookmark: null,
newestBookmark: null,
tagsCount: {}
};
// Format-Auswahl
formatOptions.forEach(option => {
option.addEventListener('click', () => {
formatOptions.forEach(opt => opt.classList.remove('active'));
option.classList.add('active');
selectedFormat = option.dataset.format;
// Update file input accept attribute
if (selectedFormat === 'html') {
fileInput.setAttribute('accept', '.html');
} else {
fileInput.setAttribute('accept', '.json,.html');
}
});
});
// Datei-Auswahl
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) {
outputDiv.innerHTML = '<div style="text-align: center; padding: 20px; color: #666;"><div style="font-size: 48px; color: #ccc; margin-bottom: 10px;">📚</div><p>Keine Datei ausgewählt.</p></div>';
fileName.textContent = 'Keine Datei ausgewählt';
errorIndicator.style.display = 'none';
loadingIndicator.style.display = 'none';
successIndicator.style.display = 'none';
statsContainer.style.display = 'none';
return;
}
fileName.textContent = file.name;
// Überprüfen, ob die Datei dem ausgewählten Format entspricht
const isHtml = file.type === 'text/html' || file.name.endsWith('.html');
const isJson = file.type === 'application/json' || file.name.endsWith('.json');
if (selectedFormat === 'html' && !isHtml) {
outputDiv.innerHTML = '';
errorIndicator.textContent = `Fehler: Bitte lade eine HTML-Datei hoch.`;
errorIndicator.style.display = 'block';
loadingIndicator.style.display = 'none';
successIndicator.style.display = 'none';
statsContainer.style.display = 'none';
return;
} else if (selectedFormat === 'json' && !isJson && !isHtml) {
outputDiv.innerHTML = '';
errorIndicator.textContent = `Fehler: Bitte lade eine JSON- oder HTML-Datei hoch.`;
errorIndicator.style.display = 'block';
loadingIndicator.style.display = 'none';
successIndicator.style.display = 'none';
statsContainer.style.display = 'none';
return;
}
const reader = new FileReader();
reader.onloadstart = () => {
outputDiv.innerHTML = ''; // Vorherige Ausgabe löschen
errorIndicator.style.display = 'none';
successIndicator.style.display = 'none';
loadingIndicator.style.display = 'block';
statsContainer.style.display = 'none';
// Statistik zurücksetzen
statistics = {
totalBookmarks: 0,
totalFolders: 0,
domains: {},
oldestBookmark: null,
newestBookmark: null,
tagsCount: {}
};
allBookmarks = [];
};
reader.onerror = () => {
loadingIndicator.style.display = 'none';
successIndicator.style.display = 'none';
errorIndicator.textContent = 'Fehler beim Lesen der Datei.';
errorIndicator.style.display = 'block';
outputDiv.innerHTML = '';
statsContainer.style.display = 'none';
};
reader.onload = (e) => {
loadingIndicator.style.display = 'none';
try {
const content = e.target.result;
if ((selectedFormat === 'html') ||
(selectedFormat === 'json' && isHtml)) {
processHtmlBookmarks(content);
} else {
processJsonBookmarks(content);
}
updateStatistics();
updateSearch();
// Erfolgsmeldung anzeigen
const successIndicator = document.getElementById('successIndicator');
successIndicator.textContent = `${statistics.totalBookmarks} Lesezeichen und ${statistics.totalFolders} Ordner erfolgreich geladen!`;
successIndicator.style.display = 'block';
// Erfolgsmeldung nach 5 Sekunden ausblenden
setTimeout(() => {
successIndicator.style.display = 'none';
}, 5000);
} catch (error) {
console.error("Fehler beim Parsen:", error);
errorIndicator.textContent = 'Fehler beim Verarbeiten der Datei: ' + error.message;
errorIndicator.style.display = 'block';
outputDiv.innerHTML = '';
statsContainer.style.display = 'none';
}
};
reader.readAsText(file);
});
// HTML-Lesezeichen verarbeiten
function processHtmlBookmarks(content) {
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
// Verbesserte Erkennung der Hauptliste
let mainDl = null;
// Versuche verschiedene gängige Strukturen von Lesezeichendateien zu erkennen
if (doc.querySelector('dl')) {
mainDl = doc.querySelector('dl');
}
// Firefox/Chrome/Edge exportieren oft mit diesem Muster
if (!mainDl && doc.querySelector('html > dl')) {
mainDl = doc.querySelector('html > dl');
}
// Mozilla/Firefox spezifisches Format
if (!mainDl && doc.querySelector('H1')) {
// Prüfen ob es eine Firefox Bookmark Datei ist
const h1Text = doc.querySelector('H1').textContent;
if (h1Text && h1Text.includes('Bookmarks')) {
const bookmarkRoot = doc.querySelector('DL');
if (bookmarkRoot) {
mainDl = bookmarkRoot;
}
}
}
// Mozilla/Firefox spezifisches Format mit DOCTYPE-Prüfung
if (!mainDl && content.includes('NETSCAPE-Bookmark-file-1')) {
const allDLs = doc.querySelectorAll('DL');
if (allDLs.length > 0) {
// Wir nehmen die erste DL-Liste
mainDl = allDLs[0];
}
}
// Falls immer noch keine DL gefunden wurde
if (!mainDl) {
console.error("Konnte die Haupt-<DL>-Liste nicht finden.");
throw new Error('Ungültige Lesezeichen-Datei-Struktur. Keine <DL>-Liste gefunden.');
}
outputDiv.innerHTML = ''; // Vorherige Inhalte löschen
parseDl(mainDl, outputDiv); // Parsen von der Hauptliste starten
updateFolderCounts(); // Aktualisiere die Anzahl der Favoriten in Ordnern
}
// JSON-Lesezeichen verarbeiten
function processJsonBookmarks(content) {
const jsonData = JSON.parse(content);
outputDiv.innerHTML = ''; // Vorherige Inhalte löschen
// Firefox JSON-Struktur verarbeiten
if (jsonData.children) {
parseJsonBookmarks(jsonData.children, outputDiv);
} else if (jsonData.roots) {
// Ein anderes mögliches Firefox JSON-Format
parseJsonBookmarks(jsonData.roots, outputDiv);
} else {
throw new Error('Unbekanntes JSON-Format. Keine Lesezeichen gefunden.');
}
updateFolderCounts(); // Aktualisiere die Anzahl der Favoriten in Ordnern
}
// JSON-Lesezeichen parsen
function parseJsonBookmarks(bookmarks, parentContainer) {
if (Array.isArray(bookmarks)) {
// Array von Lesezeichen
bookmarks.forEach(bookmark => {
processJsonBookmarkNode(bookmark, parentContainer);
});
} else if (typeof bookmarks === 'object') {
// Objekt mit Lesezeichen-Wurzeln wie "menu", "toolbar", etc.
for (const key in bookmarks) {
if (bookmarks[key] && typeof bookmarks[key] === 'object') {
if (bookmarks[key].children) {
// Dies ist ein Ordner
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = bookmarks[key].title || key;
details.appendChild(summary);
parentContainer.appendChild(details);
statistics.totalFolders++;
parseJsonBookmarks(bookmarks[key].children, details);
} else if (bookmarks[key].uri || bookmarks[key].url) {
// Dies ist ein Lesezeichen
addBookmarkFromJson(bookmarks[key], parentContainer);
}
}
}
}
}
// Einzelnes JSON-Lesezeichen verarbeiten
function processJsonBookmarkNode(node, parentContainer) {
if (!node) return;
if (node.type === 'text/x-moz-place-container' ||
(node.children && !node.uri && !node.url)) {
// Dies ist ein Ordner
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = node.title || 'Ordner ohne Namen';
details.appendChild(summary);
parentContainer.appendChild(details);
statistics.totalFolders++;
if (node.children) {
parseJsonBookmarks(node.children, details);
}
} else if (node.type === 'text/x-moz-place' || node.uri || node.url) {
// Dies ist ein Lesezeichen
addBookmarkFromJson(node, parentContainer);
}
}
// JSON-Lesezeichen zur Anzeige hinzufügen
function addBookmarkFromJson(bookmark, parentContainer) {
const link = document.createElement('a');
link.href = bookmark.uri || bookmark.url || '#';
link.textContent = bookmark.title || link.href;
link.target = '_blank';
link.rel = 'noopener noreferrer';
statistics.totalBookmarks++;
// Domäne extrahieren und zählen
try {
const url = new URL(link.href);
const domain = url.hostname;
statistics.domains[domain] = (statistics.domains[domain] || 0) + 1;
} catch (e) {
console.warn("Ungültige URL:", link.href);
}
// Zusätzliche Metadaten
const bookmarkData = {
title: bookmark.title || '',
url: bookmark.uri || bookmark.url || '',
dateAdded: bookmark.dateAdded || bookmark.add_date,
lastModified: bookmark.lastModified,
tags: bookmark.tags || (bookmark.keyword ? [bookmark.keyword] : [])
};
allBookmarks.push(bookmarkData);
// Datum verarbeiten
if (bookmarkData.dateAdded) {
let dateObj;
// Firefox speichert Datum als Mikrosekunden seit der Epoche
if (bookmarkData.dateAdded > 10000000000) { // Wahrscheinlich Mikrosekunden
dateObj = new Date(bookmarkData.dateAdded / 1000);
} else {
dateObj = new Date(bookmarkData.dateAdded * 1000); // Sekunden in Millisekunden umwandeln
}
const dateStr = dateObj.toLocaleDateString();
if (!statistics.oldestBookmark || dateObj < new Date(statistics.oldestBookmark.date)) {
statistics.oldestBookmark = { date: dateObj, title: bookmarkData.title };
}
if (!statistics.newestBookmark || dateObj > new Date(statistics.newestBookmark.date)) {
statistics.newestBookmark = { date: dateObj, title: bookmarkData.title };
}
// Info-Span für das Hinzufügedatum
const infoSpan = document.createElement('span');
infoSpan.className = 'bookmark-info';
infoSpan.textContent = `Hinzugefügt: ${dateStr}`;
link.appendChild(infoSpan);
}
// Tags verarbeiten
if (bookmarkData.tags && bookmarkData.tags.length > 0) {
const tagsDiv = document.createElement('div');
tagsDiv.className = 'bookmark-tags';
bookmarkData.tags.forEach(tag => {
const tagSpan = document.createElement('span');
tagSpan.className = 'bookmark-tag';
tagSpan.textContent = tag;
tagsDiv.appendChild(tagSpan);
// Tag für Statistik zählen
statistics.tagsCount[tag] = (statistics.tagsCount[tag] || 0) + 1;
});
link.appendChild(tagsDiv);
}
// Icon hinzufügen, falls vorhanden
if (bookmark.iconUri || bookmark.icon) {
const icon = document.createElement('img');
icon.className = 'bookmark-icon';
icon.src = bookmark.iconUri || bookmark.icon;
icon.alt = "";
link.prepend(icon);
}
parentContainer.appendChild(link);
}
function parseDl(dlElement, parentContainer) {
if (!dlElement || dlElement.tagName !== 'DL') {
return;
}
// Verarbeite alle Kinder des DL-Elements
const children = dlElement.children;
for (let i = 0; i < children.length; i++) {
let node = children[i];
// Handle the optional <p> wrapper in HTML exports
if (node.tagName === 'P') {
// Process children of <p>
for (let j = 0; j < node.children.length; j++) {
processNode(node.children[j], parentContainer);
}
} else {
processNode(node, parentContainer);
}
}
}
function processNode(node, parentContainer) {
if (!node || !node.tagName) return; // Sicherheitscheck
// Verarbeite nur DT-Elemente
if (node.tagName === 'DT') {
const h3 = node.querySelector('h3');
const a = node.querySelector('a');
// Find the next DL for this DT (folder)
let nextDl = null;
// Chrome/Firefox-Format: DT mit H3 gefolgt von DL
if (h3) {
// Suche nach DL als nächstes Geschwisterelement
let sibling = node.nextElementSibling;
while (sibling) {
if (sibling.tagName === 'DL') {
nextDl = sibling;
break;
}
sibling = sibling.nextElementSibling;
}
// Manchmal ist DL als Kind-Element in einer verschachtelten Struktur
if (!nextDl) {
const parentDl = node.closest('dl');
if (parentDl) {
const dlChildren = parentDl.querySelectorAll(':scope > dl');
if (dlChildren.length > 0) {
// Versuche das nächste DL zu finden
for (let i = 0; i < dlChildren.length; i++) {
if (dlChildren[i].previousElementSibling === node) {
nextDl = dlChildren[i];
break;
}
}
}
}
}
// Es könnte auch direkt nach dem H3 als Kind des DT sein
if (!nextDl && node.querySelector('dl')) {
nextDl = node.querySelector('dl');
}
}
if (h3) { // Es ist ein Ordner
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = h3.textContent.trim() || 'Ordner ohne Namen';
details.appendChild(summary);
parentContainer.appendChild(details);
statistics.totalFolders++;
// Rekursiv den Inhalt des Ordners verarbeiten
if (nextDl) {
parseDl(nextDl, details);
}
} else if (a) { // Es ist ein Lesezeichen
const link = document.createElement('a');
link.href = a.href;
link.textContent = a.textContent.trim() || a.href;
link.target = '_blank';
link.rel = 'noopener noreferrer';
statistics.totalBookmarks++;
// Domäne extrahieren und zählen
try {
const url = new URL(link.href);
const domain = url.hostname;
statistics.domains[domain] = (statistics.domains[domain] || 0) + 1;
} catch (e) {
console.warn("Ungültige URL:", link.href);
}
// Bookmark für Suchindex hinzufügen
const bookmarkData = {
title: link.textContent,
url: link.href
};
// Zusätzliche Attribute aus dem Original-Element extrahieren
if (a.hasAttribute('add_date')) {
const addDate = parseInt(a.getAttribute('add_date'));
bookmarkData.dateAdded = addDate;
const dateObj = new Date(addDate * 1000); // Sekunden in Millisekunden umwandeln
const dateStr = dateObj.toLocaleDateString();
if (!statistics.oldestBookmark || dateObj < new Date(statistics.oldestBookmark.date)) {
statistics.oldestBookmark = { date: dateObj, title: bookmarkData.title };
}
if (!statistics.newestBookmark || dateObj > new Date(statistics.newestBookmark.date)) {
statistics.newestBookmark = { date: dateObj, title: bookmarkData.title };
}
// Info-Span für das Hinzufügedatum
const infoSpan = document.createElement('span');
infoSpan.className = 'bookmark-info';
infoSpan.textContent = `Hinzugefügt: ${dateStr}`;
link.appendChild(infoSpan);
}
// Tags (in Firefox Keyword genannt)
if (a.hasAttribute('tags') || a.hasAttribute('keyword')) {
const tags = a.getAttribute('tags') || a.getAttribute('keyword');
if (tags) {
bookmarkData.tags = tags.split(',').map(tag => tag.trim());
const tagsDiv = document.createElement('div');
tagsDiv.className = 'bookmark-tags';
bookmarkData.tags.forEach(tag => {
const tagSpan = document.createElement('span');
tagSpan.className = 'bookmark-tag';
tagSpan.textContent = tag;
tagsDiv.appendChild(tagSpan);
// Tag für Statistik zählen
statistics.tagsCount[tag] = (statistics.tagsCount[tag] || 0) + 1;
});
link.appendChild(tagsDiv);
}
}
allBookmarks.push(bookmarkData);
// Wenn ein Icon vorhanden ist, füge es hinzu
const icon = a.querySelector('img') || (a.getAttribute('icon') ? new Image() : null);
if (icon) {
if (a.getAttribute('icon') && !icon.src) {
icon.src = a.getAttribute('icon');
}
icon.className = 'bookmark-icon';
link.prepend(icon);
}
parentContainer.appendChild(link);
}
}
}
// Statistiken aktualisieren
function updateStatistics() {
if (statistics.totalBookmarks === 0) {
statsContainer.style.display = 'none';
return;
}
statsGrid.innerHTML = '';
// Anzahl der Lesezeichen
addStatItem('Lesezeichen', statistics.totalBookmarks);
// Anzahl der Ordner
addStatItem('Ordner', statistics.totalFolders);
// Top-Domänen
const domainEntries = Object.entries(statistics.domains).sort((a, b) => b[1] - a[1]);
if (domainEntries.length > 0) {
const topDomain = domainEntries[0];
addStatItem('Top-Domain', `${topDomain[0]} (${topDomain[1]})`);
}
// Ältestes Lesezeichen
if (statistics.oldestBookmark) {
const dateStr = statistics.oldestBookmark.date.toLocaleDateString();
addStatItem('Ältestes Lesezeichen', dateStr);
}
// Neuestes Lesezeichen
if (statistics.newestBookmark) {
const dateStr = statistics.newestBookmark.date.toLocaleDateString();
addStatItem('Neuestes Lesezeichen', dateStr);
}
// Häufigste Tags
const tagEntries = Object.entries(statistics.tagsCount).sort((a, b) => b[1] - a[1]);
if (tagEntries.length > 0) {
const topTag = tagEntries[0];
addStatItem('Häufigster Tag', `${topTag[0]} (${topTag[1]})`);
}
statsContainer.style.display = 'block';
}
// Statistik-Element hinzufügen
function addStatItem(label, value) {
const statItem = document.createElement('div');
statItem.className = 'stat-item';
const statValue = document.createElement('div');
statValue.className = 'stat-value';
if (label === 'Top-Domain' && typeof value === 'string') {
// Für lange Domain-Namen
const parts = value.split(' (');
const domain = parts[0];
const count = parts[1] ? '(' + parts[1] : '';
// Domain und Anzahl als separate Elemente
const domainSpan = document.createElement('span');
domainSpan.textContent = domain;
domainSpan.className = 'domain-text';
const countSpan = document.createElement('span');
countSpan.textContent = count;
countSpan.className = 'domain-count';
statValue.appendChild(domainSpan);
statValue.appendChild(countSpan);
} else {
statValue.textContent = value;
}
const statLabel = document.createElement('div');
statLabel.className = 'stat-label';
statLabel.textContent = label;
statItem.appendChild(statValue);
statItem.appendChild(statLabel);
statsGrid.appendChild(statItem);
}
// Suche initialisieren
function updateSearch() {
searchInput.addEventListener('input', performSearch);
}
// Suche durchführen
function performSearch() {
const searchTerm = searchInput.value.toLowerCase().trim();
if (!searchTerm) {
// Alle Details-Elemente zurücksetzen
const allDetails = outputDiv.querySelectorAll('details');
allDetails.forEach(details => {
details.removeAttribute('open');
});
// Alle Links anzeigen
const allLinks = outputDiv.querySelectorAll('a');
allLinks.forEach(link => {
link.style.display = 'flex';
});
return;
}
// Lesezeichen durchsuchen
const allLinks = outputDiv.querySelectorAll('a');
allLinks.forEach(link => {
const linkText = link.textContent.toLowerCase();
const linkHref = link.href.toLowerCase();
const matchesSearch = linkText.includes(searchTerm) || linkHref.includes(searchTerm);
link.style.display = matchesSearch ? 'flex' : 'none';
// Wenn ein Link übereinstimmt, öffne alle übergeordneten Details-Elemente
if (matchesSearch) {
let parent = link.parentElement;
while (parent) {
if (parent.tagName === 'DETAILS') {
parent.setAttribute('open', '');
}
parent = parent.parentElement;
}
}
});
}
// Funktion zum Aktualisieren der Anzahl der Favoriten in Ordnern
function updateFolderCounts() {
const allDetails = outputDiv.querySelectorAll('details');
allDetails.forEach(details => {
// Zähle alle direkten Links (nicht verschachtelt in anderen Details)
const directLinks = Array.from(details.children).filter(
child => child.tagName === 'A'
).length;
// Rekursiv alle Links in verschachtelten Details zählen
const nestedDetails = details.querySelectorAll('details');
let totalNestedLinks = 0;
nestedDetails.forEach(nestedDetail => {
const nestedLinks = nestedDetail.querySelectorAll('a');
totalNestedLinks += nestedLinks.length;
});
// Gesamtanzahl der Links
const totalLinks = directLinks + totalNestedLinks;
// Aktualisiere den Summary-Text
const summary = details.querySelector('summary');
if (summary) {
// Prüfe ob bereits eine Anzahl in Klammern hinzugefügt wurde
const originalText = summary.textContent.replace(/\s*\(\d+\)\s*$/, '').trim();
summary.textContent = `${originalText} (${totalLinks})`;
}
});
}
// Aktuelle Jahreszahl im Footer
document.getElementById('current-year').textContent = new Date().getFullYear();
</script>
</body>
</html>