Initial release v1.2.0 — Hellion NewTab Browser Extension

Persoenlicher Bookmark-Dashboard als Browser-Extension.
8 Themes, Drag & Drop, Sticky Notes, JSON Export/Import.
Chrome, Edge, Brave, Opera, Vivaldi (MV3) + Firefox (MV2).

Includes GitHub Actions for security scanning, code quality
validation, and automated release packaging.
This commit is contained in:
2026-03-20 22:48:21 +01:00
commit 87c30b24d0
30 changed files with 2835 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
/* =============================================
HELLION NEWTAB — app.js
Einstiegspunkt: Init, Clock, globale Events
============================================= */
async function init() {
const savedBoards = await Store.get('boards');
const savedSettings = await Store.get('settings');
boards = savedBoards ?? getDefaultBoards();
if (savedSettings) Object.assign(settings, savedSettings);
applySettings();
renderBoards();
startClock();
bindGlobalEvents();
bindSettingsEvents();
initSearch();
initStickyNote();
initDataButtons();
Store.checkQuota();
}
// ---- CLOCK & DATE ----
function startClock() {
const DAYS = ['So','Mo','Di','Mi','Do','Fr','Sa'];
const MONTHS = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'];
function tick() {
const now = new Date();
document.getElementById('clock').textContent =
`${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
document.getElementById('date').textContent =
`${DAYS[now.getDay()]}, ${String(now.getDate()).padStart(2,'0')}. ${MONTHS[now.getMonth()]}`;
}
tick();
setInterval(tick, 1000);
}
// ---- GLOBALE EVENTS (Header-Buttons, Modals, Import) ----
function bindGlobalEvents() {
// Header
document.getElementById('btnAddBoard').addEventListener('click', openAddBoardModal);
document.getElementById('btnImport').addEventListener('click', () => {
document.getElementById('importInput').click();
});
// HTML Bookmark Import
document.getElementById('importInput').addEventListener('change', async e => {
const file = e.target.files[0];
if (!file) return;
const imported = parseBookmarkHtml(await file.text());
if (imported.length === 0) { alert('Keine Bookmarks gefunden.'); return; }
boards = [...boards, ...imported];
await saveBoards();
renderBoards();
e.target.value = '';
alert(`${imported.length} Board(s) mit ${imported.reduce((s,b) => s + b.bookmarks.length, 0)} Bookmarks importiert.`);
});
// Add Board Modal
document.getElementById('btnCancelBoard').addEventListener('click', () => closeModal('addBoardOverlay'));
document.getElementById('addBoardOverlay').addEventListener('click', e => {
if (e.target === document.getElementById('addBoardOverlay')) closeModal('addBoardOverlay');
});
document.getElementById('btnConfirmBoard').addEventListener('click', async () => {
const name = document.getElementById('newBoardName').value.trim();
if (!name) return;
boards.push({ id: uid(), title: name, bookmarks: [] });
await saveBoards();
renderBoards();
closeModal('addBoardOverlay');
});
document.getElementById('newBoardName').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('btnConfirmBoard').click();
if (e.key === 'Escape') closeModal('addBoardOverlay');
});
// Add Bookmark Modal
document.getElementById('btnCancelBookmark').addEventListener('click', () => closeModal('addBookmarkOverlay'));
document.getElementById('addBookmarkOverlay').addEventListener('click', e => {
if (e.target === document.getElementById('addBookmarkOverlay')) closeModal('addBookmarkOverlay');
});
document.getElementById('btnConfirmBookmark').addEventListener('click', async () => {
const title = document.getElementById('newBmTitle').value.trim();
const url = document.getElementById('newBmUrl').value.trim();
const desc = document.getElementById('newBmDesc').value.trim();
if (!title || !url) return;
try { new URL(url); } catch { alert('Ungültige URL. Bitte mit https:// beginnen.'); return; }
const board = boards.find(b => b.id === pendingBookmarkBoardId);
if (!board) return;
board.bookmarks.push({ id: uid(), title, url, desc });
await saveBoards();
renderBoards();
closeModal('addBookmarkOverlay');
});
document.getElementById('newBmUrl').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('btnConfirmBookmark').click();
if (e.key === 'Escape') closeModal('addBookmarkOverlay');
});
// Rename Modal
document.getElementById('btnCancelRename').addEventListener('click', () => closeModal('renameOverlay'));
document.getElementById('renameOverlay').addEventListener('click', e => {
if (e.target === document.getElementById('renameOverlay')) closeModal('renameOverlay');
});
document.getElementById('btnConfirmRename').addEventListener('click', () => {
const val = document.getElementById('renameInput').value.trim();
if (pendingRenameCallback) pendingRenameCallback(val);
pendingRenameCallback = null;
closeModal('renameOverlay');
});
document.getElementById('renameInput').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('btnConfirmRename').click();
if (e.key === 'Escape') closeModal('renameOverlay');
});
}
document.addEventListener('DOMContentLoaded', init);
+276
View File
@@ -0,0 +1,276 @@
/* =============================================
HELLION NEWTAB — boards.js
Board & Bookmark Rendering, Modals
============================================= */
let pendingBookmarkBoardId = null;
let pendingRenameCallback = null;
// ---- RENDER ----
function renderBoards() {
const wrapper = document.getElementById('boardsWrapper');
wrapper.innerHTML = '';
if (boards.length === 0) {
wrapper.innerHTML = `<div class="empty-state">
No boards yet. Click <strong style="color:var(--accent)">+ Board</strong> to create one,
or use <strong style="color:var(--accent)">Import</strong> to load your browser bookmarks.
</div>`;
return;
}
boards.forEach(board => wrapper.appendChild(createBoardEl(board)));
initBoardDragDrop();
}
function createBoardEl(board) {
const div = document.createElement('div');
div.className = 'board' + (board.blurred ? ' blurred' : '');
div.dataset.boardId = board.id;
// Header
const header = document.createElement('div');
header.className = 'board-header';
header.innerHTML = `
<span class="board-drag-handle" title="Board verschieben">
<svg width="10" height="14" viewBox="0 0 10 14" fill="currentColor">
<circle cx="2" cy="2" r="1.5"/><circle cx="8" cy="2" r="1.5"/>
<circle cx="2" cy="7" r="1.5"/><circle cx="8" cy="7" r="1.5"/>
<circle cx="2" cy="12" r="1.5"/><circle cx="8" cy="12" r="1.5"/>
</svg>
</span>
<span class="board-title" title="${escHtml(board.title)}">${escHtml(board.title)}</span>
<div class="board-actions">
<button class="board-action-btn btn-blur-board" title="${board.blurred ? 'Unblur' : 'Blur (privat)'}">🔒</button>
<button class="board-action-btn btn-rename-board" title="Umbenennen">✎</button>
<button class="board-action-btn btn-delete-board" title="Löschen">✕</button>
</div>
`;
// Blur-Overlay
const blurOverlay = document.createElement('div');
blurOverlay.className = 'board-blur-overlay';
div.appendChild(blurOverlay);
header.querySelector('.btn-blur-board').addEventListener('click', async e => {
e.stopPropagation();
board.blurred = !board.blurred;
div.classList.toggle('blurred', board.blurred);
e.currentTarget.title = board.blurred ? 'Unblur' : 'Blur (privat)';
await saveBoards();
});
blurOverlay.addEventListener('click', async () => {
board.blurred = false;
div.classList.remove('blurred');
header.querySelector('.btn-blur-board').title = 'Blur (privat)';
await saveBoards();
});
header.querySelector('.btn-rename-board').addEventListener('click', e => {
e.stopPropagation();
openRenameModal(board.title, async newName => {
if (!newName.trim()) return;
board.title = newName.trim();
await saveBoards();
renderBoards();
});
});
header.querySelector('.btn-delete-board').addEventListener('click', e => {
e.stopPropagation();
if (confirm(`Board "${board.title}" löschen?`)) {
boards = boards.filter(b => b.id !== board.id);
saveBoards().then(renderBoards);
}
});
// Bookmark List
const list = document.createElement('ul');
list.className = 'board-list';
list.dataset.boardId = board.id;
const visibleCount = settings.hideExtra ? settings.visibleCount : board.bookmarks.length;
const visible = board.bookmarks.slice(0, visibleCount);
const hidden = board.bookmarks.slice(visibleCount);
visible.forEach(bm => list.appendChild(createBmEl(bm)));
div.appendChild(header);
div.appendChild(list);
// Event Delegation für Bookmark-Klicks und -Löschungen
bindBoardListEvents(list, board);
// Show More
if (hidden.length > 0) {
let expanded = false;
let hiddenEls = [];
const showMoreBtn = document.createElement('button');
showMoreBtn.className = 'show-more-btn';
showMoreBtn.textContent = `Show ${hidden.length} more…`;
showMoreBtn.addEventListener('click', () => {
if (!expanded) {
hidden.forEach(bm => { const el = createBmEl(bm); hiddenEls.push(el); list.appendChild(el); });
showMoreBtn.textContent = 'Show less';
expanded = true;
} else {
hiddenEls.forEach(el => el.remove());
hiddenEls = [];
showMoreBtn.textContent = `Show ${hidden.length} more…`;
expanded = false;
}
});
div.appendChild(showMoreBtn);
}
// Add Bookmark
const addBtn = document.createElement('button');
addBtn.className = 'add-bm-btn';
addBtn.innerHTML = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Add link`;
addBtn.addEventListener('click', () => openAddBookmarkModal(board.id));
div.appendChild(addBtn);
initBookmarkDragDrop(list, board);
return div;
}
function createBmEl(bm) {
const li = document.createElement('li');
li.className = 'bm-item';
li.dataset.bmId = bm.id;
li.dataset.bmUrl = bm.url;
li.draggable = true;
const favicon = document.createElement('img');
favicon.className = 'bm-favicon';
favicon.width = 14;
favicon.height = 14;
favicon.src = getFaviconUrl(bm.url);
favicon.addEventListener('error', function() {
this.style.display = 'none';
this.nextElementSibling.style.display = 'flex';
});
const fallback = document.createElement('div');
fallback.className = 'bm-favicon-fallback';
fallback.style.display = 'none';
fallback.textContent = bm.title.charAt(0).toUpperCase();
const textDiv = document.createElement('div');
textDiv.className = 'bm-text';
const titleSpan = document.createElement('span');
titleSpan.className = 'bm-title';
titleSpan.title = bm.title;
titleSpan.textContent = bm.title;
const descSpan = document.createElement('span');
descSpan.className = 'bm-desc';
descSpan.textContent = bm.desc || '';
textDiv.appendChild(titleSpan);
textDiv.appendChild(descSpan);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'bm-delete';
deleteBtn.title = 'Entfernen';
deleteBtn.textContent = '✕';
li.appendChild(favicon);
li.appendChild(fallback);
li.appendChild(textDiv);
li.appendChild(deleteBtn);
return li;
}
// Event Delegation: Ein Listener pro Board-Liste statt pro Bookmark
function bindBoardListEvents(list, board) {
list.addEventListener('click', async e => {
const bmItem = e.target.closest('.bm-item');
if (!bmItem) return;
// Delete-Button geklickt
if (e.target.closest('.bm-delete')) {
e.stopPropagation();
const bmId = bmItem.dataset.bmId;
board.bookmarks = board.bookmarks.filter(b => b.id !== bmId);
await saveBoards();
renderBoards();
return;
}
// Bookmark-Link geklickt
const url = bmItem.dataset.bmUrl;
if (url) {
window.open(url, settings.newTab ? '_blank' : '_self', 'noopener,noreferrer');
}
});
}
// ---- MODALS ----
function openModal(id) { document.getElementById(id).classList.add('active'); }
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
function openAddBoardModal() {
document.getElementById('newBoardName').value = '';
openModal('addBoardOverlay');
setTimeout(() => document.getElementById('newBoardName').focus(), 50);
}
function openAddBookmarkModal(boardId) {
pendingBookmarkBoardId = boardId;
['newBmTitle','newBmUrl','newBmDesc'].forEach(id => document.getElementById(id).value = '');
openModal('addBookmarkOverlay');
setTimeout(() => document.getElementById('newBmTitle').focus(), 50);
}
function openRenameModal(currentName, callback) {
pendingRenameCallback = callback;
document.getElementById('renameInput').value = currentName;
openModal('renameOverlay');
setTimeout(() => { const i = document.getElementById('renameInput'); i.focus(); i.select(); }, 50);
}
// ---- BOOKMARK HTML IMPORT ----
function parseBookmarkHtml(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const result = [];
function parseFolder(dlEl, folderName) {
const bms = [];
dlEl.querySelectorAll(':scope > dt').forEach(dt => {
const a = dt.querySelector(':scope > a');
const h3 = dt.querySelector(':scope > h3');
if (a && a.href) {
bms.push({ id: uid(), title: a.textContent.trim() || a.href, url: a.href, desc: '' });
} else if (h3) {
const subDl = dt.querySelector(':scope > dl');
if (subDl) {
const sub = parseFolder(subDl, h3.textContent.trim());
if (sub.bookmarks.length > 0) result.push(sub);
}
}
});
return { id: uid(), title: folderName || 'Imported', bookmarks: bms };
}
const topDts = doc.querySelectorAll('body > dl > dt, body > p > dl > dt');
if (topDts.length === 0) {
const allLinks = [];
doc.querySelectorAll('a').forEach(a => {
if (a.href && !a.href.startsWith('place:'))
allLinks.push({ id: uid(), title: a.textContent.trim() || a.href, url: a.href, desc: '' });
});
if (allLinks.length > 0) result.push({ id: uid(), title: 'Imported Bookmarks', bookmarks: allLinks });
} else {
doc.querySelectorAll('body > dl > dt, body > p > dl > dt').forEach(dt => {
const h3 = dt.querySelector(':scope > h3');
const dl = dt.querySelector(':scope > dl');
if (h3 && dl) {
const folder = parseFolder(dl, h3.textContent.trim());
if (folder.bookmarks.length > 0) result.push(folder);
}
});
}
return result;
}
+55
View File
@@ -0,0 +1,55 @@
/* =============================================
HELLION NEWTAB — data.js
JSON Export / Import (Backup & Restore)
============================================= */
function initDataButtons() {
const btnExport = document.getElementById('btnExportJSON');
const btnImport = document.getElementById('btnImportJSON');
const jsonInput = document.getElementById('jsonImportInput');
if (!btnExport || !btnImport) return;
// Export
btnExport.addEventListener('click', () => {
const data = { version: '1.2.0', exported: new Date().toISOString(), boards, settings };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `hellion-newtab-backup-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
});
// Import
btnImport.addEventListener('click', () => jsonInput.click());
jsonInput.addEventListener('change', async e => {
const file = e.target.files[0];
if (!file) return;
try {
const data = JSON.parse(await file.text());
if (!Array.isArray(data.boards)) throw new Error('Ungültiges Format');
const validBoards = data.boards.filter(b => {
if (!b || typeof b.title !== 'string' || !Array.isArray(b.bookmarks)) return false;
b.id = b.id || uid();
b.blurred = !!b.blurred;
b.bookmarks = b.bookmarks.filter(bm => {
if (!bm || typeof bm.title !== 'string' || typeof bm.url !== 'string') return false;
bm.id = bm.id || uid();
bm.desc = bm.desc || '';
return true;
});
return true;
});
if (validBoards.length === 0) throw new Error('Keine gültigen Boards gefunden');
if (!confirm(`${validBoards.length} Boards importieren? Bestehende Daten bleiben erhalten.`)) return;
boards = [...boards, ...validBoards];
await saveBoards();
renderBoards();
alert(`${validBoards.length} Board(s) importiert.`);
} catch (err) {
alert('Fehler beim Import: ' + err.message);
}
e.target.value = '';
});
}
+132
View File
@@ -0,0 +1,132 @@
/* =============================================
HELLION NEWTAB — drag.js
Drag & Drop via Pointer Events
Boards: Reihenfolge per Handle
Bookmarks: Reihenfolge innerhalb eines Boards
============================================= */
// ---- BOARD DRAG (Pointer Events) ----
function initBoardDragDrop() {
const wrapper = document.getElementById('boardsWrapper');
let dragging = null;
let placeholder = null;
function getInsertTarget(clientX, clientY) {
const boardEls = Array.from(wrapper.querySelectorAll('.board:not(.dragging)'));
for (const b of boardEls) {
const r = b.getBoundingClientRect();
if (clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom) {
return { el: b, before: clientX < r.left + r.width / 2 };
}
}
return null;
}
wrapper.querySelectorAll('.board').forEach(boardEl => {
const handle = boardEl.querySelector('.board-drag-handle');
if (!handle) return;
handle.style.cursor = 'grab';
handle.addEventListener('pointerdown', e => {
e.preventDefault();
handle.setPointerCapture(e.pointerId);
handle.style.cursor = 'grabbing';
const rect = boardEl.getBoundingClientRect();
// Ghost
const ghost = boardEl.cloneNode(true);
ghost.style.cssText = `
position:fixed; left:${rect.left}px; top:${rect.top}px;
width:${rect.width}px; height:${rect.height}px;
opacity:0.75; pointer-events:none; z-index:9999;
transform:rotate(1.5deg) scale(1.02);
box-shadow:0 12px 40px rgba(0,0,0,0.6);
`;
document.body.appendChild(ghost);
// Placeholder
placeholder = document.createElement('div');
placeholder.className = 'board-placeholder';
placeholder.style.cssText = `width:${rect.width}px; height:${rect.height}px;`;
boardEl.parentNode.insertBefore(placeholder, boardEl);
boardEl.classList.add('dragging');
dragging = { el: boardEl, ghost,
offsetX: e.clientX - rect.left,
offsetY: e.clientY - rect.top
};
});
handle.addEventListener('pointermove', e => {
if (!dragging || dragging.el !== boardEl) return;
e.preventDefault();
dragging.ghost.style.left = (e.clientX - dragging.offsetX) + 'px';
dragging.ghost.style.top = (e.clientY - dragging.offsetY) + 'px';
const target = getInsertTarget(e.clientX, e.clientY);
if (target && target.el !== boardEl) {
target.before
? target.el.parentNode.insertBefore(placeholder, target.el)
: target.el.parentNode.insertBefore(placeholder, target.el.nextSibling);
}
});
handle.addEventListener('pointerup', async () => {
if (!dragging || dragging.el !== boardEl) return;
handle.style.cursor = 'grab';
placeholder.parentNode.insertBefore(boardEl, placeholder);
placeholder.remove(); placeholder = null;
boardEl.classList.remove('dragging');
dragging.ghost.remove();
dragging = null;
// Neue Reihenfolge aus DOM ablesen
const newOrder = Array.from(wrapper.querySelectorAll('.board'))
.map(el => el.dataset.boardId).filter(Boolean);
boards.sort((a, b) => newOrder.indexOf(a.id) - newOrder.indexOf(b.id));
await saveBoards();
});
handle.addEventListener('pointercancel', () => {
if (!dragging) return;
dragging.ghost.remove();
if (placeholder) { placeholder.remove(); placeholder = null; }
boardEl.classList.remove('dragging');
dragging = null;
handle.style.cursor = 'grab';
});
});
}
// ---- BOOKMARK DRAG (innerhalb eines Boards) ----
function initBookmarkDragDrop(listEl, board) {
let dragSrcBmId = null;
listEl.querySelectorAll('.bm-item').forEach(item => {
item.addEventListener('dragstart', e => {
dragSrcBmId = item.dataset.bmId;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => item.style.opacity = '0.4', 0);
});
item.addEventListener('dragend', () => { item.style.opacity = ''; });
item.addEventListener('dragover', e => {
e.preventDefault();
item.style.background = 'rgba(255,160,50,0.07)';
});
item.addEventListener('dragleave', () => { item.style.background = ''; });
item.addEventListener('drop', async e => {
e.preventDefault(); e.stopPropagation();
item.style.background = '';
const targetBmId = item.dataset.bmId;
if (!dragSrcBmId || dragSrcBmId === targetBmId) return;
const srcIdx = board.bookmarks.findIndex(b => b.id === dragSrcBmId);
const tgtIdx = board.bookmarks.findIndex(b => b.id === targetBmId);
const [moved] = board.bookmarks.splice(srcIdx, 1);
board.bookmarks.splice(tgtIdx, 0, moved);
await saveBoards();
renderBoards();
});
});
}
+41
View File
@@ -0,0 +1,41 @@
/* =============================================
HELLION NEWTAB — search.js
Suchleiste: Google / DuckDuckGo / Bing
============================================= */
function initSearch() {
const input = document.getElementById('searchInput');
const submit = document.getElementById('searchSubmit');
const toggle = document.getElementById('searchEngineToggle');
const icon = document.getElementById('searchEngineIcon');
if (!input) return;
const engines = {
google: { label: 'G', url: 'https://www.google.com/search?q=' },
ddg: { label: '⊙', url: 'https://duckduckgo.com/?q=' },
bing: { label: 'B', url: 'https://www.bing.com/search?q=' },
};
function updateIcon() {
icon.textContent = engines[settings.searchEngine]?.label ?? 'G';
}
updateIcon();
function doSearch() {
const q = input.value.trim();
if (!q) return;
const engine = engines[settings.searchEngine] ?? engines.google;
window.open(engine.url + encodeURIComponent(q), settings.newTab ? '_blank' : '_self');
input.value = '';
}
toggle.addEventListener('click', async () => {
const keys = Object.keys(engines);
settings.searchEngine = keys[(keys.indexOf(settings.searchEngine) + 1) % keys.length];
updateIcon();
await saveSettings();
});
submit.addEventListener('click', doSearch);
input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
}
+140
View File
@@ -0,0 +1,140 @@
/* =============================================
HELLION NEWTAB — settings.js
Settings Panel: Toggles, Hintergrund, Theme-Picker
============================================= */
function openSettings() {
document.getElementById('settingsPanel').classList.add('open');
document.getElementById('settingsOverlay').classList.add('active');
}
function closeSettings() {
document.getElementById('settingsPanel').classList.remove('open');
document.getElementById('settingsOverlay').classList.remove('active');
}
function applySettings() {
const body = document.body;
body.classList.toggle('compact', settings.compact);
body.classList.toggle('shorten-titles', settings.shortenTitles);
body.classList.toggle('show-desc', settings.showDesc);
document.getElementById('settingCompact').checked = settings.compact;
document.getElementById('settingShorten').checked = settings.shortenTitles;
document.getElementById('settingNewTab').checked = settings.newTab;
document.getElementById('settingShowDesc').checked = settings.showDesc;
document.getElementById('settingHideExtra').checked = settings.hideExtra;
document.getElementById('settingVisibleCount').value = String(settings.visibleCount);
document.getElementById('visibleCountRow').style.opacity = settings.hideExtra ? '1' : '0.4';
// showSearch: undefined (alter Save) → true
if (settings.showSearch === undefined) settings.showSearch = true;
const searchWrapper = document.getElementById('searchBarWrapper');
if (searchWrapper) searchWrapper.classList.toggle('hidden', !settings.showSearch);
const showSearchEl = document.getElementById('settingShowSearch');
if (showSearchEl) showSearchEl.checked = settings.showSearch;
applyTheme(settings.theme || 'astronaut', !!settings.bgUrl);
if (settings.bgUrl) {
document.getElementById('bgLayer').style.backgroundImage = `url('${settings.bgUrl}')`;
}
}
function bindSettingsEvents() {
// Panel
document.getElementById('settingsOverlay').addEventListener('click', closeSettings);
document.getElementById('btnCloseSettings').addEventListener('click', closeSettings);
document.getElementById('btnSettings').addEventListener('click', openSettings);
// Theme-Picker
document.querySelectorAll('.theme-card').forEach(card => {
card.addEventListener('click', async () => {
const name = card.dataset.value;
if (!name || name === settings.theme) return;
settings.theme = name;
settings.bgUrl = '';
document.getElementById('bgUrlInput').value = '';
applyTheme(name, false);
await saveSettings();
});
});
// Toggles
const toggleMap = {
settingCompact: v => { settings.compact = v; document.body.classList.toggle('compact', v); },
settingShorten: v => { settings.shortenTitles = v; document.body.classList.toggle('shorten-titles', v); },
settingNewTab: v => { settings.newTab = v; },
settingShowDesc: v => { settings.showDesc = v; document.body.classList.toggle('show-desc', v); },
settingHideExtra: v => {
settings.hideExtra = v;
document.getElementById('visibleCountRow').style.opacity = v ? '1' : '0.4';
renderBoards();
},
settingShowSearch: v => {
settings.showSearch = v;
document.getElementById('searchBarWrapper').classList.toggle('hidden', !v);
}
};
Object.entries(toggleMap).forEach(([id, fn]) => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('change', async e => {
fn(e.target.checked);
await saveSettings();
});
}
});
document.getElementById('settingVisibleCount').addEventListener('change', async e => {
settings.visibleCount = parseInt(e.target.value, 10);
await saveSettings();
renderBoards();
});
// Background URL
document.getElementById('btnChangeBg').addEventListener('click', () => {
const row = document.getElementById('bgInputRow');
row.style.display = row.style.display === 'none' ? 'flex' : 'none';
});
document.getElementById('btnApplyBg').addEventListener('click', async () => {
const url = document.getElementById('bgUrlInput').value.trim();
settings.bgUrl = url;
document.getElementById('bgLayer').style.backgroundImage = url ? `url('${url}')` : '';
await saveSettings();
document.getElementById('bgInputRow').style.display = 'none';
});
// Background File Upload
document.getElementById('btnBgFile').addEventListener('click', () => {
document.getElementById('bgFileInput').click();
});
document.getElementById('bgFileInput').addEventListener('change', e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async ev => {
settings.bgUrl = ev.target.result;
document.getElementById('bgLayer').style.backgroundImage = `url('${ev.target.result}')`;
await saveSettings();
};
reader.onerror = () => {
alert('Fehler beim Lesen der Datei. Bitte eine andere Datei wählen.');
};
reader.readAsDataURL(file);
});
// Reset All
document.getElementById('btnResetAll').addEventListener('click', async () => {
if (!confirm('Wirklich alle Boards und Einstellungen löschen? Nicht rückgängig machbar.')) return;
boards = [];
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'astronaut',
showSearch: true, searchEngine: 'google' };
await saveBoards();
await saveSettings();
applySettings();
renderBoards();
closeSettings();
});
}
+63
View File
@@ -0,0 +1,63 @@
/* =============================================
HELLION NEWTAB — state.js
Globaler State, Default-Werte, Hilfsfunktionen
============================================= */
let boards = [];
let settings = {
compact: false,
shortenTitles: false,
newTab: true,
showDesc: false,
hideExtra: false,
visibleCount: 10,
bgUrl: '',
theme: 'astronaut',
showSearch: true,
searchEngine: 'google'
};
function uid() {
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
}
function escHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function getFaviconUrl(url) {
try {
const u = new URL(url);
return `https://www.google.com/s2/favicons?domain=${u.hostname}&sz=16`;
} catch {
return '';
}
}
function getDefaultBoards() {
return [
{
id: uid(),
title: 'Getting Started',
bookmarks: [
{ id: uid(), title: 'GitHub', url: 'https://github.com', desc: '' },
{ id: uid(), title: 'MDN Web Docs', url: 'https://developer.mozilla.org', desc: '' },
{ id: uid(), title: 'Next.js Docs', url: 'https://nextjs.org/docs', desc: '' },
],
blurred: false
}
];
}
async function saveBoards() {
await Store.set('boards', boards);
}
async function saveSettings() {
await Store.set('settings', settings);
}
+78
View File
@@ -0,0 +1,78 @@
/* =============================================
HELLION NEWTAB — sticky.js
Sticky Note: draggable, persistent
============================================= */
function initStickyNote() {
const note = document.getElementById('stickyNote');
const body = document.getElementById('stickyNoteBody');
const header = document.getElementById('stickyNoteHeader');
const btnClose = document.getElementById('stickyNoteClose');
const btnNote = document.getElementById('btnNote');
if (!note || !body) return;
// Gespeicherten Text & Position laden
Store.get('stickyNote').then(val => { if (val) body.value = val; });
Store.get('stickyPos').then(pos => {
if (pos) {
note.style.right = 'auto'; note.style.bottom = 'auto';
note.style.left = pos.x + 'px'; note.style.top = pos.y + 'px';
}
});
Store.get('stickyVisible').then(vis => { if (vis) note.classList.add('visible'); });
// Text speichern (debounced)
let saveTimer;
body.addEventListener('input', () => {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => Store.set('stickyNote', body.value), 600);
});
// Toggle
btnNote.addEventListener('click', async () => {
const visible = note.classList.toggle('visible');
await Store.set('stickyVisible', visible);
if (visible) body.focus();
});
btnClose.addEventListener('click', async () => {
note.classList.remove('visible');
await Store.set('stickyVisible', false);
});
// Drag via Pointer Events
header.style.cursor = 'grab';
header.addEventListener('pointerdown', e => {
if (e.target === btnClose || e.target.closest('.sticky-note-close')) return;
e.preventDefault();
header.setPointerCapture(e.pointerId);
header.style.cursor = 'grabbing';
const rect = note.getBoundingClientRect();
note.style.right = 'auto'; note.style.bottom = 'auto';
note.style.left = rect.left + 'px'; note.style.top = rect.top + 'px';
const offX = e.clientX - rect.left;
const offY = e.clientY - rect.top;
function onMove(ev) {
const maxX = window.innerWidth - note.offsetWidth;
const maxY = window.innerHeight - note.offsetHeight;
note.style.left = Math.max(0, Math.min(maxX, ev.clientX - offX)) + 'px';
note.style.top = Math.max(48, Math.min(maxY, ev.clientY - offY)) + 'px';
}
async function onUp() {
header.style.cursor = 'grab';
header.releasePointerCapture(e.pointerId);
header.removeEventListener('pointermove', onMove);
header.removeEventListener('pointerup', onUp);
await Store.set('stickyPos', {
x: parseFloat(note.style.left),
y: parseFloat(note.style.top)
});
}
header.addEventListener('pointermove', onMove);
header.addEventListener('pointerup', onUp);
});
}
+59
View File
@@ -0,0 +1,59 @@
/* =============================================
HELLION NEWTAB — storage.js
Abstraction Layer: chrome.storage.local / localStorage
============================================= */
const Store = {
QUOTA_WARNING_BYTES: 8 * 1024 * 1024, // 8 MB Warnung (Limit ist 10 MB)
get(key) {
return new Promise(resolve => {
if (typeof chrome !== 'undefined' && chrome.storage) {
chrome.storage.local.get([key], r => resolve(r[key] ?? null));
} else {
try { resolve(JSON.parse(localStorage.getItem(key))); }
catch { resolve(null); }
}
});
},
set(key, value) {
return new Promise((resolve, reject) => {
if (typeof chrome !== 'undefined' && chrome.storage) {
chrome.storage.local.set({ [key]: value }, () => {
if (chrome.runtime.lastError) {
console.error('Storage-Fehler:', chrome.runtime.lastError.message);
alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.');
reject(new Error(chrome.runtime.lastError.message));
return;
}
resolve();
});
} else {
try {
localStorage.setItem(key, JSON.stringify(value));
resolve();
} catch (e) {
console.error('Storage-Fehler:', e.message);
alert('Speicher voll! Bitte lösche alte Boards oder das Hintergrundbild, um Platz zu schaffen.');
reject(e);
}
}
});
},
async checkQuota() {
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local.getBytesInUse) {
return new Promise(resolve => {
chrome.storage.local.getBytesInUse(null, bytes => {
if (bytes > Store.QUOTA_WARNING_BYTES) {
const usedMB = (bytes / 1024 / 1024).toFixed(1);
console.warn('Storage-Warnung: ' + usedMB + ' MB von 10 MB belegt.');
}
resolve(bytes);
});
});
}
return 0;
}
};
+30
View File
@@ -0,0 +1,30 @@
/* =============================================
HELLION NEWTAB — themes.js
Theme-Definitionen & Anwendungslogik
============================================= */
const THEMES = {
'astronaut': { bg: 'assets/themes/bg-astronaut.jpg' },
'cosmic-clock': { bg: 'assets/themes/bg-cosmic-clock.jpg' },
'void-mage': { bg: 'assets/themes/bg-void-mage.jpg' },
'merchantman': { bg: 'assets/themes/bg-merchantman.webp' },
'julia-jin': { bg: 'assets/themes/bg-julia-jin.png' },
'sc-sunset': { bg: 'assets/themes/bg-sc-sunset.jpg' },
'hellion-hud': { bg: 'assets/themes/bg-hellion-hud.png' },
'hellion-energy': { bg: 'assets/themes/bg-hellion-energy.jpg' }
};
function applyTheme(themeName, skipBgOverride) {
const theme = THEMES[themeName];
if (!theme) return;
document.documentElement.setAttribute('data-theme', themeName);
if (!skipBgOverride) {
document.getElementById('bgLayer').style.backgroundImage = `url('${theme.bg}')`;
}
document.querySelectorAll('.theme-card').forEach(card => {
card.classList.toggle('active', card.dataset.value === themeName);
});
}