Initial Commit
This commit is contained in:
33
apps.php
Normal file
33
apps.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
$appsDir = "/home/surillya/.apps/";
|
||||||
|
$symlinkPath = "/usr/thos/apps";
|
||||||
|
$apps = [];
|
||||||
|
|
||||||
|
if (!file_exists($appsDir)) {
|
||||||
|
mkdir($appsDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Very dirty hack to get the apps exposed to the browser
|
||||||
|
if (!is_link($symlinkPath) || readlink($symlinkPath) !== $appsDir) {
|
||||||
|
@unlink($symlinkPath);
|
||||||
|
symlink($appsDir, $symlinkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (glob($appsDir . '*', GLOB_ONLYDIR) as $appPath) {
|
||||||
|
$id = basename($appPath);
|
||||||
|
$manifestFile = $appPath . '/manifest.json';
|
||||||
|
if (file_exists($manifestFile)) {
|
||||||
|
$data = json_decode(file_get_contents($manifestFile), true);
|
||||||
|
if ($data && isset($data['name'], $data['entry'])) {
|
||||||
|
$apps[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => $data['name'],
|
||||||
|
'icon' => 'apps/' . $id . '/' . $data['icon'],
|
||||||
|
'path' => 'apps/' . $id . '/app/' . $data['entry']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($apps);
|
7
config.php
Normal file
7
config.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
// Your actual root directory on the server
|
||||||
|
define('REAL_ROOT', '/home/surillya/');
|
||||||
|
|
||||||
|
// The user's virtual path (as seen in the OS)
|
||||||
|
define('VIRTUAL_ROOT', '/');
|
||||||
|
?>
|
27
connect_wifi.php
Normal file
27
connect_wifi.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$ssid = $_GET['ssid'] ?? '';
|
||||||
|
$password = $_GET['pass'] ?? '';
|
||||||
|
|
||||||
|
// Basic input validation
|
||||||
|
if (!$ssid || !$password) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing SSID or password']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize inputs (very basic — assume safe env or sandbox it!)
|
||||||
|
$ssidSafe = escapeshellarg($ssid);
|
||||||
|
$passwordSafe = escapeshellarg($password);
|
||||||
|
|
||||||
|
// Attempt to connect using nmcli
|
||||||
|
$command = "nmcli dev wifi connect $ssidSafe password $passwordSafe";
|
||||||
|
$output = shell_exec($command);
|
||||||
|
|
||||||
|
// Check result
|
||||||
|
if (strpos($output, 'successfully activated') !== false) {
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => trim($output)]);
|
||||||
|
}
|
||||||
|
?>
|
38
delete.php
Normal file
38
delete.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
$rootDir = realpath("/home/surillya/");
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (empty($data['path'])) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing path']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve path properly
|
||||||
|
$delFullPath = realpath($rootDir . '/' . ltrim($data['path'], '/'));
|
||||||
|
|
||||||
|
// Validate path
|
||||||
|
if (!$delFullPath || strpos($delFullPath, $rootDir) !== 0) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid path']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive deletion
|
||||||
|
function deleteRecursively($path) {
|
||||||
|
if (is_dir($path)) {
|
||||||
|
$items = scandir($path);
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if ($item === '.' || $item === '..') continue;
|
||||||
|
deleteRecursively($path . DIRECTORY_SEPARATOR . $item);
|
||||||
|
}
|
||||||
|
return rmdir($path);
|
||||||
|
} elseif (is_file($path)) {
|
||||||
|
return unlink($path);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteRecursively($delFullPath)) {
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Delete failed']);
|
||||||
|
}
|
298
explorer.php
Normal file
298
explorer.php
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
<?php
|
||||||
|
function getAssociatedApp($extension)
|
||||||
|
{
|
||||||
|
static $associations = null;
|
||||||
|
if ($associations === null) {
|
||||||
|
$json = file_get_contents('file_associations.json');
|
||||||
|
$associations = json_decode($json, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $associations[strtolower($extension)] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = realpath("/home/surillya");
|
||||||
|
$cdir = isset($_GET['dir']) ? $_GET['dir'] : '';
|
||||||
|
$dir = realpath($root . DIRECTORY_SEPARATOR . $cdir);
|
||||||
|
|
||||||
|
if (!$dir || strpos($dir, $root) !== 0 || !is_dir($dir)) {
|
||||||
|
echo "Invalid directory.";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = scandir($dir);
|
||||||
|
$relPath = str_replace($root, '', $dir);
|
||||||
|
|
||||||
|
function humanFileSize($size)
|
||||||
|
{
|
||||||
|
if ($size < 1024)
|
||||||
|
return $size . ' B';
|
||||||
|
if ($size < 1048576)
|
||||||
|
return round($size / 1024, 2) . ' KB';
|
||||||
|
if ($size < 1073741824)
|
||||||
|
return round($size / 1048576, 2) . ' MB';
|
||||||
|
return round($size / 1073741824, 2) . ' GB';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Surillya Explorer</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--accent: #61dafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
background: transparent;
|
||||||
|
color: #eee;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, p {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.explorer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
gap: 10px;
|
||||||
|
background: #222;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
position: absolute;
|
||||||
|
background: #222;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: none;
|
||||||
|
z-index: 999;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu .op {
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu .op:hover {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filename {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>THOS Explorer</h1>
|
||||||
|
<p>Current directory: <code><?php echo htmlspecialchars($cdir ?: '/'); ?></code></p>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
if ($dir !== $root) {
|
||||||
|
$parentDir = dirname($cdir);
|
||||||
|
echo '<p><a href="?dir=' . urlencode($parentDir) . '">⬅️ Go Up</a></p>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="explorer" id="fileGrid">
|
||||||
|
<?php
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if ($item === '.' || $item === '..')
|
||||||
|
continue;
|
||||||
|
$itemPath = $dir . DIRECTORY_SEPARATOR . $item;
|
||||||
|
$relItem = ltrim($relPath . DIRECTORY_SEPARATOR . $item, DIRECTORY_SEPARATOR);
|
||||||
|
$isDir = is_dir($itemPath);
|
||||||
|
|
||||||
|
$ext = pathinfo($item, PATHINFO_EXTENSION);
|
||||||
|
$app = getAssociatedApp($ext);
|
||||||
|
$icon = $isDir ? '📁' : ($app['icon'] ?? '📄');
|
||||||
|
|
||||||
|
if ($isDir)
|
||||||
|
echo '<a href="?dir=' . urlencode($relItem) . '">';
|
||||||
|
else {
|
||||||
|
require_once "vfs.php";
|
||||||
|
$vPath = virtualize_path($itemPath);
|
||||||
|
echo '<a href="javascript:void(0);" onclick="openFile(\'' . htmlspecialchars($vPath) . '\')">';
|
||||||
|
}
|
||||||
|
echo '<div class="item" data-name="' . htmlspecialchars($item) . '" data-path="' . htmlspecialchars($relItem) . '" data-isdir="' . ($isDir ? '1' : '0') . '">';
|
||||||
|
echo '<div class="file-icon">' . $icon . '</div>';
|
||||||
|
echo '<span class="filename">' . htmlspecialchars($item) . '</span>';
|
||||||
|
echo '</div></a>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="context-menu" id="contextMenu">
|
||||||
|
<div class="op" id="info">Show Info</div>
|
||||||
|
<div class="op" id="rename">Rename</div>
|
||||||
|
<div class="op" id="delete">Delete</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function basename(inputPath) {
|
||||||
|
if (!inputPath) return '';
|
||||||
|
|
||||||
|
const cleanPath = inputPath.split(/[?#]/)[0];
|
||||||
|
|
||||||
|
const segments = cleanPath.split('/');
|
||||||
|
const fileName = segments.pop() || '';
|
||||||
|
|
||||||
|
return fileName.replace(/\.[^.]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function openFile(virtualPath) {
|
||||||
|
const ext = virtualPath.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('file_associations.json');
|
||||||
|
const associations = await res.json();
|
||||||
|
|
||||||
|
if (associations[ext]) {
|
||||||
|
const appUrl = associations[ext].app + "?q=" + encodeURIComponent(virtualPath);
|
||||||
|
parent.openApp(appUrl, basename(virtualPath));
|
||||||
|
} else {
|
||||||
|
window.parent.notify("Explorer", `No app found for ".${ext}" files`, {
|
||||||
|
type: "error",
|
||||||
|
icon: "⚠️",
|
||||||
|
timeout: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch file associations:', e);
|
||||||
|
window.parent.notify("Explorer", "Failed to open file", {
|
||||||
|
type: "error",
|
||||||
|
icon: "⚠️",
|
||||||
|
timeout: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// function showToast(message) {
|
||||||
|
// const toast = document.createElement('div');
|
||||||
|
// toast.innerText = message;
|
||||||
|
// toast.style.position = 'fixed';
|
||||||
|
// toast.style.bottom = '20px';
|
||||||
|
// toast.style.left = '20px';
|
||||||
|
// toast.style.background = '#333';
|
||||||
|
// toast.style.color = '#fff';
|
||||||
|
// toast.style.padding = '10px 20px';
|
||||||
|
// toast.style.borderRadius = '8px';
|
||||||
|
// toast.style.zIndex = '9999';
|
||||||
|
// document.body.appendChild(toast);
|
||||||
|
// setTimeout(() => toast.remove(), 3000);
|
||||||
|
// }
|
||||||
|
|
||||||
|
let contextMenu = document.getElementById("contextMenu");
|
||||||
|
let currentTarget = null;
|
||||||
|
document.getElementById('fileGrid').addEventListener('contextmenu', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const item = e.target.closest('.item');
|
||||||
|
if (!item) {
|
||||||
|
contextMenu.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentTarget = item;
|
||||||
|
|
||||||
|
contextMenu.style.top = `${e.clientY}px`;
|
||||||
|
contextMenu.style.left = `${e.clientX}px`;
|
||||||
|
contextMenu.style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("click", function () {
|
||||||
|
contextMenu.style.display = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('info').addEventListener('click', () => {
|
||||||
|
contextMenu.style.display = 'none';
|
||||||
|
if (!currentTarget) return;
|
||||||
|
|
||||||
|
fetch('file_info.php?path=' + encodeURIComponent(currentTarget.dataset.path))
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(info => {
|
||||||
|
alert(`Name: ${info.name}\nType: ${info.type}\nSize: ${info.size}\nModified: ${info.mtime}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('rename').addEventListener('click', () => {
|
||||||
|
contextMenu.style.display = 'none';
|
||||||
|
if (!currentTarget) return;
|
||||||
|
|
||||||
|
const oldName = currentTarget.dataset.name;
|
||||||
|
const newName = prompt("Rename file/folder", oldName);
|
||||||
|
if (!newName || newName === oldName) return;
|
||||||
|
|
||||||
|
fetch('rename.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ oldName: currentTarget.dataset.path, newName })
|
||||||
|
}).then(res => res.json())
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.success) {
|
||||||
|
showToast('Renamed successfully!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
showToast('Rename failed: ' + resp.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('delete').addEventListener('click', () => {
|
||||||
|
contextMenu.style.display = 'none';
|
||||||
|
if (!currentTarget) return;
|
||||||
|
|
||||||
|
if (!confirm(`Are you sure you want to delete "${currentTarget.dataset.name}"?`)) return;
|
||||||
|
|
||||||
|
fetch('delete.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ path: currentTarget.dataset.path })
|
||||||
|
}).then(res => res.json())
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.success) {
|
||||||
|
showToast('Deleted successfully!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
showToast('Delete failed: ' + resp.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.documentElement.style.setProperty('--accent', window.parent.THOS.getAllSettings().accentColor || '#ff69b4');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
31
file.php
Normal file
31
file.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
$rootDir = realpath("/home/surillya");
|
||||||
|
|
||||||
|
$query = $_GET['q'] ?? '';
|
||||||
|
|
||||||
|
$cleanQuery = preg_replace('#/+#', '/', ltrim($query, '/'));
|
||||||
|
|
||||||
|
$targetPath = $rootDir . DIRECTORY_SEPARATOR . $cleanQuery;
|
||||||
|
|
||||||
|
$requestedPath = realpath($targetPath);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!$requestedPath ||
|
||||||
|
strpos($requestedPath, $rootDir) !== 0 ||
|
||||||
|
!is_file($requestedPath)
|
||||||
|
) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo "File not found or access denied.";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$escapedPath = escapeshellarg($requestedPath);
|
||||||
|
$mime = trim(shell_exec("file -b --mime-type $escapedPath"));
|
||||||
|
|
||||||
|
header("Content-Type: $mime");
|
||||||
|
header("Content-Length: " . filesize($requestedPath));
|
||||||
|
header("Content-Disposition: inline; filename=\"" . basename($requestedPath) . "\"");
|
||||||
|
|
||||||
|
readfile($requestedPath);
|
||||||
|
exit;
|
||||||
|
?>
|
14
file_associations.json
Normal file
14
file_associations.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"jpg": {"app":"imager.php", "icon":"🖼️"},
|
||||||
|
"jpeg": {"app":"imager.php", "icon":"🖼️"},
|
||||||
|
"png": {"app":"imager.php", "icon":"🖼️"},
|
||||||
|
"gif": {"app":"imager.php", "icon":"🖼️"},
|
||||||
|
"webp": {"app":"imager.php", "icon":"🖼️"},
|
||||||
|
"mp3": {"app":"player.php", "icon":"🎵"},
|
||||||
|
"wav": {"app":"player.php", "icon":"🎵"},
|
||||||
|
"opus": {"app":"player.php", "icon":"🎵"},
|
||||||
|
"m4a": {"app":"player.php", "icon":"🎵"},
|
||||||
|
"thp": {"app":"thp.php", "icon":"📦"},
|
||||||
|
"mp4": {"app":"video.php", "icon":"📽️"},
|
||||||
|
"webm": {"app":"video.php", "icon":"📽️"}
|
||||||
|
}
|
29
file_info.php
Normal file
29
file_info.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
$rootDir = realpath("/home/surillya/"); // This should be absolute
|
||||||
|
$path = $_GET['path'] ?? '';
|
||||||
|
$fullPath = $rootDir . '/' . ltrim($path, '/'); // Fix slash issues
|
||||||
|
$realFullPath = realpath($fullPath);
|
||||||
|
|
||||||
|
// Check if the resolved path is still within rootDir (for security)
|
||||||
|
if (!$realFullPath || strpos($realFullPath, $rootDir) !== 0) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'File not found or access denied']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now do the info check
|
||||||
|
$info = [];
|
||||||
|
$info['name'] = basename($realFullPath);
|
||||||
|
$info['type'] = is_dir($realFullPath) ? 'Directory' : mime_content_type($realFullPath);
|
||||||
|
$info['size'] = is_file($realFullPath) ? filesize($realFullPath) : 0;
|
||||||
|
$info['mtime'] = date("Y-m-d H:i:s", filemtime($realFullPath));
|
||||||
|
|
||||||
|
if (is_file($realFullPath)) {
|
||||||
|
if ($info['size'] < 1024) $info['size'] = $info['size'] . ' B';
|
||||||
|
else if ($info['size'] < 1048576) $info['size'] = round($info['size'] / 1024, 2) . ' KB';
|
||||||
|
else if ($info['size'] < 1073741824) $info['size'] = round($info['size'] / 1048576, 2) . ' MB';
|
||||||
|
else $info['size'] = round($info['size'] / 1073741824, 2) . ' GB';
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($info);
|
45
imager.php
Normal file
45
imager.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
require_once "vfs.php";
|
||||||
|
$file = resolve_path($_GET['q'] ?? '');
|
||||||
|
|
||||||
|
if (!file_exists($file) || !is_file($file)) {
|
||||||
|
die("File not found." . $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
|
||||||
|
$mime = match ($ext) {
|
||||||
|
'jpg', 'jpeg' => 'image/jpeg',
|
||||||
|
'png' => 'image/png',
|
||||||
|
'gif' => 'image/gif',
|
||||||
|
default => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!$mime)
|
||||||
|
die("Unsupported format.");
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Image Viewer</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: transparent;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h3 style="color:white"><?= htmlspecialchars(basename($file)) ?></h3>
|
||||||
|
<img src="<?= "file.php?q=" . urlencode($_GET['q']) ?>" alt="Image">
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
934
index.php
Normal file
934
index.php
Normal file
@ -0,0 +1,934 @@
|
|||||||
|
<?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">×</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>
|
38
install_app.php
Normal file
38
install_app.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$url = $_GET['url'] ?? '';
|
||||||
|
$output = $_GET['output'] ?? '';
|
||||||
|
$verified = $_GET['verified'] ?? 0;
|
||||||
|
|
||||||
|
function fail($message) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $message]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force verify off if not surillya.com
|
||||||
|
if (!preg_match('/^https:\/\/surillya\.com\//', $url)) {
|
||||||
|
$verified = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetPath = '/home/surillya/.temp/' . basename($output);
|
||||||
|
|
||||||
|
if (!is_dir("/home/surillya/.temp/")){
|
||||||
|
if (!mkdir("/home/surillya/.temp/", 0755, true)){
|
||||||
|
fail("Failed to create temporary download directory.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$data = file_get_contents($url);
|
||||||
|
if ($data === false) fail("Failed to download file.");
|
||||||
|
|
||||||
|
file_put_contents($targetPath, $data);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'filename' => ".temp/" . basename($targetPath),
|
||||||
|
'verified' => $verified
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
fail($e->getMessage());
|
||||||
|
}
|
51
internet.html
Normal file
51
internet.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<!-- Temporarily closing this site down, since the inner IFrame is shown at max 150px somehow, I tried everything, this is annoying -->
|
||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="bg-[#0d0d0d] text-white">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>THOS Search</title>
|
||||||
|
<script src="tailwind.es"></script>
|
||||||
|
<style>
|
||||||
|
.window-content {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 32px);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="flex flex-col min-h-screen bg-gradient-to-br from-pink-500/10 via-blue-500/10 to-orange-500/10">
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 p-3 bg-white/5 backdrop-blur-md rounded-b-2xl shadow-md mx-4 mt-4">
|
||||||
|
<input id="urlBar" type="text" placeholder="Search THOS..."
|
||||||
|
class="flex-1 px-4 py-2 rounded-xl bg-white/10 text-white placeholder-gray-400 text-sm focus:ring-2 ring-pink-500" />
|
||||||
|
<button id="goBtn" class="ml-2 px-4 py-2 bg-white/10 hover:bg-white/20 rounded-xl text-sm">Search</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="window-content">
|
||||||
|
<iframe id="searchFrame" src="https://surillya.com/thos/search/thossearch.html" class="window-content"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const urlBar = document.getElementById("urlBar");
|
||||||
|
const goBtn = document.getElementById("goBtn");
|
||||||
|
const iframe = document.getElementById("searchFrame");
|
||||||
|
|
||||||
|
function sendSearchQuery(query) {
|
||||||
|
iframe.contentWindow.postMessage({ type: "search", query }, "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
goBtn.onclick = () => sendSearchQuery(urlBar.value);
|
||||||
|
urlBar.addEventListener("keydown", e => {
|
||||||
|
if (e.key === "Enter") sendSearchQuery(urlBar.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
262
oobe.php
Normal file
262
oobe.php
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Welcome to THOS</title>
|
||||||
|
<script src="tailwind.es"></script>
|
||||||
|
<style>
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.8s ease-out;
|
||||||
|
}
|
||||||
|
.wifi-network:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.wifi-signal {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 12px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
rgba(255,255,255,0.2) 25%,
|
||||||
|
rgba(255,255,255,0.6) 25%,
|
||||||
|
rgba(255,255,255,0.6) 50%,
|
||||||
|
rgba(255,255,255,0.8) 50%,
|
||||||
|
rgba(255,255,255,0.8) 75%,
|
||||||
|
rgba(255,255,255,1) 75%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-900 text-white font-sans">
|
||||||
|
<div id="oobe" class="min-h-screen flex flex-col items-center justify-center p-4">
|
||||||
|
<!-- Welcome Screen -->
|
||||||
|
<div id="step-welcome" class="animate-fade-in text-center space-y-6">
|
||||||
|
<h1 class="text-4xl font-bold text-blue-300">Welcome to THOS</h1>
|
||||||
|
<p class="text-lg">Let's get things set up for the best experience.</p>
|
||||||
|
<button onclick="nextStep('step-name')" class="px-4 py-2 bg-blue-500 rounded hover:bg-blue-600 transition">Get Started</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Name Screen -->
|
||||||
|
<div id="step-name" class="hidden animate-fade-in text-center space-y-4">
|
||||||
|
<h2 class="text-2xl">Who's using THOS?</h2>
|
||||||
|
<input id="user-name" type="text" placeholder="Enter your name" class="px-3 py-2 text-black rounded">
|
||||||
|
<button onclick="saveName()" class="px-4 py-2 bg-blue-500 rounded hover:bg-blue-600 transition">Next</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Wi-Fi Setup -->
|
||||||
|
<div id="step-wifi" class="hidden w-full max-w-md p-8 bg-gray-800 rounded-xl shadow-2xl">
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<h2 class="text-2xl font-bold text-blue-400">Connect to Wi-Fi</h2>
|
||||||
|
<p class="text-gray-400 mt-2">Select a network to continue setup</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 flex justify-between items-center">
|
||||||
|
<button id="reload-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition">
|
||||||
|
Refresh Networks
|
||||||
|
</button>
|
||||||
|
<button id="skip-btn" class="text-gray-400 hover:text-white underline">
|
||||||
|
Skip for now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wifi-list" class="space-y-3 max-h-64 overflow-y-auto">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wifi-password-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||||
|
<div class="bg-gray-800 p-6 rounded-xl w-96 shadow-2xl">
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-blue-400" id="selected-network-name">Network Name</h3>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="wifi-pass"
|
||||||
|
placeholder="Enter network password"
|
||||||
|
class="w-full p-3 bg-gray-700 rounded-lg border border-gray-600 mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<button
|
||||||
|
id="connect-btn"
|
||||||
|
class="flex-1 bg-green-600 hover:bg-green-700 text-white py-3 rounded-lg transition"
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="cancel-btn"
|
||||||
|
class="flex-1 bg-gray-600 hover:bg-gray-700 text-white py-3 rounded-lg transition"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="wifi-status" class="mt-3 text-center text-sm text-gray-400"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Final Step -->
|
||||||
|
<div id="step-final" class="hidden animate-fade-in text-center space-y-4">
|
||||||
|
<h2 class="text-3xl font-bold text-green-400">You're all set!</h2>
|
||||||
|
<p>Welcome to THOS, <span id="final-name"></span>!</p>
|
||||||
|
<div id="saving-msg" class="hidden text-sm text-gray-400 mt-2">Saving setup...</div>
|
||||||
|
<button onclick="finishSetup()" class="px-4 py-2 bg-green-500 rounded hover:bg-green-600 transition">Launch THOS</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const steps = ['step-welcome', 'step-name', 'step-wifi', 'step-final'];
|
||||||
|
|
||||||
|
function nextStep(stepId) {
|
||||||
|
steps.forEach(id => document.getElementById(id)?.classList.add('hidden'));
|
||||||
|
document.getElementById(stepId)?.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveName() {
|
||||||
|
const name = document.getElementById('user-name').value;
|
||||||
|
if (!name.trim()) return alert("Please enter your name!");
|
||||||
|
if (!isValidName(name)) {
|
||||||
|
alert("Please use only letters, numbers, spaces, dashes and underscores (max 32 characters).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem('thos_name', name);
|
||||||
|
nextStep('step-wifi');
|
||||||
|
loadWiFiList();
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedSSID = "";
|
||||||
|
|
||||||
|
function loadWiFiList() {
|
||||||
|
const list = document.getElementById("wifi-list");
|
||||||
|
list.innerHTML = `
|
||||||
|
<div class="text-center text-gray-500 py-4">
|
||||||
|
<svg class="animate-spin h-5 w-5 mx-auto text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="mt-2">Scanning for networks...</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
fetch("wifi_list.php")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
list.innerHTML = data.map(wifi => `
|
||||||
|
<div class="wifi-network bg-gray-700 rounded-lg p-3 flex justify-between items-center hover:bg-gray-600 transition">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="wifi-signal"></div>
|
||||||
|
<span>${wifi || "<i>Hidden SSID</i>"}</span>
|
||||||
|
</div>
|
||||||
|
<button onclick="showPasswordModal('${wifi.replace(/'/g, "\\'")}')" class="text-blue-400 hover:underline">
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
list.innerHTML = `
|
||||||
|
<div class="text-center text-red-500 py-4">
|
||||||
|
Failed to load networks. Please try again.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
console.error("Network fetch error:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPasswordModal(ssid) {
|
||||||
|
selectedSSID = ssid;
|
||||||
|
document.getElementById('selected-network-name').textContent = ssid;
|
||||||
|
document.getElementById('wifi-password-modal').classList.remove('hidden');
|
||||||
|
document.getElementById('wifi-password-modal').classList.add('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWiFi() {
|
||||||
|
const pass = document.getElementById('wifi-pass').value;
|
||||||
|
const status = document.getElementById('wifi-status');
|
||||||
|
status.textContent = "Connecting...";
|
||||||
|
status.className = "mt-3 text-center text-sm text-gray-400";
|
||||||
|
|
||||||
|
fetch(`connect_wifi.php?ssid=${encodeURIComponent(selectedSSID)}&pass=${encodeURIComponent(pass)}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
status.textContent = "✅ Connected successfully!";
|
||||||
|
status.classList.add('text-green-500');
|
||||||
|
|
||||||
|
// Hide modal after successful connection
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('wifi-password-modal').classList.add('hidden');
|
||||||
|
document.getElementById('wifi-password-modal').classList.remove('flex');
|
||||||
|
nextStep('step-final');
|
||||||
|
document.getElementById('final-name').innerHTML = localStorage.getItem('thos_name');
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
status.textContent = "❌ Failed to connect. Check your password.";
|
||||||
|
status.classList.add('text-red-500');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
status.textContent = "❌ Connection error. Please try again.";
|
||||||
|
status.classList.add('text-red-500');
|
||||||
|
console.error("Connection error:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
document.getElementById('reload-btn').addEventListener('click', loadWiFiList);
|
||||||
|
document.getElementById('connect-btn').addEventListener('click', connectWiFi);
|
||||||
|
document.getElementById('cancel-btn').addEventListener('click', () => {
|
||||||
|
document.getElementById('wifi-password-modal').classList.add('hidden');
|
||||||
|
document.getElementById('wifi-password-modal').classList.remove('flex');
|
||||||
|
});
|
||||||
|
document.getElementById('skip-btn').addEventListener('click', () => {
|
||||||
|
if(confirm("Wi-Fi setup is recommended. Skip anyway?")) {
|
||||||
|
document.getElementById("wifi-status").textContent = "⚠️ Some features will be limited.";
|
||||||
|
nextStep('step-final');
|
||||||
|
document.getElementById('final-name').innerHTML = localStorage.getItem('thos_name');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function isValidName(name) {
|
||||||
|
return /^[A-Za-z]+( [A-Za-z]+)?$/.test(name) && name.length <= 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTHOSState() {
|
||||||
|
try {
|
||||||
|
const cookies = document.cookie;
|
||||||
|
const localData = JSON.stringify(localStorage);
|
||||||
|
|
||||||
|
const response = await fetch('save_state.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ cookies, localData })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to save state to the server.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.text();
|
||||||
|
console.log("State saved:", result);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving THOS state:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function finishSetup() {
|
||||||
|
document.getElementById("saving-msg").classList.remove("hidden");
|
||||||
|
localStorage.setItem('thos_done', 'true');
|
||||||
|
const saved = await saveTHOSState();
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
window.location.href = 'index.php';
|
||||||
|
} else {
|
||||||
|
alert("Something went wrong saving your setup. Please try again.");
|
||||||
|
document.getElementById("saving-msg").classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
121
pkg.php
Normal file
121
pkg.php
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function validateManifest(array $manifest): array {
|
||||||
|
$requiredKeys = ['name', 'version', 'description', 'author', 'entry', 'icon', 'categories'];
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach ($requiredKeys as $key) {
|
||||||
|
if (!array_key_exists($key, $manifest)) {
|
||||||
|
$errors[] = "Missing required manifest field: '$key'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// entry file must be inside the app/ folder
|
||||||
|
if (isset($manifest['entry']) && strpos($manifest['entry'], '/') === false) {
|
||||||
|
// No slashes allowed (it must be inside app/)
|
||||||
|
} else {
|
||||||
|
$errors[] = "Entry field should be a filename inside the 'app/' folder, e.g. 'main.php'";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($manifest['categories'] ?? null)) {
|
||||||
|
$errors[] = "Categories must be an array";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function installPackage(string $zipPath, string $appsDir): bool {
|
||||||
|
if (!file_exists($zipPath)) {
|
||||||
|
die("Error: ZIP file does not exist.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($zipPath) !== true) {
|
||||||
|
die("Error: Unable to open ZIP file.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for manifest.json at root of ZIP
|
||||||
|
$manifestIndex = $zip->locateName('manifest.json', ZipArchive::FL_NODIR);
|
||||||
|
if ($manifestIndex === false) {
|
||||||
|
die("Error: manifest.json not found in ZIP root.");
|
||||||
|
$zip->close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and decode manifest.json
|
||||||
|
$manifestContent = $zip->getFromIndex($manifestIndex);
|
||||||
|
$manifest = json_decode($manifestContent, true);
|
||||||
|
|
||||||
|
if (!$manifest) {
|
||||||
|
die("Error: manifest.json contains invalid JSON.");
|
||||||
|
$zip->close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate manifest fields
|
||||||
|
$errors = validateManifest($manifest);
|
||||||
|
if (!empty($errors)) {
|
||||||
|
echo "Manifest validation errors:\n";
|
||||||
|
foreach ($errors as $error) {
|
||||||
|
echo " - $error\n";
|
||||||
|
}
|
||||||
|
$zip->close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// App name used for directory (safe slug)
|
||||||
|
$appName = preg_replace('/[^a-z0-9_-]/i', '_', $manifest['name']);
|
||||||
|
$installPath = $appsDir . $appName;
|
||||||
|
|
||||||
|
// Create app install directory if not exists
|
||||||
|
if (!is_dir($installPath)) {
|
||||||
|
if (!mkdir($installPath, 0755, true)) {
|
||||||
|
echo "Error: Unable to create app directory: $installPath\n";
|
||||||
|
$zip->close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "<script>console.log('Warning: App directory already exists, overwriting...');</script>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract all ZIP files to the install directory
|
||||||
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||||
|
$stat = $zip->statIndex($i);
|
||||||
|
$filename = $stat['name'];
|
||||||
|
|
||||||
|
// Security check: prevent path traversal
|
||||||
|
if (strpos($filename, '..') !== false) {
|
||||||
|
echo "Error: ZIP contains invalid filename (path traversal): $filename\n";
|
||||||
|
$zip->close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract file content
|
||||||
|
$content = $zip->getFromIndex($i);
|
||||||
|
|
||||||
|
// Compute target path
|
||||||
|
$targetPath = $installPath . '/' . $filename;
|
||||||
|
|
||||||
|
// Create directory if needed
|
||||||
|
$dir = dirname($targetPath);
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write file
|
||||||
|
file_put_contents($targetPath, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
echo "<script>console.log('App \'{$manifest['name']}\' installed successfully to: $installPath');</script>";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once "vfs.php";
|
||||||
|
$file = resolve_path($_GET['q'] ?? '');
|
||||||
|
|
||||||
|
installPackage($file, __DIR__ . "/apps/");
|
44
player.php
Normal file
44
player.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
require_once "vfs.php";
|
||||||
|
$file = resolve_path($_GET['q'] ?? '');
|
||||||
|
|
||||||
|
if (!file_exists($file) || !is_file($file)) {
|
||||||
|
die("File not found." . $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = htmlspecialchars(basename($file));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Now Playing: <?= $filename ?></title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio {
|
||||||
|
width: 90%;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>🎵 <?= $filename ?></h2>
|
||||||
|
<audio controls autoplay>
|
||||||
|
<source src="<?= "file.php?q=" . urlencode(virtualize_path($file)) ?>" type="audio/mpeg">
|
||||||
|
Audio cannot be played on your system.
|
||||||
|
</audio>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
31
rename.php
Normal file
31
rename.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
$rootDir = realpath("/home/surillya/");
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (empty($data['oldName']) || empty($data['newName'])) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve paths properly
|
||||||
|
$oldFullPath = realpath($rootDir . '/' . ltrim($data['oldName'], '/'));
|
||||||
|
$newFullPath = $rootDir . '/' . ltrim(dirname($data['oldName']), '/') . '/' . basename($data['newName']);
|
||||||
|
$newFullPath = realpath(dirname($newFullPath)) . '/' . basename($data['newName']); // Ensure dir exists
|
||||||
|
|
||||||
|
// Validate paths
|
||||||
|
if (!$oldFullPath || strpos($oldFullPath, $rootDir) !== 0) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid old path']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($newFullPath)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'New name already exists']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt rename
|
||||||
|
if (rename($oldFullPath, $newFullPath)) {
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Rename failed']);
|
||||||
|
}
|
6
save_state.php
Normal file
6
save_state.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if ($input) {
|
||||||
|
file_put_contents('/home/surillya/.thos_state.json', json_encode($input));
|
||||||
|
}
|
||||||
|
?>
|
132
screensaver.html
Normal file
132
screensaver.html
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>THOS Screensaver</title>
|
||||||
|
<style>
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #000;
|
||||||
|
cursor: none;
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branding {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
text-align: right;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-tagline {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<canvas id="screensaver"></canvas>
|
||||||
|
<div class="branding">
|
||||||
|
<div class="brand-name">THOS</div>
|
||||||
|
<div class="brand-tagline">Clean made easy</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('screensaver');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
|
||||||
|
class Particle {
|
||||||
|
constructor() {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.x = Math.random() * canvas.width;
|
||||||
|
this.y = Math.random() * canvas.height;
|
||||||
|
this.radius = Math.random() * 3 + 1;
|
||||||
|
this.speedX = (Math.random() - 0.5) * 3;
|
||||||
|
this.speedY = (Math.random() - 0.5) * 3;
|
||||||
|
this.color = `hsl(${Math.random() * 360}, 70%, 50%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.x += this.speedX;
|
||||||
|
this.y += this.speedY;
|
||||||
|
|
||||||
|
if (this.x < 0) this.x = canvas.width;
|
||||||
|
if (this.x > canvas.width) this.x = 0;
|
||||||
|
if (this.y < 0) this.y = canvas.height;
|
||||||
|
if (this.y > canvas.height) this.y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = this.color;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const particles = [];
|
||||||
|
const particleCount = 200;
|
||||||
|
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
particles.push(new Particle());
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
particles.forEach(particle => {
|
||||||
|
particle.update();
|
||||||
|
particle.draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
const notifyParent = () => {
|
||||||
|
window.parent.postMessage({ type: "user_active" }, "*");
|
||||||
|
};
|
||||||
|
|
||||||
|
['mousemove', 'mousedown', 'keydown', 'touchstart', 'wheel'].forEach(event => {
|
||||||
|
document.addEventListener(event, notifyParent, { passive: true });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
22
server_command.php
Normal file
22
server_command.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
if (!isset($_GET['action'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo "No action given";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $_GET['action'];
|
||||||
|
switch ($action) {
|
||||||
|
case 'shutdown':
|
||||||
|
shell_exec('shutdown now');
|
||||||
|
echo "Shutting down...";
|
||||||
|
break;
|
||||||
|
case 'reboot':
|
||||||
|
shell_exec('reboot');
|
||||||
|
echo "Rebooting...";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
http_response_code(400);
|
||||||
|
echo "Unknown action";
|
||||||
|
}
|
||||||
|
?>
|
182
settings.php
Normal file
182
settings.php
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Settings</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: transparent;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="tailwind.es"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="text-white text-white p-6 md:p-12">
|
||||||
|
<div class="container mx-auto max-w-4xl">
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<h1
|
||||||
|
class="text-4xl font-bold text-gradient bg-gradient-to-r from-indigo-400 to-pink-500 bg-clip-text text-transparent">
|
||||||
|
System Settings
|
||||||
|
</h1>
|
||||||
|
<div class="text-3xl">⚙️</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="settingsForm" class="space-y-8">
|
||||||
|
<!-- Appearance Section -->
|
||||||
|
<div class="settings-section border-b border-gray-700 pb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-indigo-300 mb-4 pb-2">
|
||||||
|
Appearance
|
||||||
|
</h2>
|
||||||
|
<div class="grid md:grid-cols-2 gap-6">
|
||||||
|
<!-- It's so time consuming to add multipme themes - and everyone sane uses dark mode anyway -->
|
||||||
|
<div>
|
||||||
|
<label for="theme" class="block text-sm text-gray-300 mb-2">Theme</label>
|
||||||
|
<select id="theme"
|
||||||
|
class="w-full bg-gray-800 text-white rounded-lg px-4 py-2 border border-gray-700 focus:ring-2 focus:ring-indigo-500 transition">
|
||||||
|
<option value="theme-glassy-dark" selected>Glassy Dark</option>
|
||||||
|
<option value="theme-frosted-blue">Frosted</option>
|
||||||
|
<option value="theme-dreamy-pink">Dreamy</option>
|
||||||
|
<option value="theme-xp">XP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="accent" class="block text-sm text-gray-300 mb-2">Accent Color</label>
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<input type="color" id="accent"
|
||||||
|
class="w-16 h-12 rounded-lg border-2 border-gray-600 bg-gray-800">
|
||||||
|
<span class="text-gray-400 text-sm">Custom Accent</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="wallpaper" class="block text-sm text-gray-300 mb-2">Wallpaper</label>
|
||||||
|
<button type="button" onclick="window.parent.openApp('wallpapers.php', 'Wallpaper Selector')"
|
||||||
|
class="w-full bg-gray-800 text-white border border-gray-700 hover:bg-gray-700 font-medium px-4 py-2 rounded-lg transition flex items-center justify-between">
|
||||||
|
<span>Select Wallpaper</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-indigo-400" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||||
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Performance Section -->
|
||||||
|
<div class="settings-section border-b border-gray-700 pb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-indigo-300 mb-4 pb-2">
|
||||||
|
System Performance
|
||||||
|
</h2>
|
||||||
|
<div class="grid md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="screenTimeout" class="block text-sm text-gray-300 mb-2">Screen Timeout</label>
|
||||||
|
<select id="screenTimeout"
|
||||||
|
class="w-full bg-gray-800 text-white rounded-lg px-4 py-2 border border-gray-700 focus:ring-2 focus:ring-indigo-500 transition">
|
||||||
|
<option value="30">30 seconds</option>
|
||||||
|
<option value="60">1 minute</option>
|
||||||
|
<option value="180">3 minutes</option>
|
||||||
|
<option value="300">5 minutes</option>
|
||||||
|
<option value="600">10 minutes</option>
|
||||||
|
<option value="900">15 minutes</option>
|
||||||
|
<option value="1800">30 minutes</option>
|
||||||
|
<option value="3600">1 hour</option>
|
||||||
|
<option value="never">Never</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<!-- Will eventually actually add this, but requires stuff in .xinitrc and really don't want to do it rn -->
|
||||||
|
<!-- <div>
|
||||||
|
<label for="powerMode" class="block text-sm text-gray-300 mb-2">Power Mode</label>
|
||||||
|
<select id="powerMode" class="w-full bg-gray-800 text-white rounded-lg px-4 py-2 border border-gray-700 focus:ring-2 focus:ring-indigo-500 transition">
|
||||||
|
<option value="balanced" selected>Balanced</option>
|
||||||
|
<option value="performance">Performance</option>
|
||||||
|
<option value="powersave">Power Saving</option>
|
||||||
|
</select>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Media & Sound Section -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2 class="text-xl font-semibold text-indigo-300 mb-4 pb-2">
|
||||||
|
Media & Sound
|
||||||
|
</h2>
|
||||||
|
<div class="grid md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="musicEnabled" class="block text-sm text-gray-300 mb-2">Background Music</label>
|
||||||
|
<select id="musicEnabled"
|
||||||
|
class="w-full bg-gray-800 text-white rounded-lg px-4 py-2 border border-gray-700 focus:ring-2 focus:ring-indigo-500 transition">
|
||||||
|
<option value="on">Enabled</option>
|
||||||
|
<option value="off" selected>Disabled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="musicVolume" class="block text-sm text-gray-300 mb-2">Music Volume</label>
|
||||||
|
<input type="range" id="musicVolume" min="0" max="1" step="0.01"
|
||||||
|
class="w-full accent-indigo-500 h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex justify-between pt-6 border-t border-gray-700">
|
||||||
|
<button type="submit"
|
||||||
|
class="bg-gradient-to-r from-indigo-500 to-pink-500 hover:from-indigo-600 hover:to-pink-600 text-white font-semibold px-6 py-3 rounded-lg shadow-lg transform hover:scale-105 transition">
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="resetSettings()"
|
||||||
|
class="bg-gray-700 hover:bg-gray-600 text-gray-300 font-medium px-6 py-3 rounded-lg transition">
|
||||||
|
Reset to Defaults
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const themeInput = document.getElementById('theme');
|
||||||
|
const accentInput = document.getElementById('accent');
|
||||||
|
const wallpaperInput = document.getElementById('wallpaper');
|
||||||
|
// const musicURLInput = document.getElementById('music');
|
||||||
|
const musicEnabled = document.getElementById('musicEnabled');
|
||||||
|
const musicVolume = document.getElementById('musicVolume');
|
||||||
|
const screenTimeout = document.getElementById('screenTimeout');
|
||||||
|
|
||||||
|
const saved = JSON.parse(localStorage.getItem('settings')) || {};
|
||||||
|
if (saved.theme) themeInput.value = saved.theme;
|
||||||
|
if (saved.accentColor) accentInput.value = saved.accentColor;
|
||||||
|
// if (saved.music) musicURLInput.value = saved.music;
|
||||||
|
if (saved.musicEnabled) musicEnabled.value = saved.musicEnabled;
|
||||||
|
if (saved.musicVolume) musicVolume.value = saved.musicVolume;
|
||||||
|
screenTimeout.value = saved.screenTimeout || 180;
|
||||||
|
|
||||||
|
document.getElementById('settingsForm').addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let settings = {
|
||||||
|
theme: themeInput.value,
|
||||||
|
accentColor: accentInput.value,
|
||||||
|
wallpaper: saved.wallpaper,
|
||||||
|
// music: musicURLInput.value,
|
||||||
|
musicEnabled: musicEnabled.value,
|
||||||
|
musicVolume: musicVolume.value,
|
||||||
|
screenTimeout: parseInt(screenTimeout.value)
|
||||||
|
};
|
||||||
|
localStorage.setItem('settings', JSON.stringify(settings));
|
||||||
|
parent.postMessage({ type: 'applySettings', settings }, '*');
|
||||||
|
window.parent.notify("Settings", `Settings applied successfully!`, {
|
||||||
|
type: "success",
|
||||||
|
icon: "✅"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function resetSettings() {
|
||||||
|
if (confirm("Reset settings to default?")) {
|
||||||
|
localStorage.removeItem('settings');
|
||||||
|
await window.parent.saveTHOSState();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
46
sysstats.php
Normal file
46
sysstats.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
// sysstats.php — returns JSON with current system stats
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// CPU load
|
||||||
|
$load = shell_exec("uptime");
|
||||||
|
preg_match('/load average: ([0-9.]+), ([0-9.]+), ([0-9.]+)/', $load, $matches);
|
||||||
|
$cpuLoad1 = $matches[1] ?? null;
|
||||||
|
$cpuLoad5 = $matches[2] ?? null;
|
||||||
|
$cpuLoad15 = $matches[3] ?? null;
|
||||||
|
|
||||||
|
// RAM
|
||||||
|
$free = shell_exec("free -m");
|
||||||
|
preg_match('/Mem:\s+(\d+)\s+(\d+)\s+(\d+)/', $free, $memMatches);
|
||||||
|
$ramTotal = $memMatches[1] ?? null;
|
||||||
|
$ramUsed = $memMatches[2] ?? null;
|
||||||
|
$ramFree = $memMatches[3] ?? null;
|
||||||
|
|
||||||
|
// Disk (root)
|
||||||
|
$disk = shell_exec("df -h /");
|
||||||
|
$diskLines = explode("\n", trim($disk));
|
||||||
|
$diskInfo = isset($diskLines[1]) ? preg_split('/\s+/', $diskLines[1]) : [];
|
||||||
|
$diskSize = $diskInfo[1] ?? null;
|
||||||
|
$diskUsed = $diskInfo[2] ?? null;
|
||||||
|
$diskAvailable = $diskInfo[3] ?? null;
|
||||||
|
$diskPercent = $diskInfo[4] ?? null;
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'cpuLoad' => [
|
||||||
|
'1min' => $cpuLoad1,
|
||||||
|
'5min' => $cpuLoad5,
|
||||||
|
'15min' => $cpuLoad15,
|
||||||
|
],
|
||||||
|
'ram' => [
|
||||||
|
'total' => $ramTotal,
|
||||||
|
'used' => $ramUsed,
|
||||||
|
'free' => $ramFree,
|
||||||
|
],
|
||||||
|
'disk' => [
|
||||||
|
'size' => $diskSize,
|
||||||
|
'used' => $diskUsed,
|
||||||
|
'available' => $diskAvailable,
|
||||||
|
'percent' => $diskPercent,
|
||||||
|
],
|
||||||
|
]);
|
83
tailwind.es
Normal file
83
tailwind.es
Normal file
File diff suppressed because one or more lines are too long
199
tasks.php
Normal file
199
tasks.php
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<?php
|
||||||
|
$load = shell_exec("uptime");
|
||||||
|
preg_match('/load average: ([0-9.]+), ([0-9.]+), ([0-9.]+)/', $load, $matches);
|
||||||
|
$cpuLoad1 = $matches[1] ?? 'N/A';
|
||||||
|
$cpuLoad5 = $matches[2] ?? 'N/A';
|
||||||
|
$cpuLoad15 = $matches[3] ?? 'N/A';
|
||||||
|
|
||||||
|
$free = shell_exec("free -m");
|
||||||
|
preg_match('/Mem:\s+(\d+)\s+(\d+)\s+(\d+)/', $free, $memMatches);
|
||||||
|
$ramTotal = $memMatches[1] ?? 'N/A';
|
||||||
|
$ramUsed = $memMatches[2] ?? 'N/A';
|
||||||
|
$ramFree = $memMatches[3] ?? 'N/A';
|
||||||
|
|
||||||
|
$disk = shell_exec("df -h /");
|
||||||
|
$diskLines = explode("\n", trim($disk));
|
||||||
|
$diskInfo = isset($diskLines[1]) ? preg_split('/\s+/', $diskLines[1]) : [];
|
||||||
|
$diskSize = $diskInfo[1] ?? 'N/A';
|
||||||
|
$diskUsed = $diskInfo[2] ?? 'N/A';
|
||||||
|
$diskAvailable = $diskInfo[3] ?? 'N/A';
|
||||||
|
$diskPercent = $diskInfo[4] ?? 'N/A';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>THOS Tasks</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
color: #eee;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.system-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
background: #222;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 0 8px var(--accent);
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.info-box h3 {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.info-box p {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: #181818;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #2e2e2e;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: var(--accent);
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #222;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>THOS Tasks</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>CPU 1m</th>
|
||||||
|
<th>CPU 5m</th>
|
||||||
|
<th>CPU 15m</th>
|
||||||
|
<th>RAM Usage</th>
|
||||||
|
<th>Disk Usage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td id="cpu1">Loading...</p>
|
||||||
|
<td id="cpu5">Loading...</p>
|
||||||
|
<td id="cpu15">Loading...</p>
|
||||||
|
<td id="ram">Loading...</p>
|
||||||
|
<td id="disk">Loading...</p>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br><hr><br>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="task-list">
|
||||||
|
<tr><td colspan="3">Loading tasks...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function updateSystemStats() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('sysstats.php');
|
||||||
|
if (!res.ok) throw new Error('Network response not ok');
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
document.getElementById('cpu1').textContent = data.cpuLoad['1min'] ?? 'N/A';
|
||||||
|
document.getElementById('cpu5').textContent = data.cpuLoad['5min'] ?? 'N/A';
|
||||||
|
document.getElementById('cpu15').textContent = data.cpuLoad['15min'] ?? 'N/A';
|
||||||
|
|
||||||
|
document.getElementById('ram').textContent = `${data.ram.used} MB / ${data.ram.total} MB`;
|
||||||
|
document.getElementById('disk').textContent = `${data.disk.used} / ${data.disk.size} (${data.disk.percent})`;
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Failed to update system stats:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function basename(inputPath) {
|
||||||
|
if (!inputPath) return '';
|
||||||
|
|
||||||
|
// Remove query string and fragment
|
||||||
|
const cleanPath = inputPath.split(/[?#]/)[0];
|
||||||
|
|
||||||
|
// Extract just the filename (after the last slash)
|
||||||
|
const segments = cleanPath.split('/');
|
||||||
|
const fileName = segments.pop() || '';
|
||||||
|
|
||||||
|
// Remove extension
|
||||||
|
return fileName.replace(/\.[^.]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function refreshTasks() {
|
||||||
|
const taskList = document.getElementById('task-list');
|
||||||
|
const parent = window.parent;
|
||||||
|
if (!parent || typeof parent.getTaskList !== 'function') {
|
||||||
|
taskList.innerHTML = '<tr><td colspan="3">Unable to fetch task list.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = parent.getTaskList();
|
||||||
|
if (!tasks.length) {
|
||||||
|
taskList.innerHTML = '<tr><td colspan="3">No active tasks.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskList.innerHTML = '';
|
||||||
|
tasks.forEach(task => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${task.title}</td>
|
||||||
|
<td>${basename(task.url)}</td>
|
||||||
|
<td>
|
||||||
|
<button onclick="window.parent.focusApp('${task.id}')">Focus</button>
|
||||||
|
<button onclick="window.parent.closeApp('${task.id}')">Close</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
taskList.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
refreshTasks();
|
||||||
|
updateSystemStats();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
refreshTasks();
|
||||||
|
updateSystemStats();
|
||||||
|
|
||||||
|
document.documentElement.style.setProperty('--accent', window.parent.THOS.getAllSettings().accentColor || '#ff69b4');
|
||||||
|
</script>
|
||||||
|
</body>
|
114
thp.php
Normal file
114
thp.php
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
function e($string) {
|
||||||
|
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once "vfs.php";
|
||||||
|
$appPath = resolve_path($_GET['q'] ?? '');
|
||||||
|
$verified = ($_GET['v'] ?? '0') === '1';
|
||||||
|
|
||||||
|
if (strpos($appPath, '..') !== false || !preg_match('/\.thp$/i', $appPath)) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit("Invalid app package.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($appPath)) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit("App package not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_layout($content) {
|
||||||
|
echo <<<HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Install App</title>
|
||||||
|
<script src="tailwind.es"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
@apply bg-gray-950 text-white font-sans;
|
||||||
|
}
|
||||||
|
.glow {
|
||||||
|
text-shadow: 0 0 8px rgba(255,255,255,0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen flex items-center justify-center px-4">
|
||||||
|
<div class="bg-gray-900 border border-pink-400 rounded-2xl p-6 shadow-2xl max-w-lg w-full space-y-5">
|
||||||
|
{$content}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$verified) {
|
||||||
|
$pathEncoded = urlencode(virtualize_path($appPath));
|
||||||
|
$content = <<<HTML
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<svg class="w-8 h-8 text-yellow-400 animate-pulse" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M12 4.5c.88 0 1.74.25 2.5.72.76.47 1.37 1.14 1.8 1.92A5.96 5.96 0 0118 12a5.96 5.96 0 01-1.2 3.86 5.96 5.96 0 01-1.8 1.92A5.96 5.96 0 0112 18a5.96 5.96 0 01-3.86-1.2 5.96 5.96 0 01-1.92-1.8A5.96 5.96 0 016 12c0-.88.25-1.74.72-2.5.47-.76 1.14-1.37 1.92-1.8A5.96 5.96 0 0112 4.5z" />
|
||||||
|
</svg>
|
||||||
|
<h2 class="text-xl font-bold text-yellow-400 glow">Unverified App Installation</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-300">
|
||||||
|
This app hasn’t been verified by THOS. Installing untrusted packages may put your system at risk.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-300">Only continue if you trust this app and its source.</p>
|
||||||
|
<a href="?v=1&q={$pathEncoded}" class="inline-block mt-4 px-4 py-2 text-sm font-semibold bg-yellow-500 text-black rounded hover:bg-yellow-400 transition">
|
||||||
|
Install Anyway
|
||||||
|
</a>
|
||||||
|
HTML;
|
||||||
|
render_layout($content);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['confirm'] ?? '') === '1') {
|
||||||
|
require_once 'pkg.php';
|
||||||
|
$result = installPackage($appPath, __DIR__ . "/apps/");
|
||||||
|
$content = $result
|
||||||
|
? <<<HTML
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<svg class="w-8 h-8 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
<h2 class="text-xl font-bold text-green-400 glow">Success!</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-300">The app has been installed successfully.</p>
|
||||||
|
<script>window.parent.reloadApps?.();</script>
|
||||||
|
HTML
|
||||||
|
: <<<HTML
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<svg class="w-8 h-8 text-red-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
<h2 class="text-xl font-bold text-red-400 glow">Installation Failed</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-300">Something went wrong. Check your server logs for more details.</p>
|
||||||
|
HTML;
|
||||||
|
|
||||||
|
render_layout($content);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm UI
|
||||||
|
$appName = basename($appPath);
|
||||||
|
$content = <<<HTML
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<svg class="w-8 h-8 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m0 14v1m8-8h1M4 12H3m15.364-6.364l.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l.707-.707" />
|
||||||
|
</svg>
|
||||||
|
<h2 class="text-xl font-bold text-blue-300 glow">Install {$appName}?</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-300">This app is ready to be installed into your system. Continue?</p>
|
||||||
|
<form method="POST" class="mt-4 flex justify-end space-x-2">
|
||||||
|
<input type="hidden" name="confirm" value="1">
|
||||||
|
<a href="/" class="px-4 py-2 text-sm bg-gray-700 rounded hover:bg-gray-600 transition">Cancel</a>
|
||||||
|
<button type="submit" class="px-4 py-2 text-sm font-semibold bg-blue-500 text-white rounded hover:bg-blue-400 transition">
|
||||||
|
Install Now
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
HTML;
|
||||||
|
|
||||||
|
render_layout($content);
|
26
uninstaller.php
Normal file
26
uninstaller.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
$id = $_GET['id'] ?? '';
|
||||||
|
if (!preg_match('/^[a-zA-Z0-9_\-]+$/', $id)) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit("Invalid app ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
$dir = __DIR__ . '/apps/' . $id;
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit("App not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively delete
|
||||||
|
function deleteDir($path) {
|
||||||
|
foreach (scandir($path) as $item) {
|
||||||
|
if ($item === '.' || $item === '..') continue;
|
||||||
|
$full = $path . '/' . $item;
|
||||||
|
is_dir($full) ? deleteDir($full) : unlink($full);
|
||||||
|
}
|
||||||
|
return rmdir($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDir($dir);
|
||||||
|
echo "App $id uninstalled.";
|
||||||
|
echo "<script>window.parent.reloadApps();</script>";
|
16
vfs.php
Normal file
16
vfs.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
require_once "config.php";
|
||||||
|
|
||||||
|
// Converts virtual path like "/Documents/file.txt" → real path like "/home/surillya/Documents/file.txt"
|
||||||
|
function resolve_path(string $virtualPath): string {
|
||||||
|
$virtualPath = str_replace('..', '', $virtualPath); // prevent traversal
|
||||||
|
return rtrim(REAL_ROOT, '/') . '/' . ltrim($virtualPath, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts real path like "/home/surillya/Documents/file.txt" → virtual path like "/Documents/file.txt"
|
||||||
|
function virtualize_path(string $realPath): string {
|
||||||
|
if (str_starts_with($realPath, REAL_ROOT)) {
|
||||||
|
return '/' . ltrim(substr($realPath, strlen(REAL_ROOT)), '/');
|
||||||
|
}
|
||||||
|
return $realPath; // fallback, possibly invalid or external
|
||||||
|
}
|
44
video.php
Normal file
44
video.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
require_once "vfs.php";
|
||||||
|
$file = resolve_path($_GET['q'] ?? '');
|
||||||
|
|
||||||
|
if (!file_exists($file) || !is_file($file)) {
|
||||||
|
die("File not found." . $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = htmlspecialchars(basename($file));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Now Playing: <?= $filename ?></title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 90%;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>🎵 <?= $filename ?></h2>
|
||||||
|
<video controls autoplay>
|
||||||
|
<source src="<?= "file.php?q=" . urlencode(virtualize_path($file)) ?>">
|
||||||
|
Video cannot be played on your system.
|
||||||
|
</video>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
49
wallpapers.php
Normal file
49
wallpapers.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Wallpaper Chooser</title>
|
||||||
|
<script src="tailwind.es"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="text-white p-6 font-sans">
|
||||||
|
<div class="max-w-5xl mx-auto">
|
||||||
|
<h1 class="text-3xl font-bold text-pink-400 mb-6">Wallpaper Picker</h1>
|
||||||
|
<div id="wallpapers" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||||
|
<?php
|
||||||
|
$wallpaperDir = "/home/surillya/Pictures/";
|
||||||
|
$images = glob($wallpaperDir . "*.{jpg,jpeg,png,gif,webp}", GLOB_BRACE);
|
||||||
|
foreach ($images as $img) {
|
||||||
|
require_once "vfs.php";
|
||||||
|
$path = virtualize_path(htmlspecialchars($img));
|
||||||
|
$url = "file.php?q=" . urlencode(virtualize_path($img));
|
||||||
|
echo <<<HTML
|
||||||
|
<div onclick="selectWallpaper('$path')" class="cursor-pointer hover:scale-105 transition transform duration-300 rounded-xl overflow-hidden shadow-lg border-2 border-transparent hover:border-pink-400">
|
||||||
|
<img src="$url" alt="Wallpaper" class="w-full h-40 object-cover">
|
||||||
|
</div>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let selectedWallpaper = '';
|
||||||
|
|
||||||
|
function selectWallpaper(path) {
|
||||||
|
selectedWallpaper = path;
|
||||||
|
let settings = JSON.parse(localStorage.getItem('settings')) || {};
|
||||||
|
settings.wallpaper = selectedWallpaper;
|
||||||
|
localStorage.setItem('settings', JSON.stringify(settings));
|
||||||
|
parent.postMessage({ type: 'applySettings', settings }, '*');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
13
wifi_list.php
Normal file
13
wifi_list.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Run nmcli to list Wi-Fi networks
|
||||||
|
$output = shell_exec("nmcli -t -f SSID device wifi list");
|
||||||
|
$lines = explode("\n", trim($output));
|
||||||
|
|
||||||
|
// Remove empty or duplicate SSIDs
|
||||||
|
$ssids = array_unique(array_filter($lines));
|
||||||
|
|
||||||
|
// Return as JSON
|
||||||
|
echo json_encode(array_values($ssids));
|
||||||
|
?>
|
Reference in New Issue
Block a user