Files
THOS-Server/index.php
2025-06-05 09:03:28 +02:00

934 lines
29 KiB
PHP
Raw 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.

<?php
$statePath = '/home/surillya/.thos_state.json';
$state = file_exists($statePath) ? json_decode(file_get_contents($statePath), true) : null;
if (!file_exists($statePath)) {
header('Location: oobe.php');
exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SurillyaOS</title>
<style>
:root {
--accent: #4CAF50;
--window-bg: rgba(28, 28, 28, 0.6);
--window-border: rgba(255, 255, 255, 0.1);
--window-blur: blur(20px);
--header-bg: rgba(30, 30, 30, 0.6);
--header-color: white;
--header-hover: var(--accent);
}
.theme-glassy-dark {
--window-bg: rgba(28, 28, 28, 0.6);
--window-border: rgba(255, 255, 255, 0.1);
--window-blur: blur(20px);
--header-bg: rgba(30, 30, 30, 0.6);
--header-color: white;
--font-color: white;
}
.theme-frosted-blue {
--window-bg: rgba(0, 40, 80, 0.4);
--window-border: rgba(173, 216, 230, 0.2);
--window-blur: blur(25px);
--header-bg: rgba(10, 20, 50, 0.4);
--header-color: #e3f8ff;
--font-color: #e3f8ff;
}
.theme-dreamy-pink {
--window-bg: rgba(80, 30, 60, 0.5);
--window-border: rgba(255, 182, 193, 0.3);
--window-blur: blur(16px);
--header-bg: rgba(100, 40, 80, 0.5);
--header-color: #ffddee;
--font-color: #ffe9f4;
}
.theme-xp {
--window-bg: #ece9d8;
--window-border: #3a6ea5;
--window-blur: none;
--header-bg: linear-gradient(to bottom, #0a246a, #1c4dbd);
--header-color: white;
--font-color: black;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: #121212;
color: white;
overflow: hidden;
}
#app-bar {
position: fixed;
top: 0;
left: 0;
width: 200px;
height: 100vh;
background: var(--window-bg);
backdrop-filter: var(--window-blur);
-webkit-backdrop-filter: var(--window-blur);
border: 1px solid var(--window-border);
padding: 10px;
box-sizing: border-box;
z-index: 9999;
}
.app-btn {
display: block;
margin: 10px 0;
padding: 10px;
background: #292929;
border: none;
color: white;
cursor: pointer;
border-radius: 8px;
transition: background 0.3s ease;
}
.app-btn:hover {
background: var(--accent);
}
.window {
position: absolute;
width: 600px;
height: 400px;
background: var(--window-bg);
backdrop-filter: var(--window-blur);
-webkit-backdrop-filter: var(--window-blur);
border: 1px solid var(--window-border);
border-radius: 12px;
resize: both;
overflow: hidden;
z-index: 1;
transition: box-shadow 0.2s ease, backdrop-filter 0.3s ease;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
.window.active {
box-shadow: 0 0 20px var(--accent), 0 8px 24px rgba(0, 0, 0, 0.3);
}
.window-header {
background: var(--header-bg);
color: var(--header-color);
padding: 6px 12px;
cursor: grab;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: bold;
backdrop-filter: var(--window-blur);
-webkit-backdrop-filter: var(--window-blur);
border-bottom: 1px solid var(--window-border);
}
.window-header:active {
cursor: grabbing;
}
.window-header button {
background: transparent;
border: none;
color: inherit;
font-weight: bold;
font-size: 1.1em;
cursor: pointer;
user-select: none;
padding: 0 6px;
transition: color 0.2s ease;
}
.window-header button:hover {
color: var(--header-hover);
}
.window-content {
background: var(--window-bg);
backdrop-filter: var(--window-blur);
-webkit-backdrop-filter: var(--window-blur);
border: 1px solid var(--window-border);
width: 100%;
height: calc(100% - 32px);
border: none;
}
.sparkle {
position: absolute;
width: 6px;
height: 6px;
background: radial-gradient(circle at center, var(--accent) 0%, transparent 70%);
border-radius: 50%;
filter: drop-shadow(0 0 6px var(--accent));
animation: sparkle 1.2s infinite ease-in-out;
pointer-events: none;
}
@keyframes sparkle {
0%,
100% {
opacity: 0;
transform: scale(0.5) rotate(0deg);
}
50% {
opacity: 1;
transform: scale(1) rotate(45deg);
}
}
#screensaver {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
border: none;
z-index: 9999;
}
#drag-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 99999;
display: none;
cursor: grabbing;
}
#fullscreen-toggle {
border: none;
background: transparent;
cursor: pointer;
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
border-radius: 4px;
}
#fullscreen-toggle:hover {
color: var(--accent);
}
.grayscale-bg {
filter: grayscale(100%) brightness(0.9);
}
.notification {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 1rem 1.25rem;
border-radius: 1rem;
background: rgba(30, 30, 30, 0.6);
/* semi-transparent dark */
border: 2px solid transparent;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
color: white;
min-width: 260px;
max-width: 340px;
pointer-events: auto;
animation: fadeInUp 0.3s ease-out;
position: relative;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.notification-dismiss {
background: transparent;
border: none;
cursor: pointer;
}
.notification.success {
border-color: #4caf50;
}
.notification.error {
border-color: #f44336;
}
.notification.info {
border-color: var(--accent, #00bcd4);
}
@keyframes slideIn {
from {
transform: translateX(100%) scale(0.95);
opacity: 0;
}
to {
transform: translateX(0) scale(1);
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
transform: translateX(0) scale(1);
}
to {
opacity: 0;
transform: translateX(50%) scale(0.95);
}
}
</style>
<script src="tailwind.es"></script>
</head>
<body class="bg-black">
<div id="appWrapper" class="transition-all duration-300">
<div id="bg-image" class="fixed inset-0 z-[-10] bg-cover bg-center transition-all duration-300"></div>
<div id="app-bar" class="bg-[#1e1e1e] fixed top-0 left-0 w-52 h-screen p-2.5 z-50">
<div class="flex flex-col h-full">
<div class="relative mb-4">
<button id="powerButton" class="text-gray-300 hover:text-white transition-colors duration-300">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
class="w-12 h-12 stroke-current hover:stroke-red-500 transition-colors duration-300">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
</svg>
</button>
</div>
<div class="flex-grow">
<button class="app-btn w-full" onclick="openApp('explorer.php', 'Explorer')">File Explorer</button>
<button class="app-btn w-full" onclick="openApp('tasks.php', 'Task Manager')">Task Manager</button>
<button class="app-btn w-full" onclick="openApp('https://surillya.com/thos/store', 'THOS Store')">THOS
Store</button>
<button class="app-btn w-full"
onclick="openApp('https://surillya.com/thos/search/thossearch.html', 'Internet', true)">Internet</button>
<button class="app-btn w-full" onclick="openApp('settings.php', 'Settings')">Settings</button>
</div>
<div id="trdp" class="grid grid-cols-2 gap-2 mt-4 p-2 bg-[#252525] rounded-lg">
</div>
<div id="version-display-container" class="w-full flex justify-center items-center mt-4">
<div id="version-display" class="flex items-center gap-2 text-sm text-gray-400 select-none">
<button id="fullscreen-toggle" title="Toggle fullscreen"
class="text-white hover:text-[var(--accent)] transition-colors duration-200 text-xs p-1 rounded bg-transparent hover:bg-[var(--glass-bg-light)]">
</button>
<span class="text-[var(--accent)]">THOS</span><span id="version-number"></span>
</div>
</div>
</div>
</div>
<div id="desktop" class="ml-52 h-screen relative">
<div id="notification-container" class="fixed top-4 right-4 z-50 flex flex-col gap-3 pointer-events-none"></div>
</div>
<iframe id="screensaver" src="screensaver.html"></iframe>
<div id="drag-overlay"></div>
</div>
<div id="powerDialog" class="fixed inset-0 z-50 hidden flex items-center justify-center">
<div class="bg-black/30 rounded-2xl shadow-2xl p-6 w-80 backdrop-blur-sm text-center text-white">
<h2 class="text-xl font-semibold mb-6 tracking-wide">Power Options</h2>
<div class="space-y-3">
<button id="shutdownBtn"
class="w-full py-2 px-4 border border-dotted rounded-xl transition-all duration-200 text-sm bg-white/5 hover:bg-white/10 backdrop-blur-sm border-red-400 text-red-300 hover:text-red-100 hover:border-red-300">Shutdown</button>
<button id="restartBtn"
class="w-full py-2 px-4 border border-dotted rounded-xl transition-all duration-200 text-sm bg-white/5 hover:bg-white/10 backdrop-blur-sm border-yellow-400 text-yellow-300 hover:text-yellow-100 hover:border-yellow-300">Restart</button>
<button id="reloadBtn"
class="w-full py-2 px-4 border border-dotted rounded-xl transition-all duration-200 text-sm bg-white/5 hover:bg-white/10 backdrop-blur-sm border-green-400 text-green-300 hover:text-green-100 hover:border-green-300">Reload</button>
<button id="cancelBtn"
class="w-full py-2 px-4 border border-dotted rounded-xl transition-all duration-200 text-sm bg-white/5 hover:bg-white/10 backdrop-blur-sm border-white/40 text-white hover:text-white hover:border-white/80">Cancel</button>
</div>
</div>
</div>
<script>
window.THOS = {
version: '6 Build-25'
};
let timeoutSeconds = 180;
const thosState = <?= json_encode($state ?? ['cookies' => '', 'localData' => '{}']) ?>;
thosState.cookies?.split('; ').forEach(cookie => document.cookie = cookie);
const localItems = JSON.parse(thosState.localData || '{}');
for (let key in localItems) {
localStorage.setItem(key, localItems[key]);
}
if (!localStorage.getItem('thos_done')) {
window.location.href = 'oobe.php';
}
class WindowManager {
constructor(container) {
this.container = container;
this.zIndexCounter = 1;
this.draggingWindow = null;
this.resizingWindow = null;
this.resizeDir = null;
this.offsetX = 0;
this.offsetY = 0;
this.startWidth = 0;
this.startHeight = 0;
this.startLeft = 0;
this.startTop = 0;
this.isMaximized = new WeakMap();
this.activeWindow = null;
this.dragOverlay = document.getElementById('drag-overlay');
document.addEventListener('mousemove', this.onMouseMove.bind(this));
document.addEventListener('mouseup', this.onMouseUp.bind(this));
this.makeWindowsDraggable();
}
makeWindowsDraggable() {
const windows = this.container.querySelectorAll('.window');
windows.forEach(win => {
const header = win.querySelector('.window-header');
this.makeDraggable(win, header);
this.makeResizable(win);
this.setupButtons(win);
this.setupFocus(win);
});
}
setupFocus(win) {
win.addEventListener('mousedown', () => {
this.bringToFront(win);
this.setActiveWindow(win);
});
win.addEventListener('focus', () => {
this.bringToFront(win);
this.setActiveWindow(win);
});
}
setActiveWindow(win) {
if (this.activeWindow && this.activeWindow !== win) {
this.activeWindow.classList.remove('active');
}
this.activeWindow = win;
win.classList.add('active');
}
makeDraggable(win, header) {
header.style.cursor = 'grab';
header.addEventListener('mousedown', (e) => {
if (e.target.closest('button')) return;
e.preventDefault();
this.bringToFront(win);
this.setActiveWindow(win);
if (this.isMaximized.get(win)) {
win.style.transition = "all 0.2s ease";
win.style.left = win.dataset.prevLeft;
win.style.top = win.dataset.prevTop;
win.style.width = win.dataset.prevWidth;
win.style.height = win.dataset.prevHeight;
this.isMaximized.set(win, false);
}
this.draggingWindow = win;
this.offsetX = e.clientX - win.offsetLeft;
this.offsetY = e.clientY - win.offsetTop;
header.style.cursor = 'grabbing';
if (this.dragOverlay) {
this.dragOverlay.style.display = 'block';
this.dragOverlay.style.cursor = 'grabbing';
}
document.body.style.userSelect = 'none';
});
}
onMouseMove(e) {
if (this.draggingWindow) {
let newLeft = e.clientX - this.offsetX;
let newTop = e.clientY - this.offsetY;
const containerRect = this.container.getBoundingClientRect();
const winRect = this.draggingWindow.getBoundingClientRect();
newLeft = Math.max(containerRect.left, Math.min(newLeft, containerRect.right - winRect.width));
newTop = Math.max(containerRect.top, Math.min(newTop, containerRect.bottom - winRect.height));
this.draggingWindow.style.left = newLeft + 'px';
this.draggingWindow.style.top = newTop + 'px';
}
}
onMouseUp() {
if (this.draggingWindow) {
const header = this.draggingWindow.querySelector('.window-header');
if (header) header.style.cursor = 'grab';
this.draggingWindow = null;
if (this.dragOverlay) {
this.dragOverlay.style.display = 'none';
this.dragOverlay.style.cursor = '';
}
document.body.style.userSelect = '';
}
if (this.resizingWindow) {
this.resizingWindow = null;
this.resizeDir = null;
}
}
bringToFront(win) {
win.style.zIndex = ++this.zIndexCounter;
}
toggleMaximize(win) {
if (!this.isMaximized.get(win)) {
win.dataset.prevLeft = win.style.left;
win.dataset.prevTop = win.style.top;
win.dataset.prevWidth = win.style.width;
win.dataset.prevHeight = win.style.height;
win.style.left = "0px";
win.style.top = "0px";
win.style.width = "100%";
win.style.height = "100%";
win.style.transition = "all 0.2s ease";
this.isMaximized.set(win, true);
} else {
win.style.transition = "all 0.2s ease";
win.style.left = win.dataset.prevLeft;
win.style.top = win.dataset.prevTop;
win.style.width = win.dataset.prevWidth;
win.style.height = win.dataset.prevHeight;
this.isMaximized.set(win, false);
}
}
}
const tasks = [];
let z = 2;
const wm = new WindowManager(document.body);
const appBar = document.getElementById('trdp');
const contextMenu = document.createElement('div');
contextMenu.id = 'context-menu';
contextMenu.style.position = 'absolute';
contextMenu.style.display = 'none';
contextMenu.style.background = '#222';
contextMenu.style.border = '1px solid #444';
contextMenu.style.borderRadius = '6px';
contextMenu.style.padding = '6px';
contextMenu.style.zIndex = '9999';
document.body.appendChild(contextMenu);
document.addEventListener('click', () => contextMenu.style.display = 'none');
async function loadApps() {
const res = await fetch('apps.php');
const apps = await res.json();
console.log(apps);
appBar.innerHTML = '';
apps.forEach(app => {
const wrapper = document.createElement('div');
wrapper.classList.add('wrapper');
wrapper.style.alignItems = 'center';
wrapper.style.height = '50px';
wrapper.style.width = '50px';
wrapper.style.margin = '8px';
wrapper.style.transition = 'transform 0.3s ease';
const btn = document.createElement('img');
btn.src = app.icon;
btn.alt = app.name;
btn.title = app.name;
btn.style.height = '50px';
btn.style.width = '50px';
btn.style.borderRadius = '16px';
btn.style.border = '2px solid #ffffff22';
btn.style.boxShadow = '0 2px 10px rgba(255, 192, 203, 0.25)';
btn.style.background = 'linear-gradient(135deg, #222, #1a1a1a)';
btn.style.padding = '6px';
btn.style.transition = 'all 0.3s ease';
btn.style.cursor = 'pointer';
btn.onmouseenter = () => {
wrapper.style.transform = 'scale(1.08)';
btn.style.borderColor = 'var(--accent)';
btn.style.boxShadow = '0 4px 16px var(--accent), 0 0 8px var(--accent)';
for (let i = 0; i < 6; i++) {
const sparkle = document.createElement('div');
sparkle.classList.add('sparkle');
sparkle.style.top = `${Math.random() * 100}%`;
sparkle.style.left = `${Math.random() * 100}%`;
sparkle.style.animationDelay = `${Math.random() * 1.5}s`;
wrapper.appendChild(sparkle);
sparkle.addEventListener('animationiteration', () => sparkle.remove());
}
};
btn.onmouseleave = () => {
wrapper.style.transform = 'scale(1)';
btn.style.borderColor = '#ffffff22';
btn.style.boxShadow = '0 2px 10px rgba(255, 192, 203, 0.25)';
const sparkles = wrapper.querySelectorAll('.sparkle');
sparkles.forEach(s => s.remove());
};
btn.onclick = () => openApp(app.path, app.name);
wrapper.appendChild(btn);
wrapper.oncontextmenu = e => {
e.preventDefault();
showContextMenu(e.pageX, e.pageY, app);
};
appBar.appendChild(wrapper);
});
}
function showContextMenu(x, y, app) {
contextMenu.innerHTML = `
<div onclick="uninstallApp('${app.id}')">🗑️ Uninstall</div>
`;
contextMenu.style.left = `${x}px`;
contextMenu.style.top = `${y}px`;
contextMenu.style.display = 'block';
}
function openApp(url, title, sandbox = false) {
const id = 'win_' + Date.now();
const win = document.createElement('div');
win.className = 'window';
win.style.top = Math.random() * 300 + 'px';
win.style.left = Math.random() * 400 + 'px';
win.style.zIndex = z++;
win.setAttribute('data-id', id);
win.innerHTML = `
<div class="window-header">
<span class="window-title">${title}</span>
<div class="window-controls">
<button class="max-btn" title="Maximize/Restore">🗖</button>
<button class="close-btn" title="Close">×</button>
</div>
</div>
`;
if (!sandbox) {
win.innerHTML += `<iframe src="${url}" class="window-content" allowtransparency="true"></iframe>`;
} else {
win.innerHTML += `<iframe src="${url}" sandbox="allow-scripts allow-forms allow-same-origin" class="window-content" allowtransparency="true"></iframe>`;
}
document.getElementById('desktop').appendChild(win);
tasks.push({ id, title, url });
let header = win.querySelector(".window-header");
wm.makeDraggable(win, header);
header.querySelector('.max-btn').addEventListener('click', (e) => {
e.stopPropagation();
wm.toggleMaximize(win);
});
header.querySelector('.close-btn').addEventListener('click', (e) => {
e.stopPropagation();
closeApp(id);
});
}
function closeApp(id) {
const win = document.querySelector(`.window[data-id='${id}']`);
if (win) win.remove();
const idx = tasks.findIndex(t => t.id === id);
if (idx !== -1) tasks.splice(idx, 1);
}
function focusApp(id) {
const win = document.querySelector(`.window[data-id='${id}']`);
if (win) win.style.zIndex = z++;
}
function uninstallApp(appId) {
if (!confirm("Are you sure you want to uninstall this app?")) return;
fetch(`uninstaller.php?id=${encodeURIComponent(appId)}`)
.then(res => res.text())
.then(() => loadApps());
}
loadApps();
window.addEventListener('message', async (event) => {
const { type, appId, packageFileUrl } = event.data || {};
if (type === 'THOS_INSTALL_APP') {
try {
const isValid = /^https:\/\/surillya\.com\//.test(packageFileUrl);
const fileName = packageFileUrl.split('/').pop().split('?')[0];
const outputPath = `/home/surillya/.temp/${fileName}`;
// I HATE UNDERSCORES!!!!
// underscore =/= dash
const url = `/install_app.php?url=${encodeURIComponent(packageFileUrl)}&output=${encodeURIComponent(outputPath)}&verified=${isValid ? 1 : 0}`;
const response = await fetch(url);
const result = await response.json();
if (!result.success) throw new Error(result.message);
const appUrl = `thp.php?q=${encodeURIComponent(result.filename)}&v=${result.verified}`;
window.openApp(appUrl, 'THOS Package Installer');
} catch (err) {
console.error('[THOS] App install failed:', err);
alert('Failed to install app.');
}
}
});
window.getTaskList = () => JSON.parse(JSON.stringify(tasks));
window.focusApp = focusApp;
window.closeApp = closeApp;
window.openApp = openApp;
window.reloadApps = loadApps;
window.saveTHOSState = saveTHOSState;
function applySettings(settings) {
console.log(settings);
function applyTheme(theme) {
document.body.className = theme;
}
const savedTheme = settings.theme;
if (savedTheme) applyTheme(savedTheme);
document.documentElement.style.setProperty('--accent', settings.accentColor || '#ff69b4');
if (settings.wallpaper) {
document.getElementById('bg-image').style.background = `url('file.php?q=${settings.wallpaper}') center/cover`;
} else {
document.getElementById('bg-image').style.background = '';
}
const audio = document.getElementById('bgMusic');
if (audio) {
if (settings.musicEnabled === 'on') {
document.addEventListener("click", function () {
audio.src = `file.php?q=${settings.music}`;
audio.volume = parseFloat(settings.musicVolume || 0.3);
audio.play().catch(() => {
alert('An error occured while starting background music. Please turn background music off, or enable autoplay.');
});
}, { once: true });
} else {
audio.pause;
}
}
timeoutSeconds = settings.screenTimeout ?? timeoutSeconds;
window.THOS = Object.assign(window.THOS || {}, {
getAllSettings() {
return settings;
}
});
saveTHOSState();
}
const savedSettings = JSON.parse(localStorage.getItem('settings')) || {};
applySettings(savedSettings);
window.addEventListener('message', (event) => {
if (event.data?.type === 'applySettings') {
applySettings(event.data.settings);
}
});
function saveTHOSState() {
const cookies = document.cookie;
const localData = JSON.stringify(localStorage);
fetch('save_state.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cookies, localData })
});
}
setInterval(saveTHOSState, 60000);
window.addEventListener('beforeunload', saveTHOSState);
const powerButton = document.getElementById('powerButton');
const powerDialog = document.getElementById('powerDialog');
const cancelBtn = document.getElementById('cancelBtn');
const shutdownBtn = document.getElementById('shutdownBtn');
const restartBtn = document.getElementById('restartBtn');
const reloadBtn = document.getElementById('reloadBtn');
powerButton.addEventListener('click', () => {
document.getElementById("appWrapper").classList.add("grayscale-bg");
powerDialog.classList.remove('hidden');
});
cancelBtn.addEventListener('click', () => {
document.getElementById("appWrapper").classList.remove("grayscale-bg");
powerDialog.classList.add('hidden');
});
shutdownBtn.addEventListener('click', () => {
sendCommand('shutdown');
document.getElementById("appWrapper").classList.remove("grayscale-bg");
powerDialog.classList.add('hidden');
});
restartBtn.addEventListener('click', () => {
sendCommand('reboot');
document.getElementById("appWrapper").classList.remove("grayscale-bg");
powerDialog.classList.add('hidden');
});
reloadBtn.addEventListener('click', () => {
location.reload();
});
powerDialog.addEventListener('click', (e) => {
if (e.target === powerDialog) {
document.getElementById("appWrapper").classList.remove("grayscale-bg");
powerDialog.classList.add('hidden');
}
});
function sendCommand(action) {
fetch(`server_command.php?action=${encodeURIComponent(action)}`)
.then(res => res.text())
.then(result => {
alert("Server response: " + result);
})
.catch(err => {
alert("Failed to send command: " + err);
});
}
document.getElementById('version-number').textContent = window.THOS.version;
document.getElementById("fullscreen-toggle").addEventListener("click", () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {
console.error(`Failed to enter fullscreen: ${err.message}`);
});
} else {
document.exitFullscreen();
}
});
const screensaver = document.getElementById("screensaver");
let screensaverTimeout;
function showScreensaver() {
screensaver.style.display = "block";
}
function hideScreensaver() {
if (screensaver.style.display === "block") {
screensaver.contentWindow.postMessage({ type: 'screensaver_hidden' }, '*'); // optional
}
screensaver.style.display = "none";
}
function resetInactivityTimer() {
hideScreensaver();
clearTimeout(screensaverTimeout);
screensaverTimeout = setTimeout(showScreensaver, timeoutSeconds * 1000);
}
['mousemove', 'mousedown', 'keydown', 'scroll', 'touchstart', 'wheel'].forEach(event => {
document.addEventListener(event, resetInactivityTimer, { passive: true });
});
window.addEventListener("message", event => {
if (event.data?.type === "user_active") {
resetInactivityTimer();
}
});
resetInactivityTimer();
function notify(title, description = "", options = {}) {
const {
type = "info",
timeout = 5000,
icon = null,
id = `notif-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
} = options;
const container = document.getElementById("notification-container");
const notification = document.createElement("div");
notification.className = `notification ${type}`;
notification.id = id;
notification.innerHTML = `
${icon ? `<div class="text-xl">${icon}</div>` : ""}
<div class="flex-1">
<div class="notification-title font-semibold text-base">${title}</div>
${description ? `<div class="notification-desc text-sm text-gray-300">${description}</div>` : ""}
</div>
<button class="notification-dismiss absolute top-2 right-2 text-white/50 hover:text-white transition-colors text-sm" aria-label="Dismiss">&times;</button>
`;
const dismiss = () => {
notification.style.animation = "fadeOut 0.25s ease-out forwards";
setTimeout(() => {
if (container.contains(notification)) container.removeChild(notification);
}, 250);
};
notification.querySelector(".notification-dismiss").onclick = dismiss;
container.appendChild(notification);
if (timeout > 0) {
setTimeout(() => {
if (container.contains(notification)) dismiss();
}, timeout);
}
}
window.notify = notify;
</script>
</body>
</html>