pc-live/nui/js/apps/browser.js
Bjoern Flessing be502d2834 IC-Computer: Schreibtisch, Fenster und Apps
Ein Computer im Spiel. Man tritt an ein Terminal, drueckt E, und bekommt eine
Oberflaeche mit Mail, Browser, Adressbuch, Kalender und weiteren Anwendungen.
Andere Resources haengen sich als App ein.

Auf ESX umgestellt:

  - Fuenf harte Abhaengigkeiten zeigten auf Resources, die es hier nicht gibt.
    Eine fehlende Abhaengigkeit verhindert den Start vollstaendig - die
    Resource waere nie hochgekommen.
  - Die Bruecke zum Framework neu geschrieben; Charakterdaten, Aktenverwaltung
    und Immobilien ausgebaut, weil die zugehoerigen Systeme fehlen.
  - pc_live_devices fehlte im Schema, wird vom Code aber gelesen. Ohne die
    Tabelle ist kein fester PC ansprechbar. Ein PC braucht ausserdem zwei
    Eintraege mit derselben Kennung: Standort in der Datenbank, Kamerafahrt in
    der config.lua.

Mail ueberarbeitet:

  - Postfaecher haengen an einer Anmeldung statt an der Person. Mehrere Leute
    koennen dasselbe Firmenpostfach gleichzeitig offen haben.
  - Ordner gehoeren zum Postfach, nicht zur Person. Eine Mail an ein geteiltes
    Postfach wird je Empfaenger einmal gespeichert; damit das Einsortieren
    trotzdem fuer alle gilt, tragen alle Kopien einer Zustellung dieselbe
    Kennung. Gelesen und geloescht bleibt persoenlich - das ist keine
    Eigenschaft der Nachricht, sondern der Person.
  - Signaturen gehoeren ebenfalls zum Postfach.
  - Kalender koennen privat, geteilt oder oeffentlich sein. Ein oeffentlicher
    Termin haengt bewusst auch an einem Postfach, sonst koennte ihn spaeter
    niemand mehr aendern oder absagen.

Neue Apps: Webhosting, Bleeter und dessen Verwaltung.

Behoben:

  - prompt(), confirm() und alert() froren das NUI ein. FiveMs CEF hat keinen
    Handler fuer die eingebauten Browserdialoge - der Aufruf oeffnet nichts und
    kehrt nie zurueck. Ersetzt durch eigene Dialoge.
  - Die Fenster tragen data-wid, keine id. Wer sie mit getElementById sucht,
    schreibt ins Leere und das Fenster bleibt leer.

Enthaelt README.md mit Einrichtung und PLUGINS.md: eine Anleitung, wie man eine
eigene App baut und einhaengt, mit vollstaendigem Beispiel von der Tabelle bis
zum Schreibtischsymbol.
2026-08-09 12:02:11 +00:00

538 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* pc-live | Browser App
* IC URL schema: ic://slug
*/
const BrowserApp = (() => {
const WIN_ID = 'app-browser';
const history = [];
let histIdx = -1;
/* ── IC page templates ─────────────────────────────────── */
const PageTemplates = {
home: () => `
<div class="ic-page fade-in">
<div class="ic-home-hero">
<h1>⚡ IC Browser</h1>
<p style="color:var(--text-muted);font-size:.82rem">Browse the internal network. Enter an <code>ic://</code> address above.</p>
<div class="ic-home-links">
<a class="ic-link-card" onclick="BrowserApp.navigate('ic://dpa')">📰 DPA News</a>
<a class="ic-link-card" onclick="BrowserApp.navigate('ic://police')">🚔 LSPD</a>
<a class="ic-link-card" onclick="BrowserApp.navigate('ic://ambulance')">🚑 LSMD</a>
<a class="ic-link-card" onclick="BrowserApp.navigate('ic://fire')">🚒 LSFD</a>
<a class="ic-link-card" onclick="BrowserApp.navigate('ic://doj')">⚖️ DOJ</a>
<a class="ic-link-card" onclick="BrowserApp.navigate('ic://parking')">🅿️ Parking</a>
<a class="ic-link-card" onclick="BrowserApp.navigate('ic://weazel')">📺 Weazel News</a>
<a class="ic-link-card" onclick="BrowserApp.navigate('ic://classifieds')">📋 LS Classifieds</a>
</div>
</div>
</div>`,
dpa: () => `
<div class="ic-page fade-in">
<h1>📰 DPA Deutsche Presse-Agentur</h1>
<div class="article">
<div class="article-date">Today, 09:15</div>
<h2>New residential district approved in Vinewood Hills</h2>
<p>The city council has approved plans for 120 new residential units in the Vinewood Hills area. Construction is set to begin next month pending environmental review.</p>
</div>
<div class="article">
<div class="article-date">Today, 07:42</div>
<h2>Port Authority reports record cargo volume</h2>
<p>Los Santos Port Authority announced a 14% increase in cargo processing year-over-year, citing improved logistics infrastructure and expanded berth capacity.</p>
</div>
<div class="article">
<div class="article-date">Yesterday, 18:00</div>
<h2>Traffic advisory: Freeway 1 maintenance</h2>
<p>LSDOT warns of lane closures on Freeway 1 near Palomino Highlands between 22:00 and 06:00 for the next three nights.</p>
</div>
</div>`,
psb: () => `
<div class="ic-page fade-in">
<h1>🚔 Los Santos Police Service Board</h1>
<p>Official information and public notices from the Los Santos Police Department.</p>
<div class="article">
<div class="article-date">Official Notice</div>
<h2>Public Safety Campaign Q1</h2>
<p>The LSPD is launching a public outreach campaign focused on traffic safety and community engagement. Residents are encouraged to report suspicious activity through official channels.</p>
</div>
<div class="article">
<div class="article-date">Recruitment</div>
<h2>LSPD Cadet Applications Open</h2>
<p>Applications for the upcoming cadet class are now open. Qualified candidates should contact the department for details on requirements and the application process.</p>
</div>
</div>`,
weazel: () => `
<div class="ic-page fade-in">
<h1>📺 Weazel News</h1>
<p style="color:var(--accent);font-size:.72rem;margin-bottom:16px">Los Santos' #1 Trusted News Source</p>
<div class="article">
<div class="article-date">Breaking</div>
<h2>Explosion reported near LSIA</h2>
<p>Witnesses report a large explosion near the Los Santos International Airport. LSFD units are on scene. Air traffic has been temporarily halted as a precaution.</p>
</div>
<div class="article">
<div class="article-date">Entertainment</div>
<h2>Vinewood Music Awards Full List of Nominees</h2>
<p>The annual Vinewood Music Awards nominees have been announced. This year's ceremony will take place at the Maze Bank Arena. Tickets available starting next week.</p>
</div>
</div>`,
// ── Org pages ────────────────────────────────────────────
org_police: () => `
<div class="ic-page fade-in">
<div style="display:flex;align-items:center;gap:14px;margin-bottom:20px">
<span style="font-size:2.4rem">🚔</span>
<div><h1 style="margin:0">Los Santos Police Department</h1><p style="margin:4px 0 0;color:var(--text-muted);font-size:.8rem">ic://police &nbsp;·&nbsp; pd.gov</p></div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-bottom:20px">
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center">
<div style="font-size:1.4rem">📞</div><div style="font-weight:700;color:var(--accent);margin:6px 0 2px">Notruf 110</div>
<div style="font-size:.72rem;color:var(--text-muted)">Polizeilicher Notruf</div>
</div>
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center">
<div style="font-size:1.4rem">✉️</div><div style="font-weight:700;color:var(--accent);margin:6px 0 2px">aktensystem@pd.gov</div>
<div style="font-size:.72rem;color:var(--text-muted)">Offizielles Aktensystem</div>
</div>
</div>
<div class="article"><div class="article-date">Dienstlich</div>
<h2>Strafverfolgung &amp; Öffentliche Sicherheit</h2>
<p>Das LSPD ist zuständig für die öffentliche Sicherheit, Strafverfolgung und Verbrechensbekämpfung in Los Santos. Bürger können sich jederzeit an unsere Beamten wenden.</p>
</div>
<div class="article"><div class="article-date">Bewerbungen</div>
<h2>Stellenausschreibung Cadet Program</h2>
<p>Das LSPD nimmt regelmäßig neue Bewerber für das Cadet-Programm an. Kontakt über das offizielle Aktensystem oder persönlich beim Department.</p>
</div>
</div>`,
org_ambulance: () => `
<div class="ic-page fade-in">
<div style="display:flex;align-items:center;gap:14px;margin-bottom:20px">
<span style="font-size:2.4rem">🚑</span>
<div><h1 style="margin:0">Los Santos Medical Department</h1><p style="margin:4px 0 0;color:var(--text-muted);font-size:.8rem">ic://ambulance &nbsp;·&nbsp; md.gov</p></div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-bottom:20px">
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center">
<div style="font-size:1.4rem">📞</div><div style="font-weight:700;color:#e53935;margin:6px 0 2px">Notruf 911</div>
<div style="font-size:.72rem;color:var(--text-muted)">Medizinischer Notfall</div>
</div>
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center">
<div style="font-size:1.4rem">✉️</div><div style="font-weight:700;color:var(--accent);margin:6px 0 2px">aktensystem@md.gov</div>
<div style="font-size:.72rem;color:var(--text-muted)">Patientenakten &amp; Anfragen</div>
</div>
</div>
<div class="article"><div class="article-date">Medizinische Versorgung</div>
<h2>Notfallversorgung rund um die Uhr</h2>
<p>Das LSMD bietet medizinische Erstversorgung, Krankentransport und chirurgische Eingriffe. Im Notfall bitte sofort den Notruf 911 wählen.</p>
</div>
</div>`,
org_fire: () => `
<div class="ic-page fade-in">
<div style="display:flex;align-items:center;gap:14px;margin-bottom:20px">
<span style="font-size:2.4rem">🚒</span>
<div><h1 style="margin:0">Los Santos Fire Department</h1><p style="margin:4px 0 0;color:var(--text-muted);font-size:.8rem">ic://fire &nbsp;·&nbsp; fd.gov</p></div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-bottom:20px">
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center">
<div style="font-size:1.4rem">📞</div><div style="font-weight:700;color:#ff6f00;margin:6px 0 2px">Notruf 112</div>
<div style="font-size:.72rem;color:var(--text-muted)">Feuerwehr &amp; Technische Hilfe</div>
</div>
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center">
<div style="font-size:1.4rem">✉️</div><div style="font-weight:700;color:var(--accent);margin:6px 0 2px">aktensystem@fd.gov</div>
<div style="font-size:.72rem;color:var(--text-muted)">Offizielle Korrespondenz</div>
</div>
</div>
<div class="article"><div class="article-date">Aufgaben</div>
<h2>Brandbekämpfung &amp; Technische Hilfe</h2>
<p>Die LSFD schützt Leben und Eigentum bei Bränden, Unfällen und Naturkatastrophen. Für technische Hilfeleistungen steht das Department rund um die Uhr bereit.</p>
</div>
</div>`,
org_doj: () => `
<div class="ic-page fade-in">
<div style="display:flex;align-items:center;gap:14px;margin-bottom:20px">
<span style="font-size:2.4rem">⚖️</span>
<div><h1 style="margin:0">Department of Justice</h1><p style="margin:4px 0 0;color:var(--text-muted);font-size:.8rem">ic://doj &nbsp;·&nbsp; doj.gov</p></div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-bottom:20px">
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center">
<div style="font-size:1.4rem">📞</div><div style="font-weight:700;color:var(--accent);margin:6px 0 2px">Hotline 200</div>
<div style="font-size:.72rem;color:var(--text-muted)">DOJ Auskunft</div>
</div>
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center">
<div style="font-size:1.4rem">✉️</div><div style="font-weight:700;color:var(--accent);margin:6px 0 2px">aktensystem@doj.gov</div>
<div style="font-size:.72rem;color:var(--text-muted)">Juristische Anfragen</div>
</div>
</div>
<div class="article"><div class="article-date">Justizsystem</div>
<h2>Strafrecht &amp; Zivilrecht in Los Santos</h2>
<p>Das Department of Justice verwaltet das Rechtssystem von Los Santos. Anwälte, Richter und Staatsanwälte koordinieren alle rechtlichen Vorgänge über dieses Department.</p>
</div>
</div>`,
org_dpa: () => `
<div class="ic-page fade-in">
<div style="display:flex;align-items:center;gap:14px;margin-bottom:20px">
<span style="font-size:2.4rem">🏛️</span>
<div><h1 style="margin:0">Department of Public Administration</h1><p style="margin:4px 0 0;color:var(--text-muted);font-size:.8rem">ic://dpa &nbsp;·&nbsp; dpa.gov</p></div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-bottom:20px">
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center">
<div style="font-size:1.4rem">✉️</div><div style="font-weight:700;color:var(--accent);margin:6px 0 2px">public@dpa.gov</div>
<div style="font-size:.72rem;color:var(--text-muted)">Öffentliche Anfragen</div>
</div>
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center">
<div style="font-size:1.4rem">✉️</div><div style="font-weight:700;color:var(--accent);margin:6px 0 2px">traffic@dpa.gov</div>
<div style="font-size:.72rem;color:var(--text-muted)">Verkehr &amp; Infrastruktur</div>
</div>
</div>
<div class="article"><div class="article-date">Verwaltung</div>
<h2>Staatliche Verwaltung &amp; Öffentlicher Dienst</h2>
<p>Die DPA koordiniert Verwaltungsaufgaben, Genehmigungen und öffentliche Dienstleistungen. Anfragen können per Mail oder telefonisch gestellt werden.</p>
</div>
</div>`,
org_parking: () => `
<div class="ic-page fade-in">
<div style="display:flex;align-items:center;gap:14px;margin-bottom:20px">
<span style="font-size:2.4rem">🅿️</span>
<div><h1 style="margin:0">Parkraumüberwachung Los Santos</h1><p style="margin:4px 0 0;color:var(--text-muted);font-size:.8rem">ic://parking &nbsp;·&nbsp; parking.gov</p></div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-bottom:20px">
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;text-align:center">
<div style="font-size:1.4rem">✉️</div><div style="font-weight:700;color:var(--accent);margin:6px 0 2px">aktensystem@parking.gov</div>
<div style="font-size:.72rem;color:var(--text-muted)">Einsprüche &amp; Anfragen</div>
</div>
</div>
<div class="article"><div class="article-date">Information</div>
<h2>Knöllchen &amp; Einsprüche</h2>
<p>Bei Fragen zu Knöllchen oder Einsprüchen bitte schriftlich über aktensystem@parking.gov oder persönlich am Schalter.</p>
</div>
</div>`,
classifieds: () => `
<div class="ic-page fade-in">
<!-- Header -->
<div style="background:linear-gradient(135deg,rgba(0,170,255,0.07) 0%,transparent 70%);
border:1px solid var(--border);border-radius:8px;
padding:22px 24px;text-align:center;margin-bottom:22px">
<div style="font-size:1.7rem;font-weight:700;color:var(--accent);letter-spacing:.06em">
📋 LS CLASSIFIEDS
</div>
<div style="color:var(--text-muted);font-size:.74rem;margin-top:5px">
Los Santos' largest private marketplace &nbsp;·&nbsp; <span style="color:var(--text-dim)">ic://classifieds</span>
</div>
</div>
<!-- Category tiles -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:26px">
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);
padding:14px 8px;text-align:center;cursor:pointer;transition:border-color .15s"
onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">
<div style="font-size:1.5rem">🚗</div>
<div style="font-size:.78rem;font-weight:600;color:var(--text);margin-top:4px">Vehicles</div>
<div style="font-size:.65rem;color:var(--text-dim);margin-top:2px">47 listings</div>
</div>
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);
padding:14px 8px;text-align:center;cursor:pointer;transition:border-color .15s"
onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">
<div style="font-size:1.5rem">🏠</div>
<div style="font-size:.78rem;font-weight:600;color:var(--text);margin-top:4px">Real Estate</div>
<div style="font-size:.65rem;color:var(--text-dim);margin-top:2px">23 listings</div>
</div>
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);
padding:14px 8px;text-align:center;cursor:pointer;transition:border-color .15s"
onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">
<div style="font-size:1.5rem">💼</div>
<div style="font-size:.78rem;font-weight:600;color:var(--text);margin-top:4px">Jobs</div>
<div style="font-size:.65rem;color:var(--text-dim);margin-top:2px">12 listings</div>
</div>
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);
padding:14px 8px;text-align:center;cursor:pointer;transition:border-color .15s"
onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='var(--border)'">
<div style="font-size:1.5rem">🔧</div>
<div style="font-size:.78rem;font-weight:600;color:var(--text);margin-top:4px">Services</div>
<div style="font-size:.65rem;color:var(--text-dim);margin-top:2px">31 listings</div>
</div>
</div>
<!-- Section: Featured -->
<div style="font-size:.68rem;text-transform:uppercase;letter-spacing:.12em;color:var(--text-dim);
margin-bottom:12px;padding-bottom:6px;border-bottom:1px solid var(--border)">
⭐ Featured Listings
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(235px,1fr));gap:10px;margin-bottom:24px">
<!-- Listing: Car -->
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);
padding:14px;display:flex;flex-direction:column;gap:7px">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:1.3rem">🚗</span>
<span class="tag tag-accent" style="font-size:.6rem">FEATURED</span>
</div>
<div style="font-weight:700;color:#fff;font-size:.85rem">2023 Ocelot Pariah</div>
<div style="font-size:.72rem;color:var(--text-muted);line-height:1.5">
Low mileage, fully tuned at Benny's. Custom matte black wrap. Clean history, no damage.
</div>
<div style="font-size:.95rem;color:var(--accent);font-weight:700">$285,000</div>
<div style="font-size:.63rem;color:var(--text-dim)">📍 Vinewood &nbsp;·&nbsp; Posted 2h ago</div>
</div>
<!-- Listing: Motorcycle -->
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);
padding:14px;display:flex;flex-direction:column;gap:7px">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:1.3rem">🏍</span>
<span class="tag">Vehicles</span>
</div>
<div style="font-weight:700;color:#fff;font-size:.85rem">Western Daemon Custom</div>
<div style="font-size:.72rem;color:var(--text-muted);line-height:1.5">
Full performance upgrade, custom exhaust. Runs clean. Selling due to relocation.
</div>
<div style="font-size:.95rem;color:var(--accent);font-weight:700">$68,500</div>
<div style="font-size:.63rem;color:var(--text-dim)">📍 Strawberry &nbsp;·&nbsp; Posted 6h ago</div>
</div>
<!-- Listing: House -->
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);
padding:14px;display:flex;flex-direction:column;gap:7px">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:1.3rem">🏠</span>
<span class="tag">Real Estate</span>
</div>
<div style="font-weight:700;color:#fff;font-size:.85rem">3-Bed House · Mirror Park</div>
<div style="font-size:.72rem;color:var(--text-muted);line-height:1.5">
Modern detached house, double garage, pool. Move-in ready. Quiet, well-connected area.
</div>
<div style="font-size:.95rem;color:var(--accent);font-weight:700">$1,200,000</div>
<div style="font-size:.63rem;color:var(--text-dim)">📍 Mirror Park &nbsp;·&nbsp; Posted 5h ago</div>
</div>
<!-- Listing: Job -->
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);
padding:14px;display:flex;flex-direction:column;gap:7px">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:1.3rem">💼</span>
<span class="tag" style="color:var(--success);border-color:rgba(34,197,94,.3)">Jobs</span>
</div>
<div style="font-weight:700;color:#fff;font-size:.85rem">Mechanic Premium Auto</div>
<div style="font-size:.72rem;color:var(--text-muted);line-height:1.5">
Looking for an experienced mechanic. Flexible shifts, good cut. Experience required.
</div>
<div style="font-size:.95rem;color:var(--success);font-weight:700">$4,500 / week</div>
<div style="font-size:.63rem;color:var(--text-dim)">📍 LSIA Area &nbsp;·&nbsp; Posted 1d ago</div>
</div>
<!-- Listing: Apartment -->
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);
padding:14px;display:flex;flex-direction:column;gap:7px">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:1.3rem">🏢</span>
<span class="tag">Real Estate</span>
</div>
<div style="font-weight:700;color:#fff;font-size:.85rem">Penthouse · Alta St.</div>
<div style="font-size:.72rem;color:var(--text-muted);line-height:1.5">
Floor 28, panoramic city view, 2 bed/2 bath, concierge. Fully furnished, immediate occupancy.
</div>
<div style="font-size:.95rem;color:var(--accent);font-weight:700">$3,800,000</div>
<div style="font-size:.63rem;color:var(--text-dim)">📍 Downtown LS &nbsp;·&nbsp; Posted 12h ago</div>
</div>
<!-- Listing: Service -->
<div style="background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);
padding:14px;display:flex;flex-direction:column;gap:7px">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:1.3rem">🔧</span>
<span class="tag">Services</span>
</div>
<div style="font-weight:700;color:#fff;font-size:.85rem">24/7 Mobile Mechanic</div>
<div style="font-size:.72rem;color:var(--text-muted);line-height:1.5">
On-call repairs, towing, custom builds. Competitive rates. Contact via IC mail.
</div>
<div style="font-size:.95rem;color:var(--text-muted);font-weight:600">Contact via mail</div>
<div style="font-size:.63rem;color:var(--text-dim)">📍 Citywide &nbsp;·&nbsp; Posted 3d ago</div>
</div>
</div>
<!-- Footer -->
<div style="text-align:center;color:var(--text-dim);font-size:.62rem;
border-top:1px solid var(--border);padding-top:12px">
LS Classifieds is not responsible for the accuracy of listings. All trades are at your own risk.
&nbsp;·&nbsp; To post an ad, send a mail to <span style="color:var(--text-muted)">classifieds@ls-net.ic</span>
</div>
</div>`,
};
/* Fehlerseite für ic-web-Domänen. Als DOM gebaut, weil der Fehlertext vom
Server kommt und die Domäne aus der Adressleiste stammt. */
function renderICError(domain, message) {
const wrap = document.createElement('div');
wrap.className = 'browser-404 fade-in';
const h = document.createElement('h2');
h.textContent = '404';
const p1 = document.createElement('p');
p1.textContent = 'ic://' + domain;
const p2 = document.createElement('p');
p2.textContent = message || 'Diese Adresse existiert nicht.';
wrap.append(h, p1, p2);
return wrap;
}
function render404(slug) {
return `
<div class="browser-404 fade-in">
<h2>404</h2>
<p>Page not found: <code>ic://${slug}</code></p>
<p>Check the address and try again.</p>
</div>`;
}
/* ── URL helpers ────────────────────────────────────────── */
function isExternal(url) {
return /^https?:\/\//i.test(url);
}
function normalizeUrl(raw) {
raw = (raw || '').trim();
if (!raw) return 'ic://home';
// Already ic:// or http(s)://
if (/^ic:\/\//i.test(raw) || /^https?:\/\//i.test(raw)) return raw;
// Looks like a domain (contains dot) → assume https
if (/^[a-z0-9\-]+\.[a-z]{2,}/i.test(raw)) return 'https://' + raw;
// Otherwise treat as ic:// slug
return 'ic://' + raw;
}
/* ── Navigate ─────────────────────────────────────────── */
async function navigate(rawUrl) {
if (!rawUrl) return;
const url = normalizeUrl(rawUrl);
// Update address bar
const bar = document.getElementById('browser-address');
if (bar) bar.value = url;
// History
if (histIdx < 0 || history[histIdx] !== url) {
history.splice(histIdx + 1);
history.push(url);
histIdx = history.length - 1;
}
updateNavButtons();
const viewport = document.getElementById('browser-viewport');
if (!viewport) return;
// ── External URL → iframe ──────────────────────────────
if (isExternal(url)) {
viewport.classList.add('browser-viewport-external');
viewport.innerHTML =
`<iframe class="browser-iframe"
src="${url.replace(/"/g, '&quot;')}"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
allowfullscreen></iframe>`;
return;
}
// ── IC internal page ───────────────────────────────────
viewport.classList.remove('browser-viewport-external');
const slug = url.replace(/^ic:\/\//i, '').replace(/\/$/, '').trim() || 'home';
if (bar) bar.value = 'ic://' + slug;
viewport.innerHTML = '<div class="empty-state"><div class="empty-icon">⏳</div><p>Loading…</p></div>';
if (PageTemplates[slug]) {
viewport.innerHTML = PageTemplates[slug]();
return;
}
// ── Von Spielern gepflegte Seiten (ic-web) ─────────────
// Erkennbar an der Domänenform name.endung, optional mit Unterseite.
// Vorrang haben die fest eingebauten Vorlagen oben.
const parts = slug.split('/');
if (/^[a-z0-9\-]+\.[a-z]{2,}$/i.test(parts[0]) && typeof ICWebNet !== 'undefined') {
const res = await ICWebNet.call('getSite', parts[0].toLowerCase(), parts[1] || 'home');
if (res && res.ok && res.data) {
// Kein innerHTML: der Inhalt stammt von Spielern und wird als
// DOM-Knoten aufgebaut, damit Markup darin niemals ausgeführt wird.
viewport.replaceChildren(ICWebRender.renderPage(res.data));
return;
}
viewport.replaceChildren(renderICError(parts[0], res && res.error));
return;
}
const resp = await fetchNui('getBrowserPage', { slug });
if (resp && resp.found && resp.page) {
const p = resp.page;
if (p.content && PageTemplates[p.content]) {
viewport.innerHTML = PageTemplates[p.content]();
} else {
viewport.innerHTML = `<div class="ic-page fade-in"><h1>${p.title || slug}</h1><p>This page has no content yet.</p></div>`;
}
} else {
viewport.innerHTML = render404(slug);
}
}
function goBack() {
if (histIdx > 0) {
histIdx--;
navigate(history[histIdx]);
}
}
function goForward() {
if (histIdx < history.length - 1) {
histIdx++;
navigate(history[histIdx]);
}
}
function updateNavButtons() {
const b = document.getElementById('browser-back');
const f = document.getElementById('browser-fwd');
if (b) b.style.opacity = histIdx > 0 ? '1' : '0.35';
if (f) f.style.opacity = histIdx < history.length - 1 ? '1' : '0.35';
}
/* ── Open window ────────────────────────────────────────── */
function open(startUrl) {
const el = WindowManager.create({
id: WIN_ID,
title: 'Browser',
icon: '🌐',
width: 900,
height: 600,
content: `
<div class="browser-chrome">
<button class="browser-nav-btn" id="browser-back" onclick="BrowserApp.goBack()" title="Zurück">◀</button>
<button class="browser-nav-btn" id="browser-fwd" onclick="BrowserApp.goForward()" title="Vor">▶</button>
<button class="browser-nav-btn" id="browser-refresh" onclick="BrowserApp.refresh()" title="Neu laden">↺</button>
<input id="browser-address" class="browser-address-input" type="text"
placeholder="ic://home oder https://…"
onkeydown="if(event.key==='Enter')BrowserApp.navigate(this.value)"/>
<button class="browser-nav-btn browser-go-btn"
onclick="BrowserApp.navigate(document.getElementById('browser-address').value)"
title="Aufrufen">→</button>
</div>
<div class="browser-viewport fade-in" id="browser-viewport"></div>
`,
});
if (!el) return;
navigate(startUrl || 'ic://home');
}
function refresh() {
if (histIdx >= 0) navigate(history[histIdx]);
}
return { open, navigate, goBack, goForward, refresh };
})();