Update.
This commit is contained in:
parent
a55d15b635
commit
3bf09f9083
src/snek
@ -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):
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user