fix(gui,android): fix Wayland icon, add Android launcher mipmaps and bundle offline assets
@ -12,6 +12,8 @@
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:label="BenzCloud Client"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/AppTheme"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
|
||||
254
android/app/src/main/assets/app.js
Normal file
@ -0,0 +1,254 @@
|
||||
// BenzCloud Client App Controller
|
||||
let currentLang = localStorage.getItem("benzcloud_client_lang") || "de";
|
||||
let clientProfile = null;
|
||||
|
||||
const i18n = {
|
||||
de: {
|
||||
pair_badge: "🔗 Client-Ersteinrichtung",
|
||||
pair_title: "Mit BenzCloud-Server verbinden",
|
||||
pair_desc: "Gib die lokale IP-Adresse deines BenzCloud-Servers und deine Anmeldedaten ein. Das Mesh-VPN (Nebula) und der DNS-Server konfigurieren sich automatisch.",
|
||||
lbl_server_url: "Server LAN-Adresse:",
|
||||
lbl_user: "Benutzername:",
|
||||
lbl_pass: "Passwort:",
|
||||
btn_pair: "🚀 Gerät sicher koppeln & verbinden",
|
||||
status_connected: "Verbunden mit BenzCloud",
|
||||
services_heading: "Deine Enterprise-Dienste (Direktzugriff)",
|
||||
card_drive_title: "BenzCloud Drive",
|
||||
card_drive_desc: "Dateien hochladen, teilen und synchronisieren.",
|
||||
card_mail_title: "BenzCloud Mail",
|
||||
card_mail_desc: "Internes Webmail & Thunderbird-kompatibles Postfach.",
|
||||
card_chat_title: "BenzCloud Chat",
|
||||
card_chat_desc: "Echtzeit-Teamkommunikation & Direktnachrichten.",
|
||||
card_web_title: "Webseiten & Portal",
|
||||
card_web_desc: "Gehostete Firmen- und Team-Webseiten aufrufen.",
|
||||
btn_unpair: "🔌 Entkoppeln / Gerät trennen",
|
||||
pairing_in_progress: "⚙️ Kopplung läuft...",
|
||||
confirm_unpair: "Möchtest du dieses Gerät wirklich vom BenzCloud-Server trennen?"
|
||||
},
|
||||
en: {
|
||||
pair_badge: "🔗 Initial Client Setup",
|
||||
pair_title: "Connect to BenzCloud Server",
|
||||
pair_desc: "Enter your local BenzCloud server IP and your credentials. Mesh-VPN (Nebula) and DNS server will configure automatically.",
|
||||
lbl_server_url: "Server LAN Address:",
|
||||
lbl_user: "Username:",
|
||||
lbl_pass: "Password:",
|
||||
btn_pair: "🚀 Securely Pair & Connect Device",
|
||||
status_connected: "Connected to BenzCloud",
|
||||
services_heading: "Your Enterprise Services (Direct Access)",
|
||||
card_drive_title: "BenzCloud Drive",
|
||||
card_drive_desc: "Upload, share, and synchronize cloud files.",
|
||||
card_mail_title: "BenzCloud Mail",
|
||||
card_mail_desc: "Internal webmail & Thunderbird-compatible mailbox.",
|
||||
card_chat_title: "BenzCloud Chat",
|
||||
card_chat_desc: "Real-time team communication & direct messages.",
|
||||
card_web_title: "Websites & Portal",
|
||||
card_web_desc: "Browse hosted team and company websites.",
|
||||
btn_unpair: "🔌 Disconnect / Unpair Device",
|
||||
pairing_in_progress: "⚙️ Pairing in progress...",
|
||||
confirm_unpair: "Are you sure you want to disconnect this device from BenzCloud?"
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
applyLanguage(currentLang);
|
||||
fetchClientProfile();
|
||||
});
|
||||
|
||||
function setLanguage(lang) {
|
||||
currentLang = lang;
|
||||
localStorage.setItem("benzcloud_client_lang", lang);
|
||||
applyLanguage(lang);
|
||||
}
|
||||
|
||||
function applyLanguage(lang) {
|
||||
document.documentElement.lang = lang;
|
||||
document.querySelectorAll("[data-i18n]").forEach(el => {
|
||||
const key = el.getAttribute("data-i18n");
|
||||
if (i18n[lang] && i18n[lang][key]) {
|
||||
el.textContent = i18n[lang][key];
|
||||
}
|
||||
});
|
||||
document.getElementById("langDE").classList.toggle("active", lang === "de");
|
||||
document.getElementById("langEN").classList.toggle("active", lang === "en");
|
||||
}
|
||||
|
||||
async function fetchClientProfile() {
|
||||
if (window.location.protocol === "file:") {
|
||||
const stored = localStorage.getItem("benzcloud_profile");
|
||||
if (stored) {
|
||||
try {
|
||||
clientProfile = JSON.parse(stored);
|
||||
showConnectedView(clientProfile);
|
||||
return;
|
||||
} catch (e) {
|
||||
localStorage.removeItem("benzcloud_profile");
|
||||
}
|
||||
}
|
||||
showPairingView();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/profile");
|
||||
if (!res.ok) {
|
||||
const stored = localStorage.getItem("benzcloud_profile");
|
||||
if (stored) {
|
||||
clientProfile = JSON.parse(stored);
|
||||
showConnectedView(clientProfile);
|
||||
return;
|
||||
}
|
||||
showPairingView();
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!data.paired) {
|
||||
showPairingView();
|
||||
return;
|
||||
}
|
||||
clientProfile = data;
|
||||
showConnectedView(data);
|
||||
} catch (err) {
|
||||
const stored = localStorage.getItem("benzcloud_profile");
|
||||
if (stored) {
|
||||
try {
|
||||
clientProfile = JSON.parse(stored);
|
||||
showConnectedView(clientProfile);
|
||||
return;
|
||||
} catch (e) {
|
||||
localStorage.removeItem("benzcloud_profile");
|
||||
}
|
||||
}
|
||||
showPairingView();
|
||||
}
|
||||
}
|
||||
|
||||
function showPairingView() {
|
||||
document.getElementById("pairingView").style.display = "block";
|
||||
document.getElementById("connectedView").style.display = "none";
|
||||
}
|
||||
|
||||
function showConnectedView(data) {
|
||||
document.getElementById("pairingView").style.display = "none";
|
||||
document.getElementById("connectedView").style.display = "block";
|
||||
|
||||
document.getElementById("dispDomain").textContent = data.base_domain || "intern";
|
||||
document.getElementById("dispOverlayIP").textContent = data.overlay_ip || "10.42.0.2";
|
||||
document.getElementById("dispLighthouse").textContent = data.server_vpn_ip || "10.42.0.1";
|
||||
|
||||
const domain = data.base_domain;
|
||||
if (data.server_url) {
|
||||
document.getElementById("subDrive").textContent = `${data.server_url}/#drive`;
|
||||
document.getElementById("cardDrive").href = `${data.server_url}/#drive`;
|
||||
|
||||
document.getElementById("subMail").textContent = `${data.server_url}/#mail`;
|
||||
document.getElementById("cardMail").href = `${data.server_url}/#mail`;
|
||||
|
||||
document.getElementById("subChat").textContent = `${data.server_url}/#chat`;
|
||||
document.getElementById("cardChat").href = `${data.server_url}/#chat`;
|
||||
|
||||
document.getElementById("subWeb").textContent = data.server_url;
|
||||
document.getElementById("cardWeb").href = data.server_url;
|
||||
} else {
|
||||
document.getElementById("subDrive").textContent = `http://drive.${domain}`;
|
||||
document.getElementById("cardDrive").href = `http://drive.${domain}`;
|
||||
|
||||
document.getElementById("subMail").textContent = `http://mail.${domain}`;
|
||||
document.getElementById("cardMail").href = `http://mail.${domain}`;
|
||||
|
||||
document.getElementById("subChat").textContent = `http://chat.${domain}`;
|
||||
document.getElementById("cardChat").href = `http://chat.${domain}`;
|
||||
|
||||
document.getElementById("subWeb").textContent = `http://${domain}`;
|
||||
document.getElementById("cardWeb").href = `http://${domain}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPairing(e) {
|
||||
e.preventDefault();
|
||||
let serverUrl = document.getElementById("serverUrl").value.trim();
|
||||
const username = document.getElementById("pairUser").value.trim();
|
||||
const password = document.getElementById("pairPass").value;
|
||||
|
||||
if (!serverUrl.startsWith("http://") && !serverUrl.startsWith("https://")) {
|
||||
serverUrl = "http://" + serverUrl;
|
||||
}
|
||||
serverUrl = serverUrl.replace(/\/+$/, "");
|
||||
|
||||
const btn = document.getElementById("btnPair");
|
||||
btn.disabled = true;
|
||||
btn.textContent = i18n[currentLang].pairing_in_progress;
|
||||
|
||||
if (window.location.protocol === "file:") {
|
||||
try {
|
||||
const res = await fetch(`${serverUrl}/api/pair`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert((currentLang === "de" ? "Kopplung fehlgeschlagen: " : "Pairing failed: ") + (data.error || "Serverfehler"));
|
||||
btn.disabled = false;
|
||||
btn.textContent = i18n[currentLang].btn_pair;
|
||||
return;
|
||||
}
|
||||
const prof = {
|
||||
paired: true,
|
||||
server_url: serverUrl,
|
||||
username: data.username || username,
|
||||
base_domain: data.base_domain || "intern",
|
||||
overlay_ip: data.overlay_ip || "10.42.0.2",
|
||||
server_vpn_ip: data.server_vpn_ip || "10.42.0.1",
|
||||
session_token: data.session_token
|
||||
};
|
||||
localStorage.setItem("benzcloud_profile", JSON.stringify(prof));
|
||||
clientProfile = prof;
|
||||
btn.disabled = false;
|
||||
btn.textContent = i18n[currentLang].btn_pair;
|
||||
showConnectedView(prof);
|
||||
} catch (err) {
|
||||
alert((currentLang === "de" ? "Netzwerkfehler: " : "Network error: ") + err);
|
||||
btn.disabled = false;
|
||||
btn.textContent = i18n[currentLang].btn_pair;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/pair", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ server_url: serverUrl, username, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert((currentLang === "de" ? "Kopplung fehlgeschlagen: " : "Pairing failed: ") + (data.error || "Serverfehler"));
|
||||
btn.disabled = false;
|
||||
btn.textContent = i18n[currentLang].btn_pair;
|
||||
return;
|
||||
}
|
||||
fetchClientProfile();
|
||||
} catch (err) {
|
||||
alert((currentLang === "de" ? "Netzwerkfehler: " : "Network error: ") + err);
|
||||
btn.disabled = false;
|
||||
btn.textContent = i18n[currentLang].btn_pair;
|
||||
}
|
||||
}
|
||||
|
||||
async function unpairClient() {
|
||||
if (!confirm(i18n[currentLang].confirm_unpair)) return;
|
||||
if (window.location.protocol === "file:") {
|
||||
localStorage.removeItem("benzcloud_profile");
|
||||
clientProfile = null;
|
||||
showPairingView();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await fetch("/api/unpair", { method: "POST" });
|
||||
localStorage.removeItem("benzcloud_profile");
|
||||
fetchClientProfile();
|
||||
} catch (err) {
|
||||
localStorage.removeItem("benzcloud_profile");
|
||||
showPairingView();
|
||||
}
|
||||
}
|
||||
BIN
android/app/src/main/assets/icon.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
124
android/app/src/main/assets/index.html
Normal file
@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BenzCloud Client – Desktop & Mobile</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<div class="logo-icon">☁️</div>
|
||||
<div class="logo-text">
|
||||
<span class="brand-title">BenzCloud Client</span>
|
||||
<span class="brand-badge">v1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="lang-switcher">
|
||||
<button id="langDE" class="btn-lang active" onclick="setLanguage('de')">DE</button>
|
||||
<button id="langEN" class="btn-lang" onclick="setLanguage('en')">EN</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<!-- PAIRING VIEW -->
|
||||
<div id="pairingView" class="view-panel" style="display:none;">
|
||||
<div class="card-box">
|
||||
<div class="badge-pill" data-i18n="pair_badge">🔗 Client-Ersteinrichtung</div>
|
||||
<h1 class="title" data-i18n="pair_title">Mit BenzCloud-Server verbinden</h1>
|
||||
<p class="desc" data-i18n="pair_desc">Gib die lokale IP-Adresse deines BenzCloud-Servers und deine Anmeldedaten ein. Das Mesh-VPN (Nebula) und der DNS-Server konfigurieren sich automatisch.</p>
|
||||
|
||||
<form id="pairForm" onsubmit="submitPairing(event)">
|
||||
<div class="form-group">
|
||||
<label for="serverUrl" data-i18n="lbl_server_url">Server LAN-Adresse:</label>
|
||||
<input type="text" id="serverUrl" required placeholder="z. B. http://192.168.0.5:8080" value="http://127.0.0.1:8080">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="pairUser" data-i18n="lbl_user">Benutzername:</label>
|
||||
<input type="text" id="pairUser" required placeholder="admin" value="admin">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pairPass" data-i18n="lbl_pass">Passwort:</label>
|
||||
<input type="password" id="pairPass" required placeholder="••••••••">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary btn-block" id="btnPair" data-i18n="btn_pair">🚀 Gerät sicher koppeln & verbinden</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONNECTED COCKPIT -->
|
||||
<div id="connectedView" class="view-panel" style="display:none;">
|
||||
<div class="status-banner">
|
||||
<div class="s-left">
|
||||
<span class="pulse-dot"></span>
|
||||
<div>
|
||||
<h2 data-i18n="status_connected">Verbunden mit BenzCloud</h2>
|
||||
<p class="s-domain" id="dispDomain">benzjeremy.de</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s-right">
|
||||
<div class="tag-row">
|
||||
<span class="badge-tag">Overlay IP: <strong id="dispOverlayIP">10.42.0.2</strong></span>
|
||||
<span class="badge-tag">Lighthouse: <strong id="dispLighthouse">10.42.0.1</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-title" data-i18n="services_heading">Deine Enterprise-Dienste (Direktzugriff)</h3>
|
||||
<div class="grid-services">
|
||||
<a class="service-card" id="cardDrive" href="#" target="_blank">
|
||||
<div class="s-icon">📁</div>
|
||||
<div class="s-details">
|
||||
<h4 data-i18n="card_drive_title">BenzCloud Drive</h4>
|
||||
<p class="s-sub" id="subDrive">http://drive.domain</p>
|
||||
<p class="s-desc" data-i18n="card_drive_desc">Dateien hochladen, teilen und synchronisieren.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="service-card" id="cardMail" href="#" target="_blank">
|
||||
<div class="s-icon">📧</div>
|
||||
<div class="s-details">
|
||||
<h4 data-i18n="card_mail_title">BenzCloud Mail</h4>
|
||||
<p class="s-sub" id="subMail">http://mail.domain</p>
|
||||
<p class="s-desc" data-i18n="card_mail_desc">Internes Webmail & Thunderbird-kompatibles Postfach.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="service-card" id="cardChat" href="#" target="_blank">
|
||||
<div class="s-icon">💬</div>
|
||||
<div class="s-details">
|
||||
<h4 data-i18n="card_chat_title">BenzCloud Chat</h4>
|
||||
<p class="s-sub" id="subChat">http://chat.domain</p>
|
||||
<p class="s-desc" data-i18n="card_chat_desc">Echtzeit-Teamkommunikation & Direktnachrichten.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="service-card" id="cardWeb" href="#" target="_blank">
|
||||
<div class="s-icon">🌐</div>
|
||||
<div class="s-details">
|
||||
<h4 data-i18n="card_web_title">Webseiten & Portal</h4>
|
||||
<p class="s-sub" id="subWeb">http://domain</p>
|
||||
<p class="s-desc" data-i18n="card_web_desc">Gehostete Firmen- und Team-Webseiten aufrufen.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="client-footer-actions">
|
||||
<button class="btn-outline btn-sm" onclick="unpairClient()" data-i18n="btn_unpair">🔌 Entkoppeln / Gerät trennen</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer">
|
||||
<span>BenzCloud Client © 2026 Jeremy Benz • GNU GPLv3 Lizenz • Nebula (MIT)</span>
|
||||
<span class="pill-pre">Pre-Release / In aktiver Entwicklung</span>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
300
android/app/src/main/assets/style.css
Normal file
@ -0,0 +1,300 @@
|
||||
:root {
|
||||
--bg-page: #0a0e17;
|
||||
--bg-surface: #121826;
|
||||
--bg-card: #182032;
|
||||
--bg-hover: #1e293f;
|
||||
--border-subtle: #243048;
|
||||
--border-focus: #38bdf8;
|
||||
|
||||
--text-main: #f1f5f9;
|
||||
--text-muted: #94a3b8;
|
||||
--text-subtle: #64748b;
|
||||
|
||||
--accent-blue: #38bdf8;
|
||||
--accent-blue-hover: #0284c7;
|
||||
--accent-green: #34d399;
|
||||
--accent-amber: #fbbf24;
|
||||
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-mono: "JetBrains Mono", monospace;
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background-color: var(--bg-page);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-sans);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
background-color: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.logo-icon { font-size: 1.5rem; }
|
||||
.brand-title { font-weight: 700; font-size: 1.2rem; }
|
||||
.brand-badge {
|
||||
background-color: rgba(56, 189, 248, 0.15);
|
||||
color: var(--accent-blue);
|
||||
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lang-switcher {
|
||||
display: flex;
|
||||
background-color: var(--bg-card);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.btn-lang {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-lang.active {
|
||||
background-color: var(--accent-blue);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.card-box {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 2.5rem;
|
||||
margin: 2rem auto;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.badge-pill {
|
||||
display: inline-block;
|
||||
background-color: rgba(56, 189, 248, 0.15);
|
||||
color: var(--accent-blue);
|
||||
border: 1px solid rgba(56, 189, 248, 0.3);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 9999px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 2rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%;
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-main);
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, input[type="password"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--accent-blue);
|
||||
color: #04101d;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.85rem 1.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-primary:hover { background-color: var(--accent-blue-hover); }
|
||||
|
||||
.btn-block { width: 100%; }
|
||||
.btn-sm { padding: 0.4rem 0.8rem; font-size: 0.8rem; }
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* Connected View */
|
||||
.status-banner {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.s-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent-green);
|
||||
box-shadow: 0 0 10px var(--accent-green);
|
||||
}
|
||||
|
||||
.s-domain {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--accent-blue);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.badge-tag {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8rem;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.grid-services {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-main);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--border-focus);
|
||||
background-color: var(--bg-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.s-icon { font-size: 2rem; }
|
||||
|
||||
.s-details h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.s-sub {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--accent-blue);
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.s-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.client-footer-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
padding: 1rem 2rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-subtle);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.pill-pre {
|
||||
background-color: rgba(251, 191, 36, 0.12);
|
||||
color: var(--accent-amber);
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
@ -28,11 +28,14 @@ public class MainActivity extends Activity {
|
||||
settings.setDomStorageEnabled(true);
|
||||
settings.setDatabaseEnabled(true);
|
||||
settings.setAllowFileAccess(true);
|
||||
settings.setAllowContentAccess(true);
|
||||
settings.setAllowFileAccessFromFileURLs(true);
|
||||
settings.setAllowUniversalAccessFromFileURLs(true);
|
||||
|
||||
webView.setWebViewClient(new AppWebViewClient());
|
||||
|
||||
// Points to local BenzCloud client control daemon or default pairing endpoint
|
||||
webView.loadUrl("http://127.0.0.1:8088");
|
||||
// Load bundled offline client dashboard & pairing wizard
|
||||
webView.loadUrl("file:///android_asset/index.html");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
BIN
android/app/src/main/res/drawable/icon.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
@ -20,6 +20,7 @@ echo "==> 2. Linking resources and generating R.java..."
|
||||
-I "$ANDROID_JAR" \
|
||||
--manifest "$DIR/app/src/main/AndroidManifest.xml" \
|
||||
--java "$WORK/gen" \
|
||||
-A "$DIR/app/src/main/assets" \
|
||||
-o "$WORK/unaligned.apk" \
|
||||
--auto-add-overlay \
|
||||
"$WORK/compiled_res.zip"
|
||||
|
||||
139
gui_linux.go
@ -4,21 +4,75 @@ package main
|
||||
|
||||
/*
|
||||
#cgo pkg-config: gtk+-3.0 webkit2gtk-4.1
|
||||
#include <stdlib.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <webkit2/webkit2.h>
|
||||
|
||||
static void activate_gtk_app(const char* title, const char* url, int width, int height) {
|
||||
gtk_init(NULL, NULL);
|
||||
static int check_display() {
|
||||
int argc = 0;
|
||||
char **argv = NULL;
|
||||
return gtk_init_check(&argc, &argv) ? 1 : 0;
|
||||
}
|
||||
|
||||
static void on_window_destroy(GtkWidget *widget, gpointer data) {
|
||||
gtk_main_quit();
|
||||
}
|
||||
|
||||
static gboolean on_context_menu(WebKitWebView *web_view, WebKitContextMenu *context_menu, GdkEvent *event, WebKitHitTestResult *hit_test_result, gpointer user_data) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void set_window_icon_from_memory(GtkWindow *window, const void *buf, gsize len) {
|
||||
if (!buf || len == 0) return;
|
||||
GError *err = NULL;
|
||||
GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
|
||||
if (loader) {
|
||||
if (gdk_pixbuf_loader_write(loader, (const guint8 *)buf, len, &err)) {
|
||||
gdk_pixbuf_loader_close(loader, &err);
|
||||
GdkPixbuf *pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
|
||||
if (pixbuf) {
|
||||
gtk_window_set_icon(window, pixbuf);
|
||||
gtk_window_set_default_icon(pixbuf);
|
||||
}
|
||||
}
|
||||
g_object_unref(loader);
|
||||
}
|
||||
gtk_window_set_default_icon_name("benzcloud-client");
|
||||
gtk_window_set_icon_name(window, "benzcloud-client");
|
||||
}
|
||||
|
||||
static void activate_gtk_app(const char* title, const char* url, int width, int height, const void *icon_buf, int icon_len) {
|
||||
int argc = 0;
|
||||
char **argv = NULL;
|
||||
if (!gtk_init_check(&argc, &argv)) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_set_prgname("benzcloud-client");
|
||||
g_set_application_name("BenzCloud Client");
|
||||
|
||||
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||
gtk_window_set_title(GTK_WINDOW(window), title);
|
||||
gtk_window_set_default_size(GTK_WINDOW(window), width, height);
|
||||
gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
|
||||
|
||||
GtkWidget *web_view = webkit_web_view_new();
|
||||
gtk_container_add(GTK_CONTAINER(window), web_view);
|
||||
if (icon_buf && icon_len > 0) {
|
||||
set_window_icon_from_memory(GTK_WINDOW(window), icon_buf, (gsize)icon_len);
|
||||
}
|
||||
|
||||
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
|
||||
GdkRGBA bg_color;
|
||||
gdk_rgba_parse(&bg_color, "#0b0f19");
|
||||
|
||||
WebKitSettings *settings = webkit_settings_new();
|
||||
webkit_settings_set_enable_developer_extras(settings, FALSE);
|
||||
webkit_settings_set_hardware_acceleration_policy(settings, WEBKIT_HARDWARE_ACCELERATION_POLICY_ALWAYS);
|
||||
|
||||
GtkWidget *web_view = webkit_web_view_new_with_settings(settings);
|
||||
webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(web_view), &bg_color);
|
||||
g_signal_connect(web_view, "context-menu", G_CALLBACK(on_context_menu), NULL);
|
||||
|
||||
gtk_container_add(GTK_CONTAINER(window), web_view);
|
||||
g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), NULL);
|
||||
|
||||
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), url);
|
||||
gtk_widget_show_all(window);
|
||||
@ -27,14 +81,85 @@ static void activate_gtk_app(const char* title, const char* url, int width, int
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import "unsafe"
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_ = os.Setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
|
||||
_ = os.Setenv("WEBKIT_FORCE_COMPOSITING_MODE", "1")
|
||||
}
|
||||
|
||||
// installDesktopIntegration automatically installs icons and desktop file into user's XDG directories
|
||||
func installDesktopIntegration() {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
iconDir := filepath.Join(home, ".local", "share", "icons", "hicolor", "512x512", "apps")
|
||||
pixmapDir := filepath.Join(home, ".local", "share", "pixmaps")
|
||||
appDir := filepath.Join(home, ".local", "share", "applications")
|
||||
_ = os.MkdirAll(iconDir, 0755)
|
||||
_ = os.MkdirAll(pixmapDir, 0755)
|
||||
_ = os.MkdirAll(appDir, 0755)
|
||||
|
||||
iconPng, _ := webFS.ReadFile("web/icon.png")
|
||||
if len(iconPng) > 0 {
|
||||
_ = os.WriteFile(filepath.Join(iconDir, "benzcloud-client.png"), iconPng, 0644)
|
||||
_ = os.WriteFile(filepath.Join(pixmapDir, "benzcloud-client.png"), iconPng, 0644)
|
||||
}
|
||||
iconSvg, _ := webFS.ReadFile("web/icon.svg")
|
||||
if len(iconSvg) > 0 {
|
||||
svgDir := filepath.Join(home, ".local", "share", "icons", "hicolor", "scalable", "apps")
|
||||
_ = os.MkdirAll(svgDir, 0755)
|
||||
_ = os.WriteFile(filepath.Join(svgDir, "benzcloud-client.svg"), iconSvg, 0644)
|
||||
}
|
||||
|
||||
desktopPath := filepath.Join(appDir, "benzcloud-client.desktop")
|
||||
execPath, _ := os.Executable()
|
||||
if execPath == "" {
|
||||
execPath = "benzcloud-client"
|
||||
}
|
||||
content := fmt.Sprintf(`[Desktop Entry]
|
||||
Name=BenzCloud Client
|
||||
Comment=Secure Self-Hosted Cloud Mesh Client
|
||||
Exec=%s
|
||||
Icon=benzcloud-client
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;FileTransfer;Utility;
|
||||
StartupWMClass=benzcloud-client
|
||||
X-Wayland-AppID=benzcloud-client
|
||||
`, execPath)
|
||||
_ = os.WriteFile(desktopPath, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// LaunchGUI launches native WebKitGTK desktop shell on Linux.
|
||||
func LaunchGUI(title, url string, width, height int) {
|
||||
installDesktopIntegration()
|
||||
|
||||
hasDisplay := os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != ""
|
||||
if !hasDisplay || C.check_display() == 0 {
|
||||
log.Println("[GUI] Kein Display gefunden, öffne Standard-Browser...")
|
||||
_ = exec.Command("xdg-open", url).Start()
|
||||
return
|
||||
}
|
||||
|
||||
cTitle := C.CString(title)
|
||||
cURL := C.CString(url)
|
||||
defer C.free(unsafe.Pointer(cTitle))
|
||||
defer C.free(unsafe.Pointer(cURL))
|
||||
|
||||
C.activate_gtk_app(cTitle, cURL, C.int(width), C.int(height))
|
||||
iconBytes, _ := webFS.ReadFile("web/icon.png")
|
||||
var iconPtr unsafe.Pointer
|
||||
if len(iconBytes) > 0 {
|
||||
iconPtr = unsafe.Pointer(&iconBytes[0])
|
||||
}
|
||||
|
||||
C.activate_gtk_app(cTitle, cURL, C.int(width), C.int(height), iconPtr, C.int(len(iconBytes)))
|
||||
}
|
||||
|
||||
102
web/app.js
@ -73,9 +73,30 @@ function applyLanguage(lang) {
|
||||
}
|
||||
|
||||
async function fetchClientProfile() {
|
||||
if (window.location.protocol === "file:") {
|
||||
const stored = localStorage.getItem("benzcloud_profile");
|
||||
if (stored) {
|
||||
try {
|
||||
clientProfile = JSON.parse(stored);
|
||||
showConnectedView(clientProfile);
|
||||
return;
|
||||
} catch (e) {
|
||||
localStorage.removeItem("benzcloud_profile");
|
||||
}
|
||||
}
|
||||
showPairingView();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/profile");
|
||||
if (!res.ok) {
|
||||
const stored = localStorage.getItem("benzcloud_profile");
|
||||
if (stored) {
|
||||
clientProfile = JSON.parse(stored);
|
||||
showConnectedView(clientProfile);
|
||||
return;
|
||||
}
|
||||
showPairingView();
|
||||
return;
|
||||
}
|
||||
@ -87,6 +108,16 @@ async function fetchClientProfile() {
|
||||
clientProfile = data;
|
||||
showConnectedView(data);
|
||||
} catch (err) {
|
||||
const stored = localStorage.getItem("benzcloud_profile");
|
||||
if (stored) {
|
||||
try {
|
||||
clientProfile = JSON.parse(stored);
|
||||
showConnectedView(clientProfile);
|
||||
return;
|
||||
} catch (e) {
|
||||
localStorage.removeItem("benzcloud_profile");
|
||||
}
|
||||
}
|
||||
showPairingView();
|
||||
}
|
||||
}
|
||||
@ -105,6 +136,19 @@ function showConnectedView(data) {
|
||||
document.getElementById("dispLighthouse").textContent = data.server_vpn_ip || "10.42.0.1";
|
||||
|
||||
const domain = data.base_domain;
|
||||
if (data.server_url) {
|
||||
document.getElementById("subDrive").textContent = `${data.server_url}/#drive`;
|
||||
document.getElementById("cardDrive").href = `${data.server_url}/#drive`;
|
||||
|
||||
document.getElementById("subMail").textContent = `${data.server_url}/#mail`;
|
||||
document.getElementById("cardMail").href = `${data.server_url}/#mail`;
|
||||
|
||||
document.getElementById("subChat").textContent = `${data.server_url}/#chat`;
|
||||
document.getElementById("cardChat").href = `${data.server_url}/#chat`;
|
||||
|
||||
document.getElementById("subWeb").textContent = data.server_url;
|
||||
document.getElementById("cardWeb").href = data.server_url;
|
||||
} else {
|
||||
document.getElementById("subDrive").textContent = `http://drive.${domain}`;
|
||||
document.getElementById("cardDrive").href = `http://drive.${domain}`;
|
||||
|
||||
@ -117,17 +161,59 @@ function showConnectedView(data) {
|
||||
document.getElementById("subWeb").textContent = `http://${domain}`;
|
||||
document.getElementById("cardWeb").href = `http://${domain}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPairing(e) {
|
||||
e.preventDefault();
|
||||
const serverUrl = document.getElementById("serverUrl").value.trim();
|
||||
let serverUrl = document.getElementById("serverUrl").value.trim();
|
||||
const username = document.getElementById("pairUser").value.trim();
|
||||
const password = document.getElementById("pairPass").value;
|
||||
|
||||
if (!serverUrl.startsWith("http://") && !serverUrl.startsWith("https://")) {
|
||||
serverUrl = "http://" + serverUrl;
|
||||
}
|
||||
serverUrl = serverUrl.replace(/\/+$/, "");
|
||||
|
||||
const btn = document.getElementById("btnPair");
|
||||
btn.disabled = true;
|
||||
btn.textContent = i18n[currentLang].pairing_in_progress;
|
||||
|
||||
if (window.location.protocol === "file:") {
|
||||
try {
|
||||
const res = await fetch(`${serverUrl}/api/pair`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert((currentLang === "de" ? "Kopplung fehlgeschlagen: " : "Pairing failed: ") + (data.error || "Serverfehler"));
|
||||
btn.disabled = false;
|
||||
btn.textContent = i18n[currentLang].btn_pair;
|
||||
return;
|
||||
}
|
||||
const prof = {
|
||||
paired: true,
|
||||
server_url: serverUrl,
|
||||
username: data.username || username,
|
||||
base_domain: data.base_domain || "intern",
|
||||
overlay_ip: data.overlay_ip || "10.42.0.2",
|
||||
server_vpn_ip: data.server_vpn_ip || "10.42.0.1",
|
||||
session_token: data.session_token
|
||||
};
|
||||
localStorage.setItem("benzcloud_profile", JSON.stringify(prof));
|
||||
clientProfile = prof;
|
||||
btn.disabled = false;
|
||||
btn.textContent = i18n[currentLang].btn_pair;
|
||||
showConnectedView(prof);
|
||||
} catch (err) {
|
||||
alert((currentLang === "de" ? "Netzwerkfehler: " : "Network error: ") + err);
|
||||
btn.disabled = false;
|
||||
btn.textContent = i18n[currentLang].btn_pair;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/pair", {
|
||||
method: "POST",
|
||||
@ -136,14 +222,14 @@ async function submitPairing(e) {
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert("Kopplung fehlgeschlagen: " + (data.error || "Serverfehler"));
|
||||
alert((currentLang === "de" ? "Kopplung fehlgeschlagen: " : "Pairing failed: ") + (data.error || "Serverfehler"));
|
||||
btn.disabled = false;
|
||||
btn.textContent = i18n[currentLang].btn_pair;
|
||||
return;
|
||||
}
|
||||
fetchClientProfile();
|
||||
} catch (err) {
|
||||
alert("Netzwerkfehler: " + err);
|
||||
alert((currentLang === "de" ? "Netzwerkfehler: " : "Network error: ") + err);
|
||||
btn.disabled = false;
|
||||
btn.textContent = i18n[currentLang].btn_pair;
|
||||
}
|
||||
@ -151,10 +237,18 @@ async function submitPairing(e) {
|
||||
|
||||
async function unpairClient() {
|
||||
if (!confirm(i18n[currentLang].confirm_unpair)) return;
|
||||
if (window.location.protocol === "file:") {
|
||||
localStorage.removeItem("benzcloud_profile");
|
||||
clientProfile = null;
|
||||
showPairingView();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await fetch("/api/unpair", { method: "POST" });
|
||||
localStorage.removeItem("benzcloud_profile");
|
||||
fetchClientProfile();
|
||||
} catch (err) {
|
||||
alert("Fehler beim Entkoppeln");
|
||||
localStorage.removeItem("benzcloud_profile");
|
||||
showPairingView();
|
||||
}
|
||||
}
|
||||
|
||||
BIN
web/icon.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
81
web/icon.svg
Normal file
@ -0,0 +1,81 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<defs>
|
||||
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0a0d1c"/>
|
||||
<stop offset="100%" stop-color="#141933"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="indigoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#818cf8"/>
|
||||
<stop offset="100%" stop-color="#4f46e5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="cyanPulse" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#38bdf8"/>
|
||||
<stop offset="100%" stop-color="#6366f1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="screenGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#1e1b4b"/>
|
||||
<stop offset="100%" stop-color="#0f172a"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="10" result="blur"/>
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Background rounded squircle -->
|
||||
<rect x="24" y="24" width="464" height="464" rx="108" fill="url(#bgGrad)" stroke="#253354" stroke-width="8"/>
|
||||
|
||||
<!-- Desktop Cockpit Monitor Body -->
|
||||
<rect x="88" y="96" width="288" height="196" rx="20" fill="url(#screenGrad)" stroke="url(#indigoGrad)" stroke-width="10" filter="url(#glow)"/>
|
||||
|
||||
<!-- Monitor Stand Base -->
|
||||
<path d="M 206 296 L 194 340 L 270 340 L 258 296" fill="#1e1b4b" stroke="#4f46e5" stroke-width="6" stroke-linejoin="round"/>
|
||||
<rect x="170" y="340" width="124" height="14" rx="7" fill="#4f46e5"/>
|
||||
|
||||
<!-- QR Pairing Matrix on Monitor Screen -->
|
||||
<!-- Outer marker 1 (top-left) -->
|
||||
<rect x="118" y="126" width="38" height="38" rx="6" fill="none" stroke="#818cf8" stroke-width="6"/>
|
||||
<rect x="127" y="135" width="20" height="20" rx="3" fill="#818cf8"/>
|
||||
|
||||
<!-- Outer marker 2 (top-right) -->
|
||||
<rect x="188" y="126" width="38" height="38" rx="6" fill="none" stroke="#818cf8" stroke-width="6"/>
|
||||
<rect x="197" y="135" width="20" height="20" rx="3" fill="#818cf8"/>
|
||||
|
||||
<!-- Outer marker 3 (bottom-left) -->
|
||||
<rect x="118" y="196" width="38" height="38" rx="6" fill="none" stroke="#818cf8" stroke-width="6"/>
|
||||
<rect x="127" y="205" width="20" height="20" rx="3" fill="#818cf8"/>
|
||||
|
||||
<!-- QR bit elements -->
|
||||
<rect x="188" y="196" width="16" height="16" rx="3" fill="#38bdf8"/>
|
||||
<rect x="210" y="218" width="16" height="16" rx="3" fill="#818cf8"/>
|
||||
<rect x="166" y="166" width="16" height="16" rx="3" fill="#818cf8"/>
|
||||
<rect x="188" y="174" width="16" height="16" rx="3" fill="#38bdf8"/>
|
||||
<rect x="136" y="174" width="16" height="16" rx="3" fill="#6366f1"/>
|
||||
|
||||
<!-- Cockpit Telemetry lines on right side of screen -->
|
||||
<line x1="250" y1="136" x2="346" y2="136" stroke="#4ade80" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="250" y1="162" x2="330" y2="162" stroke="#38bdf8" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="250" y1="188" x2="310" y2="188" stroke="#818cf8" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="250" y1="214" x2="340" y2="214" stroke="#a78bfa" stroke-width="6" stroke-linecap="round"/>
|
||||
<line x1="250" y1="240" x2="290" y2="240" stroke="#f43f5e" stroke-width="6" stroke-linecap="round"/>
|
||||
|
||||
<!-- Connected Mobile Companion Device (Overlapping Bottom Right) -->
|
||||
<g filter="url(#glow)">
|
||||
<rect x="306" y="226" width="124" height="216" rx="28" fill="#090d1c" stroke="url(#cyanPulse)" stroke-width="8"/>
|
||||
<!-- Mobile Screen -->
|
||||
<rect x="318" y="248" width="100" height="164" rx="16" fill="#141a33"/>
|
||||
<!-- Mobile Speaker notch -->
|
||||
<circle cx="368" cy="237" r="3.5" fill="#38bdf8"/>
|
||||
<!-- Mobile mini QR glyph -->
|
||||
<rect x="342" y="278" width="52" height="52" rx="8" fill="none" stroke="#38bdf8" stroke-width="5"/>
|
||||
<rect x="355" y="291" width="26" height="26" rx="4" fill="#38bdf8"/>
|
||||
<!-- Signal waves radiating from mobile -->
|
||||
<path d="M 346 364 C 360 354 376 354 390 364" fill="none" stroke="#4ade80" stroke-width="5" stroke-linecap="round"/>
|
||||
<path d="M 338 378 C 358 366 378 366 398 378" fill="none" stroke="#4ade80" stroke-width="5" stroke-linecap="round"/>
|
||||
<!-- Verified connection node badge -->
|
||||
<circle cx="368" cy="428" r="5" fill="#4ade80"/>
|
||||
</g>
|
||||
|
||||
<!-- Dynamic Connection Wave from Monitor to Mobile -->
|
||||
<path d="M 230 260 C 270 260 275 310 310 310" fill="none" stroke="#38bdf8" stroke-width="6" stroke-dasharray="6,6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |