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 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):

View File

@ -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);
});

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;
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;
}

View File

@ -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);
}

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">
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 {
constructor(container = document.body, options = {}) {
constructor({ count = 200, container = document.body } = {}) {
this.container = container;
this.starCount = count;
this.stars = [];
this.setOptions(options);
this.positionMap = {};
this.originalColor = getComputedStyle(document.documentElement).getPropertyValue("--star-color").trim();
this._createStars();
window.stars = this.positionMap;
}
setOptions({
starCount = 200,
minSize = 1,
maxSize = 3,
speed = 5,
color = "white"
}) {
this.options = { starCount, minSize, maxSize, speed, color };
_getStarPosition(star) {
const left = parseFloat(star.style.left);
const top = parseFloat(star.style.top);
if (top < 40 && left >= 40 && left <= 60) return "North";
if (top > 60 && left >= 40 && left <= 60) return "South";
if (left < 40 && top >= 40 && top <= 60) return "West";
if (left > 60 && top >= 40 && top <= 60) return "East";
if (top >= 40 && top <= 60 && left >= 40 && left <= 60) return "Center";
return "Corner or Edge";
}
clear() {
this.stars.forEach(star => star.remove());
this.stars = [];
}
generate() {
this.clear();
const { starCount, minSize, maxSize, speed, color } = this.options;
for (let i = 0; i < starCount; i++) {
_createStars() {
for (let i = 0; i < this.starCount; i++) {
const star = document.createElement("div");
star.classList.add("star");
const size = Math.random() * (maxSize - minSize) + minSize;
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._randomizeStar(star);
this._placeStar(star);
this.container.appendChild(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,
minSize: 1,
maxSize: 3,
speed: 5,
color: "white"
});
*/
renderWord(word, { font = "bold", resolution = 8, duration = 1500, rainbow = false } = {}) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = this.container.clientWidth;
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>

View File

@ -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 {

View File

@ -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: