This commit is contained in:
2026-01-17 17:07:46 +01:00
commit e73ddd52ad
22 changed files with 2609 additions and 0 deletions

315
Extension/popup/popup.css Normal file
View File

@@ -0,0 +1,315 @@
/* EShip Extension Popup Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
font-size: 14px;
color: #2D2D31;
background: #FAFAFB;
min-width: 320px;
max-width: 400px;
}
.container {
padding: 16px;
}
/* Header */
header {
text-align: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #EDEDF0;
}
header h1 {
font-size: 20px;
font-weight: 600;
color: #FD366E;
}
/* States */
.state {
display: block;
}
.state.hidden {
display: none;
}
/* Loading */
#loading {
text-align: center;
padding: 40px 0;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid #EDEDF0;
border-top-color: #FD366E;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Form */
h2 {
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
.form-group {
margin-bottom: 14px;
}
.form-group label {
display: block;
font-size: 12px;
font-weight: 500;
color: #97979B;
margin-bottom: 4px;
}
.form-group input {
width: 100%;
padding: 10px 12px;
border: 1px solid #EDEDF0;
border-radius: 6px;
font-size: 14px;
background: #fff;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input:focus {
outline: none;
border-color: #FD366E;
box-shadow: 0 0 0 3px rgba(253, 54, 110, 0.1);
}
/* Error Message */
.error {
background: #FEE2E2;
color: #B91C1C;
padding: 10px 12px;
border-radius: 6px;
font-size: 13px;
margin-bottom: 14px;
}
.error.hidden {
display: none;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
}
.btn:active {
transform: scale(0.98);
}
.btn-primary {
width: 100%;
background: #FD366E;
color: white;
}
.btn-primary:hover {
background: #E8305F;
}
.btn-primary:disabled {
background: #FDA4B8;
cursor: not-allowed;
}
.btn-secondary {
background: #EDEDF0;
color: #2D2D31;
}
.btn-secondary:hover {
background: #D8D8DB;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.btn-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.btn-spinner.hidden {
display: none;
}
/* User Info */
.user-info {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: #fff;
border: 1px solid #EDEDF0;
border-radius: 8px;
margin-bottom: 20px;
}
.user-icon {
font-size: 20px;
}
#user-email {
flex: 1;
font-size: 13px;
color: #97979B;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Tools List */
#tools-list {
background: #fff;
border: 1px solid #EDEDF0;
border-radius: 8px;
margin-bottom: 16px;
}
.tool-item {
padding: 12px;
border-bottom: 1px solid #EDEDF0;
}
.tool-item:last-child {
border-bottom: none;
}
.tool-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.tool-name {
font-weight: 500;
}
/* Toggle Switch */
.toggle {
position: relative;
width: 44px;
height: 24px;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #D8D8DB;
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle input:checked + .toggle-slider {
background-color: #FD366E;
}
.toggle input:checked + .toggle-slider:before {
transform: translateX(20px);
}
/* Tool Settings */
.tool-settings {
display: none;
padding-top: 8px;
}
.tool-settings.visible {
display: block;
}
.setting-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.setting-row:last-child {
margin-bottom: 0;
}
.setting-row label {
font-size: 12px;
color: #97979B;
min-width: 80px;
}
.setting-row input {
flex: 1;
padding: 6px 8px;
border: 1px solid #EDEDF0;
border-radius: 4px;
font-size: 12px;
}
.setting-row input:focus {
outline: none;
border-color: #FD366E;
}
/* Actions */
.actions {
margin-top: 16px;
}

View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=400, initial-scale=1.0">
<title>EShip Extension</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<header>
<h1>EShip</h1>
</header>
<!-- Loading State -->
<div id="loading" class="state">
<div class="spinner"></div>
<p>Laden...</p>
</div>
<!-- Login Form (logged out state) -->
<div id="login-form" class="state hidden">
<h2>Anmelden</h2>
<form id="auth-form">
<div class="form-group">
<label for="email">E-Mail</label>
<input type="email" id="email" name="email" required autocomplete="email">
</div>
<div class="form-group">
<label for="password">Passwort</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<div id="error-message" class="error hidden"></div>
<button type="submit" id="login-btn" class="btn btn-primary">
<span class="btn-text">Anmelden</span>
<span class="btn-spinner hidden"></span>
</button>
</form>
</div>
<!-- Tools Menu (logged in state) -->
<div id="tools-menu" class="state hidden">
<div class="user-info">
<span class="user-icon">&#128100;</span>
<span id="user-email"></span>
<button id="logout-btn" class="btn btn-small btn-secondary">Abmelden</button>
</div>
<h2>Tools</h2>
<div id="tools-list">
<!-- Tools will be rendered here dynamically -->
</div>
<div class="actions">
<button id="open-site-btn" class="btn btn-primary">Website oeffnen</button>
</div>
</div>
</div>
<script src="popup.js" type="module"></script>
</body>
</html>

319
Extension/popup/popup.js Normal file
View File

@@ -0,0 +1,319 @@
// EShip Extension Popup Logic
// DOM Elements
const loadingEl = document.getElementById('loading');
const loginFormEl = document.getElementById('login-form');
const toolsMenuEl = document.getElementById('tools-menu');
const authForm = document.getElementById('auth-form');
const emailInput = document.getElementById('email');
const passwordInput = document.getElementById('password');
const errorMessage = document.getElementById('error-message');
const loginBtn = document.getElementById('login-btn');
const logoutBtn = document.getElementById('logout-btn');
const userEmailEl = document.getElementById('user-email');
const toolsListEl = document.getElementById('tools-list');
const openSiteBtn = document.getElementById('open-site-btn');
// Protected site URL (should match config.js)
const PROTECTED_SITE_URL = 'http://localhost:5173';
// State
let currentUser = null;
let tools = [];
// Initialize popup
document.addEventListener('DOMContentLoaded', init);
async function init() {
showState('loading');
try {
// Check if service worker is available
if (!chrome.runtime || !chrome.runtime.id) {
throw new Error('Chrome Runtime nicht verfuegbar');
}
// First, test if service worker is alive
console.log('Testing service worker...');
const pingResponse = await sendMessage({ action: 'PING' });
console.log('PING response:', pingResponse);
if (!pingResponse) {
throw new Error('Service Worker antwortet nicht. Bitte Extension in chrome://extensions neu laden und Service Worker Konsole pruefen.');
}
if (!pingResponse.success) {
throw new Error('Service Worker Fehler: ' + (pingResponse.error || 'Unbekannter Fehler'));
}
console.log('Service Worker ist aktiv!');
console.log('Checking auth status...');
// Check current auth status
const response = await sendMessage({ action: 'CHECK_AUTH' });
if (!response) {
throw new Error('Keine Antwort vom Service Worker');
}
console.log('Auth response:', response);
// If there's an error, show it but still allow login
if (response.error && !response.success) {
console.error('Service Worker Error:', response.error);
showError('Hinweis: ' + response.error + ' - Login sollte trotzdem funktionieren.');
}
if (response.success && response.authenticated) {
currentUser = response.user;
await loadTools();
showLoggedInState();
} else {
// Not authenticated - show login form
showState('login-form');
}
} catch (error) {
console.error('Init error:', error);
showError('Fehler beim Laden: ' + error.message + '. Bitte Extension in chrome://extensions neu laden.');
showState('login-form');
}
}
// Show a specific state (loading, login-form, tools-menu)
function showState(stateId) {
loadingEl.classList.add('hidden');
loginFormEl.classList.add('hidden');
toolsMenuEl.classList.add('hidden');
document.getElementById(stateId).classList.remove('hidden');
}
// Show logged in state with user info
function showLoggedInState() {
userEmailEl.textContent = currentUser.email || currentUser.name || 'Benutzer';
renderTools();
showState('tools-menu');
}
// Send message to service worker
function sendMessage(message) {
return new Promise((resolve) => {
// Add timeout to prevent hanging
const timeout = setTimeout(() => {
console.error('Message timeout:', message.action);
resolve({
success: false,
error: 'Service Worker antwortet nicht. Bitte Extension in chrome://extensions neu laden und Service Worker Konsole pruefen.'
});
}, 2000); // Reduced timeout to 2 seconds for faster feedback
try {
if (!chrome.runtime || !chrome.runtime.id) {
clearTimeout(timeout);
resolve({ success: false, error: 'Chrome Runtime nicht verfuegbar' });
return;
}
chrome.runtime.sendMessage(message, (response) => {
clearTimeout(timeout);
if (chrome.runtime.lastError) {
console.error('Chrome runtime error:', chrome.runtime.lastError);
resolve({
success: false,
error: chrome.runtime.lastError.message || 'Service Worker Fehler'
});
} else {
console.log('Response received:', response);
resolve(response || { success: false, error: 'Keine Antwort vom Service Worker' });
}
});
} catch (error) {
clearTimeout(timeout);
console.error('Send message error:', error);
resolve({ success: false, error: error.message || 'Fehler beim Senden der Nachricht' });
}
});
}
// Login form submission
authForm.addEventListener('submit', async (e) => {
e.preventDefault();
const email = emailInput.value.trim();
const password = passwordInput.value;
if (!email || !password) {
showError('Bitte E-Mail und Passwort eingeben');
return;
}
setLoginLoading(true);
hideError();
const response = await sendMessage({
action: 'LOGIN',
email,
password
});
setLoginLoading(false);
if (response.success) {
currentUser = response.user;
await loadTools();
showLoggedInState();
// Open protected site after successful login
openProtectedSite();
} else {
showError(response.error || 'Anmeldung fehlgeschlagen');
}
});
// Logout button
logoutBtn.addEventListener('click', async () => {
const response = await sendMessage({ action: 'LOGOUT' });
if (response.success) {
currentUser = null;
tools = [];
emailInput.value = '';
passwordInput.value = '';
showState('login-form');
}
});
// Open site button
openSiteBtn.addEventListener('click', () => {
openProtectedSite();
});
// Open protected site in new tab
function openProtectedSite() {
chrome.tabs.create({ url: PROTECTED_SITE_URL });
}
// Load tools settings
async function loadTools() {
const response = await sendMessage({ action: 'GET_SETTINGS' });
if (response.success) {
tools = response.tools;
}
}
// Render tools list
function renderTools() {
toolsListEl.innerHTML = '';
tools.forEach((tool, index) => {
const toolEl = document.createElement('div');
toolEl.className = 'tool-item';
toolEl.innerHTML = `
<div class="tool-header">
<span class="tool-name">${escapeHtml(tool.name)}</span>
<label class="toggle">
<input type="checkbox" data-tool-id="${tool.id}" ${tool.enabled ? 'checked' : ''}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="tool-settings ${tool.enabled ? 'visible' : ''}" id="settings-${tool.id}">
${renderToolSettings(tool)}
</div>
`;
toolsListEl.appendChild(toolEl);
// Toggle event listener
const toggle = toolEl.querySelector('input[type="checkbox"]');
toggle.addEventListener('change', (e) => {
handleToolToggle(tool.id, e.target.checked);
});
// Settings change listeners
const settingsInputs = toolEl.querySelectorAll('.setting-row input');
settingsInputs.forEach(input => {
input.addEventListener('change', (e) => {
handleSettingChange(tool.id, e.target.dataset.setting, e.target.value);
});
});
});
}
// Render settings inputs for a tool
function renderToolSettings(tool) {
if (!tool.settings) return '';
let html = '';
for (const [key, value] of Object.entries(tool.settings)) {
html += `
<div class="setting-row">
<label>${escapeHtml(formatSettingName(key))}</label>
<input type="text" data-setting="${key}" value="${escapeHtml(value)}">
</div>
`;
}
return html;
}
// Format setting key to readable name
function formatSettingName(key) {
return key
.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase())
.replace(/_/g, ' ');
}
// Handle tool toggle
async function handleToolToggle(toolId, enabled) {
const tool = tools.find(t => t.id === toolId);
if (tool) {
tool.enabled = enabled;
// Show/hide settings
const settingsEl = document.getElementById(`settings-${toolId}`);
if (settingsEl) {
settingsEl.classList.toggle('visible', enabled);
}
await saveTools();
}
}
// Handle setting value change
async function handleSettingChange(toolId, settingKey, value) {
const tool = tools.find(t => t.id === toolId);
if (tool && tool.settings) {
tool.settings[settingKey] = value;
await saveTools();
}
}
// Save tools to storage
async function saveTools() {
await sendMessage({ action: 'SAVE_SETTINGS', settings: tools });
}
// Show error message
function showError(message) {
errorMessage.textContent = message;
errorMessage.classList.remove('hidden');
}
// Hide error message
function hideError() {
errorMessage.classList.add('hidden');
}
// Set login button loading state
function setLoginLoading(loading) {
loginBtn.disabled = loading;
loginBtn.querySelector('.btn-text').classList.toggle('hidden', loading);
loginBtn.querySelector('.btn-spinner').classList.toggle('hidden', !loading);
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}