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:
+119
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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(); });
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user