- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark, Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI) - aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource, an aether-core gekoppelt (Rechte über aether_users.admin_group), mit WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU) - Prototypen admin/ vervollständigt - SQL-Schema aether_admin.sql - Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2655 lines
138 KiB
HTML
2655 lines
138 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="color-scheme" content="dark light">
|
||
</head>
|
||
<body>
|
||
<title>AetherOS — RP Handy</title>
|
||
<style>
|
||
/* ===================== Design-Tokens ===================== */
|
||
:root {
|
||
/* Farbwelt der Seite (um das Handy herum) */
|
||
--page-bg: #0b0c12;
|
||
--page-fg: #e7e8f2;
|
||
--page-muted: #8a8da3;
|
||
--page-card: #14151d;
|
||
--page-border: rgba(255,255,255,0.08);
|
||
--accent: #7b6cf6;
|
||
}
|
||
/* Helles Seiten-Theme, wenn der Betrachter Light bevorzugt */
|
||
@media (prefers-color-scheme: light) {
|
||
:root {
|
||
--page-bg: #eceef4;
|
||
--page-fg: #1a1c24;
|
||
--page-muted: #5c6070;
|
||
--page-card: #ffffff;
|
||
--page-border: rgba(0,0,0,0.09);
|
||
}
|
||
}
|
||
:root[data-theme="dark"] {
|
||
--page-bg: #0b0c12; --page-fg: #e7e8f2; --page-muted: #8a8da3;
|
||
--page-card: #14151d; --page-border: rgba(255,255,255,0.08);
|
||
}
|
||
:root[data-theme="light"] {
|
||
--page-bg: #eceef4; --page-fg: #1a1c24; --page-muted: #5c6070;
|
||
--page-card: #ffffff; --page-border: rgba(0,0,0,0.09);
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; }
|
||
body {
|
||
background:
|
||
radial-gradient(1200px 700px at 80% -10%, rgba(123,108,246,0.18), transparent 60%),
|
||
radial-gradient(1000px 700px at 0% 110%, rgba(52,211,153,0.10), transparent 55%),
|
||
var(--page-bg);
|
||
color: var(--page-fg);
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||
min-height: 100vh;
|
||
display: grid;
|
||
grid-template-columns: 1fr auto 1fr;
|
||
gap: 40px;
|
||
align-items: center;
|
||
padding: 48px 24px;
|
||
}
|
||
@media (max-width: 900px) {
|
||
body { grid-template-columns: 1fr; justify-items: center; gap: 28px; }
|
||
.side-panel { max-width: 460px; }
|
||
}
|
||
|
||
/* ===================== Info-Panels neben dem Handy ===================== */
|
||
.side-panel { max-width: 300px; }
|
||
.side-panel.left { justify-self: end; }
|
||
.side-panel.right { justify-self: start; }
|
||
.brand-mark {
|
||
display: inline-flex; align-items: center; gap: 10px;
|
||
font-weight: 700; font-size: 20px; letter-spacing: -0.02em; margin-bottom: 6px;
|
||
}
|
||
.brand-mark .dot {
|
||
width: 26px; height: 26px; border-radius: 8px;
|
||
background: linear-gradient(145deg, #7b6cf6, #34d399);
|
||
box-shadow: 0 4px 14px rgba(123,108,246,0.5);
|
||
}
|
||
.side-panel h2 { font-size: 15px; margin: 22px 0 10px; letter-spacing: -0.01em; }
|
||
.side-panel p { color: var(--page-muted); font-size: 13.5px; line-height: 1.55; margin: 0 0 12px; }
|
||
.hint-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; }
|
||
.hint-list li {
|
||
background: var(--page-card); border: 1px solid var(--page-border);
|
||
border-radius: 12px; padding: 10px 12px; font-size: 12.5px; color: var(--page-muted);
|
||
display: flex; gap: 9px; align-items: flex-start;
|
||
}
|
||
.hint-list li b { color: var(--page-fg); font-weight: 600; }
|
||
.kbd {
|
||
display: inline-block; min-width: 20px; text-align: center;
|
||
background: rgba(123,108,246,0.16); color: var(--accent);
|
||
border-radius: 6px; padding: 1px 6px; font-size: 11px; font-weight: 700;
|
||
}
|
||
.feature-tag {
|
||
display: inline-block; font-size: 11px; color: var(--page-muted);
|
||
border: 1px solid var(--page-border); border-radius: 999px;
|
||
padding: 4px 10px; margin: 0 6px 6px 0;
|
||
}
|
||
|
||
/* ===================== Handy-Gehäuse ===================== */
|
||
.device {
|
||
--w: 384px;
|
||
width: min(var(--w), 94vw);
|
||
aspect-ratio: 384 / 700;
|
||
background: linear-gradient(160deg, #2a2c38, #101118 60%);
|
||
border-radius: 54px;
|
||
padding: 13px;
|
||
box-shadow:
|
||
0 2px 3px rgba(255,255,255,0.14) inset,
|
||
0 0 0 2px #050509,
|
||
0 40px 80px -20px rgba(0,0,0,0.7),
|
||
0 20px 40px -25px rgba(123,108,246,0.5);
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
}
|
||
/* Seitentasten */
|
||
.device::before {
|
||
content: ""; position: absolute; left: -3px; top: 150px; width: 3px; height: 92px;
|
||
background: #1a1b22; border-radius: 3px 0 0 3px;
|
||
}
|
||
.device::after {
|
||
content: ""; position: absolute; right: -3px; top: 180px; width: 3px; height: 64px;
|
||
background: #1a1b22; border-radius: 0 3px 3px 0;
|
||
}
|
||
|
||
/* Der eigentliche Bildschirm */
|
||
.screen {
|
||
position: relative;
|
||
width: 100%; height: 100%;
|
||
border-radius: 42px;
|
||
overflow: hidden;
|
||
background: #000;
|
||
/* Handy-interne Theme-Tokens (Standard: Dark / AetherOS Nacht) */
|
||
--s-fg: #f4f5fb;
|
||
--s-muted: #9a9db4;
|
||
--s-surface: rgba(30,32,44,0.82);
|
||
--s-surface-2: rgba(44,47,63,0.9);
|
||
--s-border: rgba(255,255,255,0.10);
|
||
--s-accent: #7b6cf6;
|
||
--s-green: #34d399;
|
||
--s-red: #f0506e;
|
||
--s-amber: #f5b23d;
|
||
--s-blue: #4aa8ff;
|
||
color: var(--s-fg);
|
||
font-size: 15px;
|
||
}
|
||
/* Handy-internes Light-Mode (über Einstellungen umschaltbar) */
|
||
.screen.light {
|
||
--s-fg: #14161d;
|
||
--s-muted: #6b6f82;
|
||
--s-surface: rgba(255,255,255,0.78);
|
||
--s-surface-2: rgba(240,241,246,0.92);
|
||
--s-border: rgba(0,0,0,0.10);
|
||
}
|
||
|
||
/* Hintergrundbild / Wallpaper */
|
||
.wallpaper {
|
||
position: absolute; inset: 0; z-index: 0;
|
||
background-size: cover; background-position: center;
|
||
transition: opacity .4s ease;
|
||
}
|
||
.wp-aurora {
|
||
background:
|
||
radial-gradient(120% 90% at 20% 0%, #3a2f7d 0%, transparent 55%),
|
||
radial-gradient(120% 90% at 90% 20%, #1c6d63 0%, transparent 50%),
|
||
radial-gradient(140% 100% at 50% 100%, #10233f 0%, transparent 60%),
|
||
linear-gradient(160deg, #0d0f1a, #05060b);
|
||
}
|
||
.wp-sunset {
|
||
background:
|
||
radial-gradient(120% 80% at 50% 110%, #ff8c42 0%, transparent 55%),
|
||
radial-gradient(100% 90% at 80% 0%, #7b2f8f 0%, transparent 55%),
|
||
linear-gradient(180deg, #241436, #4a1f4d 55%, #1a0e1f);
|
||
}
|
||
.wp-carbon {
|
||
background:
|
||
radial-gradient(100% 60% at 50% 0%, #23262f 0%, transparent 60%),
|
||
repeating-linear-gradient(45deg, #0c0d12 0 8px, #101219 8px 16px);
|
||
}
|
||
.wp-mono { background: linear-gradient(180deg, #0a0b10, #16181f); }
|
||
|
||
.screen.light .wallpaper.wp-aurora {
|
||
background:
|
||
radial-gradient(120% 90% at 15% 0%, #c3b9ff 0%, transparent 55%),
|
||
radial-gradient(120% 90% at 90% 15%, #9fe6da 0%, transparent 50%),
|
||
linear-gradient(160deg, #eef0fb, #dfe3f2);
|
||
}
|
||
|
||
/* ===================== Statusleiste ===================== */
|
||
.statusbar {
|
||
position: absolute; top: 0; left: 0; right: 0; height: 52px; z-index: 40;
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 0 30px; font-size: 14px; font-weight: 600;
|
||
color: var(--s-fg); pointer-events: none;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.statusbar .sb-right { display: flex; align-items: center; gap: 6px; }
|
||
.statusbar svg { display: block; }
|
||
/* Dynamic Island */
|
||
.island {
|
||
position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
|
||
width: 118px; height: 32px; background: #000; border-radius: 20px; z-index: 45;
|
||
display: flex; align-items: center; justify-content: flex-end; padding-right: 12px; gap: 8px;
|
||
}
|
||
.island .cam { width: 9px; height: 9px; border-radius: 50%;
|
||
background: radial-gradient(circle at 35% 30%, #34506b, #05070c); }
|
||
|
||
/* Home-Indikator */
|
||
.home-indicator {
|
||
position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%);
|
||
width: 130px; height: 5px; border-radius: 3px; background: var(--s-fg);
|
||
opacity: 0.55; z-index: 60; cursor: pointer; border: none;
|
||
}
|
||
.home-indicator:hover { opacity: 0.9; }
|
||
.screen.locked .home-indicator { display: none; }
|
||
|
||
/* ===================== Sperrbildschirm ===================== */
|
||
.lockscreen {
|
||
position: absolute; inset: 0; z-index: 55;
|
||
display: flex; flex-direction: column; align-items: center;
|
||
padding-top: 84px; text-align: center;
|
||
transition: transform .5s cubic-bezier(.6,.05,.2,1), opacity .4s ease;
|
||
}
|
||
.screen:not(.locked) .lockscreen { transform: translateY(-100%); opacity: 0; pointer-events: none; }
|
||
.lock-date { font-size: 17px; font-weight: 600; opacity: 0.9; }
|
||
.lock-time { font-size: 82px; font-weight: 700; letter-spacing: -0.04em; line-height: 1; margin-top: 2px;
|
||
font-variant-numeric: tabular-nums; text-shadow: 0 2px 30px rgba(0,0,0,0.4); }
|
||
.lock-notes { margin-top: 26px; width: calc(100% - 40px); display: grid; gap: 10px;
|
||
position: relative; z-index: 3; }
|
||
.lock-note {
|
||
background: rgba(20,22,32,0.55); backdrop-filter: blur(14px);
|
||
-webkit-backdrop-filter: blur(14px);
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
border-radius: 18px; padding: 12px 14px; text-align: left;
|
||
display: flex; gap: 11px; align-items: center;
|
||
}
|
||
.lock-note .ic { width: 34px; height: 34px; border-radius: 9px; flex-shrink: 0;
|
||
display: grid; place-items: center; }
|
||
.lock-note .txt { min-width: 0; }
|
||
.lock-note .txt .t { font-size: 13px; font-weight: 700; }
|
||
.lock-note .txt .d { font-size: 12.5px; color: rgba(244,245,251,0.75); overflow: hidden;
|
||
text-overflow: ellipsis; white-space: nowrap; }
|
||
.lock-unlock {
|
||
position: absolute; bottom: 40px; left: 0; right: 0; text-align: center;
|
||
font-size: 13px; color: rgba(244,245,251,0.7); font-weight: 500;
|
||
animation: floaty 2.2s ease-in-out infinite;
|
||
}
|
||
@keyframes floaty { 0%,100%{ transform: translateY(0);} 50%{ transform: translateY(-5px);} }
|
||
@media (prefers-reduced-motion: reduce) { .lock-unlock { animation: none; } }
|
||
.lock-btn { position:absolute; inset:0; background:transparent; border:none; cursor:pointer; z-index: 2; }
|
||
|
||
/* ===================== Homescreen ===================== */
|
||
.homescreen {
|
||
position: absolute; inset: 0; z-index: 10;
|
||
padding: 56px 20px 18px; display: flex; flex-direction: column; gap: 14px;
|
||
transition: opacity .35s ease, transform .35s ease;
|
||
}
|
||
.screen.locked .homescreen { opacity: 0; pointer-events: none; }
|
||
.app-grid {
|
||
display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px 16px;
|
||
align-content: start; flex: 1;
|
||
}
|
||
.app {
|
||
display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||
background: none; border: none; cursor: pointer; padding: 0; color: var(--s-fg);
|
||
font-family: inherit;
|
||
}
|
||
.app .icon {
|
||
width: 58px; height: 58px; border-radius: 15px; display: grid; place-items: center;
|
||
box-shadow: 0 6px 14px rgba(0,0,0,0.35); position: relative;
|
||
transition: transform .12s ease;
|
||
}
|
||
.app:hover .icon { transform: translateY(-3px) scale(1.03); }
|
||
.app:active .icon { transform: scale(0.94); }
|
||
.app .icon svg { width: 30px; height: 30px; }
|
||
.app .label { font-size: 11.5px; font-weight: 500; text-shadow: 0 1px 4px rgba(0,0,0,0.5); }
|
||
.app .badge {
|
||
position: absolute; top: -5px; right: -5px; min-width: 19px; height: 19px;
|
||
background: var(--s-red); border-radius: 999px; color: #fff; font-size: 11px; font-weight: 700;
|
||
display: grid; place-items: center; padding: 0 5px; border: 2px solid rgba(0,0,0,0.2);
|
||
}
|
||
/* Dock */
|
||
.dock {
|
||
margin: 0 -6px; padding: 14px 10px; border-radius: 30px;
|
||
background: rgba(255,255,255,0.10); backdrop-filter: blur(20px);
|
||
-webkit-backdrop-filter: blur(20px);
|
||
display: grid; grid-template-columns: repeat(4,1fr); gap: 16px;
|
||
}
|
||
.screen.light .dock { background: rgba(255,255,255,0.45); }
|
||
|
||
/* Icon-Farbverläufe */
|
||
.g-phone { background: linear-gradient(160deg,#4ade80,#16a34a); }
|
||
.g-msg { background: linear-gradient(160deg,#5ad0ff,#2563eb); }
|
||
.g-bank { background: linear-gradient(160deg,#34d399,#0f766e); }
|
||
.g-cam { background: linear-gradient(160deg,#40424f,#191a22); }
|
||
.g-birdy { background: linear-gradient(160deg,#7b6cf6,#4f46e5); }
|
||
.g-insta { background: linear-gradient(140deg,#feda75,#fa7e1e 35%,#d62976 65%,#962fbf); }
|
||
.g-set { background: linear-gradient(160deg,#9aa0b5,#4b5165); }
|
||
.g-music { background: linear-gradient(160deg,#fb7185,#e11d48); }
|
||
.g-weather{ background: linear-gradient(160deg,#4aa8ff,#1e6fd6); }
|
||
.g-clock { background: linear-gradient(160deg,#1a1c24,#0a0b10); }
|
||
.g-calc { background: linear-gradient(160deg,#2a2d38,#14151b); }
|
||
.g-garage{ background: linear-gradient(160deg,#f5b23d,#d97706); }
|
||
.g-mdt { background: linear-gradient(160deg,#1e5eff,#0a2f8f); }
|
||
.g-dispatch { background: linear-gradient(160deg,#f0506e,#1e5eff); }
|
||
.g-pages { background: linear-gradient(160deg,#fbbf24,#f59e0b); }
|
||
.g-map { background: linear-gradient(160deg,#34d399,#4aa8ff); }
|
||
.g-dark { background: linear-gradient(160deg,#374151,#111318); }
|
||
.g-staat { background: linear-gradient(160deg,#4a5578,#232a3f); }
|
||
.g-notes { background: linear-gradient(160deg,#ffe082,#f6c500); }
|
||
|
||
/* ===================== App-Fenster ===================== */
|
||
.app-view {
|
||
position: absolute; inset: 0; z-index: 30; background: transparent;
|
||
display: none; flex-direction: column;
|
||
animation: appIn .3s cubic-bezier(.2,.7,.2,1);
|
||
}
|
||
.app-view.open { display: flex; }
|
||
@keyframes appIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||
@media (prefers-reduced-motion: reduce) { .app-view { animation: none; } }
|
||
|
||
.app-bg { position: absolute; inset: 0; background: #0d0e15; z-index: -1; }
|
||
.screen.light .app-bg { background: #f4f5fa; }
|
||
|
||
.app-header {
|
||
padding: 58px 18px 12px; display: flex; align-items: center; gap: 10px;
|
||
border-bottom: 1px solid var(--s-border); position: relative;
|
||
background: var(--s-surface); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
||
}
|
||
.app-header h1 { font-size: 20px; margin: 0; font-weight: 700; letter-spacing: -0.02em; flex: 1; }
|
||
.app-header .sub { font-size: 12px; color: var(--s-muted); font-weight: 500; }
|
||
/* Apple-artiger runder Zurück-/Aktions-Button */
|
||
.icon-btn {
|
||
width: 34px; height: 34px; border-radius: 50%; padding: 0;
|
||
background: var(--s-surface-2); border: 1px solid var(--s-border);
|
||
color: var(--s-accent); cursor: pointer; font-family: inherit;
|
||
display: grid; place-items: center; flex-shrink: 0;
|
||
transition: transform .1s ease, filter .15s ease;
|
||
}
|
||
.icon-btn:hover { filter: brightness(1.12); }
|
||
.icon-btn:active { transform: scale(0.9); }
|
||
.icon-btn svg { width: 18px; height: 18px; display: block; }
|
||
.app-body { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 14px 16px 40px; -webkit-overflow-scrolling: touch; }
|
||
.app-body::-webkit-scrollbar { width: 0; }
|
||
|
||
/* Wiederverwendbare Bausteine */
|
||
.row {
|
||
display: flex; align-items: center; gap: 12px; padding: 12px 14px;
|
||
background: var(--s-surface); border-radius: 14px; margin-bottom: 8px;
|
||
border: 1px solid var(--s-border);
|
||
}
|
||
.row .avatar {
|
||
width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0; display: grid; place-items: center;
|
||
font-weight: 700; font-size: 16px; color: #fff;
|
||
}
|
||
.row .grow { flex: 1; min-width: 0; }
|
||
.row .title { font-weight: 600; font-size: 15px; }
|
||
.row .meta { font-size: 12.5px; color: var(--s-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.row .side { font-size: 12.5px; color: var(--s-muted); text-align: right; flex-shrink: 0; }
|
||
.list-title { font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em;
|
||
color: var(--s-muted); font-weight: 700; margin: 14px 4px 8px; }
|
||
|
||
.pill { font-size: 11px; font-weight: 700; padding: 3px 9px; border-radius: 999px; display: inline-block; }
|
||
.pill.green { background: rgba(52,211,153,0.16); color: var(--s-green); }
|
||
.pill.red { background: rgba(240,80,110,0.16); color: var(--s-red); }
|
||
.pill.amber { background: rgba(245,178,61,0.16); color: var(--s-amber); }
|
||
.pill.blue { background: rgba(74,168,255,0.16); color: var(--s-blue); }
|
||
.pill.violet { background: rgba(123,108,246,0.18); color: var(--s-accent); }
|
||
|
||
.btn {
|
||
background: var(--s-accent); color: #fff; border: none; border-radius: 13px;
|
||
padding: 13px 18px; font-size: 15px; font-weight: 600; cursor: pointer; font-family: inherit;
|
||
width: 100%; transition: filter .15s ease, transform .1s ease;
|
||
}
|
||
.btn:hover { filter: brightness(1.08); }
|
||
.btn:active { transform: scale(0.98); }
|
||
.btn.green { background: var(--s-green); }
|
||
.btn.ghost { background: var(--s-surface-2); color: var(--s-fg); }
|
||
.btn.red { background: var(--s-red); }
|
||
.input {
|
||
width: 100%; background: var(--s-surface-2); border: 1px solid var(--s-border);
|
||
border-radius: 12px; padding: 13px 14px; color: var(--s-fg); font-size: 15px;
|
||
font-family: inherit; outline: none;
|
||
}
|
||
.input:focus { border-color: var(--s-accent); }
|
||
/* Dropdown (Auswahlfeld) mit eigenem Pfeil */
|
||
select.input {
|
||
-webkit-appearance: none; appearance: none; cursor: pointer;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%238a8da3' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat; background-position: right 14px center; padding-right: 38px;
|
||
}
|
||
select.input option { background: #1e2029; color: var(--s-fg); }
|
||
.field-label { font-size: 12px; color: var(--s-muted); font-weight: 600; margin: 12px 4px 6px; display: block; }
|
||
|
||
/* ---- Telefon-App ---- */
|
||
.dial-display { text-align: center; font-size: 34px; font-weight: 500; padding: 16px 0 8px; min-height: 44px;
|
||
font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
|
||
.dialpad { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; padding: 6px 30px 4px; }
|
||
.dial-key {
|
||
aspect-ratio: 1; border-radius: 50%; background: var(--s-surface-2); border: 1px solid var(--s-border);
|
||
color: var(--s-fg); font-size: 27px; font-weight: 500; cursor: pointer; font-family: inherit;
|
||
display: flex; flex-direction: column; align-items: center; justify-content: center; line-height: 1;
|
||
}
|
||
.dial-key small { font-size: 9px; letter-spacing: 0.14em; color: var(--s-muted); font-weight: 700; margin-top: 2px; }
|
||
.dial-key:active { background: var(--s-accent); color:#fff; }
|
||
.call-bar { display: flex; align-items: center; justify-content: center; gap: 30px; padding: 14px 0 6px; }
|
||
.call-btn { width: 66px; height: 66px; border-radius: 50%; border: none; cursor: pointer; display: grid; place-items: center; }
|
||
.call-btn.call { background: var(--s-green); }
|
||
.call-btn.del { background: transparent; }
|
||
.seg { display: flex; background: var(--s-surface-2); border-radius: 11px; padding: 3px; margin-bottom: 12px; }
|
||
.seg button { flex: 1; border: none; background: none; color: var(--s-muted); font-weight: 600;
|
||
padding: 8px; border-radius: 8px; cursor: pointer; font-family: inherit; font-size: 13px; }
|
||
.seg button.active { background: var(--s-accent); color: #fff; }
|
||
|
||
/* aktiver Anruf */
|
||
.in-call {
|
||
position: absolute; inset: 0; z-index: 70; display: none; flex-direction: column;
|
||
align-items: center; padding-top: 100px; text-align: center;
|
||
background: radial-gradient(120% 80% at 50% 0%, #1a3a2f, #07110c);
|
||
color: #fff;
|
||
}
|
||
.in-call.show { display: flex; }
|
||
.in-call .who { font-size: 30px; font-weight: 700; }
|
||
.in-call .status { color: rgba(255,255,255,0.7); margin-top: 6px; font-variant-numeric: tabular-nums; }
|
||
.in-call .big-avatar { width: 120px; height: 120px; border-radius: 50%; margin: 30px 0;
|
||
display: grid; place-items: center; font-size: 44px; font-weight: 700;
|
||
background: linear-gradient(160deg,#4ade80,#16a34a); }
|
||
.in-call .call-buttons { margin-top: auto; margin-bottom: 70px; display: flex; gap: 40px; }
|
||
.in-call .end, .in-call .accept { width: 72px; height: 72px; border-radius: 50%;
|
||
border: none; cursor: pointer; display: grid; place-items: center; }
|
||
.in-call .end { background: var(--s-red); }
|
||
.in-call .accept { background: #34d39a; display: none; }
|
||
/* Annehmen erscheint nur bei einem eingehenden Anruf */
|
||
.in-call[data-eingehend] .accept { display: grid; }
|
||
.in-call .accept svg { animation: klingelPuls 1s ease-in-out infinite; }
|
||
@keyframes klingelPuls { 50% { transform: scale(1.12); } }
|
||
|
||
/* ---- Nachrichten ---- */
|
||
.chat-view { display: none; flex-direction: column; height: 100%; }
|
||
.chat-view.open { display: flex; }
|
||
.bubbles { flex: 1; overflow-y: auto; padding: 14px 14px 8px; display: flex; flex-direction: column; gap: 7px; }
|
||
.bubble { max-width: 76%; padding: 9px 13px; border-radius: 18px; font-size: 14.5px; line-height: 1.35; word-wrap: break-word; }
|
||
.bubble.them { background: var(--s-surface-2); align-self: flex-start; border-bottom-left-radius: 5px; }
|
||
.bubble.me { background: var(--s-accent); color: #fff; align-self: flex-end; border-bottom-right-radius: 5px; }
|
||
.bubble .time { display: block; font-size: 10px; opacity: 0.6; margin-top: 3px; }
|
||
.composer { display: flex; gap: 8px; padding: 10px 12px 14px; border-top: 1px solid var(--s-border);
|
||
background: var(--s-surface); }
|
||
.composer input { flex: 1; }
|
||
.send-btn { width: 44px; height: 44px; border-radius: 50%; background: var(--s-accent); border: none;
|
||
cursor: pointer; display: grid; place-items: center; flex-shrink: 0; }
|
||
|
||
/* ---- Bank ---- */
|
||
.bank-card {
|
||
border-radius: 22px; padding: 22px; color: #fff; position: relative; overflow: hidden;
|
||
background: linear-gradient(135deg, #1b3b34, #0c221d);
|
||
border: 1px solid rgba(52,211,153,0.3); margin-bottom: 6px;
|
||
}
|
||
.bank-card::after { content:""; position:absolute; width:200px; height:200px; border-radius:50%;
|
||
background: radial-gradient(circle, rgba(52,211,153,0.35), transparent 70%); top:-80px; right:-40px; }
|
||
.bank-card .lbl { font-size: 12px; opacity: 0.75; letter-spacing: 0.04em; }
|
||
.bank-card .bal { font-size: 40px; font-weight: 700; margin: 6px 0 2px; letter-spacing: -0.02em;
|
||
font-variant-numeric: tabular-nums; }
|
||
.bank-card .no { font-family: ui-monospace, monospace; letter-spacing: 0.14em; opacity: 0.8; margin-top: 18px; font-size: 14px; }
|
||
.bank-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 14px 0; }
|
||
.tx { display: flex; align-items: center; gap: 12px; padding: 11px 4px; border-bottom: 1px solid var(--s-border); }
|
||
.tx .ti { width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center; flex-shrink: 0;
|
||
background: var(--s-surface-2); }
|
||
.tx .amt { font-weight: 700; font-variant-numeric: tabular-nums; }
|
||
.tx .amt.plus { color: var(--s-green); }
|
||
|
||
/* ---- Social (Birdy) ---- */
|
||
.post { background: var(--s-surface); border: 1px solid var(--s-border); border-radius: 16px;
|
||
padding: 14px; margin-bottom: 10px; }
|
||
.post .ph { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||
.post .ph .avatar { width: 40px; height: 40px; }
|
||
.post .name { font-weight: 700; font-size: 14px; }
|
||
.post .handle { color: var(--s-muted); font-size: 12.5px; }
|
||
.post .txt { font-size: 14.5px; line-height: 1.4; }
|
||
.post .actions { display: flex; gap: 24px; margin-top: 10px; color: var(--s-muted); font-size: 12.5px; }
|
||
.post .actions button { background: none; border: none; color: inherit; cursor: pointer; font-family: inherit;
|
||
display: flex; align-items: center; gap: 5px; font-size: 12.5px; padding: 0; }
|
||
.post .actions button.liked { color: var(--s-red); }
|
||
.composer-box { background: var(--s-surface); border: 1px solid var(--s-border); border-radius: 16px;
|
||
padding: 12px; margin-bottom: 14px; }
|
||
.composer-box textarea { width: 100%; background: transparent; border: none; color: var(--s-fg);
|
||
resize: none; font-family: inherit; font-size: 15px; outline: none; min-height: 48px; }
|
||
|
||
/* ---- Kamera / Galerie ---- */
|
||
.gallery { display: grid; grid-template-columns: repeat(3,1fr); gap: 4px; }
|
||
.gallery .ph { aspect-ratio: 1; border-radius: 6px; overflow: hidden; position: relative; }
|
||
.cam-view { position: relative; height: 100%; display: flex; flex-direction: column; }
|
||
.cam-feed { flex: 1; position: relative; overflow: hidden;
|
||
background: radial-gradient(80% 60% at 50% 40%, #2a3b52, #0a0f18); }
|
||
.cam-controls { padding: 20px 0 40px; display: flex; align-items: center; justify-content: center; gap: 40px;
|
||
background: #000; }
|
||
.shutter { width: 68px; height: 68px; border-radius: 50%; background: #fff; border: 4px solid #000;
|
||
box-shadow: 0 0 0 3px #fff; cursor: pointer; }
|
||
.cam-thumb { width: 44px; height: 44px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.3); }
|
||
|
||
/* ---- Rechner ---- */
|
||
.calc-display { text-align: right; padding: 8px 6px 4px; font-size: 40px; font-weight: 300;
|
||
font-variant-numeric: tabular-nums; overflow: hidden; white-space: nowrap;
|
||
max-width: 296px; margin: 0 auto; width: 100%; }
|
||
.calc-pad { display: grid; grid-template-columns: repeat(4,1fr); gap: 10px; padding: 0 0 8px;
|
||
max-width: 296px; margin: 0 auto; width: 100%; }
|
||
.calc-key { aspect-ratio: 1; border-radius: 50%; border: none; font-size: 21px; cursor: pointer;
|
||
background: var(--s-surface-2); color: var(--s-fg); font-family: inherit; font-weight: 500; }
|
||
.calc-key.op { background: var(--s-amber); color: #fff; }
|
||
.calc-key.fn { background: #6b7280; color: #fff; }
|
||
.calc-key.eq { background: var(--s-accent); color: #fff; }
|
||
.calc-key.zero { aspect-ratio: auto; grid-column: span 2; border-radius: 40px; text-align: left; padding-left: 28px; }
|
||
|
||
/* ---- Wetter ---- */
|
||
.weather-hero { text-align: center; padding: 20px 0 10px; }
|
||
.weather-hero .city { font-size: 26px; font-weight: 600; }
|
||
.weather-hero .temp { font-size: 82px; font-weight: 200; line-height: 1; }
|
||
.weather-hero .cond { font-size: 15px; color: var(--s-muted); }
|
||
.weather-hour { display: flex; gap: 14px; overflow-x: auto; padding: 14px 4px; }
|
||
.weather-hour .h { display: flex; flex-direction: column; align-items: center; gap: 8px; flex-shrink: 0;
|
||
font-size: 13px; }
|
||
.weather-hour .h .t { font-weight: 700; }
|
||
|
||
/* ---- Einstellungen ---- */
|
||
.setting-row { display: flex; align-items: center; gap: 12px; padding: 13px 14px;
|
||
background: var(--s-surface); border-bottom: 1px solid var(--s-border); }
|
||
.setting-row:first-child { border-radius: 14px 14px 0 0; }
|
||
.setting-row:last-child { border-radius: 0 0 14px 14px; border-bottom: none; }
|
||
.setting-row .si { width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; flex-shrink: 0; }
|
||
.switch { width: 50px; height: 30px; border-radius: 999px; background: var(--s-surface-2); position: relative;
|
||
cursor: pointer; border: none; flex-shrink: 0; transition: background .2s; }
|
||
.switch::after { content: ""; position: absolute; top: 3px; left: 3px; width: 24px; height: 24px;
|
||
border-radius: 50%; background: #fff; transition: transform .2s; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
|
||
.switch.on { background: var(--s-green); }
|
||
.switch.on::after { transform: translateX(20px); }
|
||
.wp-picker { display: grid; grid-template-columns: repeat(4,1fr); gap: 10px; margin-top: 8px; }
|
||
.wp-opt { aspect-ratio: 9/16; border-radius: 12px; cursor: pointer; border: 2px solid transparent; }
|
||
.wp-opt.sel { border-color: var(--s-accent); }
|
||
|
||
/* ---- MDT (Polizei / Dienst) ---- */
|
||
.mdt-stat { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; }
|
||
.mdt-box { background: var(--s-surface); border: 1px solid var(--s-border); border-radius: 14px; padding: 14px; }
|
||
.mdt-box .n { font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||
.mdt-box .l { font-size: 11.5px; color: var(--s-muted); text-transform: uppercase; letter-spacing: 0.04em; }
|
||
|
||
/* Generischer Empty-State */
|
||
.empty { text-align: center; color: var(--s-muted); padding: 50px 20px; }
|
||
.empty .big { font-size: 40px; margin-bottom: 10px; }
|
||
|
||
/* Sperrbildschirm-Benachrichtigung: nach links wegwischbar */
|
||
.lock-note { touch-action: pan-y; user-select: none; }
|
||
.lock-note.swiping { cursor: grabbing; }
|
||
|
||
/* Push-Banner (oben eingeblendet) */
|
||
.notif-banner {
|
||
position: absolute; top: 8px; left: 10px; right: 10px; z-index: 80;
|
||
background: rgba(30,32,44,0.86); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
||
border: 1px solid rgba(255,255,255,0.12); border-radius: 20px; padding: 12px 14px;
|
||
display: none; gap: 11px; align-items: center; cursor: pointer;
|
||
box-shadow: 0 14px 34px -10px rgba(0,0,0,0.7);
|
||
transform: translateY(-140%); transition: transform .38s cubic-bezier(.2,.8,.2,1);
|
||
}
|
||
.notif-banner.show { display: flex; transform: translateY(0); }
|
||
.notif-banner .ic { width: 34px; height: 34px; border-radius: 9px; flex-shrink: 0; display: grid; place-items: center; }
|
||
.notif-banner .ic svg { width: 18px; height: 18px; }
|
||
.notif-banner .txt { min-width: 0; flex: 1; }
|
||
.notif-banner .txt .t { font-size: 13px; font-weight: 700; color: #fff; }
|
||
.notif-banner .txt .d { font-size: 12.5px; color: rgba(255,255,255,0.78); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
|
||
/* Staatsapp: einfaches Balkendiagramm (Einnahmen vs. Ausgaben) */
|
||
.staat-card { border-radius: 22px; padding: 20px; color: #fff; margin-bottom: 12px;
|
||
background: linear-gradient(135deg,#3a466b,#20263a);
|
||
border: 1px solid rgba(255,255,255,0.12); position: relative; overflow: hidden; }
|
||
.staat-card .lbl { font-size: 12px; opacity: 0.75; letter-spacing: 0.04em; }
|
||
.staat-card .bal { font-size: 34px; font-weight: 700; margin: 4px 0; font-variant-numeric: tabular-nums; }
|
||
.chart { display: flex; align-items: flex-end; gap: 10px; height: 130px; padding: 8px 4px 0; }
|
||
.chart .col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; height: 100%; justify-content: flex-end; }
|
||
.chart .bars { display: flex; gap: 3px; align-items: flex-end; flex: 1; min-height: 0; width: 100%; justify-content: center; }
|
||
.chart .bar { width: 11px; border-radius: 3px 3px 0 0; }
|
||
.chart .bar.ein { background: var(--s-green); }
|
||
.chart .bar.aus { background: var(--s-red); }
|
||
.chart .m { font-size: 11px; color: var(--s-muted); }
|
||
.legende { display: flex; gap: 16px; justify-content: center; font-size: 12px; color: var(--s-muted); margin-top: 6px; }
|
||
.legende span { display: inline-flex; align-items: center; gap: 6px; }
|
||
.legende i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
|
||
.vote-bar { height: 8px; border-radius: 4px; background: var(--s-surface-2); overflow: hidden; display: flex; margin: 8px 0 4px; }
|
||
.vote-bar .ja { background: var(--s-green); height: 100%; }
|
||
.vote-bar .nein { background: var(--s-red); height: 100%; }
|
||
.vote-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
|
||
|
||
/* Vollbild-Overlays: Ersteinrichtung & Entsperren (immer dunkel gehalten) */
|
||
.fullscreen-overlay {
|
||
position: absolute; inset: 0; z-index: 250; display: none; flex-direction: column;
|
||
color: #f4f5fb;
|
||
background:
|
||
radial-gradient(120% 80% at 50% 0%, rgba(123,108,246,0.35), transparent 55%),
|
||
linear-gradient(160deg, #0e1020, #05060b);
|
||
}
|
||
.fullscreen-overlay.show { display: flex; }
|
||
|
||
/* Onboarding-Schritte */
|
||
.setup-inner { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||
text-align: center; padding: 44px 30px; gap: 14px; }
|
||
.setup-logo { width: 66px; height: 66px; border-radius: 18px; display: grid; place-items: center; font-size: 32px;
|
||
background: linear-gradient(145deg,#7b6cf6,#34d399); box-shadow: 0 12px 30px rgba(123,108,246,0.5); margin-bottom: 6px; }
|
||
.setup-h1 { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; text-wrap: balance; }
|
||
.setup-p { font-size: 14px; color: #a9acc2; line-height: 1.55; max-width: 280px; }
|
||
.setup-inner .btn { max-width: 220px; }
|
||
|
||
/* PIN-Eingabe (Setup & Entsperren) */
|
||
.pin-wrap { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 30px 24px; }
|
||
.pin-icon { font-size: 44px; margin-bottom: 8px; }
|
||
.pin-title { font-size: 20px; font-weight: 700; }
|
||
.pin-sub { font-size: 13.5px; color: #a9acc2; margin-top: 4px; margin-bottom: 20px; text-align: center; }
|
||
.pin-dots { display: flex; gap: 16px; margin-bottom: 30px; }
|
||
.pin-dot { width: 14px; height: 14px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.55); transition: background .12s; }
|
||
.pin-dot.on { background: #fff; border-color: #fff; }
|
||
.pin-pad { display: grid; grid-template-columns: repeat(3, 70px); gap: 16px; justify-content: center; }
|
||
.pin-key { width: 70px; height: 70px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.12);
|
||
background: rgba(255,255,255,0.09); color: #fff; font-size: 27px; font-weight: 400; cursor: pointer; font-family: inherit; }
|
||
.pin-key:active { background: rgba(255,255,255,0.25); }
|
||
.pin-key.pin-del { background: none; border: none; font-size: 22px; }
|
||
.pin-cancel { margin-top: 22px; background: none; border: none; color: #9a8cff; font-size: 15px; font-weight: 600;
|
||
cursor: pointer; font-family: inherit; }
|
||
|
||
/* Face-ID-Scan (Apple-artiges Glyph mit Scan-Strahl, löst sich in einen Haken auf) */
|
||
.faceid { position: relative; width: 118px; height: 118px; margin: 0 auto 10px; overflow: hidden; }
|
||
.faceid-glyph, .faceid-check { position: absolute; inset: 0; width: 100%; height: 100%;
|
||
transition: opacity .3s ease, transform .3s ease; }
|
||
.faceid-glyph { animation: fidGlow 1.6s ease-in-out infinite; }
|
||
@keyframes fidGlow { 0%,100% { opacity: .82; } 50% { opacity: 1; } }
|
||
/* wandernder Scan-Strahl */
|
||
.faceid-scanline { position: absolute; left: 10px; right: 10px; height: 10px; top: 50%; margin-top: -5px;
|
||
border-radius: 6px; pointer-events: none;
|
||
background: linear-gradient(90deg, transparent, rgba(52,211,153,0.95), transparent);
|
||
box-shadow: 0 0 18px 5px rgba(52,211,153,0.45);
|
||
animation: fidScan 1.15s ease-in-out infinite alternate; }
|
||
@keyframes fidScan { from { transform: translateY(-42px); } to { transform: translateY(42px); } }
|
||
/* Erfolgs-Haken (anfangs verborgen) */
|
||
.faceid-check { opacity: 0; transform: scale(.6); }
|
||
.faceid-check .ring { stroke-dasharray: 214; stroke-dashoffset: 214; }
|
||
.faceid-check .tick { stroke-dasharray: 52; stroke-dashoffset: 52; }
|
||
/* Übergang bei Erfolg: Gesicht ausblenden, Haken zeichnen */
|
||
.faceid.done .faceid-glyph { animation: none; opacity: 0; transform: scale(.82); }
|
||
.faceid.done .faceid-scanline { opacity: 0; }
|
||
.faceid.done .faceid-check { opacity: 1; animation: fidPop .45s cubic-bezier(.2,.9,.3,1.2) forwards; }
|
||
.faceid.done .faceid-check .ring { animation: fidDraw .5s ease forwards; }
|
||
.faceid.done .faceid-check .tick { animation: fidDraw .35s .28s ease forwards; }
|
||
@keyframes fidDraw { to { stroke-dashoffset: 0; } }
|
||
@keyframes fidPop { 0% { transform: scale(.6); } 60% { transform: scale(1.06); } 100% { transform: scale(1); } }
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.faceid-glyph, .faceid-scanline { animation: none; }
|
||
.faceid.done .faceid-check { animation: none; }
|
||
.faceid.done .faceid-check .ring, .faceid.done .faceid-check .tick { animation: none; stroke-dashoffset: 0; }
|
||
}
|
||
|
||
/* Overlay bei leerem Akku / ausgeschaltetem Handy */
|
||
.power-off {
|
||
position: absolute; inset: 0; z-index: 300; background: #000; color: #d0d2dc;
|
||
display: none; flex-direction: column; align-items: center; justify-content: center;
|
||
text-align: center; padding: 0 40px;
|
||
}
|
||
.power-off.show { display: flex; }
|
||
.power-off .po-icon { font-size: 60px; margin-bottom: 14px; filter: grayscale(0.2); }
|
||
.power-off .po-title { font-size: 22px; font-weight: 700; color: #fff; }
|
||
.power-off .po-sub { font-size: 13.5px; margin-top: 8px; line-height: 1.5; color: #8a8da3; }
|
||
|
||
.toast {
|
||
position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%) translateY(20px);
|
||
background: rgba(20,22,32,0.95); color: #fff; padding: 12px 20px; border-radius: 14px;
|
||
font-size: 13.5px; font-weight: 600; z-index: 200; opacity: 0; pointer-events: none;
|
||
transition: all .3s ease; border: 1px solid rgba(255,255,255,0.12); white-space: nowrap;
|
||
max-width: 90%; text-align: center;
|
||
}
|
||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||
</style>
|
||
|
||
<!-- ==================== Linkes Info-Panel ==================== -->
|
||
<aside class="side-panel left">
|
||
<div class="brand-mark"><span class="dot"></span> AetherOS</div>
|
||
<p>Ein voll interaktiver Web-Prototyp eines RP-Smartphones für deinen deutschsprachigen FiveM GTA-Server. Alles läuft ohne Server — reines HTML/CSS/JS zum Ansehen.</p>
|
||
<h2>So bedienst du es</h2>
|
||
<ul class="hint-list">
|
||
<li><span class="kbd">↑</span> <span><b>Wischen/Tippen</b> auf den Sperrbildschirm entsperrt das Handy.</span></li>
|
||
<li><span class="kbd">←</span> <span>Mitteilungen am Sperrbildschirm <b>nach links wegwischen</b>.</span></li>
|
||
<li><span class="kbd">▬</span> <span>Die <b>Leiste unten</b> bringt dich zurück zum Homescreen.</span></li>
|
||
<li><span class="kbd">🔐</span> <span>Beim <b>Erststart Code festlegen</b>. Entsperren per <b>Face-ID</b> oder Code (Einstellungen → Sicherheit).</span></li>
|
||
</ul>
|
||
|
||
<h2>Push-Mitteilung auslösen</h2>
|
||
<p>Im Server kommen diese Events vom Backend. Hier zum Testen manuell:</p>
|
||
<div style="display:flex;flex-wrap:wrap;gap:8px">
|
||
<button class="demo-btn" data-push="messages">💬 Nachricht</button>
|
||
<button class="demo-btn" data-push="bank">🏦 Bank</button>
|
||
<button class="demo-btn" data-push="dispatch">📡 Dispatch</button>
|
||
<button class="demo-btn" data-push="birdy">🐦 Birdy</button>
|
||
</div>
|
||
|
||
<h2>Gesperrte Apps</h2>
|
||
<p>Manche Apps (z. B. Darkchat) sind nicht für jeden verfügbar — im Spiel per Gamemechanik freizuschalten. Hier zum Testen:</p>
|
||
<button class="demo-btn" id="unlock-darkweb" style="width:100%">🔓 Darkchat freischalten</button>
|
||
|
||
<h2>Hardware & Items</h2>
|
||
<p>Handy, SIM-Karte und Akku sind im Spiel eigene <b>Items</b>, die kombiniert werden. Akkustand: <b id="demo-akku">78%</b></p>
|
||
<div style="display:flex;flex-wrap:wrap;gap:8px">
|
||
<button class="demo-btn" data-hw="drain">🪫 Akku entladen</button>
|
||
<button class="demo-btn" data-hw="newbatt">🔋 Neuen Akku einsetzen</button>
|
||
<button class="demo-btn" data-hw="sim">📶 SIM entfernen / einsetzen</button>
|
||
<button class="demo-btn" data-hw="handy">📱 Handy aus / an</button>
|
||
</div>
|
||
</aside>
|
||
<style>
|
||
.demo-btn {
|
||
background: var(--page-card); border: 1px solid var(--page-border); color: var(--page-fg);
|
||
border-radius: 10px; padding: 9px 12px; font-size: 12.5px; font-weight: 600; cursor: pointer;
|
||
font-family: inherit; transition: filter .15s ease, transform .1s ease;
|
||
}
|
||
.demo-btn:hover { filter: brightness(1.06); border-color: var(--accent); }
|
||
.demo-btn:active { transform: scale(0.96); }
|
||
</style>
|
||
|
||
<!-- ==================== Das Handy ==================== -->
|
||
<div class="device">
|
||
<div class="screen locked" id="screen">
|
||
<div class="wallpaper wp-aurora" id="wallpaper"></div>
|
||
|
||
<!-- Dynamic Island + Statusleiste -->
|
||
<div class="island"><span class="cam"></span></div>
|
||
<div class="statusbar">
|
||
<span id="sb-time">14:32</span>
|
||
<span class="sb-right">
|
||
<span id="sb-net" style="font-size:11px;font-weight:700;display:none">Kein Netz</span>
|
||
<span id="sb-signal" title="Netz">
|
||
<svg width="18" height="12" viewBox="0 0 18 12" fill="currentColor"><rect x="0" y="8" width="3" height="4" rx="1"/><rect x="5" y="5" width="3" height="7" rx="1"/><rect x="10" y="2" width="3" height="10" rx="1"/><rect x="15" y="0" width="3" height="12" rx="1" opacity="0.4"/></svg>
|
||
</span>
|
||
<span style="display:flex;align-items:center;gap:4px;font-size:12px;">
|
||
<span id="sb-batt-pct" style="font-variant-numeric:tabular-nums">78%</span>
|
||
<svg width="26" height="13" viewBox="0 0 26 13"><rect x="0.5" y="0.5" width="22" height="12" rx="3.5" fill="none" stroke="currentColor" opacity="0.5"/><rect id="sb-batt-fill" x="2" y="2" width="17" height="9" rx="2" fill="currentColor"/><rect x="23.5" y="4" width="1.8" height="5" rx="1" fill="currentColor" opacity="0.5"/></svg>
|
||
</span>
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Sperrbildschirm -->
|
||
<div class="lockscreen" id="lockscreen">
|
||
<div class="lock-date" id="lock-date">Dienstag, 21. Juli</div>
|
||
<div class="lock-time" id="lock-time">14:32</div>
|
||
<div class="lock-notes" id="lock-notes"><!-- Benachrichtigungen per JS --></div>
|
||
<div class="lock-unlock">Tippen zum Entsperren · ← Mitteilung wegwischen</div>
|
||
<button class="lock-btn" id="lock-btn" aria-label="Entsperren"></button>
|
||
</div>
|
||
|
||
<!-- Homescreen -->
|
||
<div class="homescreen" id="homescreen">
|
||
<div class="app-grid" id="app-grid"><!-- Apps per JS --></div>
|
||
<div class="dock" id="dock"><!-- Dock-Apps per JS --></div>
|
||
</div>
|
||
|
||
<!-- App-Container (wird per JS befüllt) -->
|
||
<div class="app-view" id="app-view">
|
||
<div class="app-bg"></div>
|
||
<div id="app-content" style="display:flex;flex-direction:column;height:100%;"></div>
|
||
</div>
|
||
|
||
<!-- Overlay: aktiver Anruf -->
|
||
<div class="in-call" id="in-call">
|
||
<div class="who" id="call-who">Unbekannt</div>
|
||
<div class="status" id="call-status">Anruf läuft…</div>
|
||
<div class="big-avatar" id="call-avatar">?</div>
|
||
<div class="call-buttons">
|
||
<!-- Nur bei eingehenden Anrufen sichtbar (siehe CSS) -->
|
||
<button class="accept" id="call-accept" aria-label="Annehmen">
|
||
<svg width="30" height="30" viewBox="0 0 24 24" fill="#fff"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08a.996.996 0 0 1-.29-.7c0-.28.11-.53.29-.71C3.34 8.78 7.46 7 12 7s8.66 1.78 11.71 4.67c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-1.48 1.48c-.18.18-.43.29-.71.29-.27 0-.52-.11-.7-.28a11.27 11.27 0 0 0-2.66-1.85.998.998 0 0 1-.56-.9v-3.1C15.15 9.25 13.6 9 12 9Z"/></svg>
|
||
</button>
|
||
<button class="end" id="call-end" aria-label="Auflegen">
|
||
<svg width="30" height="30" viewBox="0 0 24 24" fill="#fff"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08a.996.996 0 0 1-.29-.7c0-.28.11-.53.29-.71C3.34 8.78 7.46 7 12 7s8.66 1.78 11.71 4.67c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-1.48 1.48c-.18.18-.43.29-.71.29-.27 0-.52-.11-.7-.28a11.27 11.27 0 0 0-2.66-1.85.998.998 0 0 1-.56-.9v-3.1C15.15 9.25 13.6 9 12 9Z" transform="rotate(135 12 12)"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Push-Banner (oben eingeblendet) -->
|
||
<div class="notif-banner" id="notif-banner">
|
||
<span class="ic" id="notif-ic"></span>
|
||
<div class="txt"><div class="t" id="notif-t"></div><div class="d" id="notif-d"></div></div>
|
||
</div>
|
||
|
||
<!-- Overlay: Ersteinrichtung (Code festlegen) -->
|
||
<div class="fullscreen-overlay" id="setup-screen"></div>
|
||
<!-- Overlay: Entsperren per Face-ID oder Code -->
|
||
<div class="fullscreen-overlay" id="unlock-screen"></div>
|
||
|
||
<!-- Overlay: Akku leer / Handy aus -->
|
||
<div class="power-off" id="power-off">
|
||
<div class="po-icon" id="po-icon">🪫</div>
|
||
<div class="po-title" id="po-title">Akku leer</div>
|
||
<div class="po-sub" id="po-sub">Setze einen geladenen Akku (Item) ein, um das Handy zu nutzen.</div>
|
||
</div>
|
||
|
||
<button class="home-indicator" id="home-indicator" aria-label="Startbildschirm"></button>
|
||
<div class="toast" id="toast"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ==================== Rechtes Info-Panel ==================== -->
|
||
<aside class="side-panel right">
|
||
<h2>Enthaltene Apps</h2>
|
||
<div>
|
||
<span class="feature-tag">📞 Telefon & Wähler</span>
|
||
<span class="feature-tag">💬 Nachrichten</span>
|
||
<span class="feature-tag">🏦 Bank / Fleeca</span>
|
||
<span class="feature-tag">🐦 Birdy (Social)</span>
|
||
<span class="feature-tag">📸 Kamera & Galerie</span>
|
||
<span class="feature-tag">🎵 Musik</span>
|
||
<span class="feature-tag">📡 Dispatch / Leitstelle</span>
|
||
<span class="feature-tag">🏛️ Staat / Staatskasse</span>
|
||
<span class="feature-tag">🚗 Garage</span>
|
||
<span class="feature-tag">📒 Gelbe Seiten</span>
|
||
<span class="feature-tag">🔒 Darkchat (freischaltbar)</span>
|
||
<span class="feature-tag">🌤️ Wetter</span>
|
||
<span class="feature-tag">🧮 Rechner</span>
|
||
<span class="feature-tag">⚙️ Einstellungen</span>
|
||
</div>
|
||
<h2>Für FiveM gedacht</h2>
|
||
<p>Datenmodelle (Kontakte, Konten, Chats, Posts) sind bewusst getrennt vom UI gehalten — später einfach an <code>NUI</code>-Callbacks & deine Datenbank anbinden.</p>
|
||
</aside>
|
||
|
||
<script>
|
||
"use strict";
|
||
|
||
/* =====================================================================
|
||
ZENTRALER ZUSTAND — alle "Server-Daten" gebündelt an einer Stelle,
|
||
damit später eine Anbindung an FiveM/NUI leichtfällt.
|
||
===================================================================== */
|
||
let spielerZustand = {
|
||
name: "Max Weber", // Name des Spielercharakters
|
||
nummer: "555-0182", // Telefonnummer des Spielers
|
||
kontostand: 48320, // Bankguthaben in Euro
|
||
kontonummer: "DE 4021 5589", // angezeigte Kontonummer
|
||
theme: "dark", // aktuelles Handy-Theme (dark/light)
|
||
wallpaper: "wp-aurora", // aktuell gewähltes Hintergrundbild
|
||
dockApps: ["phone", "messages", "bank", "camera"], // anpassbare Dock-Leiste (4 Apps)
|
||
freigeschaltet: { darkweb: false }, // per Gamemechanik freischaltbare Apps
|
||
// Hardware — im Spiel jeweils eigene Items, die kombiniert werden:
|
||
handy: true, // besitzt der Spieler ein Handy (Item)?
|
||
sim: true, // ist eine SIM-Karte (Item) eingelegt?
|
||
akku: 78, // Akkustand in Prozent (Akku = Item, geht leer, austauschbar)
|
||
// Sicherheit — schützt das Handy, falls es im Spiel gestohlen wird:
|
||
pin: null, // festgelegter Entsperrcode (null = noch nicht eingerichtet -> Setup)
|
||
faceId: true // bequemes Entsperren ohne Code-Eingabe
|
||
};
|
||
|
||
// Kontaktliste des Spielers
|
||
let kontakteListe = [
|
||
{ name: "Polizei (Notruf)", nummer: "110", farbe: "#1e5eff", initialen: "🚓", notruf: true },
|
||
{ name: "Rettungsdienst", nummer: "112", farbe: "#f0506e", initialen: "🚑", notruf: true },
|
||
{ name: "Familie Schmidt", nummer: "555-0147", farbe: "#7b6cf6", initialen: "FS" },
|
||
{ name: "Benny's Werkstatt", nummer: "555-0199", farbe: "#f5b23d", initialen: "BW" },
|
||
{ name: "Taxi Zentrale", nummer: "555-0100", farbe: "#facc15", initialen: "TX" },
|
||
{ name: "Dr. Weber (Anwalt)", nummer: "555-0163", farbe: "#34d399", initialen: "AW" },
|
||
{ name: "Lena Berger", nummer: "555-0134", farbe: "#e11d48", initialen: "LB" }
|
||
];
|
||
|
||
// Anrufliste (Verlauf)
|
||
let anrufListe = [
|
||
{ name: "Lena Berger", zeit: "vor 12 Min", art: "eingehend", verpasst: false },
|
||
{ name: "Benny's Werkstatt", zeit: "vor 1 Std", art: "ausgehend", verpasst: false },
|
||
{ name: "Unbekannt", zeit: "vor 2 Std", art: "eingehend", verpasst: true },
|
||
{ name: "Familie Schmidt", zeit: "Gestern", art: "ausgehend", verpasst: false }
|
||
];
|
||
|
||
// Chatverläufe je Kontakt
|
||
let chatVerlaeufe = {
|
||
"Familie Schmidt": [
|
||
{ von: "them", text: "Hey Max! Wie läuft dein Tag?", zeit: "13:40" },
|
||
{ von: "me", text: "Alles gut, bin gerade beim Tanken", zeit: "13:42" },
|
||
{ von: "them", text: "Kommst du heute zum Grillen vorbei? 🔥", zeit: "14:15" }
|
||
],
|
||
"Lena Berger": [
|
||
{ von: "them", text: "Treffen wir uns nachher am Pier?", zeit: "12:10" },
|
||
{ von: "me", text: "Klar, so gegen 18 Uhr passt", zeit: "12:12" }
|
||
],
|
||
"Benny's Werkstatt": [
|
||
{ von: "them", text: "Dein Wagen ist fertig, kannst ihn abholen!", zeit: "11:05" }
|
||
],
|
||
"Dr. Weber (Anwalt)": [
|
||
{ von: "them", text: "Die Kaution wurde hinterlegt. Alles geregelt.", zeit: "Gestern" }
|
||
]
|
||
};
|
||
|
||
// Banktransaktionen (neueste zuerst)
|
||
let transaktionsListe = [
|
||
{ titel: "Gehalt — Polizei LSPD", betrag: 2400, zeit: "Heute, 14:00", icon: "💼" },
|
||
{ titel: "Tankstelle LTD", betrag: -85, zeit: "Heute, 13:41", icon: "⛽" },
|
||
{ titel: "Benny's Werkstatt", betrag: -1250, zeit: "Heute, 11:20", icon: "🔧" },
|
||
{ titel: "Überweisung Lena Berger", betrag: -300, zeit: "Gestern", icon: "↗" },
|
||
{ titel: "Miete Apartment 4B", betrag: -1200, zeit: "20. Juli", icon: "🏠" },
|
||
{ titel: "Bargeld-Einzahlung", betrag: 5000, zeit: "19. Juli", icon: "💵" }
|
||
];
|
||
|
||
// Social-Media-Beiträge (Birdy)
|
||
const birdyBeitraege = [
|
||
{ name: "Los Santos News", handle: "@lsnews", initialen: "LS", farbe: "#4aa8ff", text: "🚨 Verkehrschaos an der Autobahn nahe Sandy Shores. Bitte weiträumig umfahren!", likes: 142, retweets: 38, gemocht: false },
|
||
{ name: "Benny's Motorworks", handle: "@bennys", initialen: "BW", farbe: "#f5b23d", text: "Neue Felgen eingetroffen! Kommt vorbei und pimpt euren Wagen. 🔧🚗", likes: 89, retweets: 12, gemocht: false },
|
||
{ name: "Lena Berger", handle: "@lena_b", initialen: "LB", farbe: "#e11d48", text: "Sonnenuntergang am Vespucci Beach heute war einfach traumhaft 🌅", likes: 231, retweets: 45, gemocht: true }
|
||
];
|
||
|
||
/* =====================================================================
|
||
APP-DEFINITIONEN — Name, Icon-Farbe, SVG-Glyph, Badge
|
||
===================================================================== */
|
||
const svgGlyphen = {
|
||
phone: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M6.6 10.8c1.4 2.8 3.8 5.1 6.6 6.6l2.2-2.2c.3-.3.7-.4 1-.2 1.1.4 2.3.6 3.6.6.6 0 1 .4 1 1V20c0 .6-.4 1-1 1C10.6 21 3 13.4 3 4c0-.6.4-1 1-1h3.5c.6 0 1 .4 1 1 0 1.2.2 2.4.6 3.6.1.4 0 .8-.3 1l-2.2 2.2Z"/></svg>',
|
||
msg: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 3C6.5 3 2 6.6 2 11c0 2.4 1.3 4.5 3.4 6-.2 1-.8 2.2-1.7 3.2 1.6-.2 3.2-.8 4.4-1.6 1.2.4 2.5.6 3.9.6 5.5 0 10-3.6 10-8s-4.5-8-10-8Z"/></svg>',
|
||
bank: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2 2 7v2h20V7L12 2ZM4 10v7H2v3h20v-3h-2v-7h-3v7h-3v-7h-3v7H7v-7H4Z"/></svg>',
|
||
cam: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M9 4 7.2 6H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-3.2L15 4H9Zm3 5a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Z"/></svg>',
|
||
birdy: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M22 5.9c-.7.3-1.5.5-2.3.6.8-.5 1.4-1.3 1.7-2.2-.8.4-1.7.8-2.6.9A4 4 0 0 0 12 8.5c0 .3 0 .6.1.9A11.5 11.5 0 0 1 3.8 5a4 4 0 0 0 1.2 5.4c-.6 0-1.2-.2-1.7-.5v.1a4 4 0 0 0 3.2 4 4 4 0 0 1-1.8.1 4 4 0 0 0 3.7 2.8A8 8 0 0 1 2 18.6a11.4 11.4 0 0 0 6.3 1.8c7.5 0 11.6-6.2 11.6-11.6v-.5c.8-.6 1.5-1.3 2.1-2.1Z"/></svg>',
|
||
settings: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm8.9 4c0-.5 0-1-.1-1.5l2-1.5-2-3.4-2.3 1a7 7 0 0 0-2.6-1.5L15.5 2h-4l-.3 2.6A7 7 0 0 0 8.6 6l-2.3-1-2 3.4 2 1.5c-.1.5-.1 1-.1 1.5s0 1 .1 1.5l-2 1.5 2 3.4 2.3-1c.8.7 1.6 1.2 2.6 1.5l.3 2.6h4l.3-2.6c1-.3 1.8-.8 2.6-1.5l2.3 1 2-3.4-2-1.5c.1-.5.1-1 .1-1.5Z"/></svg>',
|
||
music: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M9 18V6l10-2v11.5A3.5 3.5 0 1 1 17 12V7.3L11 8.6V19a3.5 3.5 0 1 1-2-3.16"/></svg>',
|
||
weather: '<svg viewBox="0 0 24 24" fill="#fff"><circle cx="8" cy="8" r="4" opacity="0.9"/><path d="M17.5 12a4.5 4.5 0 0 0-8.6-1.8A4 4 0 1 0 8 18h9a3 3 0 0 0 .5-6Z"/></svg>',
|
||
clock: '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2" stroke-linecap="round"/></svg>',
|
||
calc: '<svg viewBox="0 0 24 24" fill="#fff"><rect x="4" y="2" width="16" height="20" rx="2"/><rect x="6.5" y="4.5" width="11" height="4" rx="1" fill="#14151b"/><g fill="#14151b"><circle cx="8" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="16" cy="12" r="1.2"/><circle cx="8" cy="16" r="1.2"/><circle cx="12" cy="16" r="1.2"/><circle cx="16" cy="16" r="1.2"/></g></svg>',
|
||
garage: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M5 11l1.5-4.5A2 2 0 0 1 8.4 5h7.2a2 2 0 0 1 1.9 1.5L19 11v7h-2v-2H7v2H5v-7Zm2.5 3a1.3 1.3 0 1 0 0-2.6 1.3 1.3 0 0 0 0 2.6Zm9 0a1.3 1.3 0 1 0 0-2.6 1.3 1.3 0 0 0 0 2.6ZM6.8 10h10.4l-1-3H7.8l-1 3Z"/></svg>',
|
||
mdt: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2 4 5v6c0 5 3.4 9.4 8 11 4.6-1.6 8-6 8-11V5l-8-3Zm-1 13-3-3 1.4-1.4L11 12.2l4.6-4.6L17 9l-6 6Z"/></svg>',
|
||
dispatch: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2a8 8 0 0 0-8 8v6a3 3 0 0 0 3 3h1v-8H6v-1a6 6 0 0 1 12 0v1h-2v8h1a3 3 0 0 0 3-3v-6a8 8 0 0 0-8-8Z"/><rect x="10.5" y="6.5" width="3" height="6" rx="1.5" fill="#fff"/></svg>',
|
||
pages: '<svg viewBox="0 0 24 24" fill="#14151b"><path d="M5 3h14a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Zm2 4v2h10V7H7Zm0 4v2h10v-2H7Zm0 4v2h7v-2H7Z"/></svg>',
|
||
dark: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 3a9 9 0 1 0 9 9c0-.5 0-1-.1-1.4A6 6 0 0 1 12 3Z"/></svg>',
|
||
staat: '<svg viewBox="0 0 24 24" fill="#fff"><path d="M12 2 3 6.5V8h18V6.5L12 2Z"/><path d="M5 9v8H4v2.5h16V17h-1V9h-2v8h-3V9h-2v8H9V9H5Z"/></svg>',
|
||
notes: '<svg viewBox="0 0 24 24" fill="#3a3320"><path d="M6 2h9l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2Zm8 1.6V8h4.4L14 3.6ZM7 12h10v1.5H7V12Zm0 3.5h10V17H7v-1.5ZM7 8.5h5V10H7V8.5Z"/></svg>'
|
||
};
|
||
|
||
// Zentrale Registry aller Apps (id -> Anzeigedaten). gated = nur nach Freischaltung sichtbar.
|
||
const alleApps = {
|
||
phone: { label: "Telefon", g: "g-phone", glyph: "phone", badge: 1 },
|
||
messages: { label: "Nachrichten", g: "g-msg", glyph: "msg", badge: 3 },
|
||
bank: { label: "Bank", g: "g-bank", glyph: "bank" },
|
||
camera: { label: "Kamera", g: "g-cam", glyph: "cam" },
|
||
birdy: { label: "Birdy", g: "g-birdy", glyph: "birdy" },
|
||
insta: { label: "InstaPic", g: "g-insta", glyph: "cam" },
|
||
weather: { label: "Wetter", g: "g-weather", glyph: "weather" },
|
||
clock: { label: "Uhr", g: "g-clock", glyph: "clock" },
|
||
calc: { label: "Rechner", g: "g-calc", glyph: "calc" },
|
||
garage: { label: "Garage", g: "g-garage", glyph: "garage" },
|
||
dispatch: { label: "Dispatch", g: "g-dispatch", glyph: "dispatch" },
|
||
pages: { label: "Gelbe Seiten", g: "g-pages", glyph: "pages" },
|
||
music: { label: "Musik", g: "g-music", glyph: "music" },
|
||
contacts: { label: "Kontakte", g: "g-phone", glyph: "phone" },
|
||
staat: { label: "Staat", g: "g-staat", glyph: "staat" },
|
||
notizen: { label: "Notizen", g: "g-notes", glyph: "notes" },
|
||
settings: { label: "Einstellungen", g: "g-set", glyph: "settings" },
|
||
darkweb: { label: "Darkchat", g: "g-dark", glyph: "dark", gated: true }
|
||
};
|
||
|
||
// Master-Reihenfolge aller Apps im Grid (Dock-Apps werden beim Aufbau herausgefiltert)
|
||
const appReihenfolge = [
|
||
"phone", "messages", "bank", "camera", "birdy", "insta", "weather", "clock",
|
||
"calc", "garage", "dispatch", "pages", "music", "contacts", "staat",
|
||
"notizen", "settings", "darkweb"
|
||
];
|
||
|
||
/* =====================================================================
|
||
DOM-REFERENZEN
|
||
===================================================================== */
|
||
const screenEl = document.getElementById("screen");
|
||
const appViewEl = document.getElementById("app-view");
|
||
const appContentEl = document.getElementById("app-content");
|
||
const toastEl = document.getElementById("toast");
|
||
|
||
/* Hilfsfunktion: kurze Einblend-Nachricht ("Toast") anzeigen */
|
||
let toastTimer = null;
|
||
function zeigeToast(nachricht) {
|
||
toastEl.textContent = nachricht;
|
||
toastEl.classList.add("show");
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 2000);
|
||
}
|
||
|
||
/* Hilfsfunktion: Euro-Beträge deutsch formatieren */
|
||
function formatiereEuro(betrag) {
|
||
return betrag.toLocaleString("de-DE", { minimumFractionDigits: 0 }) + " €";
|
||
}
|
||
|
||
/* Hilfsfunktion: HTML gegen einfache Injektion absichern */
|
||
function sicher(text) {
|
||
const div = document.createElement("div");
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
/* =====================================================================
|
||
UHR — Statusleiste und Sperrbildschirm aktualisieren
|
||
===================================================================== */
|
||
function aktualisiereUhr() {
|
||
const jetzt = new Date();
|
||
const stunden = String(jetzt.getHours()).padStart(2, "0");
|
||
const minuten = String(jetzt.getMinutes()).padStart(2, "0");
|
||
const zeitText = stunden + ":" + minuten;
|
||
document.getElementById("sb-time").textContent = zeitText;
|
||
document.getElementById("lock-time").textContent = zeitText;
|
||
|
||
// Deutsches Datum auf dem Sperrbildschirm
|
||
const wochentage = ["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"];
|
||
const monate = ["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"];
|
||
document.getElementById("lock-date").textContent =
|
||
wochentage[jetzt.getDay()] + ", " + jetzt.getDate() + ". " + monate[jetzt.getMonth()];
|
||
}
|
||
aktualisiereUhr();
|
||
setInterval(aktualisiereUhr, 1000);
|
||
|
||
/* =====================================================================
|
||
SPERRBILDSCHIRM & HOMESCREEN-NAVIGATION
|
||
===================================================================== */
|
||
// Handy entsperren (Sperrbildschirm ausblenden)
|
||
function entsperreHandy() { screenEl.classList.remove("locked"); }
|
||
// Klick auf den Sperrbildschirm -> je nach Einstellung Face-ID oder Code
|
||
document.getElementById("lock-btn").addEventListener("click", versucheEntsperren);
|
||
|
||
/* =====================================================================
|
||
SICHERHEIT: Ersteinrichtung, Code-Eingabe & Face-ID
|
||
===================================================================== */
|
||
// Wiederverwendbares PIN-Eingabefeld in einen Container rendern.
|
||
// opts: { titel, unterzeile, icon, stellen=4, onFertig(code, reset), abbrechen:{label,cb} }
|
||
function renderePinFeld(container, opts) {
|
||
const stellen = opts.stellen || 4;
|
||
let code = ""; // aktuell eingegebene Ziffern
|
||
function zeichne() {
|
||
container.innerHTML =
|
||
'<div class="pin-wrap">' +
|
||
(opts.icon ? '<div class="pin-icon">' + opts.icon + '</div>' : '') +
|
||
'<div class="pin-title">' + sicher(opts.titel) + '</div>' +
|
||
(opts.unterzeile ? '<div class="pin-sub">' + sicher(opts.unterzeile) + '</div>' : '') +
|
||
'<div class="pin-dots">' + Array.from({ length: stellen }).map((_, i) =>
|
||
'<span class="pin-dot' + (i < code.length ? ' on' : '') + '"></span>').join("") + '</div>' +
|
||
'<div class="pin-pad">' +
|
||
[1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => '<button class="pin-key" data-k="' + n + '">' + n + '</button>').join("") +
|
||
'<span></span>' +
|
||
'<button class="pin-key" data-k="0">0</button>' +
|
||
'<button class="pin-key pin-del" data-k="del" aria-label="Löschen">⌫</button>' +
|
||
'</div>' +
|
||
(opts.abbrechen ? '<button class="pin-cancel" id="pin-cancel">' + sicher(opts.abbrechen.label) + '</button>' : '') +
|
||
'</div>';
|
||
// Zifferntasten
|
||
container.querySelectorAll(".pin-key").forEach(b =>
|
||
b.addEventListener("click", () => {
|
||
const k = b.dataset.k;
|
||
if (k === "del") { code = code.slice(0, -1); zeichne(); return; }
|
||
if (code.length >= stellen) return;
|
||
code += k;
|
||
zeichne();
|
||
// Bei voller Länge auswerten
|
||
if (code.length === stellen) {
|
||
const fertig = code;
|
||
setTimeout(() => opts.onFertig(fertig, () => { code = ""; zeichne(); }), 160);
|
||
}
|
||
}));
|
||
const abbr = container.querySelector("#pin-cancel");
|
||
if (abbr && opts.abbrechen) abbr.addEventListener("click", opts.abbrechen.cb);
|
||
}
|
||
zeichne();
|
||
}
|
||
|
||
// Entsperr-Versuch: Face-ID (bequem) oder Code-Eingabe
|
||
function versucheEntsperren() {
|
||
if (!spielerZustand.pin) return; // noch nicht eingerichtet -> Setup läuft
|
||
if (spielerZustand.faceId) entsperreMitFaceId();
|
||
else entsperreMitCode();
|
||
}
|
||
|
||
// Apple-artige Face-ID-Grafik: Gesichts-Glyph + Scan-Strahl + Erfolgs-Haken
|
||
const faceIdGrafik =
|
||
'<svg class="faceid-glyph" viewBox="0 0 100 100" fill="none" stroke="#f4f5fb" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round">' +
|
||
'<path d="M8 30 V22 a14 14 0 0 1 14 -14 H30"/>' +
|
||
'<path d="M70 8 H78 a14 14 0 0 1 14 14 V30"/>' +
|
||
'<path d="M92 70 V78 a14 14 0 0 1 -14 14 H70"/>' +
|
||
'<path d="M30 92 H22 a14 14 0 0 1 -14 -14 V70"/>' +
|
||
'<path d="M38 40 V50"/><path d="M62 40 V50"/>' + // Augen
|
||
'<path d="M50 44 V57 h-7"/>' + // Nase
|
||
'<path d="M38 67 q12 10 24 0"/>' + // Mund
|
||
'</svg>' +
|
||
'<div class="faceid-scanline"></div>' +
|
||
'<svg class="faceid-check" viewBox="0 0 100 100" fill="none" stroke="#34d399" stroke-width="5" stroke-linecap="round" stroke-linejoin="round">' +
|
||
'<circle class="ring" cx="50" cy="50" r="34"/>' +
|
||
'<path class="tick" d="M34 51 l11 11 l21 -23"/>' +
|
||
'</svg>';
|
||
|
||
// Face-ID: Scan zeigen, dann in den Haken auflösen und entsperren
|
||
function entsperreMitFaceId() {
|
||
const u = document.getElementById("unlock-screen");
|
||
u.classList.add("show");
|
||
u.innerHTML = '<div class="setup-inner"><div class="faceid" id="faceid-box">' + faceIdGrafik + '</div>' +
|
||
'<div class="pin-sub" id="faceid-text">Face-ID</div></div>';
|
||
const box = document.getElementById("faceid-box");
|
||
// Nach kurzer Scanzeit auf "erkannt" umschalten (Haken zeichnen)
|
||
setTimeout(() => {
|
||
box.classList.add("done");
|
||
const txt = document.getElementById("faceid-text");
|
||
if (txt) txt.textContent = "Entsperrt";
|
||
setTimeout(() => { u.classList.remove("show"); entsperreHandy(); }, 620);
|
||
}, 1150);
|
||
}
|
||
|
||
// Code-Eingabe zum Entsperren
|
||
function entsperreMitCode() {
|
||
const u = document.getElementById("unlock-screen");
|
||
u.classList.add("show");
|
||
renderePinFeld(u, {
|
||
titel: "Code eingeben", unterzeile: "Handy entsperren", icon: "🔒",
|
||
onFertig: (code, reset) => {
|
||
if (!IST_FIVEM) {
|
||
// Browser-Ansicht: lokal prüfen
|
||
if (code === spielerZustand.pin) { u.classList.remove("show"); entsperreHandy(); }
|
||
else { zeigeToast("Falscher Code"); reset(); }
|
||
return;
|
||
}
|
||
// Der Code wird ausschließlich auf dem Server geprüft
|
||
pinZuruecksetzen = reset;
|
||
nui("entsperren", { pin: code });
|
||
}
|
||
});
|
||
}
|
||
// Wird gesetzt, damit die Antwort des Servers das Feld leeren kann
|
||
let pinZuruecksetzen = null;
|
||
|
||
/* ---- Ersteinrichtung (First-Start-Setup) ---- */
|
||
function starteEinrichtung() {
|
||
document.getElementById("setup-screen").classList.add("show");
|
||
setupWillkommen();
|
||
}
|
||
function setupWillkommen() {
|
||
document.getElementById("setup-screen").innerHTML =
|
||
'<div class="setup-inner"><div class="setup-logo">📱</div>' +
|
||
'<div class="setup-h1">Willkommen bei AetherOS</div>' +
|
||
'<div class="setup-p">Lege einen Code fest, damit niemand sonst dein Handy nutzen kann – falls es dir im Spiel gestohlen wird.</div>' +
|
||
'<button class="btn" id="setup-start">Los geht\'s</button></div>';
|
||
document.getElementById("setup-start").addEventListener("click", setupCodeFestlegen);
|
||
}
|
||
function setupCodeFestlegen() {
|
||
renderePinFeld(document.getElementById("setup-screen"), {
|
||
titel: "Code festlegen", unterzeile: "Wähle einen 4-stelligen Code", icon: "🔐",
|
||
onFertig: (code) => setupCodeBestaetigen(code)
|
||
});
|
||
}
|
||
function setupCodeBestaetigen(ersterCode) {
|
||
renderePinFeld(document.getElementById("setup-screen"), {
|
||
titel: "Code bestätigen", unterzeile: "Gib den Code erneut ein", icon: "🔐",
|
||
onFertig: (code, reset) => {
|
||
if (code === ersterCode) { spielerZustand.pin = code; setupFaceId(); }
|
||
else { zeigeToast("Codes stimmen nicht überein"); reset(); }
|
||
},
|
||
abbrechen: { label: "Zurück", cb: setupCodeFestlegen }
|
||
});
|
||
}
|
||
function setupFaceId() {
|
||
document.getElementById("setup-screen").innerHTML =
|
||
'<div class="setup-inner"><div class="setup-logo" style="background:linear-gradient(145deg,#4aa8ff,#7b6cf6);font-size:34px">🙂</div>' +
|
||
'<div class="setup-h1">Face-ID einrichten</div>' +
|
||
'<div class="setup-p">Mit Face-ID entsperrst du dein Handy, ohne jedes Mal den Code einzugeben. Später jederzeit in den Einstellungen änderbar.</div>' +
|
||
'<button class="btn" id="fid-on">Face-ID aktivieren</button>' +
|
||
'<button class="btn ghost" id="fid-skip" style="margin-top:8px">Später, mit Code</button></div>';
|
||
document.getElementById("fid-on").addEventListener("click", () => {
|
||
spielerZustand.faceId = true;
|
||
if (IST_FIVEM) nui("pinEinrichten", { pin: spielerZustand.pin, faceId: true });
|
||
setupFertig();
|
||
});
|
||
document.getElementById("fid-skip").addEventListener("click", () => {
|
||
spielerZustand.faceId = false;
|
||
if (IST_FIVEM) nui("pinEinrichten", { pin: spielerZustand.pin, faceId: false });
|
||
setupFertig();
|
||
});
|
||
}
|
||
function setupFertig() {
|
||
document.getElementById("setup-screen").classList.remove("show");
|
||
screenEl.classList.add("locked");
|
||
zeigeToast("Einrichtung abgeschlossen ✅");
|
||
}
|
||
|
||
// In FiveM startet die Einrichtung erst, wenn der Server den Zustand
|
||
// geschickt hat (siehe NUI-Brücke am Ende der Datei).
|
||
if (typeof GetParentResourceName !== "function" && !spielerZustand.pin) starteEinrichtung();
|
||
|
||
/* =====================================================================
|
||
BENACHRICHTIGUNGEN (Sperrbildschirm-Mitteilungen + Push-Banner)
|
||
===================================================================== */
|
||
// Aktuelle Mitteilungen auf dem Sperrbildschirm (neueste zuerst)
|
||
const benachrichtigungen = [
|
||
{ app: "messages", titel: "Familie Schmidt", text: "Kommst du heute zum Grillen vorbei? 🔥" },
|
||
{ app: "bank", titel: "Fleeca Bank", text: "Gehalt gutgeschrieben: +2.400 €" }
|
||
];
|
||
|
||
// Sperrbildschirm-Mitteilungen zeichnen (inkl. Wisch-zum-Entfernen)
|
||
function zeichneLockNotes() {
|
||
const box = document.getElementById("lock-notes");
|
||
box.innerHTML = benachrichtigungen.map((n, i) => {
|
||
const def = alleApps[n.app] || alleApps.settings;
|
||
return '<div class="lock-note" data-index="' + i + '">' +
|
||
'<span class="ic ' + def.g + '">' + miniGlyph(def.glyph) + '</span>' +
|
||
'<div class="txt"><div class="t">' + sicher(n.titel) + '</div>' +
|
||
'<div class="d">' + sicher(n.text) + '</div></div></div>';
|
||
}).join("");
|
||
// Jede Mitteilung nach links wegwischbar machen
|
||
box.querySelectorAll(".lock-note").forEach(el =>
|
||
macheWischbar(el, () => {
|
||
benachrichtigungen.splice(+el.dataset.index, 1);
|
||
zeichneLockNotes();
|
||
}));
|
||
}
|
||
|
||
// Kleines Icon (18px) für Mitteilungen/Banner erzeugen
|
||
function miniGlyph(glyph) {
|
||
return '<span style="display:grid;place-items:center;width:18px;height:18px">' + svgGlyphen[glyph] + '</span>';
|
||
}
|
||
|
||
// Element nach links wegwischbar machen; onEntfernen wird bei genügend Wischweg ausgelöst
|
||
function macheWischbar(el, onEntfernen) {
|
||
let startX = 0, dx = 0, aktiv = false;
|
||
el.addEventListener("pointerdown", e => {
|
||
aktiv = true; startX = e.clientX; dx = 0;
|
||
el.classList.add("swiping"); el.style.transition = "none";
|
||
el.setPointerCapture(e.pointerId);
|
||
});
|
||
el.addEventListener("pointermove", e => {
|
||
if (!aktiv) return;
|
||
dx = Math.min(0, e.clientX - startX); // nur nach links
|
||
el.style.transform = "translateX(" + dx + "px)";
|
||
el.style.opacity = String(1 - Math.min(1, -dx / 180));
|
||
});
|
||
const beenden = () => {
|
||
if (!aktiv) return;
|
||
aktiv = false; el.classList.remove("swiping");
|
||
el.style.transition = "transform .25s ease, opacity .25s ease";
|
||
if (dx < -80) { // Schwelle überschritten -> entfernen
|
||
el.style.transform = "translateX(-120%)"; el.style.opacity = "0";
|
||
setTimeout(onEntfernen, 220);
|
||
} else { // zurückschnappen
|
||
el.style.transform = "translateX(0)"; el.style.opacity = "1";
|
||
}
|
||
};
|
||
el.addEventListener("pointerup", beenden);
|
||
el.addEventListener("pointercancel", beenden);
|
||
}
|
||
|
||
// Push-Benachrichtigung auslösen (im Server käme dies vom Backend)
|
||
let bannerTimer = null;
|
||
function loeseBenachrichtigungAus(spec) {
|
||
// spec: { app, titel, text }
|
||
benachrichtigungen.unshift({ app: spec.app, titel: spec.titel, text: spec.text });
|
||
if (screenEl.classList.contains("locked")) {
|
||
zeichneLockNotes(); // gesperrt: erscheint in der Mitteilungsliste
|
||
return;
|
||
}
|
||
// entsperrt: Banner oben einblenden
|
||
const def = alleApps[spec.app] || alleApps.settings;
|
||
const banner = document.getElementById("notif-banner");
|
||
document.getElementById("notif-ic").className = "ic " + def.g;
|
||
document.getElementById("notif-ic").innerHTML = miniGlyph(def.glyph);
|
||
document.getElementById("notif-t").textContent = spec.titel;
|
||
document.getElementById("notif-d").textContent = spec.text;
|
||
banner.classList.add("show");
|
||
// Tippen öffnet die zugehörige App
|
||
banner.onclick = () => { banner.classList.remove("show"); oeffneApp(spec.app, def.label); };
|
||
clearTimeout(bannerTimer);
|
||
bannerTimer = setTimeout(() => banner.classList.remove("show"), 4200);
|
||
}
|
||
|
||
zeichneLockNotes(); // Erstaufbau der Sperrbildschirm-Mitteilungen
|
||
|
||
// Demo-Buttons im linken Panel: Push-Mitteilungen auslösen
|
||
const pushBeispiele = {
|
||
messages: { app: "messages", titel: "Lena Berger", text: "Bist du noch am Pier? 🌊" },
|
||
bank: { app: "bank", titel: "Fleeca Bank", text: "Zahlung erhalten: +750 €" },
|
||
dispatch: { app: "dispatch", titel: "Leitstelle", text: "10-90 Raubüberfall · Vinewood" },
|
||
birdy: { app: "birdy", titel: "Birdy", text: "@lsnews hat einen neuen Beitrag gepostet" }
|
||
};
|
||
document.querySelectorAll("[data-push]").forEach(btn =>
|
||
btn.addEventListener("click", () => loeseBenachrichtigungAus(pushBeispiele[btn.dataset.push])));
|
||
|
||
// Demo-Button: Darkchat freischalten
|
||
document.getElementById("unlock-darkweb").addEventListener("click", () => {
|
||
spielerZustand.freigeschaltet.darkweb = !spielerZustand.freigeschaltet.darkweb;
|
||
baueHomescreen();
|
||
zeigeToast(spielerZustand.freigeschaltet.darkweb
|
||
? "Darkchat freigeschaltet 🔓" : "Darkchat wieder gesperrt 🔒");
|
||
});
|
||
|
||
/* =====================================================================
|
||
HARDWARE (Akku, SIM, Handy) — im Spiel jeweils kombinierbare Items
|
||
===================================================================== */
|
||
// Statusleiste (Akkustand, Netz) an den aktuellen Hardware-Zustand anpassen
|
||
function aktualisiereHardwareUI() {
|
||
const stand = Math.max(0, Math.min(100, Math.round(spielerZustand.akku)));
|
||
const fuellung = document.getElementById("sb-batt-fill");
|
||
const prozentText = document.getElementById("sb-batt-pct");
|
||
// Akkubalken füllen und je nach Stand einfärben
|
||
if (fuellung) {
|
||
fuellung.setAttribute("width", Math.max(0.5, 17 * stand / 100).toFixed(1));
|
||
fuellung.setAttribute("fill", stand <= 15 ? "#f0506e" : (stand <= 30 ? "#f5b23d" : "currentColor"));
|
||
}
|
||
if (prozentText) { prozentText.textContent = stand + "%"; prozentText.style.color = stand <= 15 ? "#f0506e" : ""; }
|
||
const demoAnzeige = document.getElementById("demo-akku");
|
||
if (demoAnzeige) demoAnzeige.textContent = stand + "%";
|
||
// Netz-Anzeige abhängig von der SIM-Karte
|
||
const netzText = document.getElementById("sb-net");
|
||
const signal = document.getElementById("sb-signal");
|
||
if (netzText) netzText.style.display = spielerZustand.sim ? "none" : "inline";
|
||
if (signal) signal.style.opacity = spielerZustand.sim ? "1" : "0.25";
|
||
}
|
||
|
||
// Handy ausschalten / Akku leer: Overlay einblenden
|
||
function handyAusschalten(titel, unterzeile, icon) {
|
||
document.getElementById("po-icon").textContent = icon;
|
||
document.getElementById("po-title").textContent = titel;
|
||
document.getElementById("po-sub").textContent = unterzeile;
|
||
document.getElementById("power-off").classList.add("show");
|
||
geheZumHomescreen(); // eventuell offene App schließen
|
||
}
|
||
// Handy wieder einschalten (startet gesperrt, wie ein frischer Boot)
|
||
function handyEinschalten() {
|
||
document.getElementById("power-off").classList.remove("show");
|
||
screenEl.classList.add("locked");
|
||
aktualisiereHardwareUI();
|
||
}
|
||
|
||
// Automatische Akku-Entladung (~1 % alle 12 s)
|
||
setInterval(() => {
|
||
if (!spielerZustand.handy || spielerZustand.akku <= 0) return;
|
||
spielerZustand.akku = Math.max(0, spielerZustand.akku - 1);
|
||
aktualisiereHardwareUI();
|
||
if (spielerZustand.akku <= 0) {
|
||
handyAusschalten("Akku leer", "Setze einen geladenen Akku (Item) ein, um das Handy zu nutzen.", "🪫");
|
||
}
|
||
}, 12000);
|
||
|
||
// Demo-Buttons: Hardware/Items steuern
|
||
document.querySelectorAll("[data-hw]").forEach(btn =>
|
||
btn.addEventListener("click", () => {
|
||
const aktion = btn.dataset.hw;
|
||
if (aktion === "drain") { // Akku schnell entladen (zum Testen)
|
||
spielerZustand.akku = Math.max(0, spielerZustand.akku - 20);
|
||
aktualisiereHardwareUI();
|
||
if (spielerZustand.akku <= 0) handyAusschalten("Akku leer", "Setze einen geladenen Akku (Item) ein, um das Handy zu nutzen.", "🪫");
|
||
else zeigeToast("Akku: " + spielerZustand.akku + " %");
|
||
} else if (aktion === "newbatt") { // frischen Akku einsetzen
|
||
spielerZustand.akku = 100;
|
||
if (!spielerZustand.handy) { aktualisiereHardwareUI(); return; } // ohne Handy nichts tun
|
||
// Nur neu starten, wenn das Handy vorher wegen leerem Akku aus war
|
||
const warAus = document.getElementById("power-off").classList.contains("show");
|
||
if (warAus) handyEinschalten(); else aktualisiereHardwareUI();
|
||
zeigeToast("Neuer Akku eingesetzt 🔋 (100 %)");
|
||
} else if (aktion === "sim") { // SIM-Karte entfernen/einsetzen
|
||
spielerZustand.sim = !spielerZustand.sim;
|
||
aktualisiereHardwareUI();
|
||
zeigeToast(spielerZustand.sim ? "SIM-Karte eingesetzt 📶" : "SIM-Karte entfernt – kein Netz");
|
||
} else if (aktion === "handy") { // Handy als Item aus/an
|
||
spielerZustand.handy = !spielerZustand.handy;
|
||
if (!spielerZustand.handy) handyAusschalten("Kein Handy", "Im Spiel muss zuerst ein Handy (Item) gekauft werden.", "📱");
|
||
else if (spielerZustand.akku > 0) handyEinschalten();
|
||
else handyAusschalten("Akku leer", "Setze einen geladenen Akku (Item) ein, um das Handy zu nutzen.", "🪫");
|
||
}
|
||
}));
|
||
|
||
aktualisiereHardwareUI(); // Erstanzeige der Hardware
|
||
|
||
// Zurück zum Homescreen (schließt offene App und leert die Historie)
|
||
function geheZumHomescreen() {
|
||
appViewEl.classList.remove("open");
|
||
appContentEl.innerHTML = "";
|
||
navStack = [];
|
||
}
|
||
document.getElementById("home-indicator").addEventListener("click", () => {
|
||
if (appViewEl.classList.contains("open")) geheZumHomescreen();
|
||
else screenEl.classList.add("locked"); // Auf dem Homescreen: sperren
|
||
});
|
||
|
||
/* Baut eine einzelne App-Kachel (für Grid oder Dock) anhand ihrer id */
|
||
function baueAppKachel(appId) {
|
||
const def = alleApps[appId];
|
||
const button = document.createElement("button");
|
||
button.className = "app";
|
||
button.innerHTML =
|
||
'<span class="icon ' + def.g + '">' + svgGlyphen[def.glyph] +
|
||
(def.badge ? '<span class="badge">' + def.badge + '</span>' : '') +
|
||
'</span><span class="label">' + def.label + '</span>';
|
||
button.addEventListener("click", () => oeffneApp(appId, def.label));
|
||
return button;
|
||
}
|
||
|
||
// Homescreen-Grid und Dock aus dem Zustand neu aufbauen (nach Änderungen erneut aufrufen)
|
||
function baueHomescreen() {
|
||
const gridEl = document.getElementById("app-grid");
|
||
const dockEl = document.getElementById("dock");
|
||
gridEl.innerHTML = "";
|
||
dockEl.innerHTML = "";
|
||
// Grid: alle Apps außer den Dock-Apps; gesperrte Apps nur wenn freigeschaltet
|
||
appReihenfolge.forEach(appId => {
|
||
if (spielerZustand.dockApps.includes(appId)) return;
|
||
if (alleApps[appId].gated && !spielerZustand.freigeschaltet[appId]) return;
|
||
gridEl.appendChild(baueAppKachel(appId));
|
||
});
|
||
// Dock: genau die konfigurierten Apps
|
||
spielerZustand.dockApps.forEach(appId => dockEl.appendChild(baueAppKachel(appId)));
|
||
}
|
||
baueHomescreen();
|
||
|
||
/* =====================================================================
|
||
APP-ÖFFNEN — ruft die passende Render-Funktion auf
|
||
===================================================================== */
|
||
/* Navigations-Historie: jede Seite ist eine Render-Funktion auf dem Stapel.
|
||
So gelangt der Zurück-Button immer eine Ebene zurück statt direkt nach Hause. */
|
||
let navStack = [];
|
||
|
||
// App vom Homescreen öffnen (startet eine frische Historie)
|
||
function oeffneApp(appId, titel) {
|
||
navStack = [];
|
||
const renderFunktion = appRenderer[appId] || (() => { appContentEl.innerHTML = platzhalterApp(titel); });
|
||
appViewEl.classList.add("open");
|
||
navigiere(renderFunktion);
|
||
}
|
||
|
||
// Neue Unterseite anzeigen und auf den Stapel legen
|
||
function navigiere(renderFunktion) {
|
||
navStack.push(renderFunktion);
|
||
renderFunktion();
|
||
verdrahteZurueck();
|
||
}
|
||
|
||
// Eine Ebene zurück (oder nach Hause, wenn keine Historie mehr da ist)
|
||
function zurueck() {
|
||
navStack.pop(); // aktuelle Seite entfernen
|
||
const vorherigeSeite = navStack[navStack.length - 1];
|
||
if (vorherigeSeite) {
|
||
vorherigeSeite();
|
||
verdrahteZurueck();
|
||
} else {
|
||
geheZumHomescreen();
|
||
}
|
||
}
|
||
|
||
// Alle Zurück-Buttons der aktuell gerenderten Seite mit "zurück" verbinden
|
||
function verdrahteZurueck() {
|
||
appContentEl.querySelectorAll("[data-back]").forEach(btn => { btn.onclick = zurueck; });
|
||
}
|
||
|
||
/* SVG-Symbole für die Kopfzeile */
|
||
const chevronLinks = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>';
|
||
const telefonSymbol = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6.6 10.8c1.4 2.8 3.8 5.1 6.6 6.6l2.2-2.2c.3-.3.7-.4 1-.2 1.1.4 2.3.6 3.6.6.6 0 1 .4 1 1V20c0 .6-.4 1-1 1C10.6 21 3 13.4 3 4c0-.6.4-1 1-1h3.5c.6 0 1 .4 1 1 0 1.2.2 2.4.6 3.6.1.4 0 .8-.3 1l-2.2 2.2Z"/></svg>';
|
||
|
||
/* Standard-Kopfzeile für Apps */
|
||
function appKopf(titel, unterzeile) {
|
||
return '<div class="app-header">' +
|
||
'<button class="icon-btn" data-back aria-label="Zurück">' + chevronLinks + '</button>' +
|
||
'<div style="flex:1;text-align:center;">' +
|
||
'<h1>' + sicher(titel) + '</h1>' +
|
||
(unterzeile ? '<div class="sub">' + sicher(unterzeile) + '</div>' : '') +
|
||
'</div>' +
|
||
'<span style="width:34px;"></span>' +
|
||
'</div>';
|
||
}
|
||
|
||
/* Platzhalter für noch nicht ausgebaute Apps */
|
||
function platzhalterApp(titel) {
|
||
return appKopf(titel) +
|
||
'<div class="app-body"><div class="empty"><div class="big">🚧</div>' +
|
||
'<div style="font-weight:600;color:var(--s-fg);">In Entwicklung</div>' +
|
||
'<div style="margin-top:6px;">Diese App wird für den finalen Server noch gebaut.</div></div></div>';
|
||
}
|
||
|
||
/* =====================================================================
|
||
APP: TELEFON (Wähler, Verlauf, Kontakte)
|
||
===================================================================== */
|
||
let telefonNummer = ""; // aktuell eingegebene Nummer im Wähler
|
||
|
||
const appRenderer = {}; // Register aller App-Render-Funktionen
|
||
|
||
appRenderer.phone = function () {
|
||
appContentEl.innerHTML =
|
||
appKopf("Telefon") +
|
||
'<div class="app-body">' +
|
||
'<div class="seg" id="phone-seg">' +
|
||
'<button data-tab="dial" class="active">Ziffern</button>' +
|
||
'<button data-tab="recents">Verlauf</button>' +
|
||
'<button data-tab="contacts">Kontakte</button>' +
|
||
'</div>' +
|
||
'<div id="phone-panel"></div>' +
|
||
'</div>';
|
||
telefonNummer = "";
|
||
zeigePhoneTab("dial");
|
||
// Tab-Umschaltung
|
||
appContentEl.querySelectorAll("#phone-seg button").forEach(b =>
|
||
b.addEventListener("click", () => {
|
||
appContentEl.querySelectorAll("#phone-seg button").forEach(x => x.classList.remove("active"));
|
||
b.classList.add("active");
|
||
zeigePhoneTab(b.dataset.tab);
|
||
}));
|
||
};
|
||
|
||
// Inhalt des jeweiligen Telefon-Tabs rendern
|
||
function zeigePhoneTab(tab) {
|
||
const panel = document.getElementById("phone-panel");
|
||
if (tab === "dial") {
|
||
const tasten = [
|
||
["1",""],["2","ABC"],["3","DEF"],["4","GHI"],["5","JKL"],["6","MNO"],
|
||
["7","PQRS"],["8","TUV"],["9","WXYZ"],["*",""],["0","+"],["#",""]
|
||
];
|
||
panel.innerHTML =
|
||
'<div class="dial-display" id="dial-display">' + (telefonNummer || '<span style="color:var(--s-muted)">Nummer eingeben</span>') + '</div>' +
|
||
'<div class="dialpad">' + tasten.map(t =>
|
||
'<button class="dial-key" data-key="' + t[0] + '">' + t[0] +
|
||
(t[1] ? '<small>' + t[1] + '</small>' : '') + '</button>').join("") + '</div>' +
|
||
'<div class="call-bar">' +
|
||
'<span style="width:66px"></span>' +
|
||
'<button class="call-btn call" id="dial-call" aria-label="Anrufen">' + svgGlyphen.phone + '</button>' +
|
||
'<button class="call-btn del" id="dial-del" aria-label="Löschen">' +
|
||
'<svg width="30" height="24" viewBox="0 0 24 24" fill="var(--s-muted)"><path d="M22 3H7c-.6 0-1.1.3-1.5.8L0 12l5.5 8.2c.4.5.9.8 1.5.8h15a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2Zm-3.7 12.9-1.4 1.4L14 14.4l-2.9 2.9-1.4-1.4L12.6 13l-2.9-2.9 1.4-1.4L14 11.6l2.9-2.9 1.4 1.4L15.4 13l2.9 2.9Z"/></svg>' +
|
||
'</button>' +
|
||
'</div>';
|
||
// Ziffern-Tasten
|
||
panel.querySelectorAll(".dial-key").forEach(k =>
|
||
k.addEventListener("click", () => { telefonNummer += k.dataset.key; zeigePhoneTab("dial"); }));
|
||
// Löschen (letzte Ziffer)
|
||
document.getElementById("dial-del").addEventListener("click", () => {
|
||
telefonNummer = telefonNummer.slice(0, -1); zeigePhoneTab("dial");
|
||
});
|
||
// Anrufen
|
||
document.getElementById("dial-call").addEventListener("click", () => {
|
||
if (!telefonNummer) { zeigeToast("Bitte zuerst eine Nummer eingeben"); return; }
|
||
starteAnruf(telefonNummer, telefonNummer);
|
||
});
|
||
} else if (tab === "recents") {
|
||
panel.innerHTML = anrufListe.map(a => {
|
||
const pfeil = a.verpasst ? "↙" : (a.art === "eingehend" ? "↙" : "↗");
|
||
const farbe = a.verpasst ? "var(--s-red)" : "var(--s-muted)";
|
||
return '<div class="row" style="cursor:pointer" data-call="' + sicher(a.name) + '">' +
|
||
'<span style="color:' + farbe + ';font-size:18px">' + pfeil + '</span>' +
|
||
'<div class="grow"><div class="title" style="color:' + (a.verpasst ? "var(--s-red)" : "var(--s-fg)") + '">' + sicher(a.name) + '</div>' +
|
||
'<div class="meta">' + (a.art === "eingehend" ? "Eingehend" : "Ausgehend") + (a.verpasst ? " · Verpasst" : "") + '</div></div>' +
|
||
'<div class="side">' + a.zeit + '</div></div>';
|
||
}).join("");
|
||
panel.querySelectorAll("[data-call]").forEach(r =>
|
||
r.addEventListener("click", () => starteAnruf(r.dataset.call, r.dataset.call)));
|
||
} else {
|
||
panel.innerHTML = kontakteHtml();
|
||
panel.querySelectorAll("[data-call]").forEach(r =>
|
||
r.addEventListener("click", () => starteAnruf(r.dataset.call, r.dataset.call)));
|
||
}
|
||
}
|
||
|
||
// Wiederverwendbare Kontaktliste
|
||
function kontakteHtml() {
|
||
return kontakteListe.map(k =>
|
||
'<div class="row" style="cursor:pointer" data-call="' + sicher(k.name) + '">' +
|
||
'<span class="avatar" style="background:' + k.farbe + '">' + sicher(k.initialen) + '</span>' +
|
||
'<div class="grow"><div class="title">' + sicher(k.name) + '</div>' +
|
||
'<div class="meta">' + sicher(k.nummer) + '</div></div>' +
|
||
(k.notruf ? '<span class="pill red">Notruf</span>' : '<span class="icon-btn" style="pointer-events:none">' + telefonSymbol + '</span>') +
|
||
'</div>').join("");
|
||
}
|
||
|
||
// Kontakte-App (eigene Kachel)
|
||
appRenderer.contacts = function () {
|
||
appContentEl.innerHTML = appKopf("Kontakte", spielerZustand.name + " · " + spielerZustand.nummer) +
|
||
'<div class="app-body">' + kontakteHtml() + '</div>';
|
||
appContentEl.querySelectorAll("[data-call]").forEach(r =>
|
||
r.addEventListener("click", () => starteAnruf(r.dataset.call, r.dataset.call)));
|
||
};
|
||
|
||
/* Anruf starten — Overlay einblenden und Gesprächszeit hochzählen */
|
||
let anrufTimer = null;
|
||
function starteAnruf(anzeigeName, initialenQuelle, nummer) {
|
||
// Ohne SIM-Karte kein Netz -> kein Anruf möglich
|
||
if (!spielerZustand.sim) { zeigeToast("Keine SIM-Karte – kein Netz"); return; }
|
||
|
||
// In FiveM baut der Server die Verbindung auf; das Overlay zeigt den Status
|
||
if (IST_FIVEM) {
|
||
const ziel = nummer || nummerZuName(anzeigeName) || anzeigeName;
|
||
nui("anrufen", { nummer: ziel });
|
||
}
|
||
|
||
const overlay = document.getElementById("in-call");
|
||
document.getElementById("call-who").textContent = anzeigeName;
|
||
document.getElementById("call-avatar").textContent =
|
||
/^\d/.test(initialenQuelle) ? "☎" : initialenQuelle.substring(0, 2).toUpperCase();
|
||
overlay.classList.add("show");
|
||
|
||
let sekunden = 0;
|
||
const statusEl = document.getElementById("call-status");
|
||
statusEl.textContent = "Verbinde…";
|
||
clearInterval(anrufTimer);
|
||
// Nach kurzer "Verbindungszeit" Gesprächsdauer hochzählen
|
||
setTimeout(() => {
|
||
anrufTimer = setInterval(() => {
|
||
sekunden++;
|
||
const m = String(Math.floor(sekunden / 60)).padStart(2, "0");
|
||
const s = String(sekunden % 60).padStart(2, "0");
|
||
statusEl.textContent = m + ":" + s;
|
||
}, 1000);
|
||
}, 1200);
|
||
}
|
||
// Auflegen — bei einem eingehenden Anruf zuerst annehmen
|
||
document.getElementById("call-end").addEventListener("click", () => {
|
||
const overlay = document.getElementById("in-call");
|
||
if (IST_FIVEM) nui("auflegen", {});
|
||
overlay.classList.remove("show");
|
||
delete overlay.dataset.eingehend;
|
||
clearInterval(anrufTimer);
|
||
});
|
||
|
||
// Anruf annehmen (nur sichtbar bei eingehendem Anruf)
|
||
const annehmenBtn = document.getElementById("call-accept");
|
||
if (annehmenBtn) {
|
||
annehmenBtn.addEventListener("click", () => {
|
||
if (IST_FIVEM) nui("annehmen", {});
|
||
});
|
||
}
|
||
|
||
/* =====================================================================
|
||
APP: NACHRICHTEN (Chat-Liste + Konversation)
|
||
===================================================================== */
|
||
let aktiverChat = null; // Name des aktuell geöffneten Chats
|
||
|
||
appRenderer.messages = function () {
|
||
zeigeChatListe();
|
||
};
|
||
|
||
// Übersicht aller Chats
|
||
function zeigeChatListe() {
|
||
aktiverChat = null;
|
||
const eintraege = Object.keys(chatVerlaeufe).map(name => {
|
||
const verlauf = chatVerlaeufe[name];
|
||
const letzte = verlauf[verlauf.length - 1];
|
||
const kontakt = kontakteListe.find(k => k.name === name);
|
||
const farbe = kontakt ? kontakt.farbe : "#7b6cf6";
|
||
const initialen = kontakt ? kontakt.initialen : name.substring(0, 2);
|
||
return '<div class="row" style="cursor:pointer" data-chat="' + sicher(name) + '">' +
|
||
'<span class="avatar" style="background:' + farbe + '">' + sicher(initialen) + '</span>' +
|
||
'<div class="grow"><div class="title">' + sicher(name) + '</div>' +
|
||
'<div class="meta">' + (letzte.von === "me" ? "Du: " : "") + sicher(letzte.text) + '</div></div>' +
|
||
'<div class="side">' + letzte.zeit + '</div></div>';
|
||
}).join("");
|
||
appContentEl.innerHTML = appKopf("Nachrichten") + '<div class="app-body">' + eintraege + '</div>';
|
||
appContentEl.querySelectorAll("[data-chat]").forEach(r =>
|
||
r.addEventListener("click", () => navigiere(() => oeffneChat(r.dataset.chat))));
|
||
}
|
||
|
||
// Einzelne Konversation öffnen
|
||
function oeffneChat(name) {
|
||
aktiverChat = name;
|
||
appContentEl.innerHTML =
|
||
'<div class="app-header">' +
|
||
'<button class="icon-btn" data-back aria-label="Zurück">' + chevronLinks + '</button>' +
|
||
'<div style="flex:1;text-align:center"><h1 style="font-size:17px">' + sicher(name) + '</h1></div>' +
|
||
'<button class="icon-btn" data-call2="' + sicher(name) + '" aria-label="Anrufen">' + telefonSymbol + '</button>' +
|
||
'</div>' +
|
||
'<div class="bubbles" id="bubbles"></div>' +
|
||
'<div class="composer">' +
|
||
'<input class="input" id="chat-input" placeholder="Nachricht…" autocomplete="off">' +
|
||
'<button class="send-btn" id="chat-send" aria-label="Senden">' +
|
||
'<svg width="20" height="20" viewBox="0 0 24 24" fill="#fff"><path d="M3 20l18-8L3 4v6l12 2-12 2v6Z"/></svg>' +
|
||
'</button>' +
|
||
'</div>';
|
||
zeichneBubbles();
|
||
appContentEl.querySelector("[data-call2]").addEventListener("click", () => starteAnruf(name, name));
|
||
const input = document.getElementById("chat-input");
|
||
const sende = () => sendeNachricht(input);
|
||
document.getElementById("chat-send").addEventListener("click", sende);
|
||
input.addEventListener("keydown", e => { if (e.key === "Enter") sende(); });
|
||
input.focus();
|
||
}
|
||
|
||
// Nachrichtenblasen zeichnen
|
||
function zeichneBubbles() {
|
||
const box = document.getElementById("bubbles");
|
||
box.innerHTML = chatVerlaeufe[aktiverChat].map(m =>
|
||
'<div class="bubble ' + m.von + '">' + sicher(m.text) +
|
||
'<span class="time">' + m.zeit + '</span></div>').join("");
|
||
box.scrollTop = box.scrollHeight;
|
||
}
|
||
|
||
// Nachricht senden
|
||
function sendeNachricht(input) {
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
const jetzt = new Date();
|
||
const zeit = String(jetzt.getHours()).padStart(2, "0") + ":" + String(jetzt.getMinutes()).padStart(2, "0");
|
||
|
||
if (IST_FIVEM) {
|
||
// Der Server stellt zu und schickt den aktualisierten Verlauf zurück.
|
||
// Die eigene Nachricht wird sofort angezeigt, damit es sich flüssig anfühlt.
|
||
chatVerlaeufe[aktiverChat] = chatVerlaeufe[aktiverChat] || [];
|
||
chatVerlaeufe[aktiverChat].push({ von: "me", text: text, zeit: zeit });
|
||
input.value = "";
|
||
zeichneBubbles();
|
||
nui("senden", { nummer: aktiverChat, text: text });
|
||
return;
|
||
}
|
||
|
||
// Browser-Ansicht: lokal mit simulierter Antwort
|
||
chatVerlaeufe[aktiverChat].push({ von: "me", text: text, zeit: zeit });
|
||
input.value = "";
|
||
zeichneBubbles();
|
||
setTimeout(() => {
|
||
const antworten = ["Alles klar! 👍", "Verstanden.", "Bin gleich da.", "Ok, bis später!", "Klingt gut."];
|
||
chatVerlaeufe[aktiverChat].push({
|
||
von: "them",
|
||
text: antworten[Math.floor(Math.random() * antworten.length)],
|
||
zeit: zeit
|
||
});
|
||
if (aktiverChat) zeichneBubbles();
|
||
}, 1400);
|
||
}
|
||
|
||
/* =====================================================================
|
||
APP: BANK (Konto, Aktionen, Transaktionen)
|
||
===================================================================== */
|
||
appRenderer.bank = function () {
|
||
zeigeBankUebersicht();
|
||
};
|
||
|
||
function zeigeBankUebersicht() {
|
||
appContentEl.innerHTML = appKopf("Fleeca Bank") +
|
||
'<div class="app-body">' +
|
||
'<div class="bank-card">' +
|
||
'<div class="lbl">GIROKONTO</div>' +
|
||
'<div class="bal" id="bank-balance">' + formatiereEuro(spielerZustand.kontostand) + '</div>' +
|
||
'<div class="lbl">' + sicher(spielerZustand.name) + '</div>' +
|
||
'<div class="no">' + sicher(spielerZustand.kontonummer) + '</div>' +
|
||
'</div>' +
|
||
'<button class="btn" id="bank-transfer" style="margin:14px 0">Überweisen</button>' +
|
||
'<div class="list-title">Transaktionen</div>' +
|
||
'<div id="tx-list">' + transaktionenHtml() + '</div>' +
|
||
'</div>';
|
||
document.getElementById("bank-transfer").addEventListener("click", () => navigiere(zeigeUeberweisung));
|
||
}
|
||
|
||
// Transaktionsliste als HTML
|
||
function transaktionenHtml() {
|
||
return transaktionsListe.map(t =>
|
||
'<div class="tx"><span class="ti">' + t.icon + '</span>' +
|
||
'<div class="grow"><div class="title">' + sicher(t.titel) + '</div>' +
|
||
'<div class="meta">' + t.zeit + '</div></div>' +
|
||
'<div class="amt ' + (t.betrag > 0 ? "plus" : "") + '">' +
|
||
(t.betrag > 0 ? "+" : "") + formatiereEuro(t.betrag) + '</div></div>').join("");
|
||
}
|
||
|
||
// Überweisungs-Formular
|
||
function zeigeUeberweisung() {
|
||
appContentEl.innerHTML = appKopf("Überweisen") +
|
||
'<div class="app-body">' +
|
||
'<div style="text-align:center;margin:6px 0 18px;color:var(--s-muted);font-size:13px">Verfügbar: <b style="color:var(--s-green)">' + formatiereEuro(spielerZustand.kontostand) + '</b></div>' +
|
||
'<label class="field-label">Empfänger (Name oder Kontonummer)</label>' +
|
||
'<input class="input" id="tr-to" placeholder="z. B. Lena Berger" autocomplete="off">' +
|
||
'<label class="field-label">Betrag (€)</label>' +
|
||
'<input class="input" id="tr-amount" type="number" placeholder="0" inputmode="numeric">' +
|
||
'<label class="field-label">Verwendungszweck</label>' +
|
||
'<input class="input" id="tr-note" placeholder="optional" autocomplete="off">' +
|
||
'<div style="height:16px"></div>' +
|
||
'<button class="btn green" id="tr-send">Jetzt überweisen</button>' +
|
||
'<div style="height:8px"></div>' +
|
||
'<button class="btn ghost" id="tr-cancel">Abbrechen</button>' +
|
||
'</div>';
|
||
document.getElementById("tr-cancel").addEventListener("click", zurueck);
|
||
document.getElementById("tr-send").addEventListener("click", fuehreUeberweisungAus);
|
||
}
|
||
|
||
// Überweisung durchführen (mit Validierung)
|
||
function fuehreUeberweisungAus() {
|
||
const empfaenger = document.getElementById("tr-to").value.trim();
|
||
const betrag = parseInt(document.getElementById("tr-amount").value, 10);
|
||
const zweck = document.getElementById("tr-note").value.trim();
|
||
if (!empfaenger) { zeigeToast("Bitte einen Empfänger angeben"); return; }
|
||
if (!betrag || betrag <= 0) { zeigeToast("Bitte einen gültigen Betrag angeben"); return; }
|
||
if (betrag > spielerZustand.kontostand) { zeigeToast("Nicht genügend Guthaben"); return; }
|
||
// Kontostand anpassen und Transaktion vormerken
|
||
spielerZustand.kontostand -= betrag;
|
||
transaktionsListe.unshift({
|
||
titel: "Überweisung an " + empfaenger + (zweck ? " (" + zweck + ")" : ""),
|
||
betrag: -betrag, zeit: "Gerade eben", icon: "↗"
|
||
});
|
||
zeigeToast(formatiereEuro(betrag) + " an " + empfaenger + " gesendet");
|
||
zurueck(); // zurück zur Kontoübersicht (mit aktualisiertem Guthaben)
|
||
}
|
||
|
||
/* =====================================================================
|
||
APP: BIRDY (Social Media)
|
||
===================================================================== */
|
||
appRenderer.birdy = function () {
|
||
appContentEl.innerHTML = appKopf("Birdy", "Was passiert in Los Santos?") +
|
||
'<div class="app-body">' +
|
||
'<div class="composer-box">' +
|
||
'<textarea id="birdy-input" placeholder="Was gibt\'s Neues, ' + sicher(spielerZustand.name.split(" ")[0]) + '?" maxlength="180"></textarea>' +
|
||
'<div style="display:flex;justify-content:flex-end"><button class="btn" style="width:auto;padding:8px 20px" id="birdy-post">Birden</button></div>' +
|
||
'</div>' +
|
||
'<div id="birdy-feed">' + birdyFeedHtml() + '</div>' +
|
||
'</div>';
|
||
document.getElementById("birdy-post").addEventListener("click", sendeBirdyBeitrag);
|
||
verdrahteBirdyAktionen();
|
||
};
|
||
|
||
// Feed als HTML
|
||
function birdyFeedHtml() {
|
||
return birdyBeitraege.map((p, i) =>
|
||
'<div class="post">' +
|
||
'<div class="ph"><span class="avatar" style="background:' + p.farbe + '">' + sicher(p.initialen) + '</span>' +
|
||
'<div><div class="name">' + sicher(p.name) + '</div><div class="handle">' + sicher(p.handle) + '</div></div></div>' +
|
||
'<div class="txt">' + sicher(p.text) + '</div>' +
|
||
'<div class="actions">' +
|
||
'<button data-like="' + i + '" class="' + (p.gemocht ? "liked" : "") + '">♥ <span>' + p.likes + '</span></button>' +
|
||
'<button data-rt="' + i + '">↻ <span>' + p.retweets + '</span></button>' +
|
||
'<button>💬 <span>Antworten</span></button>' +
|
||
'</div>' +
|
||
'</div>').join("");
|
||
}
|
||
|
||
// Like/Retweet-Buttons verdrahten
|
||
function verdrahteBirdyAktionen() {
|
||
appContentEl.querySelectorAll("[data-like]").forEach(b =>
|
||
b.addEventListener("click", () => {
|
||
const p = birdyBeitraege[+b.dataset.like];
|
||
p.gemocht = !p.gemocht;
|
||
p.likes += p.gemocht ? 1 : -1;
|
||
document.getElementById("birdy-feed").innerHTML = birdyFeedHtml();
|
||
verdrahteBirdyAktionen();
|
||
}));
|
||
appContentEl.querySelectorAll("[data-rt]").forEach(b =>
|
||
b.addEventListener("click", () => {
|
||
birdyBeitraege[+b.dataset.rt].retweets++;
|
||
document.getElementById("birdy-feed").innerHTML = birdyFeedHtml();
|
||
verdrahteBirdyAktionen();
|
||
zeigeToast("Beitrag geteilt");
|
||
}));
|
||
}
|
||
|
||
// Eigenen Beitrag posten
|
||
function sendeBirdyBeitrag() {
|
||
const input = document.getElementById("birdy-input");
|
||
const text = input.value.trim();
|
||
if (!text) { zeigeToast("Bitte etwas schreiben"); return; }
|
||
birdyBeitraege.unshift({
|
||
name: spielerZustand.name, handle: "@" + spielerZustand.name.toLowerCase().replace(/\s/g, "_"),
|
||
initialen: spielerZustand.name.substring(0, 2).toUpperCase(), farbe: "#7b6cf6",
|
||
text: text, likes: 0, retweets: 0, gemocht: false
|
||
});
|
||
input.value = "";
|
||
document.getElementById("birdy-feed").innerHTML = birdyFeedHtml();
|
||
verdrahteBirdyAktionen();
|
||
zeigeToast("Beitrag veröffentlicht");
|
||
}
|
||
|
||
/* =====================================================================
|
||
APP: KAMERA (Sucher + Auslöser) & GALERIE
|
||
===================================================================== */
|
||
// Vordefinierte "Fotos" als Farbverläufe für die Galerie
|
||
const galerieFotos = [
|
||
"linear-gradient(135deg,#ff8c42,#7b2f8f)", "linear-gradient(135deg,#4aa8ff,#0a2f8f)",
|
||
"linear-gradient(135deg,#34d399,#0f766e)", "linear-gradient(135deg,#f0506e,#7b2f3f)",
|
||
"linear-gradient(135deg,#f5b23d,#b45309)", "linear-gradient(135deg,#7b6cf6,#312e81)"
|
||
];
|
||
|
||
appRenderer.camera = function () {
|
||
appContentEl.innerHTML = appKopf("Kamera") +
|
||
'<div class="cam-view">' +
|
||
'<div class="cam-feed" id="cam-feed">' +
|
||
'<div style="position:absolute;top:14px;left:0;right:0;text-align:center;color:rgba(255,255,255,0.6);font-size:12px">📍 Vespucci Beach, Los Santos</div>' +
|
||
'<div style="position:absolute;inset:0;border:1px solid rgba(255,255,255,0.15);margin:40px 20px;border-radius:8px"></div>' +
|
||
'</div>' +
|
||
'<div class="cam-controls">' +
|
||
'<div class="cam-thumb" style="background:' + galerieFotos[0] + '"></div>' +
|
||
'<button class="shutter" id="cam-shutter" aria-label="Foto aufnehmen"></button>' +
|
||
'<button style="background:none;border:none;color:#fff;font-size:22px;cursor:pointer" id="cam-gallery" aria-label="Galerie">🖼️</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
// Auslöser: kurzer Weißblitz + Galerie-Eintrag
|
||
document.getElementById("cam-shutter").addEventListener("click", () => {
|
||
const feed = document.getElementById("cam-feed");
|
||
const blitz = document.createElement("div");
|
||
blitz.style.cssText = "position:absolute;inset:0;background:#fff;z-index:5";
|
||
feed.appendChild(blitz);
|
||
setTimeout(() => blitz.remove(), 150);
|
||
galerieFotos.unshift("linear-gradient(135deg,#2a3b52,#0a0f18)");
|
||
zeigeToast("Foto gespeichert 📸");
|
||
});
|
||
document.getElementById("cam-gallery").addEventListener("click", () => navigiere(zeigeGalerie));
|
||
};
|
||
|
||
// Galerie-Ansicht
|
||
function zeigeGalerie() {
|
||
appContentEl.innerHTML = appKopf("Galerie", galerieFotos.length + " Fotos") +
|
||
'<div class="app-body"><div class="gallery">' +
|
||
galerieFotos.map(f => '<div class="ph" style="background:' + f + '"></div>').join("") +
|
||
'</div></div>';
|
||
}
|
||
appRenderer.insta = zeigeGalerie; // InstaPic zeigt vereinfacht die Galerie
|
||
|
||
/* =====================================================================
|
||
APP: MUSIK (Player-Mock)
|
||
===================================================================== */
|
||
appRenderer.music = function () {
|
||
appContentEl.innerHTML = appKopf("Musik") +
|
||
'<div class="app-body" style="text-align:center">' +
|
||
'<div style="width:210px;height:210px;border-radius:20px;margin:20px auto;background:linear-gradient(145deg,#7b6cf6,#e11d48);box-shadow:0 20px 40px -12px rgba(123,108,246,0.5);display:grid;place-items:center;font-size:60px">🎧</div>' +
|
||
'<div style="font-size:20px;font-weight:700">Midnight Drive</div>' +
|
||
'<div style="color:var(--s-muted);margin-bottom:18px">Los Santos Radio · Non-Stop</div>' +
|
||
'<div style="height:4px;background:var(--s-surface-2);border-radius:2px;margin:0 10px"><div style="width:38%;height:100%;background:var(--s-accent);border-radius:2px"></div></div>' +
|
||
'<div style="display:flex;justify-content:space-between;color:var(--s-muted);font-size:11px;margin:6px 10px 20px"><span>1:24</span><span>3:42</span></div>' +
|
||
'<div style="display:flex;align-items:center;justify-content:center;gap:34px;font-size:30px">' +
|
||
'<button style="background:none;border:none;color:var(--s-fg);cursor:pointer;font-size:26px">⏮</button>' +
|
||
'<button id="music-play" style="width:70px;height:70px;border-radius:50%;background:var(--s-accent);border:none;color:#fff;cursor:pointer;font-size:28px">⏸</button>' +
|
||
'<button style="background:none;border:none;color:var(--s-fg);cursor:pointer;font-size:26px">⏭</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
let spielt = true;
|
||
document.getElementById("music-play").addEventListener("click", (e) => {
|
||
spielt = !spielt;
|
||
e.currentTarget.textContent = spielt ? "⏸" : "▶";
|
||
});
|
||
};
|
||
|
||
/* =====================================================================
|
||
APP: WETTER
|
||
===================================================================== */
|
||
appRenderer.weather = function () {
|
||
const stunden = [["Jetzt","24°","☀️"],["15h","25°","☀️"],["16h","24°","⛅"],["17h","22°","⛅"],["18h","20°","🌤️"],["19h","18°","🌙"],["20h","16°","🌙"]];
|
||
appContentEl.innerHTML = appKopf("Wetter") +
|
||
'<div class="app-body">' +
|
||
'<div class="weather-hero">' +
|
||
'<div class="city">Los Santos</div>' +
|
||
'<div class="temp">24°</div>' +
|
||
'<div class="cond">Sonnig · H:26° T:15°</div>' +
|
||
'</div>' +
|
||
'<div class="row" style="display:block">' +
|
||
'<div class="weather-hour">' + stunden.map(h =>
|
||
'<div class="h"><span>' + h[0] + '</span><span style="font-size:22px">' + h[2] + '</span><span class="t">' + h[1] + '</span></div>').join("") + '</div>' +
|
||
'</div>' +
|
||
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px">' +
|
||
wetterKachel("Wind", "12 km/h", "💨") + wetterKachel("Luftfeuchte", "48 %", "💧") +
|
||
wetterKachel("Sicht", "16 km", "👁️") + wetterKachel("UV-Index", "6 · Hoch", "☀️") +
|
||
'</div>' +
|
||
'</div>';
|
||
};
|
||
function wetterKachel(titel, wert, icon) {
|
||
return '<div class="row" style="flex-direction:column;align-items:flex-start;gap:4px;margin:0">' +
|
||
'<div class="meta">' + icon + ' ' + titel + '</div><div style="font-size:22px;font-weight:700">' + wert + '</div></div>';
|
||
}
|
||
|
||
/* =====================================================================
|
||
APP: RECHNER (voll funktionsfähig)
|
||
===================================================================== */
|
||
appRenderer.calc = function () {
|
||
appContentEl.innerHTML = appKopf("Rechner") +
|
||
'<div class="app-body" style="padding:0 0 24px;display:flex;flex-direction:column;justify-content:flex-end;overflow:hidden">' +
|
||
'<div class="calc-display" id="calc-display">0</div>' +
|
||
'<div class="calc-pad" id="calc-pad">' +
|
||
calcTaste("C","fn") + calcTaste("±","fn") + calcTaste("%","fn") + calcTaste("÷","op") +
|
||
calcTaste("7") + calcTaste("8") + calcTaste("9") + calcTaste("×","op") +
|
||
calcTaste("4") + calcTaste("5") + calcTaste("6") + calcTaste("−","op") +
|
||
calcTaste("1") + calcTaste("2") + calcTaste("3") + calcTaste("+","op") +
|
||
calcTaste("0","zero") + calcTaste(",") + calcTaste("=","eq") +
|
||
'</div>' +
|
||
'</div>';
|
||
document.getElementById("calc-pad").querySelectorAll("button").forEach(b =>
|
||
b.addEventListener("click", () => rechnerEingabe(b.textContent)));
|
||
};
|
||
function calcTaste(zeichen, klasse) {
|
||
return '<button class="calc-key ' + (klasse || "") + '">' + zeichen + '</button>';
|
||
}
|
||
|
||
// Rechner-Logik
|
||
let calcEingabe = "0"; // aktuelle Anzeige
|
||
let calcVorher = null; // gespeicherter Operand
|
||
let calcOperator = null;// aktive Rechenoperation
|
||
let calcNeu = true; // ob die nächste Ziffer neu beginnt
|
||
|
||
function rechnerEingabe(taste) {
|
||
const display = document.getElementById("calc-display");
|
||
const operatoren = { "÷": "/", "×": "*", "−": "-", "+": "+" };
|
||
|
||
if (taste >= "0" && taste <= "9") {
|
||
calcEingabe = calcNeu ? taste : (calcEingabe === "0" ? taste : calcEingabe + taste);
|
||
calcNeu = false;
|
||
} else if (taste === ",") {
|
||
if (!calcEingabe.includes(",")) calcEingabe += ",";
|
||
calcNeu = false;
|
||
} else if (taste === "C") {
|
||
calcEingabe = "0"; calcVorher = null; calcOperator = null; calcNeu = true;
|
||
} else if (taste === "±") {
|
||
calcEingabe = calcEingabe.startsWith("-") ? calcEingabe.slice(1) : "-" + calcEingabe;
|
||
} else if (taste === "%") {
|
||
calcEingabe = String(parseFloat(calcEingabe.replace(",", ".")) / 100).replace(".", ",");
|
||
} else if (operatoren[taste]) {
|
||
calcVorher = parseFloat(calcEingabe.replace(",", "."));
|
||
calcOperator = operatoren[taste];
|
||
calcNeu = true;
|
||
} else if (taste === "=") {
|
||
if (calcOperator !== null && calcVorher !== null) {
|
||
const jetzt = parseFloat(calcEingabe.replace(",", "."));
|
||
let ergebnis = 0;
|
||
if (calcOperator === "/") ergebnis = calcVorher / jetzt;
|
||
else if (calcOperator === "*") ergebnis = calcVorher * jetzt;
|
||
else if (calcOperator === "-") ergebnis = calcVorher - jetzt;
|
||
else if (calcOperator === "+") ergebnis = calcVorher + jetzt;
|
||
// Ergebnis runden und deutsch anzeigen
|
||
calcEingabe = String(Math.round(ergebnis * 1e8) / 1e8).replace(".", ",");
|
||
calcOperator = null; calcVorher = null; calcNeu = true;
|
||
}
|
||
}
|
||
display.textContent = calcEingabe;
|
||
}
|
||
|
||
/* =====================================================================
|
||
APP: GARAGE (Fahrzeuge des Spielers)
|
||
===================================================================== */
|
||
const fahrzeugListe = [
|
||
{ name: "Sultan RS", kennzeichen: "LS-MW 182", status: "Ausgeparkt", zustand: 92, farbe: "#f0506e" },
|
||
{ name: "Bati 801 (Motorrad)", kennzeichen: "LS-MW 044", status: "In Garage", zustand: 100, farbe: "#4aa8ff" },
|
||
{ name: "Bison (Pickup)", kennzeichen: "LS-MW 210", status: "In Garage", zustand: 76, farbe: "#34d399" }
|
||
];
|
||
appRenderer.garage = function () {
|
||
appContentEl.innerHTML = appKopf("Garage", fahrzeugListe.length + " Fahrzeuge") +
|
||
'<div class="app-body">' + fahrzeugListe.map(f =>
|
||
'<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">' +
|
||
'<div style="display:flex;align-items:center;gap:12px">' +
|
||
'<span class="avatar" style="background:' + f.farbe + '">🚗</span>' +
|
||
'<div class="grow"><div class="title">' + sicher(f.name) + '</div>' +
|
||
'<div class="meta">' + sicher(f.kennzeichen) + '</div></div>' +
|
||
'<span class="pill ' + (f.status === "In Garage" ? "green" : "amber") + '">' + f.status + '</span>' +
|
||
'</div>' +
|
||
'<div><div class="meta" style="margin-bottom:4px">Zustand: ' + f.zustand + ' %</div>' +
|
||
'<div style="height:6px;background:var(--s-surface-2);border-radius:3px"><div style="width:' + f.zustand + '%;height:100%;background:' + (f.zustand > 80 ? "var(--s-green)" : "var(--s-amber)") + ';border-radius:3px"></div></div></div>' +
|
||
'</div>').join("") +
|
||
'</div>';
|
||
};
|
||
|
||
/* =====================================================================
|
||
APP: DISPATCH (Notfall an die Leitstelle MELDEN — kein Annehmen)
|
||
===================================================================== */
|
||
// Vordefinierte Meldegründe für das Dropdown (Code, Bezeichnung, Priorität)
|
||
const dispatchGruende = [
|
||
{ code: "10-90", label: "Raubüberfall", prio: "red" },
|
||
{ code: "10-71", label: "Schusswaffengebrauch", prio: "red" },
|
||
{ code: "10-70", label: "Brand", prio: "red" },
|
||
{ code: "10-52", label: "Medizinischer Notfall", prio: "amber" },
|
||
{ code: "10-50", label: "Verkehrsunfall", prio: "amber" },
|
||
{ code: "10-55", label: "Fahrzeugdiebstahl", prio: "amber" },
|
||
{ code: "10-66", label: "Verdächtige Person", prio: "blue" },
|
||
{ code: "10-16", label: "Ruhestörung", prio: "blue" },
|
||
{ code: "10-00", label: "Sonstiges", prio: "blue" }
|
||
];
|
||
// Session-Historie der eigenen Meldungen (neueste zuerst)
|
||
const dispatchMeldungen = [];
|
||
|
||
appRenderer.dispatch = function () {
|
||
appContentEl.innerHTML = appKopf("Dispatch", "Notfall an die Leitstelle melden") +
|
||
'<div class="app-body">' +
|
||
'<label class="field-label">Grund der Meldung</label>' +
|
||
'<select class="input" id="disp-grund">' +
|
||
dispatchGruende.map((g, i) =>
|
||
'<option value="' + i + '">' + sicher(g.code) + ' · ' + sicher(g.label) + '</option>').join("") +
|
||
'</select>' +
|
||
'<label class="field-label">Standort</label>' +
|
||
'<input class="input" id="disp-ort" placeholder="z. B. Legion Square" value="Aktueller Standort">' +
|
||
'<label class="field-label">Beschreibung (optional)</label>' +
|
||
'<input class="input" id="disp-info" placeholder="Kurze Zusatzinfo für die Leitstelle" autocomplete="off">' +
|
||
'<div style="height:16px"></div>' +
|
||
'<button class="btn red" id="disp-send">📡 Notruf absetzen</button>' +
|
||
'<div class="list-title">Deine Meldungen</div>' +
|
||
'<div id="disp-log"></div>' +
|
||
'</div>';
|
||
zeichneMeldungen();
|
||
document.getElementById("disp-send").addEventListener("click", sendeMeldung);
|
||
};
|
||
|
||
// Eine Meldung an die Leitstelle absenden
|
||
function sendeMeldung() {
|
||
const grund = dispatchGruende[+document.getElementById("disp-grund").value];
|
||
const ort = document.getElementById("disp-ort").value.trim() || "Unbekannter Standort";
|
||
const info = document.getElementById("disp-info").value.trim();
|
||
const jetzt = new Date();
|
||
const zeit = String(jetzt.getHours()).padStart(2, "0") + ":" + String(jetzt.getMinutes()).padStart(2, "0");
|
||
// Meldung vorne in die Historie einfügen
|
||
dispatchMeldungen.unshift({ code: grund.code, label: grund.label, prio: grund.prio, ort: ort, info: info, zeit: zeit });
|
||
document.getElementById("disp-info").value = "";
|
||
zeichneMeldungen();
|
||
zeigeToast("Notruf " + grund.code + " abgesetzt 📡");
|
||
}
|
||
|
||
// Historie der eigenen Meldungen zeichnen
|
||
function zeichneMeldungen() {
|
||
const container = document.getElementById("disp-log");
|
||
if (dispatchMeldungen.length === 0) {
|
||
container.innerHTML = '<div class="empty"><div class="big">📻</div><div>Noch keine Meldungen abgesetzt.</div></div>';
|
||
return;
|
||
}
|
||
container.innerHTML = dispatchMeldungen.map(m =>
|
||
'<div class="row" style="flex-direction:column;align-items:stretch;gap:6px">' +
|
||
'<div style="display:flex;align-items:center;gap:10px">' +
|
||
'<div class="grow"><div class="title">' + sicher(m.code) + ' · ' + sicher(m.label) + '</div>' +
|
||
'<div class="meta">📍 ' + sicher(m.ort) + (m.info ? ' · ' + sicher(m.info) : '') + '</div></div>' +
|
||
'<span class="pill ' + m.prio + '">' + prioText(m.prio) + '</span>' +
|
||
'</div>' +
|
||
'<div class="meta">Abgesetzt um ' + m.zeit + ' Uhr · Status: <span style="color:var(--s-green)">übermittelt</span></div>' +
|
||
'</div>').join("");
|
||
}
|
||
|
||
// Prioritätsstufe als Text
|
||
function prioText(prio) {
|
||
if (prio === "red") return "Priorität 1";
|
||
if (prio === "amber") return "Priorität 2";
|
||
return "Priorität 3";
|
||
}
|
||
|
||
/* =====================================================================
|
||
APP: GELBE SEITEN (Anzeigen / Unternehmen)
|
||
===================================================================== */
|
||
appRenderer.pages = function () {
|
||
const anzeigen = [
|
||
{ name: "Benny's Motorworks", kat: "Werkstatt & Tuning", tel: "555-0199", icon: "🔧", farbe: "#f5b23d" },
|
||
{ name: "Taxi Zentrale LS", kat: "Personenbeförderung", tel: "555-0100", icon: "🚕", farbe: "#facc15" },
|
||
{ name: "Kanzlei Dr. Weber", kat: "Rechtsanwalt", tel: "555-0163", icon: "⚖️", farbe: "#34d399" },
|
||
{ name: "Vanilla Unicorn", kat: "Nachtclub & Bar", tel: "555-0170", icon: "🍸", farbe: "#e11d48" },
|
||
{ name: "Los Santos Customs", kat: "Fahrzeugveredelung", tel: "555-0188", icon: "🎨", farbe: "#4aa8ff" }
|
||
];
|
||
appContentEl.innerHTML = appKopf("Gelbe Seiten", "Unternehmen in Los Santos") +
|
||
'<div class="app-body">' + anzeigen.map(a =>
|
||
'<div class="row" style="cursor:pointer" data-call="' + sicher(a.name) + '">' +
|
||
'<span class="avatar" style="background:' + a.farbe + '">' + a.icon + '</span>' +
|
||
'<div class="grow"><div class="title">' + sicher(a.name) + '</div>' +
|
||
'<div class="meta">' + sicher(a.kat) + ' · ' + sicher(a.tel) + '</div></div>' +
|
||
'<span class="icon-btn" style="pointer-events:none">' + telefonSymbol + '</span></div>').join("") +
|
||
'</div>';
|
||
appContentEl.querySelectorAll("[data-call]").forEach(r =>
|
||
r.addEventListener("click", () => starteAnruf(r.dataset.call, r.dataset.call)));
|
||
};
|
||
|
||
/* =====================================================================
|
||
APP: EINSTELLUNGEN (Theme, Wallpaper, Schalter)
|
||
===================================================================== */
|
||
appRenderer.settings = function () {
|
||
const istHell = screenEl.classList.contains("light");
|
||
appContentEl.innerHTML = appKopf("Einstellungen") +
|
||
'<div class="app-body">' +
|
||
'<div class="row"><span class="avatar" style="background:linear-gradient(145deg,#7b6cf6,#34d399)">' + spielerZustand.name.substring(0,2).toUpperCase() + '</span>' +
|
||
'<div class="grow"><div class="title">' + sicher(spielerZustand.name) + '</div><div class="meta">' + sicher(spielerZustand.nummer) + ' · AetherOS 3.1</div></div></div>' +
|
||
|
||
'<div class="list-title">Startbildschirm</div>' +
|
||
'<div class="setting-row" id="set-dock" style="border-radius:14px;cursor:pointer">' +
|
||
'<span class="si g-set" style="font-size:16px">▦</span>' +
|
||
'<div class="grow"><div class="title">Dock anpassen</div><div class="meta">Untere App-Leiste ändern</div></div>' +
|
||
'<span style="color:var(--s-muted);font-size:20px">›</span></div>' +
|
||
|
||
'<div class="list-title">Anzeige</div>' +
|
||
'<div class="setting-row"><span class="si g-set">🌙</span><div class="grow"><div class="title">Helles Design</div></div>' +
|
||
'<button class="switch ' + (istHell ? "on" : "") + '" id="sw-theme" aria-label="Design umschalten"></button></div>' +
|
||
'<div class="setting-row"><span class="si g-weather">✈️</span><div class="grow"><div class="title">Flugmodus</div></div>' +
|
||
'<button class="switch" id="sw-airplane" aria-label="Flugmodus"></button></div>' +
|
||
'<div class="setting-row"><span class="si g-music">🔔</span><div class="grow"><div class="title">Nicht stören</div></div>' +
|
||
'<button class="switch" id="sw-dnd" aria-label="Nicht stören"></button></div>' +
|
||
|
||
'<div class="list-title">Sicherheit</div>' +
|
||
'<div class="setting-row" style="border-radius:14px 14px 0 0"><span class="si g-birdy" style="font-size:15px">🙂</span>' +
|
||
'<div class="grow"><div class="title">Face-ID</div><div class="meta">Ohne Code-Eingabe entsperren</div></div>' +
|
||
'<button class="switch ' + (spielerZustand.faceId ? "on" : "") + '" id="sw-faceid" aria-label="Face-ID"></button></div>' +
|
||
'<div class="setting-row" id="set-pin" style="border-radius:0 0 14px 14px;cursor:pointer"><span class="si g-set" style="font-size:15px">🔒</span>' +
|
||
'<div class="grow"><div class="title">Code ändern</div></div>' +
|
||
'<span style="color:var(--s-muted);font-size:20px">›</span></div>' +
|
||
|
||
'<div class="list-title">Hintergrundbild</div>' +
|
||
'<div class="wp-picker" id="wp-picker">' +
|
||
wpOption("wp-aurora","Aurora") + wpOption("wp-sunset","Sonnenuntergang") +
|
||
wpOption("wp-carbon","Carbon") + wpOption("wp-mono","Schlicht") +
|
||
'</div>' +
|
||
|
||
'<div class="list-title">Info</div>' +
|
||
'<div class="row"><div class="grow"><div class="title">Modell</div></div><div class="side">Aether X · 128 GB</div></div>' +
|
||
'<div class="row"><div class="grow"><div class="title">Netzbetreiber</div></div><div class="side">LS-Mobile</div></div>' +
|
||
'</div>';
|
||
|
||
// Dock anpassen öffnen
|
||
document.getElementById("set-dock").addEventListener("click", () => navigiere(zeigeDockEditor));
|
||
// Face-ID an/aus
|
||
document.getElementById("sw-faceid").addEventListener("click", (e) => {
|
||
e.currentTarget.classList.toggle("on");
|
||
spielerZustand.faceId = e.currentTarget.classList.contains("on");
|
||
zeigeToast(spielerZustand.faceId ? "Face-ID aktiviert" : "Face-ID aus – Code nötig");
|
||
});
|
||
// Code ändern öffnen
|
||
document.getElementById("set-pin").addEventListener("click", () => navigiere(zeigeCodeAendern));
|
||
// Theme umschalten
|
||
document.getElementById("sw-theme").addEventListener("click", (e) => {
|
||
screenEl.classList.toggle("light");
|
||
spielerZustand.theme = screenEl.classList.contains("light") ? "light" : "dark";
|
||
e.currentTarget.classList.toggle("on");
|
||
});
|
||
// Flugmodus schaltet die Netz-Anzeige
|
||
document.getElementById("sw-airplane").addEventListener("click", (e) => {
|
||
e.currentTarget.classList.toggle("on");
|
||
const an = e.currentTarget.classList.contains("on");
|
||
document.getElementById("sb-signal").style.opacity = an ? "0.25" : "1";
|
||
zeigeToast(an ? "Flugmodus aktiviert ✈️" : "Flugmodus deaktiviert");
|
||
});
|
||
// Nicht stören (nur Rückmeldung)
|
||
document.getElementById("sw-dnd").addEventListener("click", (e) => {
|
||
e.currentTarget.classList.toggle("on");
|
||
zeigeToast(e.currentTarget.classList.contains("on") ? "Nicht stören aktiv 🔕" : "Nicht stören aus");
|
||
});
|
||
// Wallpaper auswählen
|
||
appContentEl.querySelectorAll("[data-wp]").forEach(opt =>
|
||
opt.addEventListener("click", () => {
|
||
const klasse = opt.dataset.wp;
|
||
document.getElementById("wallpaper").className = "wallpaper " + klasse;
|
||
spielerZustand.wallpaper = klasse;
|
||
appContentEl.querySelectorAll("[data-wp]").forEach(o => o.classList.remove("sel"));
|
||
opt.classList.add("sel");
|
||
}));
|
||
};
|
||
// Einzelne Wallpaper-Vorschau
|
||
function wpOption(klasse, name) {
|
||
const gewaehlt = spielerZustand.wallpaper === klasse ? " sel" : "";
|
||
return '<div class="wp-opt ' + klasse + gewaehlt + '" data-wp="' + klasse + '" title="' + name + '"></div>';
|
||
}
|
||
|
||
/* Dock-Editor: die 4 Plätze der unteren Leiste per Dropdown belegen */
|
||
function zeigeDockEditor() {
|
||
// Nur echte, nicht gesperrte Apps zur Auswahl anbieten
|
||
const auswahl = appReihenfolge.filter(id => !(alleApps[id].gated && !spielerZustand.freigeschaltet[id]));
|
||
appContentEl.innerHTML = appKopf("Dock anpassen", "Wähle die 4 Apps der unteren Leiste") +
|
||
'<div class="app-body">' +
|
||
'<div class="dock" style="position:relative;margin-bottom:18px;background:var(--s-surface)">' + '</div>' +
|
||
spielerZustand.dockApps.map((id, platz) =>
|
||
'<label class="field-label">Platz ' + (platz + 1) + '</label>' +
|
||
'<select class="input dock-slot" data-slot="' + platz + '">' +
|
||
auswahl.map(oid => '<option value="' + oid + '"' + (oid === id ? " selected" : "") + '>' +
|
||
sicher(alleApps[oid].label) + '</option>').join("") +
|
||
'</select>').join("") +
|
||
'<div style="height:12px"></div>' +
|
||
'<div class="meta" style="text-align:center">Änderungen werden sofort übernommen.</div>' +
|
||
'</div>';
|
||
zeichneDockVorschau();
|
||
// Auswahl ändern -> Dock aktualisieren
|
||
appContentEl.querySelectorAll(".dock-slot").forEach(sel =>
|
||
sel.addEventListener("change", () => {
|
||
spielerZustand.dockApps[+sel.dataset.slot] = sel.value;
|
||
baueHomescreen();
|
||
zeichneDockVorschau();
|
||
}));
|
||
}
|
||
|
||
// Kleine Live-Vorschau des Docks im Editor
|
||
function zeichneDockVorschau() {
|
||
const vorschau = appContentEl.querySelector(".dock");
|
||
if (!vorschau) return;
|
||
vorschau.innerHTML = "";
|
||
spielerZustand.dockApps.forEach(id => vorschau.appendChild(baueAppKachel(id)));
|
||
}
|
||
|
||
/* Code ändern (neuen Code eingeben und bestätigen) */
|
||
function zeigeCodeAendern() {
|
||
// Dunkler Hintergrund, damit das PIN-Feld in jedem Theme gut lesbar ist
|
||
appContentEl.innerHTML = appKopf("Code ändern") +
|
||
'<div class="app-body" id="code-body" style="padding:0;background:#0b0d16"></div>';
|
||
const body = document.getElementById("code-body");
|
||
renderePinFeld(body, {
|
||
titel: "Neuer Code", unterzeile: "4-stelligen Code eingeben", icon: "🔐",
|
||
onFertig: (neuerCode) => {
|
||
renderePinFeld(body, {
|
||
titel: "Code bestätigen", unterzeile: "Neuen Code erneut eingeben", icon: "🔐",
|
||
onFertig: (bestaetigung, reset) => {
|
||
if (bestaetigung === neuerCode) { spielerZustand.pin = neuerCode; zeigeToast("Code geändert ✅"); zurueck(); }
|
||
else { zeigeToast("Codes stimmen nicht überein"); reset(); }
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
/* =====================================================================
|
||
APP: DARKCHAT (anonymer Marktplatz — RP-Flair)
|
||
===================================================================== */
|
||
appRenderer.darkweb = function () {
|
||
const angebote = [
|
||
{ user: "@ghost_44", text: "Biete Dienstleistungen. Diskret. Nur Bargeld. 💼", zeit: "vor 5 Min" },
|
||
{ user: "@nachtschatten", text: "Suche zuverlässigen Fahrer für heute Nacht. Gute Bezahlung.", zeit: "vor 22 Min" },
|
||
{ user: "@anon_ls", text: "Verkaufe elektronische Bauteile. DM für Details.", zeit: "vor 1 Std" }
|
||
];
|
||
appContentEl.innerHTML = appKopf("Darkchat", "🔒 Verbindung anonymisiert") +
|
||
'<div class="app-body">' +
|
||
'<div class="row" style="background:rgba(240,80,110,0.1);border-color:rgba(240,80,110,0.3)"><span>⚠️</span><div class="meta" style="white-space:normal">Reines Rollenspiel-Feature. Alle Inhalte sind fiktiv.</div></div>' +
|
||
angebote.map(a =>
|
||
'<div class="post"><div class="ph"><span class="avatar" style="background:#374151">?</span>' +
|
||
'<div><div class="name">' + sicher(a.user) + '</div><div class="handle">' + a.zeit + '</div></div></div>' +
|
||
'<div class="txt">' + sicher(a.text) + '</div></div>').join("") +
|
||
'</div>';
|
||
};
|
||
|
||
/* =====================================================================
|
||
APP: UHR (Weltzeit / Timer-Mock)
|
||
===================================================================== */
|
||
// Gestellte Wecker des Spielers
|
||
const weckerListe = [
|
||
{ zeit: "07:00", label: "Dienstbeginn · Werktags", an: true },
|
||
{ zeit: "22:30", label: "Feierabend", an: false }
|
||
];
|
||
let uhrAppTimer = null; // Timer der Live-Uhr (läuft nur während die App offen ist)
|
||
|
||
appRenderer.clock = function () {
|
||
appContentEl.innerHTML = appKopf("Uhr", "Los Santos · Ortszeit") +
|
||
'<div class="app-body">' +
|
||
'<div style="text-align:center;font-size:58px;font-weight:300;font-variant-numeric:tabular-nums;margin:12px 0" id="clock-big">--:--:--</div>' +
|
||
'<div class="list-title">Neuer Wecker</div>' +
|
||
'<div style="display:flex;gap:8px;margin-bottom:8px">' +
|
||
'<input class="input" id="al-time" type="time" value="08:00" style="max-width:130px">' +
|
||
'<input class="input" id="al-label" placeholder="Bezeichnung" autocomplete="off">' +
|
||
'</div>' +
|
||
'<button class="btn" id="al-add">Wecker hinzufügen</button>' +
|
||
'<div class="list-title">Wecker</div>' +
|
||
'<div id="al-list"></div>' +
|
||
'</div>';
|
||
// Live-Uhr: sekündlich aktualisieren, stoppt automatisch beim Verlassen der App
|
||
clearInterval(uhrAppTimer);
|
||
const tick = () => {
|
||
const el = document.getElementById("clock-big");
|
||
if (!el) { clearInterval(uhrAppTimer); return; }
|
||
const j = new Date();
|
||
el.textContent = String(j.getHours()).padStart(2, "0") + ":" +
|
||
String(j.getMinutes()).padStart(2, "0") + ":" + String(j.getSeconds()).padStart(2, "0");
|
||
};
|
||
tick();
|
||
uhrAppTimer = setInterval(tick, 1000);
|
||
zeichneWecker();
|
||
// Neuen Wecker hinzufügen
|
||
document.getElementById("al-add").addEventListener("click", () => {
|
||
const zeit = document.getElementById("al-time").value;
|
||
const label = document.getElementById("al-label").value.trim() || "Wecker";
|
||
if (!zeit) { zeigeToast("Bitte eine Uhrzeit wählen"); return; }
|
||
weckerListe.push({ zeit: zeit, label: label, an: true });
|
||
weckerListe.sort((a, b) => a.zeit.localeCompare(b.zeit));
|
||
document.getElementById("al-label").value = "";
|
||
zeichneWecker();
|
||
zeigeToast("Wecker " + zeit + " gestellt ⏰");
|
||
});
|
||
};
|
||
|
||
// Weckerliste zeichnen (an/aus schalten, löschen)
|
||
function zeichneWecker() {
|
||
const box = document.getElementById("al-list");
|
||
if (!box) return;
|
||
if (weckerListe.length === 0) {
|
||
box.innerHTML = '<div class="empty"><div class="big">⏰</div><div>Keine Wecker gestellt.</div></div>';
|
||
return;
|
||
}
|
||
box.innerHTML = weckerListe.map((w, i) =>
|
||
'<div class="setting-row" style="border-radius:14px;margin-bottom:8px">' +
|
||
'<div class="grow"><div style="font-size:26px;font-weight:300;color:' + (w.an ? "var(--s-fg)" : "var(--s-muted)") + '">' + w.zeit + '</div>' +
|
||
'<div class="meta">' + sicher(w.label) + '</div></div>' +
|
||
'<button class="switch ' + (w.an ? "on" : "") + '" data-al-toggle="' + i + '" aria-label="Wecker an/aus"></button>' +
|
||
'<button class="icon-btn" data-al-del="' + i + '" style="color:var(--s-red);font-size:15px" aria-label="Wecker löschen">🗑</button>' +
|
||
'</div>').join("");
|
||
// An/Aus umschalten
|
||
box.querySelectorAll("[data-al-toggle]").forEach(b =>
|
||
b.addEventListener("click", () => {
|
||
weckerListe[+b.dataset.alToggle].an = !weckerListe[+b.dataset.alToggle].an;
|
||
zeichneWecker();
|
||
}));
|
||
// Löschen
|
||
box.querySelectorAll("[data-al-del]").forEach(b =>
|
||
b.addEventListener("click", () => {
|
||
weckerListe.splice(+b.dataset.alDel, 1);
|
||
zeichneWecker();
|
||
zeigeToast("Wecker gelöscht");
|
||
}));
|
||
}
|
||
|
||
/* =====================================================================
|
||
APP: STAAT (Staatskasse mit Graph, Einnahmen/Ausgaben, Abstimmungen)
|
||
===================================================================== */
|
||
const staatDaten = {
|
||
kontostand: 12480000, // aktueller Kontostand der Staatskasse in Euro
|
||
// Verlauf der letzten 6 Monate (Werte in Mio € für den Graphen)
|
||
verlauf: [
|
||
{ monat: "Feb", ein: 2.1, aus: 1.8 }, { monat: "Mär", ein: 2.4, aus: 2.0 },
|
||
{ monat: "Apr", ein: 2.0, aus: 2.3 }, { monat: "Mai", ein: 2.6, aus: 2.1 },
|
||
{ monat: "Jun", ein: 2.9, aus: 2.4 }, { monat: "Jul", ein: 3.1, aus: 2.2 }
|
||
],
|
||
// Aufschlüsselung der Einnahmen
|
||
einnahmen: [
|
||
{ q: "Steuern", v: 1250000 }, { q: "Bußgelder & Strafen", v: 340000 },
|
||
{ q: "Gewerbelizenzen", v: 210000 }, { q: "Zoll & Gebühren", v: 95000 }
|
||
],
|
||
// Aufschlüsselung der Ausgaben
|
||
ausgaben: [
|
||
{ q: "Gehälter Beamte", v: 820000 }, { q: "Infrastruktur", v: 540000 },
|
||
{ q: "Gesundheitswesen", v: 310000 }, { q: "Subventionen", v: 150000 }
|
||
],
|
||
// Umsetzungsprojekte, über die abgestimmt wird
|
||
projekte: [
|
||
{ titel: "Neue Feuerwache Sandy Shores", kosten: 450000, ja: 128, nein: 34, gestimmt: null },
|
||
{ titel: "Ausbau Autobahn N1", kosten: 1200000, ja: 87, nein: 96, gestimmt: null },
|
||
{ titel: "Steuersenkung für Kleingewerbe", kosten: 300000, ja: 203, nein: 41, gestimmt: null }
|
||
]
|
||
};
|
||
|
||
appRenderer.staat = function () {
|
||
// Höchsten Monatswert für die Skalierung des Graphen bestimmen
|
||
const maxWert = Math.max(...staatDaten.verlauf.flatMap(m => [m.ein, m.aus]));
|
||
const graphHtml = '<div class="chart">' + staatDaten.verlauf.map(m =>
|
||
'<div class="col"><div class="bars">' +
|
||
'<div class="bar ein" style="height:' + (m.ein / maxWert * 100).toFixed(0) + '%"></div>' +
|
||
'<div class="bar aus" style="height:' + (m.aus / maxWert * 100).toFixed(0) + '%"></div>' +
|
||
'</div><div class="m">' + m.monat + '</div></div>').join("") + '</div>';
|
||
|
||
appContentEl.innerHTML = appKopf("Staat", "Staatskasse Los Santos") +
|
||
'<div class="app-body">' +
|
||
'<div class="staat-card">' +
|
||
'<div class="lbl">STAATSKONTO · GESAMTBESTAND</div>' +
|
||
'<div class="bal">' + formatiereEuro(staatDaten.kontostand) + '</div>' +
|
||
'<div class="lbl">Haushaltsjahr 2026 · öffentlich einsehbar</div>' +
|
||
'</div>' +
|
||
'<div class="list-title">Einnahmen & Ausgaben (Mio €)</div>' +
|
||
graphHtml +
|
||
'<div class="legende"><span><i style="background:var(--s-green)"></i>Einnahmen</span>' +
|
||
'<span><i style="background:var(--s-red)"></i>Ausgaben</span></div>' +
|
||
|
||
'<div class="list-title">Einnahmen (Monat)</div>' +
|
||
staatDaten.einnahmen.map(e =>
|
||
'<div class="tx"><div class="grow"><div class="title">' + sicher(e.q) + '</div></div>' +
|
||
'<div class="amt plus">+' + formatiereEuro(e.v) + '</div></div>').join("") +
|
||
|
||
'<div class="list-title">Ausgaben (Monat)</div>' +
|
||
staatDaten.ausgaben.map(a =>
|
||
'<div class="tx"><div class="grow"><div class="title">' + sicher(a.q) + '</div></div>' +
|
||
'<div class="amt" style="color:var(--s-red)">-' + formatiereEuro(a.v) + '</div></div>').join("") +
|
||
|
||
'<div class="list-title">Umsetzungsprojekte · Abstimmung</div>' +
|
||
'<div id="staat-projekte"></div>' +
|
||
'</div>';
|
||
zeichneProjekte();
|
||
};
|
||
|
||
// Projekte inkl. Abstimmungsbalken zeichnen
|
||
function zeichneProjekte() {
|
||
const box = document.getElementById("staat-projekte");
|
||
box.innerHTML = staatDaten.projekte.map((p, i) => {
|
||
const gesamt = p.ja + p.nein;
|
||
const jaProzent = gesamt ? Math.round(p.ja / gesamt * 100) : 50;
|
||
return '<div class="row" style="flex-direction:column;align-items:stretch;gap:4px">' +
|
||
'<div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">' +
|
||
'<div class="title">' + sicher(p.titel) + '</div>' +
|
||
'<span class="pill blue" style="flex-shrink:0">' + formatiereEuro(p.kosten) + '</span></div>' +
|
||
'<div class="vote-bar"><div class="ja" style="width:' + jaProzent + '%"></div>' +
|
||
'<div class="nein" style="width:' + (100 - jaProzent) + '%"></div></div>' +
|
||
'<div class="meta">' + p.ja + ' dafür · ' + p.nein + ' dagegen' +
|
||
(p.gestimmt ? ' · <b style="color:var(--s-fg)">deine Stimme: ' + (p.gestimmt === "ja" ? "Dafür" : "Dagegen") + '</b>' : '') + '</div>' +
|
||
(p.gestimmt ? '' :
|
||
'<div class="vote-actions">' +
|
||
'<button class="btn green" style="padding:9px;font-size:14px" data-vote="ja" data-p="' + i + '">Dafür</button>' +
|
||
'<button class="btn red" style="padding:9px;font-size:14px" data-vote="nein" data-p="' + i + '">Dagegen</button></div>') +
|
||
'</div>';
|
||
}).join("");
|
||
// Abstimmungs-Buttons verdrahten (nur eine Stimme pro Projekt)
|
||
box.querySelectorAll("[data-vote]").forEach(b =>
|
||
b.addEventListener("click", () => {
|
||
const projekt = staatDaten.projekte[+b.dataset.p];
|
||
if (projekt.gestimmt) return;
|
||
projekt.gestimmt = b.dataset.vote;
|
||
if (b.dataset.vote === "ja") projekt.ja++; else projekt.nein++;
|
||
zeichneProjekte();
|
||
zeigeToast("Deine Stimme wurde gezählt 🗳️");
|
||
}));
|
||
}
|
||
|
||
/* =====================================================================
|
||
APP: NOTIZEN (einfache, funktionierende Notizen)
|
||
===================================================================== */
|
||
const notizenListe = [
|
||
{ titel: "Einkaufsliste", text: "Verband, Munition, Snacks für die Tour" },
|
||
{ titel: "Wichtig", text: "Treffen mit Anwalt Dr. Weber am Donnerstag, 18 Uhr" }
|
||
];
|
||
appRenderer.notizen = function () {
|
||
const liste = notizenListe.length
|
||
? notizenListe.map((n, i) =>
|
||
'<div class="row" style="cursor:pointer;flex-direction:column;align-items:stretch;gap:3px" data-note="' + i + '">' +
|
||
'<div class="title">' + sicher(n.titel) + '</div>' +
|
||
'<div class="meta" style="white-space:normal">' + sicher(n.text) + '</div></div>').join("")
|
||
: '<div class="empty"><div class="big">📝</div><div>Noch keine Notizen.</div></div>';
|
||
appContentEl.innerHTML = appKopf("Notizen", notizenListe.length + " Notizen") +
|
||
'<div class="app-body">' +
|
||
'<button class="btn" id="note-new" style="margin-bottom:14px">+ Neue Notiz</button>' +
|
||
liste +
|
||
'</div>';
|
||
document.getElementById("note-new").addEventListener("click", () => navigiere(() => zeigeNotizEditor(null)));
|
||
// Tippen auf eine Notiz öffnet den Editor
|
||
appContentEl.querySelectorAll("[data-note]").forEach(r =>
|
||
r.addEventListener("click", () => navigiere(() => zeigeNotizEditor(+r.dataset.note))));
|
||
};
|
||
|
||
// Notiz-Editor (index === null -> neue Notiz, sonst bestehende bearbeiten)
|
||
function zeigeNotizEditor(index) {
|
||
const vorhanden = index !== null;
|
||
const notiz = vorhanden ? notizenListe[index] : { titel: "", text: "" };
|
||
appContentEl.innerHTML = appKopf(vorhanden ? "Notiz bearbeiten" : "Neue Notiz") +
|
||
'<div class="app-body">' +
|
||
'<label class="field-label">Titel</label>' +
|
||
'<input class="input" id="note-titel" placeholder="Titel" value="' + sicher(notiz.titel) + '">' +
|
||
'<label class="field-label">Inhalt</label>' +
|
||
'<textarea class="input" id="note-text" style="min-height:140px;resize:none">' + sicher(notiz.text) + '</textarea>' +
|
||
'<div style="height:16px"></div>' +
|
||
'<button class="btn green" id="note-save">Speichern</button>' +
|
||
(vorhanden ? '<div style="height:8px"></div><button class="btn red" id="note-del">Notiz löschen</button>' : '') +
|
||
'</div>';
|
||
// Speichern (neu anlegen oder aktualisieren)
|
||
document.getElementById("note-save").addEventListener("click", () => {
|
||
const titel = document.getElementById("note-titel").value.trim();
|
||
const text = document.getElementById("note-text").value.trim();
|
||
if (!titel && !text) { zeigeToast("Bitte etwas eingeben"); return; }
|
||
const eintrag = { titel: titel || "Ohne Titel", text: text || "—" };
|
||
if (vorhanden) notizenListe[index] = eintrag; else notizenListe.unshift(eintrag);
|
||
zeigeToast("Notiz gespeichert");
|
||
zurueck();
|
||
});
|
||
// Löschen
|
||
if (vorhanden) {
|
||
document.getElementById("note-del").addEventListener("click", () => {
|
||
notizenListe.splice(index, 1);
|
||
zeigeToast("Notiz gelöscht");
|
||
zurueck();
|
||
});
|
||
}
|
||
}
|
||
|
||
/* =====================================================================
|
||
NUI-BRÜCKE — Verbindung zum FiveM-Client
|
||
===================================================================== */
|
||
const IST_FIVEM = (typeof GetParentResourceName === "function");
|
||
const RESSOURCE = IST_FIVEM ? GetParentResourceName() : "aether-phone";
|
||
|
||
function nui(aktion, daten) {
|
||
if (!IST_FIVEM) return Promise.resolve();
|
||
return fetch(`https://${RESSOURCE}/${aktion}`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json; charset=UTF-8" },
|
||
body: JSON.stringify(daten || {}),
|
||
}).catch(() => {});
|
||
}
|
||
|
||
/* Sucht die Nummer zu einem Kontaktnamen (für Anrufe aus der Kontaktliste) */
|
||
function nummerZuName(name) {
|
||
const treffer = kontakteListe.find(k => k.name === name);
|
||
return treffer ? treffer.nummer : null;
|
||
}
|
||
|
||
/* Sucht den Anzeigenamen zu einer Nummer (unbekannt = Nummer selbst) */
|
||
function nameZuNummer(nummer) {
|
||
const treffer = kontakteListe.find(k => k.nummer === nummer);
|
||
return treffer ? treffer.name : nummer;
|
||
}
|
||
|
||
/* Übernimmt den kompletten Zustand vom Server */
|
||
function setzeZustand(z) {
|
||
spielerZustand.name = z.name || "—";
|
||
spielerZustand.nummer = z.nummer || "—";
|
||
spielerZustand.kontostand = z.kontostand || 0;
|
||
spielerZustand.kontonummer = z.kontonummer || "—";
|
||
spielerZustand.theme = z.theme || "dark";
|
||
spielerZustand.wallpaper = z.wallpaper || "wp-aurora";
|
||
spielerZustand.dockApps = z.dockApps || spielerZustand.dockApps;
|
||
spielerZustand.freigeschaltet = z.freigeschaltet || {};
|
||
spielerZustand.handy = z.handy;
|
||
spielerZustand.sim = z.sim;
|
||
spielerZustand.akku = z.akku;
|
||
spielerZustand.faceId = z.faceId;
|
||
/* Wichtig: Die echte PIN kommt nie an. Hier steht nur ein Platzhalter,
|
||
damit die Oberfläche weiß, dass bereits eine eingerichtet ist. */
|
||
spielerZustand.pin = z.pinGesetzt ? "****" : null;
|
||
|
||
kontakteListe = (z.kontakte || []).map(k => ({
|
||
id: k.id, name: k.name, nummer: k.nummer, notruf: k.notruf || false,
|
||
farbe: k.notruf ? "#f0506e" : "#7b6cf6",
|
||
initialen: k.notruf ? "🚨" : (k.name || "?").substring(0, 2).toUpperCase(),
|
||
}));
|
||
|
||
/* Chats sind nach Nummer geschlüsselt; für die Anzeige den Kontaktnamen
|
||
verwenden, sofern die Nummer gespeichert ist. */
|
||
chatVerlaeufe = z.chats || {};
|
||
|
||
anrufListe = (z.anrufe || []).map(a => ({
|
||
name: nameZuNummer(a.nummer), zeit: a.zeit,
|
||
art: a.art, verpasst: a.verpasst,
|
||
}));
|
||
|
||
if (typeof zeichneHome === "function") { try { zeichneHome(); } catch (e) {} }
|
||
if (z.hinweis) zeigeToast(z.hinweis);
|
||
}
|
||
|
||
window.addEventListener("message", (ereignis) => {
|
||
const n = ereignis.data || {};
|
||
|
||
if (n.aktion === "oeffnen") {
|
||
document.body.style.display = "";
|
||
setzeZustand(n);
|
||
// Ersteinrichtung, falls noch keine PIN gesetzt ist
|
||
if (!spielerZustand.pin) starteEinrichtung();
|
||
}
|
||
else if (n.aktion === "zustand") {
|
||
setzeZustand(n);
|
||
}
|
||
else if (n.aktion === "entsperrt") {
|
||
if (n.erfolg) {
|
||
const u = document.getElementById("unlock-screen");
|
||
if (u) u.classList.remove("show");
|
||
entsperreHandy();
|
||
} else {
|
||
zeigeToast("Falscher Code");
|
||
if (pinZuruecksetzen) pinZuruecksetzen();
|
||
}
|
||
}
|
||
else if (n.aktion === "neueNachricht") {
|
||
const von = n.nachricht.von;
|
||
chatVerlaeufe[von] = chatVerlaeufe[von] || [];
|
||
chatVerlaeufe[von].push({ von: "them", text: n.nachricht.text, zeit: n.nachricht.zeit });
|
||
if (typeof aktiverChat !== "undefined" && aktiverChat === von) zeichneBubbles();
|
||
zeigeToast("Neue Nachricht von " + nameZuNummer(von));
|
||
}
|
||
else if (n.aktion === "eingehenderAnruf") {
|
||
document.body.style.display = "";
|
||
const overlay = document.getElementById("in-call");
|
||
if (overlay) {
|
||
document.getElementById("call-who").textContent = nameZuNummer(n.nummer);
|
||
document.getElementById("call-avatar").textContent = "☎";
|
||
document.getElementById("call-status").textContent = "Eingehender Anruf …";
|
||
overlay.classList.add("show");
|
||
overlay.dataset.eingehend = "1";
|
||
}
|
||
}
|
||
else if (n.aktion === "anrufStatus") {
|
||
const statusEl = document.getElementById("call-status");
|
||
const overlay = document.getElementById("in-call");
|
||
if (n.status === "klingelt" && statusEl) statusEl.textContent = "Klingelt …";
|
||
else if (n.status === "verbunden" && statusEl) statusEl.textContent = "Verbunden";
|
||
else if (n.status === "besetzt") { zeigeToast("Besetzt"); if (overlay) overlay.classList.remove("show"); }
|
||
else if (n.status === "nichtErreichbar") { zeigeToast("Nicht erreichbar"); if (overlay) overlay.classList.remove("show"); }
|
||
else if (n.status === "notruf") { zeigeToast("Notruf abgesetzt: " + (n.zusatz || "")); if (overlay) overlay.classList.remove("show"); }
|
||
else if (n.status === "beendet") { if (overlay) overlay.classList.remove("show"); }
|
||
}
|
||
else if (n.aktion === "akku") {
|
||
spielerZustand.akku = n.stand;
|
||
}
|
||
else if (n.aktion === "fehler") {
|
||
zeigeToast(n.text || "Nicht möglich");
|
||
}
|
||
else if (n.aktion === "schliessen") {
|
||
document.body.style.display = "none";
|
||
}
|
||
});
|
||
|
||
/* ESC schließt das Handy */
|
||
document.addEventListener("keydown", (e) => {
|
||
if (e.key === "Escape" && IST_FIVEM) nui("schliessen", {});
|
||
});
|
||
|
||
/* In FiveM zunächst verstecken — die Seite wartet auf "oeffnen" */
|
||
if (IST_FIVEM) document.body.style.display = "none";
|
||
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|