fix(gui,android): fix Wayland icon, add Android launcher mipmaps and bundle offline assets

This commit is contained in:
benzj 2026-09-12 21:51:15 +02:00
parent 44f646cb3b
commit fc9ff063e5
24 changed files with 1005 additions and 21 deletions

View File

@ -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">

View 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();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>

View 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;
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

View File

@ -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"

View File

@ -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)))
}

View File

@ -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}`;
@ -116,18 +160,60 @@ 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

81
web/icon.svg Normal file
View 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