ProjectX / frontend /index.html
EricaLuvGemma's picture
Rename index.html to frontend/index.html
0c2b508 verified
Raw
History Blame Contribute Delete
69 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>TreyOS</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
:root {
--bg: #eef0f8;
--surface: rgba(255,255,255,0.72);
--surface-hover: rgba(255,255,255,0.9);
--border: rgba(255,255,255,0.6);
--text: #1a1d2e;
--text-secondary: #6b7280;
--accent: #6366f1;
--accent2: #818cf8;
--accent3: #a5b4fc;
--danger: #ef4444;
--success: #22c55e;
--shadow: 0 4px 24px rgba(99,102,241,0.10);
--shadow-lg: 0 8px 40px rgba(99,102,241,0.16);
--radius: 20px;
--radius-sm: 12px;
--blur: blur(20px);
--font: 'Inter', system-ui, sans-serif;
--dock-h: 90px;
}
html, body {
height: 100%; width: 100%;
font-family: var(--font);
background: var(--bg);
overflow: hidden;
color: var(--text);
}
/* ─── SPLASH ─── */
#splash {
position: fixed; inset: 0; z-index: 9999;
background: linear-gradient(145deg, #dde3f8 0%, #eef0fb 40%, #d8dcf5 100%);
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 28px;
transition: opacity 0.6s ease, transform 0.6s ease;
}
#splash.hidden { opacity: 0; transform: scale(1.04); pointer-events: none; }
.splash-logo {
width: 120px; height: 120px;
background: rgba(255,255,255,0.6);
backdrop-filter: var(--blur);
border-radius: 50%;
border: 2px solid rgba(99,102,241,0.3);
display: flex; align-items: center; justify-content: center;
box-shadow: 0 0 60px rgba(99,102,241,0.25), 0 0 120px rgba(99,102,241,0.1);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%,100% { box-shadow: 0 0 60px rgba(99,102,241,0.25), 0 0 120px rgba(99,102,241,0.1); }
50% { box-shadow: 0 0 80px rgba(99,102,241,0.4), 0 0 160px rgba(99,102,241,0.15); }
}
.splash-t { font-size: 14px; color: #6366f1; font-weight: 500; }
.splash-t span { color: var(--text); font-weight: 700; }
.splash-bar { width: 200px; height: 4px; background: rgba(99,102,241,0.15); border-radius: 99px; overflow: hidden; }
.splash-fill { height: 100%; background: linear-gradient(90deg, #6366f1, #818cf8); border-radius: 99px; animation: load 1.8s ease forwards; }
@keyframes load { from { width: 0 } to { width: 100% } }
.splash-sub { font-size: 12px; color: var(--text-secondary); }
/* ─── WALLPAPER ─── */
#os {
position: fixed; inset: 0;
background: linear-gradient(160deg, #c7d2fe 0%, #e0e7ff 30%, #f0f4ff 60%, #dde8ff 100%);
display: flex; flex-direction: column;
overflow: hidden;
}
/* ─── STATUS BAR ─── */
.statusbar {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 20px 4px;
font-size: 12px; font-weight: 600;
color: #1a1d2e;
flex-shrink: 0;
}
.statusbar-right { display: flex; gap: 6px; align-items: center; }
.sb-icon { font-size: 11px; }
/* ─── SCREENS ─── */
.screen { display: none; flex: 1; overflow: hidden; flex-direction: column; }
.screen.active { display: flex; }
/* HOME SCREEN */
#screen-home {
padding: 8px 0 0;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.home-widgets {
display: grid; grid-template-columns: 1fr 1fr;
gap: 12px; padding: 8px 16px 4px;
}
.widget {
background: var(--surface);
backdrop-filter: var(--blur);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
box-shadow: var(--shadow);
}
.widget-welcome { grid-column: span 1; }
.widget-welcome .greeting { font-size: 11px; color: var(--accent); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; }
.widget-welcome h2 { font-size: 20px; font-weight: 700; line-height: 1.2; margin: 4px 0 8px; }
.widget-welcome h2 span { color: var(--accent); }
.widget-ai {
display: flex; align-items: center; gap: 8px;
background: rgba(99,102,241,0.08); border-radius: 10px;
padding: 8px 10px; font-size: 11px; color: var(--text-secondary);
}
.ai-orb {
width: 22px; height: 22px; border-radius: 50%;
background: conic-gradient(from 0deg, #6366f1, #818cf8, #a5b4fc, #6366f1);
flex-shrink: 0; animation: spin 4s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.widget-time { text-align: center; display: flex; flex-direction: column; justify-content: center; align-items: center; }
.widget-time .clock { font-size: 32px; font-weight: 300; letter-spacing: -1px; }
.widget-time .date-str { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
.widget-sys { grid-column: span 2; }
.sys-stats { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; }
.stat-item label { font-size: 10px; color: var(--text-secondary); font-weight: 500; display: block; margin-bottom: 4px; }
.stat-bar { height: 5px; background: rgba(99,102,241,0.12); border-radius: 99px; overflow: hidden; }
.stat-fill { height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--accent), var(--accent2)); }
.stat-val { font-size: 12px; font-weight: 600; margin-top: 3px; }
.home-search {
margin: 12px 16px 4px;
display: flex; align-items: center; gap: 10px;
background: var(--surface); backdrop-filter: var(--blur);
border: 1px solid var(--border); border-radius: 50px;
padding: 12px 18px; box-shadow: var(--shadow);
}
.home-search input {
border: none; background: transparent; font-size: 14px;
font-family: var(--font); color: var(--text); width: 100%;
outline: none;
}
.home-search input::placeholder { color: var(--text-secondary); }
.search-ai { width: 22px; height: 22px; background: linear-gradient(135deg, var(--accent), var(--accent2)); border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; cursor: pointer; }
.app-section { padding: 12px 16px; }
.section-label { font-size: 11px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 12px; }
.app-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.app-icon-wrap { display: flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer; }
.app-icon {
width: 60px; height: 60px; border-radius: 16px;
display: flex; align-items: center; justify-content: center;
font-size: 28px; box-shadow: var(--shadow);
transition: transform 0.15s ease, box-shadow 0.15s ease;
position: relative; overflow: hidden;
}
.app-icon:active { transform: scale(0.9); }
.app-icon::before {
content: ''; position: absolute; inset: 0;
background: linear-gradient(145deg, rgba(255,255,255,0.35) 0%, transparent 60%);
border-radius: inherit;
}
.app-label { font-size: 11px; font-weight: 500; color: var(--text); text-align: center; }
.ai-files { background: linear-gradient(145deg, #3b82f6, #60a5fa); }
.ai-browser { background: linear-gradient(145deg, #10b981, #34d399); }
.ai-editor { background: linear-gradient(145deg, #1a1d2e, #374151); }
.ai-settings { background: linear-gradient(145deg, #6b7280, #9ca3af); }
.ai-video { background: linear-gradient(145deg, #8b5cf6, #a78bfa); }
.ai-gallery { background: linear-gradient(145deg, #f59e0b, #fbbf24); }
.ai-terminal { background: linear-gradient(145deg, #111827, #1f2937); }
.ai-docker { background: linear-gradient(145deg, #0ea5e9, #38bdf8); }
/* ─── FILE MANAGER ─── */
#screen-files { background: #f8f9ff; }
.app-header {
display: flex; align-items: center; gap: 12px;
padding: 12px 16px 8px;
background: rgba(248,249,255,0.95);
backdrop-filter: var(--blur);
border-bottom: 1px solid rgba(0,0,0,0.06);
flex-shrink: 0;
}
.app-header-icon { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; }
.app-header h1 { font-size: 18px; font-weight: 700; flex: 1; }
.header-btn { width: 34px; height: 34px; border-radius: 10px; background: rgba(0,0,0,0.06); border: none; font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.files-layout { display: flex; flex: 1; overflow: hidden; }
.files-sidebar {
width: 140px; flex-shrink: 0;
padding: 10px 8px;
overflow-y: auto;
border-right: 1px solid rgba(0,0,0,0.06);
}
.sidebar-section { margin-bottom: 16px; }
.sidebar-label { font-size: 10px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.06em; padding: 0 4px 6px; display: block; }
.sidebar-item {
display: flex; align-items: center; gap: 7px;
padding: 8px 8px; border-radius: 10px; cursor: pointer;
font-size: 12px; font-weight: 500; color: var(--text);
transition: background 0.15s;
}
.sidebar-item:hover, .sidebar-item.active { background: rgba(99,102,241,0.1); color: var(--accent); }
.sidebar-item .si-icon { font-size: 14px; }
.storage-stat { margin: 8px; padding: 10px; background: rgba(99,102,241,0.08); border-radius: 12px; }
.storage-stat .st-label { font-size: 10px; color: var(--text-secondary); }
.storage-stat .st-bar { height: 4px; background: rgba(0,0,0,0.1); border-radius: 99px; margin: 5px 0; overflow: hidden; }
.storage-stat .st-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent2)); width: 50%; border-radius: 99px; }
.storage-stat .st-val { font-size: 11px; font-weight: 600; }
.files-main { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 10px 12px; }
.quick-access { margin-bottom: 16px; }
.qa-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.qa-item {
background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
padding: 12px; cursor: pointer; transition: transform 0.15s;
display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
backdrop-filter: var(--blur);
}
.qa-item:active { transform: scale(0.97); }
.qa-icon { font-size: 22px; }
.qa-name { font-size: 12px; font-weight: 600; }
.qa-count { font-size: 10px; color: var(--text-secondary); }
.file-list { display: flex; flex-direction: column; gap: 2px; }
.file-row {
display: flex; align-items: center; gap: 10px;
padding: 10px 10px; border-radius: 12px; cursor: pointer;
transition: background 0.12s;
}
.file-row:hover, .file-row:active { background: rgba(99,102,241,0.07); }
.file-icon { font-size: 22px; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 10px; background: rgba(0,0,0,0.04); flex-shrink: 0; }
.file-info { flex: 1; min-width: 0; }
.file-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-meta { font-size: 10px; color: var(--text-secondary); margin-top: 1px; }
.file-menu { font-size: 16px; color: var(--text-secondary); padding: 4px; border-radius: 6px; cursor: pointer; }
/* ─── EDITOR ─── */
#screen-editor { background: #0d1117; color: #e6edf3; }
#screen-editor .app-header { background: #161b22; border-bottom-color: #30363d; }
#screen-editor .app-header h1 { color: #e6edf3; }
#screen-editor .header-btn { background: rgba(255,255,255,0.08); color: #e6edf3; }
.editor-layout { display: flex; flex: 1; overflow: hidden; flex-direction: column; }
.editor-tabs { display: flex; background: #161b22; border-bottom: 1px solid #30363d; overflow-x: auto; flex-shrink: 0; }
.editor-tab { padding: 9px 14px; font-size: 12px; color: #8b949e; cursor: pointer; white-space: nowrap; border-right: 1px solid #30363d; display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.editor-tab.active { color: #e6edf3; background: #0d1117; border-bottom: 2px solid var(--accent); }
.editor-body { display: flex; flex: 1; overflow: hidden; }
.code-area { flex: 1; overflow: auto; padding: 16px; font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.7; color: #e6edf3; position: relative; }
.code-line { display: flex; gap: 16px; }
.ln { color: #3d444d; min-width: 24px; text-align: right; user-select: none; }
.kw { color: #ff7b72; }
.fn { color: #d2a8ff; }
.str { color: #a5d6ff; }
.cm { color: #8b949e; font-style: italic; }
.num { color: #79c0ff; }
.editor-terminal { height: 130px; background: #010409; border-top: 1px solid #30363d; padding: 10px 14px; font-family: 'Courier New', monospace; font-size: 12px; color: #7ee787; overflow-y: auto; flex-shrink: 0; }
.term-line { margin-bottom: 2px; }
.term-prompt { color: #56d364; }
.term-input { display: flex; align-items: center; gap: 6px; margin-top: 4px; }
.term-input input { background: transparent; border: none; color: #e6edf3; font-family: inherit; font-size: 12px; flex: 1; outline: none; }
/* ─── SETTINGS ─── */
#screen-settings { background: #f8f9ff; }
.settings-list { overflow-y: auto; flex: 1; padding: 10px 16px; }
.settings-section { margin-bottom: 20px; }
.settings-section-title { font-size: 11px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 8px; padding: 0 4px; }
.settings-card { background: var(--surface); backdrop-filter: var(--blur); border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; box-shadow: var(--shadow); }
.settings-row {
display: flex; align-items: center; gap: 12px;
padding: 13px 14px; border-bottom: 1px solid rgba(0,0,0,0.05); cursor: pointer;
transition: background 0.12s;
}
.settings-row:last-child { border-bottom: none; }
.settings-row:hover { background: rgba(99,102,241,0.04); }
.sr-icon { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
.sr-label { flex: 1; font-size: 13px; font-weight: 500; }
.sr-value { font-size: 12px; color: var(--text-secondary); }
.sr-arrow { color: var(--text-secondary); font-size: 12px; }
.toggle { width: 40px; height: 22px; background: #d1d5db; border-radius: 11px; position: relative; cursor: pointer; transition: background 0.2s; flex-shrink: 0; }
.toggle.on { background: var(--accent); }
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; background: white; border-radius: 50%; transition: left 0.2s; box-shadow: 0 1px 4px rgba(0,0,0,0.2); }
.toggle.on::after { left: 20px; }
.theme-row { display: flex; gap: 8px; padding: 12px 14px; flex-wrap: wrap; }
.theme-dot { width: 28px; height: 28px; border-radius: 50%; cursor: pointer; border: 3px solid transparent; transition: border 0.15s; }
.theme-dot.active { border-color: #1a1d2e; }
.server-info { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px; box-shadow: var(--shadow); }
.info-row { display: flex; justify-content: space-between; padding: 5px 0; font-size: 12px; border-bottom: 1px solid rgba(0,0,0,0.04); }
.info-row:last-child { border-bottom: none; }
.info-key { color: var(--text-secondary); }
.info-val { font-weight: 600; font-family: 'Courier New', monospace; }
/* ─── VIDEO ─── */
#screen-video { background: #000; color: white; }
#screen-video .app-header { background: rgba(0,0,0,0.8); border-bottom-color: rgba(255,255,255,0.1); }
#screen-video .app-header h1 { color: white; }
#screen-video .header-btn { background: rgba(255,255,255,0.12); color: white; }
.video-player-area { width: 100%; background: #111; position: relative; overflow: hidden; flex-shrink: 0; aspect-ratio: 16/9; }
.video-thumb { width: 100%; height: 100%; object-fit: cover; opacity: 0; }
.video-overlay {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
background: linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.6) 100%);
}
.video-play-btn {
width: 64px; height: 64px; background: rgba(255,255,255,0.2); backdrop-filter: blur(8px);
border-radius: 50%; border: 2px solid rgba(255,255,255,0.4); display: flex; align-items: center; justify-content: center;
cursor: pointer; font-size: 24px; transition: transform 0.15s;
}
.video-play-btn:hover { transform: scale(1.08); }
.video-controls { padding: 14px 16px; background: #111; }
.video-progress { height: 4px; background: rgba(255,255,255,0.2); border-radius: 99px; margin-bottom: 12px; overflow: hidden; }
.video-fill { height: 100%; background: var(--accent2); width: 35%; border-radius: 99px; }
.video-ctrl-row { display: flex; align-items: center; gap: 16px; }
.vc-btn { background: none; border: none; color: white; font-size: 18px; cursor: pointer; padding: 4px; }
.vc-time { font-size: 12px; color: rgba(255,255,255,0.7); flex: 1; }
.video-list { flex: 1; overflow-y: auto; padding: 8px 12px; }
.video-item {
display: flex; align-items: center; gap: 10px; padding: 10px 8px;
border-radius: 12px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.06);
transition: background 0.12s;
}
.video-item:hover { background: rgba(255,255,255,0.05); }
.video-thumb-mini { width: 72px; height: 44px; background: #222; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
.vi-name { font-size: 13px; font-weight: 500; margin-bottom: 3px; }
.vi-meta { font-size: 11px; color: rgba(255,255,255,0.4); }
/* ─── GALLERY ─── */
#screen-gallery { background: #f8f9ff; }
.gallery-toolbar { display: flex; gap: 8px; padding: 8px 16px; border-bottom: 1px solid rgba(0,0,0,0.06); overflow-x: auto; flex-shrink: 0; }
.gallery-tab { padding: 6px 14px; border-radius: 50px; font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap; background: transparent; border: 1.5px solid rgba(0,0,0,0.1); color: var(--text-secondary); transition: all 0.15s; }
.gallery-tab.active { background: var(--accent); color: white; border-color: var(--accent); }
.gallery-grid { flex: 1; overflow-y: auto; padding: 10px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px; }
.gallery-cell {
aspect-ratio: 1; border-radius: 6px; overflow: hidden;
background: linear-gradient(135deg, #e0e7ff, #c7d2fe);
display: flex; align-items: center; justify-content: center;
font-size: 28px; cursor: pointer;
transition: opacity 0.15s;
position: relative;
}
.gallery-cell:active { opacity: 0.8; }
.gallery-cell .cell-overlay { position: absolute; inset: 0; background: linear-gradient(to bottom, transparent 60%, rgba(0,0,0,0.3)); }
/* ─── BROWSER ─── */
#screen-browser { background: #f1f3f9; }
.browser-bar {
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
background: rgba(241,243,249,0.95); border-bottom: 1px solid rgba(0,0,0,0.08); flex-shrink: 0;
}
.url-bar {
flex: 1; background: white; border: 1.5px solid rgba(0,0,0,0.1);
border-radius: 50px; padding: 8px 14px; font-size: 13px;
font-family: var(--font); outline: none; color: var(--text);
}
.url-bar:focus { border-color: var(--accent); }
.browser-btn { width: 32px; height: 32px; border-radius: 8px; background: rgba(0,0,0,0.06); border: none; font-size: 14px; cursor: pointer; }
.browser-tabs { display: flex; gap: 2px; padding: 6px 12px 0; overflow-x: auto; flex-shrink: 0; }
.btab { padding: 6px 12px 7px; background: rgba(0,0,0,0.06); border-radius: 10px 10px 0 0; font-size: 12px; cursor: pointer; white-space: nowrap; }
.btab.active { background: white; }
.browser-content { flex: 1; background: white; overflow: hidden; position: relative; }
.browser-mock { padding: 20px; }
.bm-hero { width: 100%; height: 120px; background: linear-gradient(135deg, #6366f1, #818cf8); border-radius: 12px; margin-bottom: 14px; display: flex; align-items: center; justify-content: center; color: white; font-size: 18px; font-weight: 700; }
.bm-card { background: #f8f9ff; border: 1px solid rgba(0,0,0,0.08); border-radius: 10px; padding: 12px; margin-bottom: 10px; }
.bm-card h3 { font-size: 13px; font-weight: 600; margin-bottom: 5px; }
.bm-card p { font-size: 11px; color: var(--text-secondary); line-height: 1.5; }
/* ─── DOCK ─── */
.dock-spacer { height: var(--dock-h); flex-shrink: 0; }
.dock {
position: fixed; bottom: 0; left: 0; right: 0;
height: var(--dock-h);
padding: 8px 12px 18px;
display: flex; align-items: flex-end; justify-content: space-around;
z-index: 100;
}
.dock-bg {
position: absolute; inset: 0;
background: rgba(255,255,255,0.55);
backdrop-filter: blur(30px);
border-top: 1px solid rgba(255,255,255,0.6);
}
.dock-item {
position: relative; z-index: 1;
display: flex; flex-direction: column; align-items: center; gap: 4px;
cursor: pointer;
}
.dock-icon {
width: 52px; height: 52px; border-radius: 14px;
display: flex; align-items: center; justify-content: center;
font-size: 26px; box-shadow: 0 4px 16px rgba(0,0,0,0.12);
transition: transform 0.15s ease;
position: relative; overflow: hidden;
}
.dock-icon::before {
content: ''; position: absolute; inset: 0;
background: linear-gradient(145deg, rgba(255,255,255,0.3) 0%, transparent 60%);
}
.dock-icon:active { transform: scale(0.88) translateY(-4px); }
.dock-dot { width: 4px; height: 4px; border-radius: 50%; background: var(--text); opacity: 0.4; }
.dock-dot.active { opacity: 1; background: var(--accent); }
.notch {
width: 36px; height: 4px; background: rgba(0,0,0,0.2); border-radius: 99px;
margin: 0 auto 8px; display: none;
}
/* Animations */
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.screen.active { animation: slideUp 0.25s ease; }
/* Scrollbar */
::-webkit-scrollbar { width: 3px; height: 3px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 99px; }
.hidden { display: none !important; }
</style>
</head>
<body>
<!-- SPLASH -->
<div id="splash">
<div class="splash-logo">
<svg width="60" height="60" viewBox="0 0 60 60" fill="none">
<path d="M12 30 Q20 10 30 20 Q40 30 48 18" stroke="url(#sg)" stroke-width="5" stroke-linecap="round" fill="none"/>
<path d="M12 42 Q24 22 36 32 Q44 40 50 30" stroke="url(#sg2)" stroke-width="5" stroke-linecap="round" fill="none"/>
<defs>
<linearGradient id="sg" x1="12" y1="20" x2="48" y2="20" gradientUnits="userSpaceOnUse">
<stop stop-color="#6366f1"/><stop offset="1" stop-color="#a5b4fc"/>
</linearGradient>
<linearGradient id="sg2" x1="12" y1="36" x2="50" y2="36" gradientUnits="userSpaceOnUse">
<stop stop-color="#818cf8"/><stop offset="1" stop-color="#c7d2fe"/>
</linearGradient>
</defs>
</svg>
</div>
<div>
<div class="splash-t"><span>TreyOS</span></div>
</div>
<div class="splash-bar"><div class="splash-fill"></div></div>
<div class="splash-sub">Loading your experience...</div>
</div>
<!-- OS SHELL -->
<div id="os">
<!-- Status Bar -->
<div class="statusbar">
<span id="sb-time">10:30</span>
<div class="statusbar-right">
<span class="sb-icon">▲▲▲▲</span>
<span class="sb-icon"></span>
<span class="sb-icon">🔋</span>
<span>100%</span>
</div>
</div>
<!-- HOME SCREEN -->
<div class="screen active" id="screen-home">
<div class="home-widgets">
<div class="widget widget-welcome">
<div class="greeting">Good morning</div>
<h2>Welcome to<br><span>TreyOS</span></h2>
<div class="widget-ai">
<div class="ai-orb"></div>
<span>How can I help you?</span>
</div>
</div>
<div class="widget widget-time">
<div class="clock" id="clock">10:30</div>
<div class="date-str" id="datestr">Sun, Jun 28</div>
</div>
<div class="widget widget-sys">
<div class="section-label" style="margin-bottom:10px">System</div>
<div class="sys-stats">
<div class="stat-item">
<label>CPU</label>
<div class="stat-bar"><div class="stat-fill" id="cpu-bar" style="width:0%"></div></div>
<div class="stat-val" id="cpu-val"></div>
</div>
<div class="stat-item">
<label>RAM</label>
<div class="stat-bar"><div class="stat-fill" id="ram-bar" style="width:0%"></div></div>
<div class="stat-val" id="ram-val"></div>
</div>
<div class="stat-item">
<label>Disk</label>
<div class="stat-bar"><div class="stat-fill" id="disk-bar" style="width:0%"></div></div>
<div class="stat-val" id="disk-val"></div>
</div>
</div>
</div>
</div>
<div class="home-search">
<span>🔍</span>
<input type="text" placeholder="Search apps, files, and more...">
</div>
<div class="app-section">
<div class="section-label">Core Apps</div>
<div class="app-grid">
<div class="app-icon-wrap" onclick="openApp('files')">
<div class="app-icon ai-files">📁</div>
<div class="app-label">Files</div>
</div>
<div class="app-icon-wrap" onclick="openApp('browser')">
<div class="app-icon ai-browser">🌐</div>
<div class="app-label">Browser</div>
</div>
<div class="app-icon-wrap" onclick="openApp('editor')">
<div class="app-icon ai-editor">💻</div>
<div class="app-label">Editor</div>
</div>
<div class="app-icon-wrap" onclick="openApp('settings')">
<div class="app-icon ai-settings">⚙️</div>
<div class="app-label">Settings</div>
</div>
<div class="app-icon-wrap" onclick="openApp('video')">
<div class="app-icon ai-video">▶️</div>
<div class="app-label">Videos</div>
</div>
<div class="app-icon-wrap" onclick="openApp('gallery')">
<div class="app-icon ai-gallery">🖼️</div>
<div class="app-label">Gallery</div>
</div>
<div class="app-icon-wrap" onclick="openApp('terminal')">
<div class="app-icon ai-terminal" style="font-family:monospace;font-size:20px;color:#56d364">$_</div>
<div class="app-label">Terminal</div>
</div>
</div>
</div>
<div class="dock-spacer"></div>
</div>
<!-- FILE MANAGER -->
<div class="screen" id="screen-files">
<div class="app-header">
<div class="app-icon-wrap" onclick="goHome()" style="cursor:pointer">
<div style="font-size:20px"></div>
</div>
<div class="app-header-icon ai-files" style="font-size:18px">📁</div>
<h1>Files</h1>
<button class="header-btn">🔍</button>
<button class="header-btn"></button>
</div>
<div class="files-layout">
<div class="files-sidebar">
<div class="sidebar-section">
<span class="sidebar-label">Browse</span>
<div class="sidebar-item active" onclick="loadFiles('/')"><span class="si-icon">🏠</span> Root</div>
<div class="sidebar-item" onclick="loadFiles('/app')"><span class="si-icon">⚙️</span> App</div>
<div class="sidebar-item" onclick="loadFiles('/trey/backend')"><span class="si-icon">🐍</span> Backend</div>
<div class="sidebar-item" onclick="loadFiles('/trey/frontend')"><span class="si-icon">🌐</span> Frontend</div>
<div class="sidebar-item" onclick="loadFiles('/tmp')"><span class="si-icon">🗂️</span> Temp</div>
</div>
<div class="sidebar-section">
<span class="sidebar-label">Locations</span>
<div class="sidebar-item" onclick="loadFiles('/')"><span class="si-icon">💾</span> /</div>
<div class="sidebar-item" onclick="loadFiles('/app')"><span class="si-icon">📦</span> /app</div>
<div class="sidebar-item" onclick="loadFiles('/tmp')"><span class="si-icon">🗂️</span> /tmp</div>
<div class="sidebar-item" onclick="loadFiles('/home')"><span class="si-icon">🏠</span> /home</div>
</div>
<div class="storage-stat">
<div class="st-label">Storage</div>
<div class="st-bar"><div class="st-fill"></div></div>
<div class="st-val" id="storage-val">— GB</div>
</div>
</div>
<div class="files-main" id="files-main">
<div id="files-breadcrumb" style="font-size:11px;color:var(--text-secondary);padding:2px 2px 10px;font-family:monospace">/</div>
<div id="files-content">
<div style="text-align:center;padding:40px 0;color:var(--text-secondary);font-size:13px">Loading filesystem...</div>
</div>
<div style="height:80px"></div>
</div>
</div>
<div class="dock-spacer"></div>
</div>
<!-- CODE EDITOR -->
<div class="screen" id="screen-editor">
<div class="app-header">
<div onclick="goHome()" style="cursor:pointer;font-size:20px;color:#e6edf3;padding:4px"></div>
<div class="app-header-icon ai-terminal" style="font-size:16px;font-family:monospace;font-weight:700;color:#56d364">$_</div>
<h1>Editor</h1>
<button class="header-btn"></button>
<button class="header-btn"></button>
</div>
<div class="editor-layout">
<div class="editor-tabs" id="editor-tabs">
<div class="editor-tab active">Terminal</div>
</div>
<div class="editor-body">
<div class="code-area" id="code-area" style="display:none"></div>
</div>
<div class="editor-terminal" id="editor-terminal">
<div class="term-input">
<span class="term-prompt">$</span>
<input type="text" id="term-input" placeholder="Connecting to terminal...">
</div>
</div>
</div>
<div class="dock-spacer"></div>
</div>
<!-- SETTINGS -->
<div class="screen" id="screen-settings">
<div class="app-header">
<div onclick="goHome()" style="cursor:pointer;font-size:20px;padding:4px"></div>
<div class="app-header-icon ai-settings" style="font-size:18px">⚙️</div>
<h1>Settings</h1>
</div>
<div class="settings-list">
<div class="settings-section">
<div class="settings-section-title">Appearance</div>
<div class="settings-card">
<div class="settings-row">
<div class="sr-icon" style="background:#f0f4ff">🎨</div>
<div class="sr-label">Accent Color</div>
<div class="theme-row" style="padding:0;gap:6px">
<div class="theme-dot active" style="background:#6366f1" onclick="setAccent('#6366f1',this)"></div>
<div class="theme-dot" style="background:#ec4899" onclick="setAccent('#ec4899',this)"></div>
<div class="theme-dot" style="background:#10b981" onclick="setAccent('#10b981',this)"></div>
<div class="theme-dot" style="background:#f59e0b" onclick="setAccent('#f59e0b',this)"></div>
</div>
</div>
<div class="settings-row">
<div class="sr-icon" style="background:#f0f4ff">🌙</div>
<div class="sr-label">Dark Mode</div>
<div class="toggle" id="dark-toggle" onclick="this.classList.toggle('on')"></div>
</div>
<div class="settings-row">
<div class="sr-icon" style="background:#f0f4ff">🖼️</div>
<div class="sr-label">Wallpaper</div>
<div class="sr-value">Periwinkle</div>
<div class="sr-arrow"></div>
</div>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">System</div>
<div class="settings-card">
<div class="settings-row">
<div class="sr-icon" style="background:#fff0f0">🔒</div>
<div class="sr-label">Security</div>
<div class="sr-arrow"></div>
</div>
<div class="settings-row">
<div class="sr-icon" style="background:#f0fff4">👥</div>
<div class="sr-label">Users</div>
<div class="sr-value">1 active</div>
<div class="sr-arrow"></div>
</div>
<div class="settings-row">
<div class="sr-icon" style="background:#f0f0ff">🐳</div>
<div class="sr-label">Docker</div>
<div class="toggle on" onclick="this.classList.toggle('on')"></div>
</div>
<div class="settings-row">
<div class="sr-icon" style="background:#fffbf0">💾</div>
<div class="sr-label">Storage</div>
<div class="sr-value">128 / 256 GB</div>
<div class="sr-arrow"></div>
</div>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">Server Info</div>
<div class="server-info">
<div class="info-row"><span class="info-key">Hostname</span><span class="info-val" id="info-hostname"></span></div>
<div class="info-row"><span class="info-key">OS</span><span class="info-val" id="info-os"></span></div>
<div class="info-row"><span class="info-key">Kernel</span><span class="info-val" id="info-kernel"></span></div>
<div class="info-row"><span class="info-key">Uptime</span><span class="info-val" id="info-uptime"></span></div>
<div class="info-row"><span class="info-key">TreyOS</span><span class="info-val">v1.0.0</span></div>
<div class="info-row"><span class="info-key">IP</span><span class="info-val" id="info-ip"></span></div>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">About</div>
<div class="settings-card">
<div class="settings-row">
<div class="sr-icon" style="background:#f0f4ff">ℹ️</div>
<div class="sr-label">TreyOS Version</div>
<div class="sr-value">1.0.0</div>
</div>
<div class="settings-row">
<div class="sr-icon" style="background:#f0f4ff">📦</div>
<div class="sr-label">App Store</div>
<div class="sr-arrow"></div>
</div>
</div>
</div>
<div style="height:80px"></div>
</div>
<div class="dock-spacer"></div>
</div>
<!-- VIDEO PLAYER -->
<div class="screen" id="screen-video">
<div class="app-header">
<div onclick="goHome()" style="cursor:pointer;font-size:20px;color:white;padding:4px"></div>
<div class="app-header-icon ai-video" style="font-size:18px">▶️</div>
<h1>Videos</h1>
<button class="header-btn"></button>
</div>
<div class="video-player-area">
<div class="video-overlay">
<div class="video-play-btn"></div>
</div>
<div id="video-now-playing" style="position:absolute;bottom:0;left:0;right:0;padding:10px 14px;background:linear-gradient(transparent,rgba(0,0,0,0.7));display:none">
<div id="video-title" style="font-size:13px;font-weight:600;color:white"></div>
<div id="video-info" style="font-size:10px;color:rgba(255,255,255,0.6)"></div>
</div>
</div>
<div class="video-controls">
<div class="video-progress"><div class="video-fill"></div></div>
<div class="video-ctrl-row">
<button class="vc-btn"></button>
<button class="vc-btn"></button>
<button class="vc-btn" style="font-size:24px"></button>
<button class="vc-btn"></button>
<button class="vc-btn"></button>
<span class="vc-time">3:29 / 9:56</span>
<button class="vc-btn">📋</button>
<button class="vc-btn"></button>
</div>
</div>
<div class="video-list" id="video-list">
<div style="text-align:center;padding:40px 0;color:rgba(255,255,255,0.3);font-size:13px">Loading videos...</div>
</div>
<div class="dock-spacer"></div>
</div>
<!-- GALLERY -->
<div class="screen" id="screen-gallery">
<div class="app-header">
<div onclick="goHome()" style="cursor:pointer;font-size:20px;padding:4px"></div>
<div class="app-header-icon ai-gallery" style="font-size:18px">🖼️</div>
<h1>Gallery</h1>
<button class="header-btn">🔍</button>
<button class="header-btn"></button>
</div>
<div class="gallery-toolbar">
<div class="gallery-tab active">All</div>
<div class="gallery-tab">Photos</div>
<div class="gallery-tab">Videos</div>
<div class="gallery-tab">Screenshots</div>
<div class="gallery-tab">Downloads</div>
</div>
<div class="gallery-grid" id="gallery-grid">
<div style="grid-column:span 3;text-align:center;padding:40px 0;color:var(--text-secondary);font-size:13px">Loading images...</div>
</div>
<div class="dock-spacer"></div>
</div>
<!-- BROWSER -->
<div class="screen" id="screen-browser">
<div class="app-header">
<div onclick="goHome()" style="cursor:pointer;font-size:20px;padding:4px"></div>
<div class="app-header-icon ai-browser" style="font-size:18px">🌐</div>
<h1>Browser</h1>
<button class="header-btn"></button>
</div>
<div class="browser-tabs">
<div class="btab active">🌐 New Tab</div>
<div class="btab"></div>
</div>
<div class="browser-bar">
<button class="browser-btn"></button>
<button class="browser-btn"></button>
<input class="url-bar" type="text" value="https://localhost:8000" placeholder="Search or enter URL">
<button class="browser-btn"></button>
</div>
<div class="browser-content" style="display:flex;align-items:center;justify-content:center;flex:1;background:#1a1a2e;">
<div style="text-align:center;color:rgba(255,255,255,0.3)">
<div style="font-size:32px;margin-bottom:12px">🌐</div>
<div style="font-size:13px">Enter a URL above and press Enter</div>
<div style="font-size:11px;margin-top:6px;opacity:0.6">Browser requires Playwright backend</div>
</div>
</div>
<div class="dock-spacer"></div>
</div>
<!-- TERMINAL -->
<div class="screen" id="screen-terminal">
<div class="app-header" style="background:#161b22;border-bottom-color:#30363d;">
<div onclick="goHome()" style="cursor:pointer;font-size:20px;color:#e6edf3;padding:4px"></div>
<div class="app-header-icon ai-terminal" style="font-family:monospace;font-size:14px;font-weight:700;color:#56d364">$_</div>
<h1 style="color:#e6edf3">Terminal</h1>
</div>
<div style="flex:1;display:flex;flex-direction:column;background:#010409;overflow:hidden;">
<div id="terminal-output" style="flex:1;overflow-y:auto;padding:12px 14px;font-family:'Courier New',monospace;font-size:13px;color:#7ee787;"></div>
<div style="display:flex;align-items:center;gap:8px;padding:10px 14px;border-top:1px solid #30363d;background:#0d1117;">
<span style="color:#56d364;font-family:monospace;font-size:13px">$</span>
<input id="terminal-input" type="text" placeholder="Connecting..."
style="flex:1;background:transparent;border:none;color:#e6edf3;font-family:'Courier New',monospace;font-size:13px;outline:none;">
</div>
</div>
<div class="dock-spacer"></div>
</div>
<!-- DOCK -->
<div class="dock">
<div class="dock-bg"></div>
<div class="dock-item" onclick="openApp('home')">
<div class="dock-icon ai-files">🏠</div>
<div class="dock-dot" id="dot-home"></div>
</div>
<div class="dock-item" onclick="openApp('files')">
<div class="dock-icon ai-files">📁</div>
<div class="dock-dot" id="dot-files"></div>
</div>
<div class="dock-item" onclick="openApp('editor')">
<div class="dock-icon ai-terminal" style="font-family:monospace;font-size:16px;color:#56d364">$_</div>
<div class="dock-dot" id="dot-editor"></div>
</div>
<div class="dock-item" onclick="openApp('browser')">
<div class="dock-icon ai-browser">🌐</div>
<div class="dock-dot" id="dot-browser"></div>
</div>
<div class="dock-item" onclick="openApp('terminal')">
<div class="dock-icon ai-terminal" style="font-family:monospace;font-size:18px;color:#56d364">$_</div>
<div class="dock-dot" id="dot-terminal"></div>
</div>
<div class="dock-item" onclick="openApp('settings')">
<div class="dock-icon ai-settings">⚙️</div>
<div class="dock-dot" id="dot-settings"></div>
</div>
</div>
</div>
<script>
// Clock
function updateClock() {
const now = new Date();
const t = now.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
const d = now.toLocaleDateString([], {weekday:'short',month:'short',day:'numeric'});
document.getElementById('clock').textContent = t;
document.getElementById('datestr').textContent = d;
document.getElementById('sb-time').textContent = t;
}
updateClock();
setInterval(updateClock, 10000);
// Splash
setTimeout(() => {
document.getElementById('splash').classList.add('hidden');
}, 2000);
// Navigation
let current = 'home';
function openApp(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.dock-dot').forEach(d => d.classList.remove('active'));
const target = id === 'home' ? 'screen-home' : `screen-${id}`;
const el = document.getElementById(target);
if (el) el.classList.add('active');
const dot = document.getElementById(`dot-${id}`);
if (dot) dot.classList.add('active');
const homeDot = document.getElementById('dot-home');
if (id === 'home' && homeDot) homeDot.classList.add('active');
current = id;
}
function goHome() { openApp('home'); }
// Gallery tabs
document.querySelectorAll('.gallery-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.gallery-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
});
// Accent color
function setAccent(color, el) {
document.documentElement.style.setProperty('--accent', color);
document.querySelectorAll('.theme-dot').forEach(d => d.classList.remove('active'));
el.classList.add('active');
}
// ── BACKEND ───────────────────────────────────────────────────────────────
const API = window.location.origin;
const WS = API.replace(/^http/, 'ws');
// ── UTILS ─────────────────────────────────────────────────────────────────
function formatBytes(b) {
if (!b) return '0 B';
if (b > 1e9) return (b/1e9).toFixed(1)+' GB';
if (b > 1e6) return (b/1e6).toFixed(1)+' MB';
if (b > 1e3) return (b/1e3).toFixed(1)+' KB';
return b+' B';
}
function fileIcon(ext) {
const m = {'.pdf':'📕','.doc':'📘','.docx':'📘','.xls':'📗','.xlsx':'📗',
'.jpg':'🖼️','.jpeg':'🖼️','.png':'🖼️','.gif':'🖼️','.webp':'🖼️',
'.mp4':'🎬','.mkv':'🎬','.mov':'🎬','.webm':'🎬','.avi':'🎬',
'.mp3':'🎵','.wav':'🎵','.flac':'🎵','.ogg':'🎵',
'.py':'🐍','.js':'📜','.ts':'📜','.json':'📋','.sh':'⚡',
'.zip':'📦','.tar':'📦','.gz':'📦','.txt':'📝','.md':'📝',
'.html':'🌐','.css':'🎨','.env':'🔐','.yml':'⚙️','.yaml':'⚙️'};
return m[ext] || '📄';
}
function isImage(ext) {
return ['.jpg','.jpeg','.png','.gif','.webp','.bmp','.svg'].includes(ext);
}
function isVideo(ext) {
return ['.mp4','.mkv','.webm','.mov','.avi','.m4v'].includes(ext);
}
// ── SYSTEM STATS ──────────────────────────────────────────────────────────
function connectSystemStats() {
const ws = new WebSocket(`${WS}/ws/system?interval=3`);
ws.onmessage = e => {
try {
const msg = JSON.parse(e.data);
if (msg.type !== 'stats') return;
const { cpu, memory, disk } = msg.data;
// Home screen bars
const cb = document.getElementById('cpu-bar'); if(cb) cb.style.width = cpu.percent+'%';
const cv = document.getElementById('cpu-val'); if(cv) cv.textContent = cpu.percent+'%';
const rb = document.getElementById('ram-bar'); if(rb) rb.style.width = memory.percent+'%';
const rv = document.getElementById('ram-val'); if(rv) rv.textContent = memory.used_gb+' GB';
const db = document.getElementById('disk-bar'); if(db) db.style.width = disk.percent+'%';
const dv = document.getElementById('disk-val'); if(dv) dv.textContent = disk.used_gb+' GB';
// Settings server info
const d = msg.data;
const set = (id, v) => { const el = document.getElementById(id); if(el) el.textContent = v; };
set('info-hostname', d.hostname);
set('info-os', d.os);
set('info-kernel', d.os);
set('info-uptime', d.uptime);
// Files sidebar storage
const sf = document.getElementById('storage-fill');
const sv = document.getElementById('storage-val');
if(sf) sf.style.width = disk.percent+'%';
if(sv) sv.textContent = `${disk.used_gb} / ${disk.total_gb} GB`;
} catch(_) {}
};
ws.onclose = () => setTimeout(connectSystemStats, 5000);
ws.onerror = () => ws.close();
}
connectSystemStats();
// ── FILE MANAGER ──────────────────────────────────────────────────────────
let currentPath = '/';
async function loadFiles(path) {
path = path || '/';
currentPath = path;
const breadcrumb = document.getElementById('files-breadcrumb');
const content = document.getElementById('files-content');
if (!content) return;
if (breadcrumb) breadcrumb.textContent = path;
content.innerHTML = '<div style="text-align:center;padding:30px 0;color:var(--text-secondary);font-size:12px">Loading...</div>';
try {
const res = await fetch(`${API}/api/files?path=${encodeURIComponent(path)}`);
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
if (!data.entries.length) {
content.innerHTML = '<div style="text-align:center;padding:30px 0;color:var(--text-secondary);font-size:12px">Empty folder</div>';
return;
}
// Back button if not root
let html = '';
if (data.parent && path !== '/') {
html += `<div class="file-row" onclick="loadFiles('${data.parent}')">
<div class="file-icon">⬆️</div>
<div class="file-info"><div class="file-name">..</div><div class="file-meta">Parent directory</div></div>
</div>`;
}
html += data.entries.map(e => {
const click = e.type === 'dir'
? `loadFiles('${e.path.replace(/'/g,"\\'")}')`
: `openFile('${e.path.replace(/'/g,"\\'")}','${e.extension||''}')`;
return `<div class="file-row" onclick="${click}">
<div class="file-icon">${e.type === 'dir' ? '📁' : fileIcon(e.extension)}</div>
<div class="file-info">
<div class="file-name">${e.name}</div>
<div class="file-meta">${e.type === 'dir' ? 'Folder' : formatBytes(e.size)} • ${e.modified ? e.modified.split('T')[0] : ''}</div>
</div>
<div class="file-menu"></div>
</div>`;
}).join('');
content.innerHTML = '<div class="file-list">'+html+'</div>';
} catch(err) {
content.innerHTML = `<div style="text-align:center;padding:30px;color:#ef4444;font-size:12px">Error: ${err.message}</div>`;
}
}
function openFile(path, ext) {
if (isImage(ext)) {
window.open(`${API}/api/files/download?path=${encodeURIComponent(path)}`, '_blank');
} else if (isVideo(ext)) {
openApp('video');
playVideo(path);
} else {
// Open in editor
openApp('editor');
loadFileInEditor(path);
}
}
// ── GALLERY ───────────────────────────────────────────────────────────────
async function loadGallery(filterExt = null) {
const grid = document.getElementById('gallery-grid');
if (!grid) return;
grid.innerHTML = '<div style="grid-column:span 3;text-align:center;padding:30px 0;color:var(--text-secondary);font-size:12px">Scanning for images...</div>';
try {
// Search entire filesystem for images
const exts = filterExt
? [filterExt]
: ['.jpg','.jpeg','.png','.gif','.webp','.bmp'];
const results = [];
for (const ext of exts) {
const res = await fetch(`${API}/api/files/search?path=/&query=${ext}&limit=30`);
if (res.ok) {
const data = await res.json();
results.push(...data.results.filter(r => r.type === 'file' && isImage(r.extension)));
}
}
if (!results.length) {
grid.innerHTML = '<div style="grid-column:span 3;text-align:center;padding:40px 0;color:var(--text-secondary);font-size:13px">No images found on this system</div>';
return;
}
grid.innerHTML = results.map(img => `
<div class="gallery-cell" onclick="window.open('${API}/api/files/download?path=${encodeURIComponent(img.path)}','_blank')"
style="background:#1a1a2e;position:relative;">
<img src="${API}/api/files/download?path=${encodeURIComponent(img.path)}"
style="width:100%;height:100%;object-fit:cover;position:absolute;inset:0;"
onerror="this.style.display='none'"
loading="lazy">
<div class="cell-overlay"></div>
</div>`).join('');
} catch(err) {
grid.innerHTML = `<div style="grid-column:span 3;text-align:center;padding:30px;color:#ef4444;font-size:12px">Error: ${err.message}</div>`;
}
}
// Gallery tab filtering
document.querySelectorAll('.gallery-tab').forEach((tab, i) => {
tab.addEventListener('click', () => {
document.querySelectorAll('.gallery-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
});
// ── VIDEO PLAYER ──────────────────────────────────────────────────────────
async function loadVideoLibrary() {
const list = document.getElementById('video-list');
if (!list) return;
list.innerHTML = '<div style="text-align:center;padding:20px;color:rgba(255,255,255,0.3);font-size:12px">Scanning for videos...</div>';
try {
const res = await fetch(`${API}/api/files/search?path=/&query=.mp4&limit=20`);
const data = await res.json();
const videos = data.results.filter(r => isVideo(r.extension));
// Also search for mkv and webm
const res2 = await fetch(`${API}/api/files/search?path=/&query=.mkv&limit=10`);
const data2 = await res2.json();
videos.push(...data2.results.filter(r => isVideo(r.extension)));
if (!videos.length) {
list.innerHTML = '<div style="text-align:center;padding:40px;color:rgba(255,255,255,0.3);font-size:13px">No videos found on this system</div>';
return;
}
list.innerHTML = '<div style="font-size:11px;font-weight:600;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:0.06em;padding:8px 8px 4px">Library</div>'
+ videos.map(v => `
<div class="video-item" onclick="playVideo('${v.path.replace(/'/g,"\\'")}','${v.name}')">
<div class="video-thumb-mini">🎬</div>
<div>
<div class="vi-name">${v.name}</div>
<div class="vi-meta">${formatBytes(v.size)} • ${v.modified ? v.modified.split('T')[0] : ''}</div>
</div>
</div>`).join('');
} catch(err) {
list.innerHTML = `<div style="text-align:center;padding:20px;color:#ef4444;font-size:12px">Error: ${err.message}</div>`;
}
}
function playVideo(path, name) {
const url = `${API}/api/files/download?path=${encodeURIComponent(path)}`;
const overlay = document.querySelector('.video-overlay');
const area = document.querySelector('.video-player-area');
// Remove old video element
const old = area.querySelector('video');
if (old) old.remove();
// Create real video element
const vid = document.createElement('video');
vid.src = url;
vid.controls = false;
vid.style = 'position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000';
area.appendChild(vid);
if (overlay) overlay.style.display = 'none';
// Update now playing
document.getElementById('video-title').textContent = name || path.split('/').pop();
document.getElementById('video-now-playing').style.display = 'block';
vid.play();
// Wire play button
document.querySelector('.video-play-btn').onclick = () => vid.paused ? vid.play() : vid.pause();
}
// ── EDITOR / TERMINAL ─────────────────────────────────────────────────────
async function loadFileInEditor(path) {
const area = document.getElementById('code-area');
if (!area) return;
area.style.display = 'block';
area.textContent = 'Loading...';
try {
const res = await fetch(`${API}/api/files/read?path=${encodeURIComponent(path)}`);
const data = await res.json();
area.textContent = data.content;
} catch(e) {
area.textContent = 'Could not read file.';
}
}
// Terminal WebSocket
let termWs = null;
let termReady = false;
function connectTerminal() {
const terminal = document.getElementById('editor-terminal');
const input = document.getElementById('term-input');
if (!input) return;
termWs = new WebSocket(`${WS}/ws/terminal?cwd=/`);
termWs.onopen = () => {
termReady = true;
input.placeholder = 'Enter command...';
};
termWs.onmessage = e => {
try {
const msg = JSON.parse(e.data);
const text = msg.data || msg.message || '';
if (!text && msg.type === 'prompt') {
input.placeholder = `${msg.cwd} $`;
return;
}
if (text) {
const line = document.createElement('div');
line.className = 'term-line';
line.style.whiteSpace = 'pre-wrap';
line.textContent = text;
terminal.insertBefore(line, input.parentElement);
terminal.scrollTop = terminal.scrollHeight;
}
if (msg.type === 'prompt') input.placeholder = `${msg.cwd || '/'} $`;
} catch(_) {}
};
termWs.onclose = () => {
termReady = false;
input.placeholder = 'Disconnected — reconnecting...';
setTimeout(connectTerminal, 3000);
};
input.addEventListener('keydown', ev => {
if (ev.key !== 'Enter') return;
const cmd = input.value.trim();
if (!cmd) return;
const echo = document.createElement('div');
echo.className = 'term-line';
echo.style.color = '#56d364';
echo.textContent = '$ ' + cmd;
terminal.insertBefore(echo, input.parentElement);
if (termReady) termWs.send(JSON.stringify({ type: 'command', data: cmd }));
input.value = '';
});
}
connectTerminal();
// ── NAVIGATION ────────────────────────────────────────────────────────────
function openApp(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.dock-dot').forEach(d => d.classList.remove('active'));
const target = id === 'home' ? 'screen-home' : `screen-${id}`;
const el = document.getElementById(target);
if (el) el.classList.add('active');
const dot = document.getElementById(`dot-${id}`);
if (dot) dot.classList.add('active');
if (id === 'home') { const h = document.getElementById('dot-home'); if(h) h.classList.add('active'); }
// Lazy-load each app on first open
if (id === 'files') loadFiles('/');
if (id === 'terminal') initStandaloneTerminal();
if (id === 'gallery') loadGallery();
if (id === 'browser' && !browserWsReady) browserConnect();
if (id === 'video') loadVideoLibrary();
}
function goHome() { openApp('home'); }
// ── EDITOR ────────────────────────────────────────────────────────────────
const editorTabs = [];
let editorActive = null;
function editorNewFile() {
const name = 'untitled-' + (editorTabs.length + 1) + '.txt';
const tab = { name, path: null, content: '', dirty: false };
editorTabs.push(tab);
editorActivate(tab);
}
function editorActivate(tab) {
editorActive = tab;
renderEditorTabs();
const code = document.getElementById('editor-code');
const empty = document.getElementById('editor-empty');
const pathBar = document.getElementById('editor-path-bar');
const saveBtn = document.getElementById('editor-save-btn');
if (code && empty) {
code.style.display = 'block';
empty.style.display = 'none';
code.value = tab.content;
code.oninput = () => {
tab.content = code.value;
if (!tab.dirty) { tab.dirty = true; renderEditorTabs(); }
};
}
if (pathBar) {
pathBar.textContent = tab.path || 'New File — unsaved';
pathBar.style.display = 'block';
}
if (saveBtn) saveBtn.style.display = 'flex';
}
function renderEditorTabs() {
const bar = document.getElementById('editor-tabs');
if (!bar) return;
bar.innerHTML = editorTabs.map((t, i) => `
<div onclick="editorActivate(editorTabs[${i}])"
style="padding:8px 14px;font-size:12px;cursor:pointer;white-space:nowrap;flex-shrink:0;
border-right:1px solid #30363d;display:flex;align-items:center;gap:6px;
background:${t===editorActive?'#0d1117':'transparent'};
color:${t===editorActive?'#e6edf3':'#8b949e'};
border-bottom:${t===editorActive?'2px solid #6366f1':'2px solid transparent'}">
<span>${t.dirty?'●':''} ${t.name}</span>
<span onclick="event.stopPropagation();editorCloseTab(${i})"
style="color:#6e7681;font-size:14px;line-height:1;">×</span>
</div>`).join('');
}
function editorCloseTab(i) {
editorTabs.splice(i, 1);
if (editorActive === editorTabs[i]) editorActive = editorTabs[i-1] || editorTabs[0] || null;
if (!editorTabs.length) {
editorActive = null;
const code = document.getElementById('editor-code');
const empty = document.getElementById('editor-empty');
const pathBar = document.getElementById('editor-path-bar');
const saveBtn = document.getElementById('editor-save-btn');
if (code) code.style.display = 'none';
if (empty) empty.style.display = 'flex';
if (pathBar) pathBar.style.display = 'none';
if (saveBtn) saveBtn.style.display = 'none';
} else {
editorActivate(editorActive || editorTabs[0]);
}
renderEditorTabs();
}
async function editorSave() {
if (!editorActive) return;
const code = document.getElementById('editor-code');
if (code) editorActive.content = code.value;
if (!editorActive.path) {
const p = prompt('Save as (full path):', '/app/' + editorActive.name);
if (!p) return;
editorActive.path = p;
editorActive.name = p.split('/').pop();
}
try {
const res = await fetch(`${API}/api/files/write?path=${encodeURIComponent(editorActive.path)}&content=${encodeURIComponent(editorActive.content)}`, {method:'POST'});
if (!res.ok) throw new Error(await res.text());
editorActive.dirty = false;
renderEditorTabs();
document.getElementById('editor-path-bar').textContent = editorActive.path;
} catch(e) {
alert('Save failed: ' + e.message);
}
}
async function editorOpenFile(path) {
// Check if already open
const existing = editorTabs.find(t => t.path === path);
if (existing) { editorActivate(existing); return; }
try {
const res = await fetch(`${API}/api/files/read?path=${encodeURIComponent(path)}`);
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
const name = path.split('/').pop();
const tab = { name, path, content: data.content, dirty: false };
editorTabs.push(tab);
editorActivate(tab);
} catch(e) {
alert('Cannot open file: ' + e.message);
}
}
async function editorOpenPicker() {
const picker = document.getElementById('editor-picker');
if (!picker) return;
picker.style.display = 'flex';
picker.style.flexDirection = 'column';
await editorPickerLoad('/');
document.getElementById('editor-picker-path').addEventListener('keydown', async ev => {
if (ev.key === 'Enter') await editorPickerLoad(ev.target.value);
});
}
async function editorPickerLoad(path) {
document.getElementById('editor-picker-path').value = path;
const list = document.getElementById('editor-picker-list');
list.innerHTML = '<div style="color:#8b949e;padding:8px;font-size:12px">Loading...</div>';
try {
const res = await fetch(`${API}/api/files?path=${encodeURIComponent(path)}`);
const data = await res.json();
const parent = data.parent;
let html = parent ? `<div onclick="editorPickerLoad('${parent}')"
style="padding:10px;color:#8b949e;font-family:monospace;font-size:12px;cursor:pointer;border-bottom:1px solid #21262d;">⬆ ..</div>` : '';
html += data.entries.map(e => `
<div onclick="${e.type==='dir' ? `editorPickerLoad('${e.path.replace(/'/g,"\'")}')` : `editorPickOpenFile('${e.path.replace(/'/g,"\'")}')` }"
style="padding:10px 12px;font-size:13px;cursor:pointer;border-bottom:1px solid #21262d;
color:${e.type==='dir'?'#a5b4fc':'#e6edf3'};font-family:monospace;
display:flex;align-items:center;gap:8px;">
${e.type==='dir'?'📁':'📄'} ${e.name}
</div>`).join('');
list.innerHTML = html || '<div style="color:#6e7681;padding:12px;font-size:12px">Empty</div>';
} catch(e) {
list.innerHTML = `<div style="color:#ef4444;padding:12px;font-size:12px">${e.message}</div>`;
}
}
function editorPickOpenFile(path) {
document.getElementById('editor-picker').style.display = 'none';
editorOpenFile(path);
}
window.editorPickOpenFile = editorPickOpenFile;
window.editorPickerLoad = editorPickerLoad;
// ── BROWSER (Playwright WebSocket) ────────────────────────────────────────
let browserWs = null, browserWsReady = false;
function browserConnect() {
browserWs = new WebSocket(`${WS}/ws/browser`);
browserWs.onopen = () => { browserWsReady = true; };
browserWs.onmessage = e => {
try {
const msg = JSON.parse(e.data);
if (msg.type === 'screenshot') {
const shot = document.getElementById('browser-shot');
const empty = document.getElementById('browser-empty');
const loading = document.getElementById('browser-loading');
const errEl = document.getElementById('browser-error');
if (shot) { shot.src = 'data:image/jpeg;base64,' + msg.data; shot.style.display='block'; }
if (empty) empty.style.display = 'none';
if (loading) loading.style.display = 'none';
if (errEl) errEl.style.display = 'none';
const urlInput = document.getElementById('browser-url');
if (urlInput && msg.url) urlInput.value = msg.url;
} else if (msg.type === 'loading') {
document.getElementById('browser-loading').style.display = 'block';
document.getElementById('browser-shot').style.display = 'none';
document.getElementById('browser-error').style.display = 'none';
} else if (msg.type === 'error') {
const errEl = document.getElementById('browser-error');
if (errEl) { errEl.textContent = msg.message; errEl.style.display = 'block'; }
document.getElementById('browser-loading').style.display = 'none';
}
} catch(_) {}
};
browserWs.onclose = () => { browserWsReady = false; setTimeout(browserConnect, 3000); };
browserWs.onerror = () => browserWs.close();
}
function browserSend(msg) {
if (browserWs && browserWsReady) browserWs.send(JSON.stringify(msg));
}
function browserNav(url) {
if (!url) return;
if (!url.startsWith('http')) url = 'https://' + url;
document.getElementById('browser-url').value = url;
browserSend({ type: 'navigate', url });
}
function browserNew() {
document.getElementById('browser-url').value = '';
document.getElementById('browser-shot').style.display = 'none';
document.getElementById('browser-empty').style.display = 'flex';
document.getElementById('browser-empty').style.flexDirection = 'column';
document.getElementById('browser-empty').style.alignItems = 'center';
document.getElementById('browser-empty').style.justifyContent = 'center';
}
function browserClick(ev) {
const img = document.getElementById('browser-shot');
if (!img) return;
const rect = img.getBoundingClientRect();
// Scale click coords to 390x700 viewport
const x = Math.round((ev.clientX - rect.left) / rect.width * 390);
const y = Math.round((ev.clientY - rect.top) / rect.height * 700);
browserSend({ type: 'click', x, y });
}
document.getElementById('browser-url')?.addEventListener('keydown', ev => {
if (ev.key === 'Enter') browserNav(ev.target.value.trim());
});
// Connect browser WebSocket when browser app is opened
window.browserConnect = browserConnect;
// ── STANDALONE TERMINAL ───────────────────────────────────────────────────
let stermWs = null, stermReady = false, stermInit = false;
function initStandaloneTerminal() {
if (stermInit) return;
stermInit = true;
const out = document.getElementById('terminal-output');
const input = document.getElementById('terminal-input');
function addLine(text, color) {
const line = document.createElement('div');
line.style.whiteSpace = 'pre-wrap';
line.style.marginBottom = '2px';
if (color) line.style.color = color;
line.textContent = text;
out.appendChild(line);
out.scrollTop = out.scrollHeight;
}
function connect() {
stermWs = new WebSocket(`${WS}/ws/terminal?cwd=/`);
stermWs.onopen = () => { stermReady = true; input.placeholder = 'Enter command...'; };
stermWs.onmessage = e => {
try {
const msg = JSON.parse(e.data);
if (msg.type === 'welcome') { addLine(msg.message, '#a5b4fc'); return; }
if (msg.type === 'prompt') { input.placeholder = (msg.cwd || '/') + ' $'; return; }
if (msg.type === 'output' && msg.data) addLine(msg.data);
if (msg.type === 'error' && msg.data) addLine(msg.data, '#ef4444');
} catch(_) {}
};
stermWs.onclose = () => {
stermReady = false;
input.placeholder = 'Reconnecting...';
setTimeout(connect, 3000);
};
}
connect();
input.addEventListener('keydown', ev => {
if (ev.key !== 'Enter') return;
const cmd = input.value.trim();
if (!cmd) return;
addLine('$ ' + cmd, '#56d364');
if (stermReady) stermWs.send(JSON.stringify({ type: 'command', data: cmd }));
input.value = '';
});
}
// Init
openApp('home');
</script>
</body>
</html>