This commit is contained in:
retoor 2025-05-23 06:48:18 +02:00
parent a55d15b635
commit 3bf09f9083
7 changed files with 638 additions and 530 deletions

View File

@ -3,6 +3,7 @@ import logging
import pathlib import pathlib
import time import time
import uuid import uuid
from datetime import datetime
from snek import snode from snek import snode
from snek.view.threads import ThreadsView from snek.view.threads import ThreadsView
import json import json
@ -96,6 +97,7 @@ class Application(BaseApplication):
self.jinja2_env.add_extension(LinkifyExtension) self.jinja2_env.add_extension(LinkifyExtension)
self.jinja2_env.add_extension(PythonExtension) self.jinja2_env.add_extension(PythonExtension)
self.jinja2_env.add_extension(EmojiExtension) self.jinja2_env.add_extension(EmojiExtension)
self.time_start = datetime.now()
self.ssh_host = "0.0.0.0" self.ssh_host = "0.0.0.0"
self.ssh_port = 2242 self.ssh_port = 2242
self.setup_router() 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_user_availability_service)
self.on_startup.append(self.start_ssh_server) self.on_startup.append(self.start_ssh_server)
self.on_startup.append(self.prepare_database) 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): async def start_user_availability_service(self, app):
app.user_availability_service_task = asyncio.create_task(app.services.socket.user_availability_service()) app.user_availability_service_task = asyncio.create_task(app.services.socket.user_availability_service())
async def snode_sync(self, app): async def snode_sync(self, app):

View File

@ -66,7 +66,7 @@ export class Chat extends EventHandler {
return new Promise((resolve) => { return new Promise((resolve) => {
this._waitConnect = resolve; this._waitConnect = resolve;
console.debug("Connecting.."); console.debug("Connecting..");
try { try {
this._socket = new WebSocket(this._url); this._socket = new WebSocket(this._url);
} catch (e) { } catch (e) {
@ -196,7 +196,9 @@ export class App extends EventHandler {
this.ws.addEventListener("connected", (data) => { this.ws.addEventListener("connected", (data) => {
this.ping("online"); this.ping("online");
}); });
this.ws.addEventListener("reconnecting", (data) => {
this.starField?.showNotify("Connecting..","#CC0000")
})
this.ws.addEventListener("channel-message", (data) => { this.ws.addEventListener("channel-message", (data) => {
me.emit("channel-message", data); me.emit("channel-message", data);
}); });

View File

@ -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; position: absolute;
width: 2px; font-size: 12px;
height: 2px; color: white;
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-family: sans-serif; font-family: sans-serif;
text-align: center; pointer-events: none;
top: 40%; z-index: 9999;
transform: translateY(-40%); 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;
}

View File

@ -95,7 +95,8 @@ export class Socket extends EventHandler {
if (this.shouldReconnect) if (this.shouldReconnect)
setTimeout(() => { setTimeout(() => {
console.log("Reconnecting"); console.log("Reconnecting");
return this.connect(); this.emit("reconnecting");
return this.connect();
}, 0); }, 0);
} }

View File

@ -1,504 +1,392 @@
<div id="star-tooltip" class="star-tooltip"></div>
<div id="star-popup" class="star-popup"></div>
<script type="module"> <script type="module">
import { app } from "/app.js"; import { app } from "/app.js";
const STAR_COUNT = 200;
const body = document.body;
function getStarPosition(star) {
const leftPercent = parseFloat(star.style.left);
const topPercent = parseFloat(star.style.top);
let position;
if (topPercent < 40 && leftPercent >= 40 && leftPercent <= 60) {
position = 'North';
} else if (topPercent > 60 && leftPercent >= 40 && leftPercent <= 60) {
position = 'South';
} else if (leftPercent < 40 && topPercent >= 40 && topPercent <= 60) {
position = 'West';
} else if (leftPercent > 60 && topPercent >= 40 && topPercent <= 60) {
position = 'East';
} else if (topPercent >= 40 && topPercent <= 60 && leftPercent >= 40 && leftPercent <= 60) {
position = 'Center';
} else {
position = 'Corner or Edge';
}
return position
}
let stars = {}
window.stars = stars
function createStar() {
const star = document.createElement('div');
star.classList.add('star');
star.style.left = `${Math.random() * 100}%`;
star.style.top = `${Math.random() * 100}%`;
star.shuffle = () => {
star.style.left = `${Math.random() * 100}%`;
star.style.top = `${Math.random() * 100}%`;
star.position = getStarPosition(star)
}
star.position = getStarPosition(star)
function moveStarToPosition(star, position) {
let top, left;
switch (position) {
case 'North':
top = `${Math.random() * 20}%`;
left = `${40 + Math.random() * 20}%`;
break;
case 'South':
top = `${80 + Math.random() * 10}%`;
left = `${40 + Math.random() * 20}%`;
break;
case 'West':
top = `${40 + Math.random() * 20}%`;
left = `${Math.random() * 20}%`;
break;
case 'East':
top = `${40 + Math.random() * 20}%`;
left = `${80 + Math.random() * 10}%`;
break;
case 'Center':
top = `${45 + Math.random() * 10}%`;
left = `${45 + Math.random() * 10}%`;
break;
default: // 'Corner or Edge' fallback
top = `${Math.random() * 100}%`;
left = `${Math.random() * 100}%`;
break;
}
star.style.top = top;
star.style.left = left;
star.position = getStarPosition(star)
}
if(!stars[star.position])
stars[star.position] = []
stars[star.position].push(star)
const size = Math.random() * 2 + 1;
star.style.width = `${size}px`;
star.style.height = `${size}px`;
const duration = Math.random() * 3 + 2;
const delay = Math.random() * 5;
star.style.animationDuration = `${duration}s`;
star.style.animationDelay = `${delay}s`;
body.appendChild(star);
}
Array.from({ length: STAR_COUNT }, createStar);
function lightenColor(hex, percent) {
const num = parseInt(hex.replace("#", ""), 16);
let r = (num >> 16) + Math.round(255 * percent / 100);
let g = ((num >> 8) & 0x00FF) + Math.round(255 * percent / 100);
let b = (num & 0x0000FF) + Math.round(255 * percent / 100);
r = Math.min(255, r);
g = Math.min(255, g);
b = Math.min(255, b);
return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}`;
}
const originalColor = document.documentElement.style.getPropertyValue("--star-color").trim();
function glowCSSVariable(varName, glowColor, duration = 500) {
const root = document.documentElement;
//igetComputedStyle(root).getPropertyValue(varName).trim();
glowColor = lightenColor(glowColor, 10);
root.style.setProperty(varName, glowColor);
setTimeout(() => {
root.style.setProperty(varName, originalColor);
}, duration);
}
function updateStarColorDelayed(color) {
glowCSSVariable('--star-color', color, 2500);
}
app.updateStarColor = updateStarColorDelayed;
app.ws.addEventListener("set_typing", (data) => {
updateStarColorDelayed(data.color);
});
window.createAvatar = () => {
let avatar = document.createElement("avatar-face")
document.querySelector("main").appendChild(avatar)
return avatar
}
class AvatarFace extends HTMLElement {
static get observedAttributes(){
return ['emotion','face-color','eye-color','text','balloon-color','text-color'];
}
constructor(){
super();
this._shadow = this.attachShadow({mode:'open'});
this._shadow.innerHTML = `
<style>
:host { display:block; position:relative; }
canvas { width:100%; height:100%; display:block; }
</style>
<canvas></canvas>
`;
this._c = this._shadow.querySelector('canvas');
this._ctx = this._c.getContext('2d');
// state
this._mouse = {x:0,y:0};
this._blinkTimer = 0;
this._blinking = false;
this._lastTime = 0;
// defaults
this._emotion = 'neutral';
this._faceColor = '#ffdfba';
this._eyeColor = '#000';
this._text = '';
this._balloonColor = '#fff';
this._textColor = '#000';
}
attributeChangedCallback(name,_old,newV){
if (name==='emotion') this._emotion = newV||'neutral';
else if (name==='face-color') this._faceColor = newV||'#ffdfba';
else if (name==='eye-color') this._eyeColor = newV||'#000';
else if (name==='text') this._text = newV||'';
else if (name==='balloon-color')this._balloonColor = newV||'#fff';
else if (name==='text-color') this._textColor = newV||'#000';
}
connectedCallback(){
// watch size so canvas buffer matches display
this._ro = new ResizeObserver(entries=>{
for(const ent of entries){
const w = ent.contentRect.width;
const h = ent.contentRect.height;
const dpr = devicePixelRatio||1;
this._c.width = w*dpr;
this._c.height = h*dpr;
this._ctx.scale(dpr,dpr);
}
});
this._ro.observe(this);
// track mouse so eyes follow
this._shadow.addEventListener('mousemove', e=>{
const r = this._c.getBoundingClientRect();
this._mouse.x = e.clientX - r.left;
this._mouse.y = e.clientY - r.top;
});
this._lastTime = performance.now();
this._raf = requestAnimationFrame(t=>this._loop(t));
}
disconnectedCallback(){
cancelAnimationFrame(this._raf);
this._ro.disconnect();
}
_updateBlink(dt){
this._blinkTimer -= dt;
if (this._blinkTimer<=0){
this._blinking = !this._blinking;
this._blinkTimer = this._blinking
? 0.1
: 2 + Math.random()*3;
}
}
_roundRect(x,y,w,h,r){
const ctx = this._ctx;
ctx.beginPath();
ctx.moveTo(x+r,y);
ctx.lineTo(x+w-r,y);
ctx.quadraticCurveTo(x+w,y, x+w,y+r);
ctx.lineTo(x+w,y+h-r);
ctx.quadraticCurveTo(x+w,y+h, x+w-r,y+h);
ctx.lineTo(x+r,y+h);
ctx.quadraticCurveTo(x,y+h, x,y+h-r);
ctx.lineTo(x,y+r);
ctx.quadraticCurveTo(x,y, x+r,y);
ctx.closePath();
}
_draw(ts){
const ctx = this._ctx;
const W = this._c.clientWidth;
const H = this._c.clientHeight;
ctx.clearRect(0,0,W,H);
// HEAD + BOB
const cx = W/2;
const cy = H/2 + Math.sin(ts*0.002)*8;
const R = Math.min(W,H)*0.25;
// SPEECH BALLOON
if (this._text){
const pad = 6;
ctx.font = `${R*0.15}px sans-serif`;
const m = ctx.measureText(this._text);
const tw = m.width, th = R*0.18;
const bw = tw + pad*2, bh = th + pad*2;
const bx = cx - bw/2, by = cy - R - bh - 10;
// bubble
ctx.fillStyle = this._balloonColor;
this._roundRect(bx,by,bw,bh,6);
ctx.fill();
ctx.strokeStyle = '#888';
ctx.lineWidth = 1.2;
ctx.stroke();
// tail
ctx.beginPath();
ctx.moveTo(cx-6, by+bh);
ctx.lineTo(cx+6, by+bh);
ctx.lineTo(cx, cy-R+4);
ctx.closePath();
ctx.fill();
ctx.stroke();
// text
ctx.fillStyle = this._textColor;
ctx.textBaseline = 'top';
ctx.fillText(this._text, bx+pad, by+pad);
}
// FACE
ctx.fillStyle = this._faceColor;
ctx.beginPath();
ctx.arc(cx,cy,R,0,2*Math.PI);
ctx.fill();
// EYES
const eyeY = cy - R*0.2;
const eyeX = R*0.4;
const eyeR= R*0.12;
const pupR= eyeR*0.5;
for(let i=0;i<2;i++){
const ex = cx + (i? eyeX:-eyeX);
const ey = eyeY;
// eyeball
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(ex,ey,eyeR,0,2*Math.PI);
ctx.fill();
// pupil follows
let dx = this._mouse.x - ex;
let dy = this._mouse.y - ey;
const d = Math.hypot(dx,dy);
const max = eyeR - pupR - 2;
if (d>max){ dx=dx/d*max; dy=dy/d*max; }
if (this._blinking){
ctx.strokeStyle='#000';
ctx.lineWidth=3;
ctx.beginPath();
ctx.moveTo(ex-eyeR,ey);
ctx.lineTo(ex+eyeR,ey);
ctx.stroke();
} else {
ctx.fillStyle = this._eyeColor;
ctx.beginPath();
ctx.arc(ex+dx,ey+dy,pupR,0,2*Math.PI);
ctx.fill();
}
}
// ANGRY BROWS
if (this._emotion==='angry'){
ctx.strokeStyle='#000';
ctx.lineWidth=4;
[[-eyeX,1],[ eyeX,-1]].forEach(([off,dir])=>{
const sx = cx+off - eyeR;
const sy = eyeY - eyeR*1.3;
const ex = cx+off + eyeR;
const ey2= sy + dir*6;
ctx.beginPath();
ctx.moveTo(sx,sy);
ctx.lineTo(ex,ey2);
ctx.stroke();
});
}
// MOUTH by emotion
const mw = R*0.6;
const my = cy + R*0.25;
ctx.strokeStyle='#a33';
ctx.lineWidth=4;
if (this._emotion==='surprised'){
ctx.fillStyle='#a33';
ctx.beginPath();
ctx.arc(cx,my,mw*0.3,0,2*Math.PI);
ctx.fill();
}
else if (this._emotion==='sad'){
ctx.beginPath();
ctx.arc(cx,my,mw/2,1.15*Math.PI,1.85*Math.PI,true);
ctx.stroke();
}
else if (this._emotion==='angry'){
ctx.beginPath();
ctx.moveTo(cx-mw/2,my+2);
ctx.lineTo(cx+mw/2,my-2);
ctx.stroke();
}
else {
const s = this._emotion==='happy'? 0.15*Math.PI:0.2*Math.PI;
const e = this._emotion==='happy'? 0.85*Math.PI:0.8*Math.PI;
ctx.beginPath();
ctx.arc(cx,my,mw/2,s,e);
ctx.stroke();
}
}
_loop(ts){
const dt = (ts - this._lastTime)/1000;
this._lastTime = ts;
this._updateBlink(dt);
this._draw(ts);
this._raf = requestAnimationFrame(t=>this._loop(t));
}
}
customElements.define('avatar-face', AvatarFace);
class AvatarReplacer {
constructor(target, opts={}){
this.target = target;
// record original inline styles so we can restore
this._oldVis = target.style.visibility || '';
this._oldPos = target.style.position || '';
// hide the target
target.style.visibility = 'hidden';
// measure
const rect = target.getBoundingClientRect();
// create avatar
this.avatar = document.createElement('avatar-face');
// copy all supported opts into attributes
['emotion','faceColor','eyeColor','text','balloonColor','textColor']
.forEach(k => {
const attr = k.replace(/[A-Z]/g,m=>'-'+m.toLowerCase());
if (opts[k] != null) this.avatar.setAttribute(attr, opts[k]);
});
// position absolutely
const scrollX = window.pageXOffset;
const scrollY = window.pageYOffset;
Object.assign(this.avatar.style, {
position: 'absolute',
left: (rect.left + scrollX) + 'px',
top: (rect.top + scrollY) + 'px',
width: rect.width + 'px',
height: rect.height + 'px',
zIndex: 9999
});
document.body.appendChild(this.avatar);
}
detach(){
// remove avatar and restore target
if (this.avatar && this.avatar.parentNode) {
this.avatar.parentNode.removeChild(this.avatar);
this.avatar = null;
}
this.target.style.visibility = this._oldVis;
this.target.style.position = this._oldPos;
}
// static convenience method
static attach(target, opts){
return new AvatarReplacer(target, opts);
}
}
/*
// DEMO wiring
const btnGo = document.getElementById('go');
const btnReset = document.getElementById('reset');
let repl1, repl2;
btnGo.addEventListener('click', ()=>{
// replace #one with a happy avatar saying "Hi!"
repl1 = AvatarReplacer.attach(
document.getElementById('one'),
{emotion:'happy', text:'Hi!', balloonColor:'#fffbdd', textColor:'#333'}
);
// replace #two with a surprised avatar
repl2 = AvatarReplacer.attach(
document.getElementById('two'),
{emotion:'surprised', faceColor:'#eeffcc', text:'Wow!', balloonColor:'#ddffdd'}
);
});
btnReset.addEventListener('click', ()=>{
if (repl1) repl1.detach();
if (repl2) repl2.detach();
});
*/
/*
class StarField { class StarField {
constructor(container = document.body, options = {}) { constructor({ count = 200, container = document.body } = {}) {
this.container = container; this.container = container;
this.starCount = count;
this.stars = []; this.stars = [];
this.setOptions(options); this.positionMap = {};
this.originalColor = getComputedStyle(document.documentElement).getPropertyValue("--star-color").trim();
this._createStars();
window.stars = this.positionMap;
} }
setOptions({ _getStarPosition(star) {
starCount = 200, const left = parseFloat(star.style.left);
minSize = 1, const top = parseFloat(star.style.top);
maxSize = 3, if (top < 40 && left >= 40 && left <= 60) return "North";
speed = 5, if (top > 60 && left >= 40 && left <= 60) return "South";
color = "white" if (left < 40 && top >= 40 && top <= 60) return "West";
}) { if (left > 60 && top >= 40 && top <= 60) return "East";
this.options = { starCount, minSize, maxSize, speed, color }; if (top >= 40 && top <= 60 && left >= 40 && left <= 60) return "Center";
return "Corner or Edge";
} }
clear() { _createStars() {
this.stars.forEach(star => star.remove()); for (let i = 0; i < this.starCount; i++) {
this.stars = [];
}
generate() {
this.clear();
const { starCount, minSize, maxSize, speed, color } = this.options;
for (let i = 0; i < starCount; i++) {
const star = document.createElement("div"); const star = document.createElement("div");
star.classList.add("star"); star.classList.add("star");
const size = Math.random() * (maxSize - minSize) + minSize; this._randomizeStar(star);
this._placeStar(star);
Object.assign(star.style, {
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
width: `${size}px`,
height: `${size}px`,
backgroundColor: color,
position: "absolute",
borderRadius: "50%",
opacity: "0.8",
animation: `twinkle ${speed}s ease-in-out infinite`,
});
this.container.appendChild(star); this.container.appendChild(star);
this.stars.push(star); this.stars.push(star);
} }
} }
_randomizeStar(star) {
star.style.left = `${Math.random() * 100}%`;
star.style.top = `${Math.random() * 100}%`;
star.style.width = `${Math.random() * 2 + 1}px`;
star.style.height = `${Math.random() * 2 + 1}px`;
star.style.animationDelay = `${Math.random() * 2}s`;
star.style.position = "absolute";
star.style.transition = "top 1s ease, left 1s ease, opacity 1s ease";
star.shuffle = () => this._randomizeStar(star);
star.position = this._getStarPosition(star);
}
_placeStar(star) {
const pos = star.position;
if (!this.positionMap[pos]) this.positionMap[pos] = [];
this.positionMap[pos].push(star);
}
shuffleAll(duration = 1000) {
this.stars.forEach(star => {
star.style.transition = `top ${duration}ms ease, left ${duration}ms ease, opacity ${duration}ms ease`;
star.style.filter = "drop-shadow(0 0 2px white)";
const left = Math.random() * 100;
const top = Math.random() * 100;
star.style.left = `${left}%`;
star.style.top = `${top}%`;
setTimeout(() => {
star.style.filter = "";
star.position = this._getStarPosition(star);
}, duration);
});
}
glowColor(tempColor, duration = 2500) {
const lighten = (hex, percent) => {
const num = parseInt(hex.replace("#", ""), 16);
const r = Math.min(255, (num >> 16) + Math.round(255 * percent / 100));
const g = Math.min(255, ((num >> 8) & 0xff) + Math.round(255 * percent / 100));
const b = Math.min(255, (num & 0xff) + Math.round(255 * percent / 100));
return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}`;
};
const glow = lighten(tempColor, 10);
document.documentElement.style.setProperty("--star-color", glow);
setTimeout(() => {
document.documentElement.style.setProperty("--star-color", this.originalColor);
}, duration);
}
showNotify(text, { duration = 3000, color = "white", fontSize = "1.2em" } = {}) {
// Create container if needed
if (!this._notifyContainer) {
this._notifyContainer = document.createElement("div");
this._notifyContainer.className = "star-notify-container";
document.body.appendChild(this._notifyContainer);
}
const messages = document.querySelectorAll('.star-notify');
const count = Array.from(messages).filter(el => el.textContent.trim() === text).length;
if (count) return;
const note = document.createElement("div");
note.className = "star-notify";
note.textContent = text;
note.style.color = color;
note.style.fontSize = fontSize;
this._notifyContainer.appendChild(note);
// Trigger animation
setTimeout(() => {
note.style.opacity = 1;
note.style.transform = "translateY(0)";
}, 10);
// Remove after duration
setTimeout(() => {
note.style.opacity = 0;
note.style.transform = "translateY(-10px)";
setTimeout(() => note.remove(), 500);
}, duration);
} }
const starField = new StarField(document.body, {
starCount: 200, renderWord(word, { font = "bold", resolution = 8, duration = 1500, rainbow = false } = {}) {
minSize: 1, const canvas = document.createElement("canvas");
maxSize: 3, const ctx = canvas.getContext("2d");
speed: 5, canvas.width = this.container.clientWidth;
color: "white" canvas.height = this.container.clientHeight / 3;
});
*/ const fontSize = Math.floor(canvas.height / 2.5);
ctx.font = `${fontSize}px sans-serif`;
ctx.fillStyle = "black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(word, canvas.width / 2, canvas.height / 2);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
const targetPositions = [];
for (let y = 0; y < canvas.height; y += resolution) {
for (let x = 0; x < canvas.width; x += resolution) {
const i = (y * canvas.width + x) * 4;
if (imageData[i + 3] > 128) {
targetPositions.push([
(x / canvas.width) * 100,
(y / canvas.height) * 100,
]);
}
}
}
targetPositions.sort(() => Math.random() - 0.5);
const used = targetPositions.slice(0, this.stars.length);
this.stars.forEach((star, i) => {
star.style.transition = `top ${duration}ms ease, left ${duration}ms ease, opacity ${duration}ms ease, background-color 1s ease`;
if (i < used.length) {
const [left, top] = used[i];
star.style.left = `${left}%`;
star.style.top = `${top}%`;
star.style.opacity = 1;
if (rainbow) {
const hue = (i / used.length) * 360;
star.style.backgroundColor = `hsl(${hue}, 100%, 70%)`;
}
} else {
star.style.opacity = 0;
}
});
setTimeout(() => {
this.stars.forEach(star => {
star.position = this._getStarPosition(star);
if (rainbow) star.style.backgroundColor = "";
});
}, duration);
}
explodeAndReturn(duration = 1000) {
const originalPositions = this.stars.map(star => ({
left: star.style.left,
top: star.style.top
}));
this.stars.forEach(star => {
const angle = Math.random() * 2 * Math.PI;
const radius = Math.random() * 200;
const x = 50 + Math.cos(angle) * radius;
const y = 50 + Math.sin(angle) * radius;
star.style.transition = `top ${duration / 2}ms ease-out, left ${duration / 2}ms ease-out`;
star.style.left = `${x}%`;
star.style.top = `${y}%`;
});
setTimeout(() => {
this.stars.forEach((star, i) => {
star.style.transition = `top ${duration}ms ease-in, left ${duration}ms ease-in`;
star.style.left = originalPositions[i].left;
star.style.top = originalPositions[i].top;
});
}, duration / 2);
}
startColorCycle() {
let hue = 0;
if (this._colorInterval) clearInterval(this._colorInterval);
this._colorInterval = setInterval(() => {
hue = (hue + 2) % 360;
this.stars.forEach((star, i) => {
star.style.backgroundColor = `hsl(${(hue + i * 3) % 360}, 100%, 75%)`;
});
}, 100);
}
stopColorCycle() {
if (this._colorInterval) {
clearInterval(this._colorInterval);
this._colorInterval = null;
this.stars.forEach(star => star.style.backgroundColor = "");
}
}
addSpecialStar({ title, content, category = "Info", color = "gold", onClick }) {
const star = this.stars.find(s => !s._dataAttached);
if (!star) return;
star.classList.add("special");
star.style.backgroundColor = color;
star._dataAttached = true;
star._specialData = { title, content, category, color, onClick };
const tooltip = document.getElementById("star-tooltip");
const showTooltip = (e) => {
tooltip.innerText = `${title} (${category})`;
tooltip.style.display = "block";
tooltip.style.left = `${e.clientX + 10}px`;
tooltip.style.top = `${e.clientY + 10}px`;
};
const hideTooltip = () => tooltip.style.display = "none";
star.addEventListener("mouseenter", showTooltip);
star.addEventListener("mouseleave", hideTooltip);
star.addEventListener("mousemove", showTooltip);
const showPopup = (e) => {
e.stopPropagation();
this._showPopup(star, star._specialData);
if (onClick) onClick(star._specialData);
};
star.addEventListener("click", showPopup);
star.addEventListener("touchend", showPopup);
return star;
}
_showPopup(star, data) {
const popup = document.getElementById("star-popup");
popup.innerHTML = `<strong>${data.title}</strong><br><small>${data.category}</small><div style="margin-top: 5px;">${data.content}</div>`;
popup.style.display = "block";
const rect = star.getBoundingClientRect();
popup.style.left = `${rect.left + window.scrollX + 10}px`;
popup.style.top = `${rect.top + window.scrollY + 10}px`;
const closeHandler = () => {
popup.style.display = "none";
document.removeEventListener("click", closeHandler);
};
setTimeout(() => document.addEventListener("click", closeHandler), 100);
}
removeSpecialStar(star) {
if (!star._specialData) return;
star.classList.remove("special");
delete star._specialData;
star._dataAttached = false;
const clone = star.cloneNode(true);
star.replaceWith(clone);
const index = this.stars.indexOf(star);
if (index >= 0) this.stars[index] = clone;
}
getSpecialStars() {
return this.stars
.filter(star => star._specialData)
.map(star => ({
star,
data: star._specialData
}));
}
}
const starField = new StarField({starCount: 200});
app.starField = starField;
class DemoSequence {
constructor(steps = []) {
this.steps = steps;
this.overlay = document.createElement("div");
this.overlay.className = "demo-overlay";
document.body.appendChild(this.overlay);
}
start() {
this._runStep(0);
}
_runStep(index) {
if (index >= this.steps.length) {
this._clearOverlay();
return;
}
const { target, text, duration = 3000 } = this.steps[index];
this._clearHighlights();
this._showOverlay(text);
if (target) {
const el = typeof target === 'string' ? document.querySelector(target) : target;
if (el) {
el.classList.remove("demo-highlight");
void el.offsetWidth; // force reflow to reset animation
el.classList.add("demo-highlight");
el.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
setTimeout(() => {
this._hideOverlay();
this._runStep(index + 1);
}, duration);
}
_showOverlay(text) {
this.overlay.innerText = text;
this.overlay.style.animation = "demoFadeIn 0.8s ease forwards";
}
_hideOverlay() {
this.overlay.style.opacity = 0;
this._clearHighlights();
}
_clearOverlay() {
this.overlay.remove();
this._clearHighlights();
}
_clearHighlights() {
document.querySelectorAll(".demo-highlight").forEach(el => {
el.classList.remove("demo-highlight");
});
}
}
/*
const demo = new DemoSequence([
{
text: "💬 Welcome to the Snek Developer Community!",
duration: 3000
},
{
target: ".channels-list",
text: "🔗 Channels help you organize conversations.",
duration: 3000
},
{
target: ".user-icon",
text: "👥 Invite team members here.",
duration: 3000
},
{
target: ".chat-input",
text: "⌨️ Type your message and hit Enter!",
duration: 3000
}
]);*/
// Start when ready (e.g., after load or user action)
//demo.start();
</script> </script>

View File

@ -37,7 +37,21 @@
showHelp(); 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 const textBox = document.querySelector("chat-input").textarea
textBox.addEventListener("paste", async (e) => { textBox.addEventListener("paste", async (e) => {
try { try {

View File

@ -9,7 +9,7 @@
import json import json
import traceback import traceback
import asyncio
from aiohttp import web from aiohttp import web
from snek.system.model import now from snek.system.model import now
@ -21,13 +21,29 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RPCView(BaseView): class RPCView(BaseView):
class RPCApi: class RPCApi:
def __init__(self, view, ws): def __init__(self, view, ws):
self.view = view self.view = view
self.app = self.view.app self.app = self.view.app
self.services = self.app.services self.services = self.app.services
self.ws = ws 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): async def db_insert(self, table_name, record):
self._require_login() self._require_login()
@ -323,6 +339,11 @@ class RPCView(BaseView):
async for record in self.services.channel.get_users(channel_uid) 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): async def ping(self, callId, *args):
if self.user_uid: if self.user_uid:
user = await self.services.user.get(uid=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) await self.services.user.save(user)
return {"pong": args} return {"pong": args}
async def get(self): 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() ws = web.WebSocketResponse()
await ws.prepare(self.request) await ws.prepare(self.request)
if self.request.session.get("logged_in"): if self.request.session.get("logged_in"):
@ -343,6 +371,16 @@ class RPCView(BaseView):
await self.services.socket.subscribe( await self.services.socket.subscribe(
ws, subscription["channel_uid"], self.request.session.get("uid") 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) rpc = RPCView.RPCApi(self, ws)
async for msg in ws: async for msg in ws:
if msg.type == web.WSMsgType.TEXT: if msg.type == web.WSMsgType.TEXT: