diff --git a/src/snek/app.py b/src/snek/app.py index 227c23a..e2273ab 100644 --- a/src/snek/app.py +++ b/src/snek/app.py @@ -3,6 +3,7 @@ import logging import pathlib import time import uuid +from datetime import datetime from snek import snode from snek.view.threads import ThreadsView import json @@ -96,6 +97,7 @@ class Application(BaseApplication): self.jinja2_env.add_extension(LinkifyExtension) self.jinja2_env.add_extension(PythonExtension) self.jinja2_env.add_extension(EmojiExtension) + self.time_start = datetime.now() self.ssh_host = "0.0.0.0" self.ssh_port = 2242 self.setup_router() @@ -112,7 +114,34 @@ class Application(BaseApplication): self.on_startup.append(self.start_user_availability_service) self.on_startup.append(self.start_ssh_server) self.on_startup.append(self.prepare_database) - + + @property + def uptime_seconds(self): + return (datetime.now() - self.time_start).total_seconds() + + @property + def uptime(self): + return self._format_uptime(self.uptime_seconds) + + def _format_uptime(self,seconds): + seconds = int(seconds) + days, seconds = divmod(seconds, 86400) + hours, seconds = divmod(seconds, 3600) + minutes, seconds = divmod(seconds, 60) + + parts = [] + if days > 0: + parts.append(f"{days} day{'s' if days != 1 else ''}") + if hours > 0: + parts.append(f"{hours} hour{'s' if hours != 1 else ''}") + if minutes > 0: + parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}") + if seconds > 0 or not parts: + parts.append(f"{seconds} second{'s' if seconds != 1 else ''}") + + return ", ".join(parts) + + async def start_user_availability_service(self, app): app.user_availability_service_task = asyncio.create_task(app.services.socket.user_availability_service()) async def snode_sync(self, app): diff --git a/src/snek/static/app.js b/src/snek/static/app.js index f8c9939..a5c52de 100644 --- a/src/snek/static/app.js +++ b/src/snek/static/app.js @@ -66,7 +66,7 @@ export class Chat extends EventHandler { return new Promise((resolve) => { this._waitConnect = resolve; console.debug("Connecting.."); - + try { this._socket = new WebSocket(this._url); } catch (e) { @@ -196,7 +196,9 @@ export class App extends EventHandler { this.ws.addEventListener("connected", (data) => { this.ping("online"); }); - + this.ws.addEventListener("reconnecting", (data) => { + this.starField?.showNotify("Connecting..","#CC0000") + }) this.ws.addEventListener("channel-message", (data) => { me.emit("channel-message", data); }); diff --git a/src/snek/static/sandbox.css b/src/snek/static/sandbox.css index c2ffffb..323098a 100644 --- a/src/snek/static/sandbox.css +++ b/src/snek/static/sandbox.css @@ -1,42 +1,178 @@ + :root { + --star-color: white; + --background-color: black; + } -.star { + body.day { + --star-color: #444; + --background-color: #e6f0ff; + } + + body.night { + --star-color: white; + --background-color: black; + } + + body { + margin: 0; + overflow: hidden; + background-color: var(--background-color); + transition: background-color 0.5s; + } + + .star { + position: absolute; + border-radius: 50%; + background-color: var(--star-color); + animation: twinkle 2s infinite ease-in-out; + } + + @keyframes twinkle { + 0%, 100% { opacity: 0.8; transform: scale(1); } + 50% { opacity: 1; transform: scale(1.2); } + } + + #themeToggle { + position: absolute; + top: 10px; + left: 10px; + padding: 8px 12px; + font-size: 14px; + z-index: 1000; + } + +.star.special { + box-shadow: 0 0 10px 3px gold; + transform: scale(1.4); + z-index: 10; +} + +.star-tooltip { position: absolute; - width: 2px; - height: 2px; - background: var(--star-color, #fff); - border-radius: 50%; - opacity: 0; - transition: background 0.5s ease; - animation: twinkle ease-in-out infinite; -} - -@keyframes twinkle { - 0%, 100% { opacity: 0; } - 50% { opacity: 1; } -} - -@keyframes star-glow-frames { - 0% { - box-shadow: 0 0 5px --star-color; - } - 50% { - box-shadow: 0 0 20px --star-color, 0 0 30px --star-color; - } - 100% { - box-shadow: 0 0 5px --star-color; - } -} - -.star-glow { - animation: star-glow-frames 1s; -} - -.content { - position: relative; - z-index: 1; - color: var(--star-content-color, #eee); + font-size: 12px; + color: white; font-family: sans-serif; - text-align: center; - top: 40%; - transform: translateY(-40%); + pointer-events: none; + z-index: 9999; + white-space: nowrap; + text-shadow: 1px 1px 2px black; + display: none; + padding: 2px 6px; } +.star-popup { + position: absolute; + max-width: 300px; + color: #fff; + font-family: sans-serif; + font-size: 14px; + z-index: 10000; + text-shadow: 1px 1px 3px black; + display: none; + padding: 10px; + border-radius: 12px; +} + + +.star:hover { + cursor: pointer; +} + +.star-popup { + position: absolute; + max-width: 300px; + background: white; + color: black; + padding: 15px; + border-radius: 12px; + box-shadow: 0 8px 20px rgba(0,0,0,0.3); + z-index: 10000; + font-family: sans-serif; + font-size: 14px; + display: none; +} + +.star-popup h3 { + margin: 0 0 5px; + font-size: 16px; +} + +.star-popup button { + margin-top: 10px; +} + +.demo-overlay { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 3em; + color: white; + font-family: 'Segoe UI', sans-serif; + font-weight: 300; + text-align: center; + text-shadow: 0 0 20px rgba(0,0,0,0.8); + z-index: 9999; + opacity: 0; + transition: opacity 0.6s ease; + max-width: 80vw; + pointer-events: none; +} + +@keyframes demoFadeIn { + from { + opacity: 0; + transform: translate(-50%, -60%) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +@keyframes demoPulse { + 0% { + box-shadow: 0 0 0 rgba(255, 255, 150, 0); + transform: scale(1); + } + 30% { + box-shadow: 0 0 30px 15px rgba(255, 255, 150, 0.9); + transform: scale(1.05); + } + 100% { + box-shadow: 0 0 0 rgba(255, 255, 150, 0); + transform: scale(1); + } +} + +.demo-highlight { + animation: demoPulse 1.5s ease-out; + font-weight: bold; + + position: relative; + z-index: 9999; +} + +.star-notify-container { + position: fixed; + top: 50px; + right: 20px; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10px; + z-index: 9999; + pointer-events: none; +} + +.star-notify { + opacity: 0; + background: transparent; + padding: 5px 10px; + color: white; + font-weight: 300; + text-shadow: 0 0 10px rgba(0,0,0,0.7); + transition: opacity 0.5s ease, transform 0.5s ease; + transform: translateY(-10px); + font-family: 'Segoe UI', sans-serif; +} + diff --git a/src/snek/static/socket.js b/src/snek/static/socket.js index b49b683..77fbd3c 100644 --- a/src/snek/static/socket.js +++ b/src/snek/static/socket.js @@ -95,7 +95,8 @@ export class Socket extends EventHandler { if (this.shouldReconnect) setTimeout(() => { console.log("Reconnecting"); - return this.connect(); + this.emit("reconnecting"); + return this.connect(); }, 0); } diff --git a/src/snek/templates/sandbox.html b/src/snek/templates/sandbox.html index 46a617a..53e9d48 100644 --- a/src/snek/templates/sandbox.html +++ b/src/snek/templates/sandbox.html @@ -1,504 +1,392 @@ - +
+ diff --git a/src/snek/templates/web.html b/src/snek/templates/web.html index cbb8ea4..bd58809 100644 --- a/src/snek/templates/web.html +++ b/src/snek/templates/web.html @@ -37,7 +37,21 @@ showHelp(); } } - + app.ws.addEventListener("refresh", (data) => { + app.starField.showNotify(data.message); + setTimeout(() => { + window.location.reload(); + },4000) + }) + app.ws.addEventListener("deployed", (data) => { + app.starField.renderWord("Deployed",{"rainbow":true,"resolution":8}); + setTimeout(() => { + app.starField.shuffleAll(5000); + },10000) + }) + app.ws.addEventListener("starfield.render_word", (data) => { + app.starField.renderWord(data.word,data); + }) const textBox = document.querySelector("chat-input").textarea textBox.addEventListener("paste", async (e) => { try { diff --git a/src/snek/view/rpc.py b/src/snek/view/rpc.py index fd48124..8c024df 100644 --- a/src/snek/view/rpc.py +++ b/src/snek/view/rpc.py @@ -9,7 +9,7 @@ import json import traceback - +import asyncio from aiohttp import web from snek.system.model import now @@ -21,13 +21,29 @@ import logging logger = logging.getLogger(__name__) class RPCView(BaseView): - class RPCApi: def __init__(self, view, ws): self.view = view self.app = self.view.app self.services = self.app.services self.ws = ws + self.user_session = {} + + async def _session_ensure(self): + uid = await self.view.session_get("uid") + if not uid in self.user_session: + self.user_session[uid] = { + "said_hello": False, + } + + async def session_get(self, key, default): + await self._session_ensure() + return self.user_session[self.user_uid].get(key, default) + + async def session_set(self, key, value): + await self._session_ensure() + self.user_session[self.user_uid][key] = value + return True async def db_insert(self, table_name, record): self._require_login() @@ -323,6 +339,11 @@ class RPCView(BaseView): async for record in self.services.channel.get_users(channel_uid) ] + async def _schedule(self, uid, seconds, call): + await asyncio.sleep(seconds) + await self.services.socket.send_to_user(uid, call) + + async def ping(self, callId, *args): if self.user_uid: user = await self.services.user.get(uid=self.user_uid) @@ -330,7 +351,14 @@ class RPCView(BaseView): await self.services.user.save(user) return {"pong": args} + + + async def get(self): + async def schedule(uid, seconds, call): + await asyncio.sleep(seconds) + await self.services.socket.send_to_user(uid, call) + ws = web.WebSocketResponse() await ws.prepare(self.request) if self.request.session.get("logged_in"): @@ -343,6 +371,16 @@ class RPCView(BaseView): await self.services.socket.subscribe( ws, subscription["channel_uid"], self.request.session.get("uid") ) + if self.request.app.uptime_seconds < 10: + await schedule(self.request.session.get("uid"),1,{"event":"refresh", "data": { + "message": "Finishing deployment"} + } + ) + await schedule(self.request.session.get("uid"),10,{"event": "deployed", "data": { + "uptime": self.request.app.uptime} + } + ) + rpc = RPCView.RPCApi(self, ws) async for msg in ws: if msg.type == web.WSMsgType.TEXT: