Merge branch 'main' into bugfix/opacity-for-last-message
This commit is contained in:
commit
2c506db4e4
src/snek
app.py
static
app.jschat-input.jschat-window.jsdumb-term.jsevent-handler.jsfancy-button.jsfile-manager.jsgeneric-form.jshtml-frame.jsmarkdown-frame.jsmedia-upload.jsmessage-list-manager.jsmessage-list.jsmodels.jsonline-users.jspush.jssandbox.cssservice-worker.jssocket.jsupload-button.jsuser-list.js
templates
view
@ -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()
|
||||
@ -113,6 +115,33 @@ class Application(BaseApplication):
|
||||
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):
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
// MIT License
|
||||
|
||||
import { Schedule } from './schedule.js';
|
||||
import { Schedule } from "./schedule.js";
|
||||
import { EventHandler } from "./event-handler.js";
|
||||
import { Socket } from "./socket.js";
|
||||
|
||||
@ -16,11 +16,11 @@ export class RESTClient {
|
||||
|
||||
async get(url, params = {}) {
|
||||
const encodedParams = new URLSearchParams(params);
|
||||
if (encodedParams) url += '?' + encodedParams;
|
||||
if (encodedParams) url += "?" + encodedParams;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const result = await response.json();
|
||||
@ -32,9 +32,9 @@ export class RESTClient {
|
||||
|
||||
async post(url, data) {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
@ -50,7 +50,10 @@ export class RESTClient {
|
||||
export class Chat extends EventHandler {
|
||||
constructor() {
|
||||
super();
|
||||
this._url = window.location.hostname === 'localhost' ? 'ws://localhost/chat.ws' : 'wss://' + window.location.hostname + '/chat.ws';
|
||||
this._url =
|
||||
window.location.hostname === "localhost"
|
||||
? "ws://localhost/chat.ws"
|
||||
: "wss://" + window.location.hostname + "/chat.ws";
|
||||
this._socket = null;
|
||||
this._waitConnect = null;
|
||||
this._promises = {};
|
||||
@ -81,7 +84,7 @@ export class Chat extends EventHandler {
|
||||
}
|
||||
|
||||
generateUniqueId() {
|
||||
return 'id-' + Math.random().toString(36).substr(2, 9);
|
||||
return "id-" + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
call(method, ...args) {
|
||||
@ -109,7 +112,7 @@ export class Chat extends EventHandler {
|
||||
this._socket.onclose = () => {
|
||||
this._waitSocket = null;
|
||||
this._socket = null;
|
||||
this.emit('close');
|
||||
this.emit("close");
|
||||
};
|
||||
}
|
||||
|
||||
@ -127,19 +130,21 @@ export class NotificationAudio {
|
||||
}
|
||||
|
||||
sounds = {
|
||||
"message": "/audio/soundfx.d_beep3.mp3",
|
||||
"mention": "/audio/750607__deadrobotmusic__notification-sound-1.wav",
|
||||
"messageOtherChannel": "/audio/750608__deadrobotmusic__notification-sound-2.wav",
|
||||
"ping": "/audio/750609__deadrobotmusic__notification-sound-3.wav",
|
||||
}
|
||||
message: "/audio/soundfx.d_beep3.mp3",
|
||||
mention: "/audio/750607__deadrobotmusic__notification-sound-1.wav",
|
||||
messageOtherChannel:
|
||||
"/audio/750608__deadrobotmusic__notification-sound-2.wav",
|
||||
ping: "/audio/750609__deadrobotmusic__notification-sound-3.wav",
|
||||
};
|
||||
|
||||
play(soundIndex = 0) {
|
||||
this.schedule.delay(() => {
|
||||
new Audio(this.sounds[soundIndex]).play()
|
||||
new Audio(this.sounds[soundIndex])
|
||||
.play()
|
||||
.then(() => {
|
||||
console.debug("Gave sound notification");
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error("Notification failed:", error);
|
||||
});
|
||||
});
|
||||
@ -153,52 +158,54 @@ export class App extends EventHandler {
|
||||
audio = null;
|
||||
user = {};
|
||||
typeLock = null;
|
||||
typeListener = null
|
||||
typeEventChannelUid = null
|
||||
async set_typing(channel_uid){
|
||||
this.typeEventChannel_uid = channel_uid
|
||||
typeListener = null;
|
||||
typeEventChannelUid = null;
|
||||
async set_typing(channel_uid) {
|
||||
this.typeEventChannel_uid = channel_uid;
|
||||
}
|
||||
|
||||
async ping(...args) {
|
||||
if (this.is_pinging) return false
|
||||
this.is_pinging = true
|
||||
if (this.is_pinging) return false;
|
||||
this.is_pinging = true;
|
||||
await this.rpc.ping(...args);
|
||||
this.is_pinging = false
|
||||
this.is_pinging = false;
|
||||
}
|
||||
|
||||
async forcePing(...arg) {
|
||||
await this.rpc.ping(...args);
|
||||
}
|
||||
|
||||
starField = null
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new Socket();
|
||||
this.rpc = this.ws.client;
|
||||
this.audio = new NotificationAudio(500);
|
||||
this.is_pinging = false
|
||||
this.is_pinging = false;
|
||||
this.ping_interval = setInterval(() => {
|
||||
this.ping("active")
|
||||
}, 15000)
|
||||
this.typeEventChannelUid = null
|
||||
this.typeListener = setInterval(()=>{
|
||||
if(this.typeEventChannelUid){
|
||||
this.rpc.set_typing(this.typeEventChannelUid)
|
||||
this.typeEventChannelUid = null
|
||||
this.ping("active");
|
||||
}, 15000);
|
||||
this.typeEventChannelUid = null;
|
||||
this.typeListener = setInterval(() => {
|
||||
if (this.typeEventChannelUid) {
|
||||
this.rpc.set_typing(this.typeEventChannelUid);
|
||||
this.typeEventChannelUid = null;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const me = this
|
||||
const me = this;
|
||||
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) => {
|
||||
me.emit("channel-message", data);
|
||||
});
|
||||
this.ws.addEventListener("event",(data)=>{
|
||||
console.info("aaaa")
|
||||
})
|
||||
this.rpc.getUser(null).then(user => {
|
||||
this.ws.addEventListener("event", (data) => {
|
||||
console.info("aaaa");
|
||||
});
|
||||
this.rpc.getUser(null).then((user) => {
|
||||
me.user = user;
|
||||
});
|
||||
}
|
||||
@ -218,31 +225,35 @@ export class App extends EventHandler {
|
||||
timeAgo(date1, date2) {
|
||||
const diffMs = Math.abs(date2 - date1);
|
||||
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const hours = Math.floor(
|
||||
(diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
|
||||
);
|
||||
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
||||
|
||||
if (days) {
|
||||
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
|
||||
return `${days} ${days > 1 ? "days" : "day"} ago`;
|
||||
}
|
||||
if (hours) {
|
||||
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
|
||||
return `${hours} ${hours > 1 ? "hours" : "hour"} ago`;
|
||||
}
|
||||
if (minutes) {
|
||||
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
|
||||
return `${minutes} ${minutes > 1 ? "minutes" : "minute"} ago`;
|
||||
}
|
||||
return 'just now';
|
||||
return "just now";
|
||||
}
|
||||
|
||||
async benchMark(times = 100, message = "Benchmark Message") {
|
||||
const promises = [];
|
||||
const me = this;
|
||||
for (let i = 0; i < times; i++) {
|
||||
promises.push(this.rpc.getChannels().then(channels => {
|
||||
channels.forEach(channel => {
|
||||
promises.push(
|
||||
this.rpc.getChannels().then((channels) => {
|
||||
channels.forEach((channel) => {
|
||||
me.rpc.sendMessage(channel.uid, `${message} ${i}`);
|
||||
});
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,10 @@
|
||||
|
||||
import { app } from '../app.js';
|
||||
import { app } from "../app.js";
|
||||
|
||||
class ChatInputComponent extends HTMLElement {
|
||||
autoCompletions = {
|
||||
'example 1': () => {
|
||||
|
||||
},
|
||||
'example 2': () => {
|
||||
|
||||
}
|
||||
}
|
||||
"example 1": () => {},
|
||||
"example 2": () => {},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -40,8 +35,7 @@ class ChatInputComponent extends HTMLElement {
|
||||
value = key;
|
||||
}
|
||||
});
|
||||
if (count == 1)
|
||||
return value;
|
||||
if (count == 1) return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -53,10 +47,15 @@ class ChatInputComponent extends HTMLElement {
|
||||
this.textarea.focus();
|
||||
}
|
||||
|
||||
|
||||
async connectedCallback() {
|
||||
this.user = await app.rpc.getUser(null);
|
||||
this.user = null
|
||||
app.rpc.getUser(null).then((user) => {
|
||||
this.user=user
|
||||
})
|
||||
this.liveType = this.getAttribute("live-type") === "true";
|
||||
this.liveTypeInterval = parseInt(this.getAttribute("live-type-interval")) || 3;
|
||||
this.liveTypeInterval =
|
||||
parseInt(this.getAttribute("live-type-interval")) || 3;
|
||||
this.channelUid = this.getAttribute("channel");
|
||||
this.messageUid = null;
|
||||
|
||||
@ -79,10 +78,10 @@ class ChatInputComponent extends HTMLElement {
|
||||
this.appendChild(this.uploadButton);
|
||||
|
||||
this.textarea.addEventListener("keyup", (e) => {
|
||||
if(e.key === 'Enter' && !e.shiftKey) {
|
||||
this.value = ''
|
||||
e.target.value = '';
|
||||
return
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
this.value = "";
|
||||
e.target.value = "";
|
||||
return;
|
||||
}
|
||||
this.value = e.target.value;
|
||||
this.changed = true;
|
||||
@ -100,13 +99,13 @@ class ChatInputComponent extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
|
||||
const message = e.target.value;
|
||||
this.messageUid = null;
|
||||
this.value = '';
|
||||
this.previousValue = '';
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
@ -114,15 +113,15 @@ class ChatInputComponent extends HTMLElement {
|
||||
|
||||
let autoCompletion = this.autoCompletions[message];
|
||||
if (autoCompletion) {
|
||||
this.value = '';
|
||||
this.previousValue = '';
|
||||
e.target.value = '';
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
e.target.value = "";
|
||||
autoCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
e.target.value = '';
|
||||
this.value = '';
|
||||
e.target.value = "";
|
||||
this.value = "";
|
||||
this.messageUid = null;
|
||||
this.sendMessage(this.channelUid, message).then((uid) => {
|
||||
this.messageUid = uid;
|
||||
@ -135,9 +134,12 @@ class ChatInputComponent extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
if (this.value !== this.previousValue) {
|
||||
if (this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval) {
|
||||
this.value = '';
|
||||
this.previousValue = '';
|
||||
if (
|
||||
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
||||
this.liveTypeInterval
|
||||
) {
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
}
|
||||
this.lastChange = new Date();
|
||||
}
|
||||
@ -154,6 +156,9 @@ class ChatInputComponent extends HTMLElement {
|
||||
});
|
||||
app.rpc.sendMessage(this.channelUid, message);
|
||||
});
|
||||
setTimeout(()=>{
|
||||
this.focus();
|
||||
},1000)
|
||||
}
|
||||
|
||||
trackSecondsBetweenEvents(event1Time, event2Time) {
|
||||
@ -163,7 +168,7 @@ class ChatInputComponent extends HTMLElement {
|
||||
|
||||
newMessage() {
|
||||
if (!this.messageUid) {
|
||||
this.messageUid = '?';
|
||||
this.messageUid = "?";
|
||||
}
|
||||
|
||||
this.sendMessage(this.channelUid, this.value).then((uid) => {
|
||||
@ -179,10 +184,14 @@ class ChatInputComponent extends HTMLElement {
|
||||
this.newMessage();
|
||||
return false;
|
||||
}
|
||||
if (this.messageUid === '?') {
|
||||
if (this.messageUid === "?") {
|
||||
return false;
|
||||
}
|
||||
if (typeof app !== "undefined" && app.rpc && typeof app.rpc.updateMessageText === "function") {
|
||||
if (
|
||||
typeof app !== "undefined" &&
|
||||
app.rpc &&
|
||||
typeof app.rpc.updateMessageText === "function"
|
||||
) {
|
||||
app.rpc.updateMessageText(this.messageUid, this.value);
|
||||
}
|
||||
}
|
||||
@ -193,15 +202,21 @@ class ChatInputComponent extends HTMLElement {
|
||||
}
|
||||
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {
|
||||
this.lastUpdateEvent = new Date();
|
||||
if (typeof app !== "undefined" && app.rpc && typeof app.rpc.set_typing === "function") {
|
||||
app.rpc.set_typing(this.channelUid,this.user.color);
|
||||
if (
|
||||
typeof app !== "undefined" &&
|
||||
app.rpc &&
|
||||
typeof app.rpc.set_typing === "function"
|
||||
) {
|
||||
app.rpc.set_typing(this.channelUid, this.user.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
const expired = this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= this.liveTypeInterval;
|
||||
const changed = (this.value !== this.previousValue);
|
||||
const expired =
|
||||
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
||||
this.liveTypeInterval;
|
||||
const changed = this.value !== this.previousValue;
|
||||
|
||||
if (changed || expired) {
|
||||
this.lastChange = new Date();
|
||||
@ -232,4 +247,4 @@ class ChatInputComponent extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chat-input', ChatInputComponent);
|
||||
customElements.define("chat-input", ChatInputComponent);
|
||||
|
@ -13,11 +13,11 @@
|
||||
|
||||
class ChatWindowElement extends HTMLElement {
|
||||
receivedHistory = false;
|
||||
channel = null
|
||||
channel = null;
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement('section');
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.component = document.createElement("section");
|
||||
this.app = app;
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
@ -27,9 +27,9 @@ class ChatWindowElement extends HTMLElement {
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/base.css';
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = "/base.css";
|
||||
this.component.appendChild(link);
|
||||
this.component.classList.add("chat-area");
|
||||
|
||||
@ -39,7 +39,7 @@ class ChatWindowElement extends HTMLElement {
|
||||
const chatHeader = document.createElement("div");
|
||||
chatHeader.classList.add("chat-header");
|
||||
|
||||
const chatTitle = document.createElement('h2');
|
||||
const chatTitle = document.createElement("h2");
|
||||
chatTitle.classList.add("chat-title");
|
||||
chatTitle.classList.add("no-select");
|
||||
chatTitle.innerText = "Loading...";
|
||||
@ -51,11 +51,11 @@ class ChatWindowElement extends HTMLElement {
|
||||
this.channel = channel;
|
||||
chatTitle.innerText = channel.name;
|
||||
|
||||
const channelElement = document.createElement('message-list');
|
||||
const channelElement = document.createElement("message-list");
|
||||
channelElement.setAttribute("channel", channel.uid);
|
||||
this.container.appendChild(channelElement);
|
||||
|
||||
const chatInput = document.createElement('chat-input');
|
||||
const chatInput = document.createElement("chat-input");
|
||||
chatInput.chatWindow = this;
|
||||
chatInput.addEventListener("submit", (e) => {
|
||||
app.rpc.sendMessage(channel.uid, e.detail);
|
||||
@ -65,8 +65,8 @@ class ChatWindowElement extends HTMLElement {
|
||||
this.component.appendChild(this.container);
|
||||
|
||||
const messages = await app.rpc.getMessages(channel.uid);
|
||||
messages.forEach(message => {
|
||||
if (!message['user_nick']) return;
|
||||
messages.forEach((message) => {
|
||||
if (!message["user_nick"]) return;
|
||||
channelElement.addMessage(message);
|
||||
});
|
||||
|
||||
@ -74,9 +74,9 @@ class ChatWindowElement extends HTMLElement {
|
||||
channelElement.addEventListener("message", (message) => {
|
||||
if (me.user.uid !== message.detail.user_uid) app.playSound(0);
|
||||
|
||||
message.detail.element.scrollIntoView({"block": "end"});
|
||||
message.detail.element.scrollIntoView({ block: "end" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('chat-window', ChatWindowElement);
|
||||
customElements.define("chat-window", ChatWindowElement);
|
||||
|
150
src/snek/static/dumb-term.js
Normal file
150
src/snek/static/dumb-term.js
Normal file
@ -0,0 +1,150 @@
|
||||
class DumbTerminal extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
--terminal-bg: #111;
|
||||
--terminal-fg: #0f0;
|
||||
--terminal-accent: #0ff;
|
||||
--terminal-font: monospace;
|
||||
|
||||
display: block;
|
||||
background: var(--terminal-bg);
|
||||
color: var(--terminal-fg);
|
||||
font-family: var(--terminal-font);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-y: auto;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.output {
|
||||
white-space: pre-wrap;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: var(--terminal-accent);
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--terminal-fg);
|
||||
outline: none;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
dialog {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dialog-backdrop {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="output" id="output"></div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">></span>
|
||||
<input type="text" id="input" autocomplete="off" autofocus />
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.outputEl = this.shadowRoot.getElementById("output");
|
||||
this.inputEl = this.shadowRoot.getElementById("input");
|
||||
|
||||
this.history = [];
|
||||
this.historyIndex = -1;
|
||||
|
||||
this.inputEl.addEventListener("keydown", (e) => this.onKeyDown(e));
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
const value = this.inputEl.value;
|
||||
|
||||
switch (event.key) {
|
||||
case "Enter":
|
||||
this.executeCommand(value);
|
||||
this.history.push(value);
|
||||
this.historyIndex = this.history.length;
|
||||
this.inputEl.value = "";
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
if (this.historyIndex > 0) {
|
||||
this.historyIndex--;
|
||||
this.inputEl.value = this.history[this.historyIndex];
|
||||
}
|
||||
break;
|
||||
|
||||
case "ArrowDown":
|
||||
if (this.historyIndex < this.history.length - 1) {
|
||||
this.historyIndex++;
|
||||
this.inputEl.value = this.history[this.historyIndex];
|
||||
} else {
|
||||
this.historyIndex = this.history.length;
|
||||
this.inputEl.value = "";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
executeCommand(command) {
|
||||
const outputLine = document.createElement("div");
|
||||
outputLine.textContent = `> ${command}`;
|
||||
this.outputEl.appendChild(outputLine);
|
||||
|
||||
const resultLine = document.createElement("div");
|
||||
resultLine.textContent = this.mockExecute(command);
|
||||
this.outputEl.appendChild(resultLine);
|
||||
|
||||
this.outputEl.scrollTop = this.outputEl.scrollHeight;
|
||||
}
|
||||
|
||||
mockExecute(command) {
|
||||
switch (command.trim()) {
|
||||
case "help":
|
||||
return "Available commands: help, clear, date";
|
||||
case "date":
|
||||
return new Date().toString();
|
||||
case "clear":
|
||||
this.outputEl.innerHTML = "";
|
||||
return "";
|
||||
default:
|
||||
return `Unknown command: ${command}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to create a modal dialog with the terminal
|
||||
* @returns {HTMLDialogElement}
|
||||
*/
|
||||
static createModal() {
|
||||
const dialog = document.createElement("dialog");
|
||||
dialog.innerHTML = `
|
||||
<div class="dialog-backdrop">
|
||||
<web-terminal></web-terminal>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(dialog);
|
||||
dialog.showModal();
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("web-terminal", WebTerminal);
|
@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
export class EventHandler {
|
||||
constructor() {
|
||||
this.subscribers = {};
|
||||
@ -11,6 +9,7 @@ export class EventHandler {
|
||||
}
|
||||
|
||||
emit(type, ...data) {
|
||||
if (this.subscribers[type]) this.subscribers[type].forEach(handler => handler(...data));
|
||||
if (this.subscribers[type])
|
||||
this.subscribers[type].forEach((handler) => handler(...data));
|
||||
}
|
||||
}
|
@ -2,25 +2,22 @@
|
||||
|
||||
// This JavaScript class defines a custom HTML element <fancy-button>, which creates a styled, clickable button element with customizable size, text, and URL redirect functionality.
|
||||
|
||||
|
||||
// MIT License
|
||||
|
||||
|
||||
|
||||
class FancyButton extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.url = null;
|
||||
this.type = "button";
|
||||
this.value = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.container = document.createElement('span');
|
||||
let size = this.getAttribute('size');
|
||||
this.container = document.createElement("span");
|
||||
let size = this.getAttribute("size");
|
||||
console.info({ GG: size });
|
||||
size = size === 'auto' ? '1%' : '33%';
|
||||
size = size === "auto" ? "1%" : "33%";
|
||||
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
@ -50,19 +47,20 @@ class FancyButton extends HTMLElement {
|
||||
`;
|
||||
|
||||
this.container.appendChild(this.styleElement);
|
||||
this.buttonElement = document.createElement('button');
|
||||
this.buttonElement = document.createElement("button");
|
||||
this.container.appendChild(this.buttonElement);
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
|
||||
this.url = this.getAttribute('url');
|
||||
this.url = this.getAttribute("url");
|
||||
|
||||
|
||||
this.value = this.getAttribute('value');
|
||||
this.buttonElement.appendChild(document.createTextNode(this.getAttribute("text")));
|
||||
this.value = this.getAttribute("value");
|
||||
this.buttonElement.appendChild(
|
||||
document.createTextNode(this.getAttribute("text")),
|
||||
);
|
||||
this.buttonElement.addEventListener("click", () => {
|
||||
if(this.url == 'submit'){
|
||||
this.closest('form').submit()
|
||||
return
|
||||
if (this.url == "submit") {
|
||||
this.closest("form").submit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.url === "/back" || this.url === "/back/") {
|
||||
|
@ -40,19 +40,30 @@ class FileBrowser extends HTMLElement {
|
||||
<button id="next">Next</button>
|
||||
</nav>
|
||||
`;
|
||||
this.shadowRoot.getElementById("up").addEventListener("click", () => this.goUp());
|
||||
this.shadowRoot
|
||||
.getElementById("up")
|
||||
.addEventListener("click", () => this.goUp());
|
||||
this.shadowRoot.getElementById("prev").addEventListener("click", () => {
|
||||
if (this.offset > 0) { this.offset -= this.limit; this.load(); }
|
||||
if (this.offset > 0) {
|
||||
this.offset -= this.limit;
|
||||
this.load();
|
||||
}
|
||||
});
|
||||
this.shadowRoot.getElementById("next").addEventListener("click", () => {
|
||||
this.offset += this.limit; this.load();
|
||||
this.offset += this.limit;
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Networking ----------------------------------------------
|
||||
async load() {
|
||||
const r = await fetch(`/drive.json?path=${encodeURIComponent(this.path)}&offset=${this.offset}&limit=${this.limit}`);
|
||||
if (!r.ok) { console.error(await r.text()); return; }
|
||||
const r = await fetch(
|
||||
`/drive.json?path=${encodeURIComponent(this.path)}&offset=${this.offset}&limit=${this.limit}`,
|
||||
);
|
||||
if (!r.ok) {
|
||||
console.error(await r.text());
|
||||
return;
|
||||
}
|
||||
const data = await r.json();
|
||||
this.renderTiles(data.items);
|
||||
this.updateNav(data.pagination);
|
||||
@ -62,13 +73,17 @@ class FileBrowser extends HTMLElement {
|
||||
renderTiles(items) {
|
||||
const grid = this.shadowRoot.getElementById("grid");
|
||||
grid.innerHTML = "";
|
||||
items.forEach(item => {
|
||||
items.forEach((item) => {
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "tile";
|
||||
|
||||
if (item.type === "directory") {
|
||||
tile.innerHTML = `<div class="icon">📂</div><div>${item.name}</div>`;
|
||||
tile.addEventListener("click", () => { this.path = item.path; this.offset = 0; this.load(); });
|
||||
tile.addEventListener("click", () => {
|
||||
this.path = item.path;
|
||||
this.offset = 0;
|
||||
this.load();
|
||||
});
|
||||
} else {
|
||||
if (item.mimetype?.startsWith("image/")) {
|
||||
tile.innerHTML = `<img class="thumb" src="${item.url}" alt="${item.name}"><div>${item.name}</div>`;
|
||||
|
@ -40,7 +40,7 @@ class GenericField extends HTMLElement {
|
||||
}
|
||||
|
||||
set value(val) {
|
||||
val = val ?? '';
|
||||
val = val ?? "";
|
||||
this.inputElement.value = val;
|
||||
this.inputElement.setAttribute("value", val);
|
||||
}
|
||||
@ -62,9 +62,9 @@ class GenericField extends HTMLElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.container = document.createElement('div');
|
||||
this.styleElement = document.createElement('style');
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.container = document.createElement("div");
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
|
||||
h1 {
|
||||
@ -174,7 +174,7 @@ class GenericField extends HTMLElement {
|
||||
|
||||
if (this.inputElement == null && this.field) {
|
||||
this.inputElement = document.createElement(this.field.tag);
|
||||
if (this.field.tag === 'button' && this.field.value === "submit") {
|
||||
if (this.field.tag === "button" && this.field.value === "submit") {
|
||||
this.action = this.field.value;
|
||||
}
|
||||
this.inputElement.name = this.field.name;
|
||||
@ -182,26 +182,39 @@ class GenericField extends HTMLElement {
|
||||
|
||||
const me = this;
|
||||
this.inputElement.addEventListener("keyup", (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const event = new CustomEvent("change", {detail: me, bubbles: true});
|
||||
if (e.key === "Enter") {
|
||||
const event = new CustomEvent("change", {
|
||||
detail: me,
|
||||
bubbles: true,
|
||||
});
|
||||
me.dispatchEvent(event);
|
||||
|
||||
me.dispatchEvent(new Event("submit"));
|
||||
} else if (me.field.value !== e.target.value) {
|
||||
const event = new CustomEvent("change", {detail: me, bubbles: true});
|
||||
const event = new CustomEvent("change", {
|
||||
detail: me,
|
||||
bubbles: true,
|
||||
});
|
||||
me.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
this.inputElement.addEventListener("click", (e) => {
|
||||
const event = new CustomEvent("click", {detail: me, bubbles: true});
|
||||
const event = new CustomEvent("click", { detail: me, bubbles: true });
|
||||
me.dispatchEvent(event);
|
||||
});
|
||||
|
||||
this.inputElement.addEventListener("blur", (e) => {
|
||||
const event = new CustomEvent("change", {detail: me, bubbles: true});
|
||||
this.inputElement.addEventListener(
|
||||
"blur",
|
||||
(e) => {
|
||||
const event = new CustomEvent("change", {
|
||||
detail: me,
|
||||
bubbles: true,
|
||||
});
|
||||
me.dispatchEvent(event);
|
||||
}, true);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
this.container.appendChild(this.inputElement);
|
||||
}
|
||||
@ -210,8 +223,8 @@ class GenericField extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.inputElement.setAttribute("type", this.field.type ?? 'input');
|
||||
this.inputElement.setAttribute("name", this.field.name ?? '');
|
||||
this.inputElement.setAttribute("type", this.field.type ?? "input");
|
||||
this.inputElement.setAttribute("name", this.field.name ?? "");
|
||||
|
||||
if (this.field.text != null) {
|
||||
this.inputElement.innerText = this.field.text;
|
||||
@ -239,14 +252,14 @@ class GenericField extends HTMLElement {
|
||||
this.inputElement.removeAttribute("required");
|
||||
}
|
||||
if (!this.footerElement) {
|
||||
this.footerElement = document.createElement('div');
|
||||
this.footerElement.style.clear = 'both';
|
||||
this.footerElement = document.createElement("div");
|
||||
this.footerElement.style.clear = "both";
|
||||
this.container.appendChild(this.footerElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('generic-field', GenericField);
|
||||
customElements.define("generic-field", GenericField);
|
||||
|
||||
class GenericForm extends HTMLElement {
|
||||
fields = {};
|
||||
@ -254,7 +267,7 @@ class GenericForm extends HTMLElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
|
||||
@ -281,27 +294,29 @@ class GenericForm extends HTMLElement {
|
||||
}
|
||||
}`;
|
||||
|
||||
this.container = document.createElement('div');
|
||||
this.container = document.createElement("div");
|
||||
this.container.appendChild(this.styleElement);
|
||||
this.container.classList.add("generic-form-container");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const preloadedForm = this.getAttribute('preloaded-structure');
|
||||
const preloadedForm = this.getAttribute("preloaded-structure");
|
||||
if (preloadedForm) {
|
||||
try {
|
||||
const form = JSON.parse(preloadedForm);
|
||||
this.constructForm(form)
|
||||
this.constructForm(form);
|
||||
} catch (error) {
|
||||
console.error(error, preloadedForm);
|
||||
}
|
||||
}
|
||||
const url = this.getAttribute('url');
|
||||
const url = this.getAttribute("url");
|
||||
if (url) {
|
||||
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
|
||||
const fullUrl = url.startsWith("/")
|
||||
? window.location.origin + url
|
||||
: new URL(window.location.origin + "/http-get");
|
||||
if (!url.startsWith("/")) {
|
||||
fullUrl.searchParams.set('url', url);
|
||||
fullUrl.searchParams.set("url", url);
|
||||
}
|
||||
this.loadForm(fullUrl.toString());
|
||||
} else {
|
||||
@ -318,10 +333,10 @@ class GenericForm extends HTMLElement {
|
||||
let hasAutoFocus = Object.keys(this.fields).length !== 0;
|
||||
|
||||
fields.sort((a, b) => a.index - b.index);
|
||||
fields.forEach(field => {
|
||||
const updatingField = field.name in this.fields
|
||||
fields.forEach((field) => {
|
||||
const updatingField = field.name in this.fields;
|
||||
|
||||
this.fields[field.name] ??= document.createElement('generic-field');
|
||||
this.fields[field.name] ??= document.createElement("generic-field");
|
||||
|
||||
const fieldElement = this.fields[field.name];
|
||||
|
||||
@ -362,7 +377,7 @@ class GenericForm extends HTMLElement {
|
||||
window.location.pathname = saveResult.redirect_url;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@ -374,7 +389,9 @@ class GenericForm extends HTMLElement {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
|
||||
throw new Error(
|
||||
`Failed to fetch: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.constructForm(await response.json());
|
||||
@ -387,15 +404,15 @@ class GenericForm extends HTMLElement {
|
||||
const url = this.getAttribute("url");
|
||||
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({"action": "validate", "form": this.form})
|
||||
body: JSON.stringify({ action: "validate", form: this.form }),
|
||||
});
|
||||
|
||||
const form = await response.json();
|
||||
Object.values(form.fields).forEach(field => {
|
||||
Object.values(form.fields).forEach((field) => {
|
||||
if (!this.form.fields[field.name]) {
|
||||
return;
|
||||
}
|
||||
@ -409,23 +426,23 @@ class GenericForm extends HTMLElement {
|
||||
this.fields[field.name].setAttribute("field", field);
|
||||
this.fields[field.name].updateAttributes();
|
||||
});
|
||||
Object.values(form.fields).forEach(field => {
|
||||
Object.values(form.fields).forEach((field) => {
|
||||
this.fields[field.name].setErrors(field.errors);
|
||||
});
|
||||
return form['is_valid'];
|
||||
return form["is_valid"];
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const url = this.getAttribute("url");
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({"action": "submit", "form": this.form})
|
||||
body: JSON.stringify({ action: "submit", form: this.form }),
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('generic-form', GenericForm);
|
||||
customElements.define("generic-form", GenericForm);
|
||||
|
@ -9,21 +9,23 @@
|
||||
class HTMLFrame extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.container = document.createElement('div');
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.container = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.container.classList.add("html_frame");
|
||||
let url = this.getAttribute('url');
|
||||
let url = this.getAttribute("url");
|
||||
if (!url.startsWith("https")) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
if (url) {
|
||||
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
|
||||
const fullUrl = url.startsWith("/")
|
||||
? window.location.origin + url
|
||||
: new URL(window.location.origin + "/http-get");
|
||||
if (!url.startsWith("/")) {
|
||||
fullUrl.searchParams.set('url', url);
|
||||
fullUrl.searchParams.set("url", url);
|
||||
}
|
||||
this.loadAndRender(fullUrl.toString());
|
||||
} else {
|
||||
@ -39,7 +41,7 @@ class HTMLFrame extends HTMLElement {
|
||||
}
|
||||
const html = await response.text();
|
||||
if (url.endsWith(".md")) {
|
||||
const markdownElement = document.createElement('div');
|
||||
const markdownElement = document.createElement("div");
|
||||
markdownElement.innerHTML = html;
|
||||
this.outerHTML = html;
|
||||
} else {
|
||||
@ -51,4 +53,4 @@ class HTMLFrame extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('html-frame', HTMLFrame);
|
||||
customElements.define("html-frame", HTMLFrame);
|
||||
|
@ -12,22 +12,22 @@
|
||||
class HTMLFrame extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.container = document.createElement('div');
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.container = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.container.classList.add('html_frame');
|
||||
const url = this.getAttribute('url');
|
||||
this.container.classList.add("html_frame");
|
||||
const url = this.getAttribute("url");
|
||||
if (url) {
|
||||
const fullUrl = url.startsWith('/')
|
||||
const fullUrl = url.startsWith("/")
|
||||
? window.location.origin + url
|
||||
: new URL(window.location.origin + '/http-get');
|
||||
if (!url.startsWith('/')) fullUrl.searchParams.set('url', url);
|
||||
: new URL(window.location.origin + "/http-get");
|
||||
if (!url.startsWith("/")) fullUrl.searchParams.set("url", url);
|
||||
this.loadAndRender(fullUrl.toString());
|
||||
} else {
|
||||
this.container.textContent = 'No source URL!';
|
||||
this.container.textContent = "No source URL!";
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,4 +45,4 @@ class HTMLFrame extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('markdown-frame', HTMLFrame);
|
||||
customElements.define("markdown-frame", HTMLFrame);
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
// No external libraries or dependencies are used other than standard web components.
|
||||
|
||||
|
||||
// MIT License
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
@ -13,19 +12,18 @@
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
class TileGridElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.gridId = this.getAttribute('grid');
|
||||
this.component = document.createElement('div');
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.gridId = this.getAttribute("grid");
|
||||
this.component = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('connected');
|
||||
this.styleElement = document.createElement('style');
|
||||
console.log("connected");
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.textContent = `
|
||||
.grid {
|
||||
padding: 10px;
|
||||
@ -48,26 +46,26 @@ class TileGridElement extends HTMLElement {
|
||||
}
|
||||
`;
|
||||
this.component.appendChild(this.styleElement);
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add('gallery');
|
||||
this.container = document.createElement("div");
|
||||
this.container.classList.add("gallery");
|
||||
this.component.appendChild(this.container);
|
||||
}
|
||||
|
||||
addImage(src) {
|
||||
const item = document.createElement('img');
|
||||
const item = document.createElement("img");
|
||||
item.src = src;
|
||||
item.classList.add('tile');
|
||||
item.style.width = '100px';
|
||||
item.style.height = '100px';
|
||||
item.classList.add("tile");
|
||||
item.style.width = "100px";
|
||||
item.style.height = "100px";
|
||||
this.container.appendChild(item);
|
||||
}
|
||||
|
||||
addImages(srcs) {
|
||||
srcs.forEach(src => this.addImage(src));
|
||||
srcs.forEach((src) => this.addImage(src));
|
||||
}
|
||||
|
||||
addElement(element) {
|
||||
element.classList.add('tile');
|
||||
element.classList.add("tile");
|
||||
this.container.appendChild(element);
|
||||
}
|
||||
}
|
||||
@ -75,14 +73,14 @@ class TileGridElement extends HTMLElement {
|
||||
class UploadButton extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement('div');
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.component = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
window.u = this;
|
||||
}
|
||||
|
||||
get gridSelector() {
|
||||
return this.getAttribute('grid');
|
||||
return this.getAttribute("grid");
|
||||
}
|
||||
grid = null;
|
||||
|
||||
@ -91,8 +89,8 @@ class UploadButton extends HTMLElement {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
console.log('connected');
|
||||
this.styleElement = document.createElement('style');
|
||||
console.log("connected");
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.textContent = `
|
||||
.upload-button {
|
||||
display: flex;
|
||||
@ -116,14 +114,14 @@ class UploadButton extends HTMLElement {
|
||||
}
|
||||
`;
|
||||
this.shadowRoot.appendChild(this.styleElement);
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add('upload-button');
|
||||
this.container = document.createElement("div");
|
||||
this.container.classList.add("upload-button");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.multiple = true;
|
||||
input.addEventListener('change', (e) => {
|
||||
input.addEventListener("change", (e) => {
|
||||
const files = e.target.files;
|
||||
const urls = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
@ -137,39 +135,39 @@ class UploadButton extends HTMLElement {
|
||||
reader.readAsDataURL(files[i]);
|
||||
}
|
||||
});
|
||||
const label = document.createElement('label');
|
||||
label.textContent = 'Upload Images';
|
||||
const label = document.createElement("label");
|
||||
label.textContent = "Upload Images";
|
||||
label.appendChild(input);
|
||||
this.container.appendChild(label);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('upload-button', UploadButton);
|
||||
customElements.define('tile-grid', TileGridElement);
|
||||
customElements.define("upload-button", UploadButton);
|
||||
customElements.define("tile-grid", TileGridElement);
|
||||
|
||||
class MeniaUploadElement extends HTMLElement {
|
||||
constructor(){
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.component = document.createElement("div");
|
||||
alert('aaaa');
|
||||
alert("aaaa");
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.container = document.createElement("div");
|
||||
this.component.style.height = '100%';
|
||||
this.component.style.backgroundColor = 'blue';
|
||||
this.component.style.height = "100%";
|
||||
this.component.style.backgroundColor = "blue";
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
|
||||
this.tileElement = document.createElement("tile-grid");
|
||||
this.tileElement.style.backgroundColor = 'red';
|
||||
this.tileElement.style.height = '100%';
|
||||
this.tileElement.style.backgroundColor = "red";
|
||||
this.tileElement.style.height = "100%";
|
||||
this.component.appendChild(this.tileElement);
|
||||
|
||||
this.uploadButton = document.createElement('upload-button');
|
||||
this.uploadButton = document.createElement("upload-button");
|
||||
this.component.appendChild(this.uploadButton);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('menia-upload', MeniaUploadElement);
|
||||
customElements.define("menia-upload", MeniaUploadElement);
|
||||
|
@ -22,18 +22,17 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
|
||||
class MessageListManagerElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.container = document.createElement("div");
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
const channels = await app.rpc.getChannels();
|
||||
channels.forEach(channel => {
|
||||
channels.forEach((channel) => {
|
||||
const messageList = document.createElement("message-list");
|
||||
messageList.setAttribute("channel", channel.uid);
|
||||
this.container.appendChild(messageList);
|
||||
|
@ -5,50 +5,60 @@
|
||||
// The code seems to rely on some external dependencies like 'models.Message', 'app', and 'Schedule'. These should be imported or defined elsewhere in your application.
|
||||
|
||||
// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
|
||||
import {app} from '../app.js'
|
||||
class MessageList extends HTMLElement {
|
||||
import { app } from "../app.js";
|
||||
class MessageList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
app.ws.addEventListener("update_message_text",(data)=>{
|
||||
this.updateMessageText(data.data.uid,data.data)
|
||||
})
|
||||
app.ws.addEventListener("set_typing",(data)=>{
|
||||
this.triggerGlow(data.data.user_uid)
|
||||
|
||||
})
|
||||
app.ws.addEventListener("update_message_text", (data) => {
|
||||
this.updateMessageText(data.uid, data);
|
||||
});
|
||||
app.ws.addEventListener("set_typing", (data) => {
|
||||
this.triggerGlow(data.user_uid,data.color);
|
||||
});
|
||||
|
||||
this.items = [];
|
||||
}
|
||||
updateMessageText(uid,message){
|
||||
const messageDiv = this.querySelector("div[data-uid=\""+uid+"\"]")
|
||||
scrollToBottom(force) {
|
||||
console.info("Scrolling down")
|
||||
// if (force) {
|
||||
this.scrollTop = this.scrollHeight;
|
||||
|
||||
if(!messageDiv){
|
||||
return
|
||||
}
|
||||
const receivedHtml = document.createElement("div")
|
||||
receivedHtml.innerHTML = message.html
|
||||
const html = receivedHtml.querySelector(".text").innerHTML
|
||||
const textElement = messageDiv.querySelector(".text")
|
||||
textElement.innerHTML = html
|
||||
textElement.style.display = message.text == '' ? 'none' : 'block'
|
||||
this.querySelector(".message-list-bottom").scrollIntoView();
|
||||
setTimeout(() => {
|
||||
|
||||
this.scrollTop = this.scrollHeight;
|
||||
this.querySelector(".message-list-bottom").scrollIntoView();
|
||||
},200)
|
||||
// }
|
||||
}
|
||||
triggerGlow(uid) {
|
||||
updateMessageText(uid, message) {
|
||||
const messageDiv = this.querySelector('div[data-uid="' + uid + '"]');
|
||||
|
||||
if (!messageDiv) {
|
||||
return;
|
||||
}
|
||||
const receivedHtml = document.createElement("div");
|
||||
receivedHtml.innerHTML = message.html;
|
||||
const html = receivedHtml.querySelector(".text").innerHTML;
|
||||
const textElement = messageDiv.querySelector(".text");
|
||||
textElement.innerHTML = html;
|
||||
textElement.style.display = message.text == "" ? "none" : "block";
|
||||
}
|
||||
triggerGlow(uid,color) {
|
||||
app.starField.glowColor(color)
|
||||
let lastElement = null;
|
||||
this.querySelectorAll(".avatar").forEach((el)=>{
|
||||
const div = el.closest('a');
|
||||
if(el.href.indexOf(uid)!=-1){
|
||||
lastElement = el
|
||||
this.querySelectorAll(".avatar").forEach((el) => {
|
||||
const div = el.closest("a");
|
||||
if (el.href.indexOf(uid) != -1) {
|
||||
lastElement = el;
|
||||
}
|
||||
|
||||
})
|
||||
if(lastElement){
|
||||
lastElement.classList.add("glow")
|
||||
setTimeout(()=>{
|
||||
lastElement.classList.remove("glow")
|
||||
},1000)
|
||||
});
|
||||
if (lastElement) {
|
||||
lastElement.classList.add("glow");
|
||||
setTimeout(() => {
|
||||
lastElement.classList.remove("glow");
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
set data(items) {
|
||||
@ -56,175 +66,10 @@
|
||||
this.render();
|
||||
}
|
||||
render() {
|
||||
this.innerHTML = '';
|
||||
this.innerHTML = "";
|
||||
|
||||
//this.insertAdjacentHTML("beforeend", html);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
customElements.define('message-list', MessageList);
|
||||
|
||||
class MessageListElementOLD extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ["messages"];
|
||||
}
|
||||
|
||||
messages = [];
|
||||
room = null;
|
||||
url = null;
|
||||
container = null;
|
||||
messageEventSchedule = null;
|
||||
observer = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.component = document.createElement('div');
|
||||
this.shadowRoot.appendChild(this.component);
|
||||
}
|
||||
|
||||
linkifyText(text) {
|
||||
const urlRegex = /https?:\/\/[^\s]+/g;
|
||||
return text.replace(urlRegex, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`);
|
||||
}
|
||||
|
||||
timeAgo(date1, date2) {
|
||||
const diffMs = Math.abs(date2 - date1);
|
||||
const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((diffMs % (1000 * 60)) / 1000);
|
||||
|
||||
if (days) {
|
||||
return `${days} ${days > 1 ? 'days' : 'day'} ago`;
|
||||
}
|
||||
if (hours) {
|
||||
return `${hours} ${hours > 1 ? 'hours' : 'hour'} ago`;
|
||||
}
|
||||
if (minutes) {
|
||||
return `${minutes} ${minutes > 1 ? 'minutes' : 'minute'} ago`;
|
||||
}
|
||||
return 'just now';
|
||||
}
|
||||
|
||||
timeDescription(isoDate) {
|
||||
const date = new Date(isoDate);
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
let timeStr = `${hours}:${minutes}, ${this.timeAgo(new Date(isoDate), Date.now())}`;
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
createElement(message) {
|
||||
const element = document.createElement("div");
|
||||
element.dataset.uid = message.uid;
|
||||
element.dataset.color = message.color;
|
||||
element.dataset.channel_uid = message.channel_uid;
|
||||
element.dataset.user_nick = message.user_nick;
|
||||
element.dataset.created_at = message.created_at;
|
||||
element.dataset.user_uid = message.user_uid;
|
||||
element.dataset.message = message.message;
|
||||
|
||||
element.classList.add("message");
|
||||
if (!this.messages.length || this.messages[this.messages.length - 1].user_uid != message.user_uid) {
|
||||
element.classList.add("switch-user");
|
||||
}
|
||||
|
||||
const avatar = document.createElement("div");
|
||||
avatar.classList.add("avatar");
|
||||
avatar.classList.add("no-select");
|
||||
avatar.style.backgroundColor = message.color;
|
||||
avatar.style.color = "black";
|
||||
avatar.innerText = message.user_nick[0];
|
||||
|
||||
const messageContent = document.createElement("div");
|
||||
messageContent.classList.add("message-content");
|
||||
|
||||
const author = document.createElement("div");
|
||||
author.classList.add("author");
|
||||
author.style.color = message.color;
|
||||
author.textContent = message.user_nick;
|
||||
|
||||
const text = document.createElement("div");
|
||||
text.classList.add("text");
|
||||
if (message.html) text.innerHTML = message.html;
|
||||
|
||||
const time = document.createElement("div");
|
||||
time.classList.add("time");
|
||||
time.dataset.created_at = message.created_at;
|
||||
time.textContent = this.timeDescription(message.created_at);
|
||||
|
||||
messageContent.appendChild(author);
|
||||
messageContent.appendChild(text);
|
||||
messageContent.appendChild(time);
|
||||
|
||||
element.appendChild(avatar);
|
||||
element.appendChild(messageContent);
|
||||
|
||||
message.element = element;
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
addMessage(message) {
|
||||
const obj = new models.Message(
|
||||
message.uid,
|
||||
message.channel_uid,
|
||||
message.user_uid,
|
||||
message.user_nick,
|
||||
message.color,
|
||||
message.message,
|
||||
message.html,
|
||||
message.created_at,
|
||||
message.updated_at
|
||||
);
|
||||
|
||||
const element = this.createElement(obj);
|
||||
this.messages.push(obj);
|
||||
this.container.appendChild(element);
|
||||
|
||||
this.messageEventSchedule.delay(() => {
|
||||
this.dispatchEvent(new CustomEvent("message", { detail: obj, bubbles: true }));
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
scrollBottom() {
|
||||
this.container.scrollTop = this.container.scrollHeight;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/base.css';
|
||||
this.component.appendChild(link);
|
||||
this.component.classList.add("chat-messages");
|
||||
|
||||
this.container = document.createElement('div');
|
||||
this.component.appendChild(this.container);
|
||||
|
||||
this.messageEventSchedule = new Schedule(500);
|
||||
this.messages = [];
|
||||
this.channel_uid = this.getAttribute("channel");
|
||||
|
||||
app.addEventListener(this.channel_uid, (data) => {
|
||||
this.addMessage(data);
|
||||
});
|
||||
|
||||
this.dispatchEvent(new CustomEvent("rendered", { detail: this, bubbles: true }));
|
||||
|
||||
this.timeUpdateInterval = setInterval(() => {
|
||||
this.messages.forEach((message) => {
|
||||
const newText = this.timeDescription(message.created_at);
|
||||
if (newText != message.element.innerText) {
|
||||
message.element.querySelector(".time").innerText = newText;
|
||||
}
|
||||
});
|
||||
}, 30000);
|
||||
}
|
||||
}
|
||||
|
||||
//customElements.define('message-list', MessageListElement);
|
||||
customElements.define("message-list", MessageList);
|
||||
|
@ -7,20 +7,30 @@
|
||||
// MIT License
|
||||
|
||||
class MessageModel {
|
||||
constructor(uid, channel_uid, user_uid, user_nick, color, message, html, created_at, updated_at) {
|
||||
this.uid = uid
|
||||
this.message = message
|
||||
this.html = html
|
||||
this.user_uid = user_uid
|
||||
this.user_nick = user_nick
|
||||
this.color = color
|
||||
this.channel_uid = channel_uid
|
||||
this.created_at = created_at
|
||||
this.updated_at = updated_at
|
||||
this.element = null
|
||||
constructor(
|
||||
uid,
|
||||
channel_uid,
|
||||
user_uid,
|
||||
user_nick,
|
||||
color,
|
||||
message,
|
||||
html,
|
||||
created_at,
|
||||
updated_at,
|
||||
) {
|
||||
this.uid = uid;
|
||||
this.message = message;
|
||||
this.html = html;
|
||||
this.user_uid = user_uid;
|
||||
this.user_nick = user_nick;
|
||||
this.color = color;
|
||||
this.channel_uid = channel_uid;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
this.element = null;
|
||||
}
|
||||
}
|
||||
|
||||
const models = {
|
||||
Message: MessageModel
|
||||
}
|
||||
Message: MessageModel,
|
||||
};
|
||||
|
@ -1 +0,0 @@
|
||||
|
@ -2,9 +2,9 @@ this.onpush = (event) => {
|
||||
console.log(event.data);
|
||||
// From here we can write the data to IndexedDB, send it to any open
|
||||
// windows, display a notification, etc.
|
||||
};
|
||||
};
|
||||
|
||||
navigator.serviceWorker
|
||||
navigator.serviceWorker
|
||||
.register("/service-worker.js")
|
||||
.then((serviceWorkerRegistration) => {
|
||||
serviceWorkerRegistration.pushManager.subscribe().then(
|
||||
@ -12,13 +12,17 @@ this.onpush = (event) => {
|
||||
const subscriptionObject = {
|
||||
endpoint: pushSubscription.endpoint,
|
||||
keys: {
|
||||
p256dh: pushSubscription.getKey('p256dh'),
|
||||
auth: pushSubscription.getKey('auth'),
|
||||
p256dh: pushSubscription.getKey("p256dh"),
|
||||
auth: pushSubscription.getKey("auth"),
|
||||
},
|
||||
encoding: PushManager.supportedContentEncodings,
|
||||
/* other app-specific data, such as user identity */
|
||||
};
|
||||
console.log(pushSubscription.endpoint, pushSubscription, subscriptionObject);
|
||||
console.log(
|
||||
pushSubscription.endpoint,
|
||||
pushSubscription,
|
||||
subscriptionObject,
|
||||
);
|
||||
// The push subscription details needed by the application
|
||||
// server are now available, and can be sent to it using,
|
||||
// for example, the fetch() API.
|
||||
|
@ -1,42 +1,178 @@
|
||||
|
||||
.star {
|
||||
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;
|
||||
:root {
|
||||
--star-color: white;
|
||||
--background-color: black;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px --star-color, 0 0 30px --star-color;
|
||||
|
||||
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;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
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 5px --star-color;
|
||||
box-shadow: 0 0 0 rgba(255, 255, 150, 0);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.star-glow {
|
||||
animation: star-glow-frames 1s;
|
||||
.demo-highlight {
|
||||
animation: demoPulse 1.5s ease-out;
|
||||
font-weight: bold;
|
||||
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--star-content-color, #eee);
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
top: 40%;
|
||||
transform: translateY(-40%);
|
||||
.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;
|
||||
}
|
||||
|
||||
|
@ -1,33 +1,34 @@
|
||||
async function requestNotificationPermission() {
|
||||
const permission = await Notification.requestPermission();
|
||||
return permission === 'granted';
|
||||
return permission === "granted";
|
||||
}
|
||||
|
||||
// Subscribe to Push Notifications
|
||||
async function subscribeUser() {
|
||||
const registration = await navigator.serviceWorker.register('/service-worker.js');
|
||||
const registration =
|
||||
await navigator.serviceWorker.register("/service-worker.js");
|
||||
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
|
||||
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
|
||||
});
|
||||
|
||||
// Send subscription to your backend
|
||||
await fetch('/subscribe', {
|
||||
method: 'POST',
|
||||
await fetch("/subscribe", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(subscription),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Service Worker (service-worker.js)
|
||||
self.addEventListener('push', event => {
|
||||
self.addEventListener("push", (event) => {
|
||||
const data = event.data.json();
|
||||
self.registration.showNotification(data.title, {
|
||||
body: data.message,
|
||||
icon: data.icon
|
||||
icon: data.icon,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
import {EventHandler} from "./event-handler.js";
|
||||
import { EventHandler } from "./event-handler.js";
|
||||
|
||||
export class Socket extends EventHandler {
|
||||
/**
|
||||
* @type {URL}
|
||||
*/
|
||||
url
|
||||
url;
|
||||
/**
|
||||
* @type {WebSocket|null}
|
||||
*/
|
||||
ws = null
|
||||
ws = null;
|
||||
|
||||
/**
|
||||
* @type {null|PromiseWithResolvers<Socket>&{resolved?:boolean}}
|
||||
*/
|
||||
connection = null
|
||||
connection = null;
|
||||
|
||||
shouldReconnect = true;
|
||||
|
||||
@ -28,10 +28,10 @@ export class Socket extends EventHandler {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.url = new URL('/rpc.ws', window.location.origin);
|
||||
this.url.protocol = this.url.protocol.replace('http', 'ws');
|
||||
this.url = new URL("/rpc.ws", window.location.origin);
|
||||
this.url.protocol = this.url.protocol.replace("http", "ws");
|
||||
|
||||
this.connect()
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
@ -40,7 +40,7 @@ export class Socket extends EventHandler {
|
||||
}
|
||||
|
||||
if (!this.connection || this.connection.resolved) {
|
||||
this.connection = Promise.withResolvers()
|
||||
this.connection = Promise.withResolvers();
|
||||
}
|
||||
|
||||
this.ws = new WebSocket(this.url);
|
||||
@ -52,12 +52,12 @@ export class Socket extends EventHandler {
|
||||
|
||||
this.ws.addEventListener("close", () => {
|
||||
console.log("Connection closed");
|
||||
this.disconnect()
|
||||
})
|
||||
this.disconnect();
|
||||
});
|
||||
this.ws.addEventListener("error", (e) => {
|
||||
console.error("Connection error", e);
|
||||
this.disconnect()
|
||||
})
|
||||
this.disconnect();
|
||||
});
|
||||
this.ws.addEventListener("message", (e) => {
|
||||
if (e.data instanceof Blob || e.data instanceof ArrayBuffer) {
|
||||
console.error("Binary data not supported");
|
||||
@ -68,10 +68,9 @@ export class Socket extends EventHandler {
|
||||
console.error("Failed to parse message", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
onData(data) {
|
||||
if (data.success !== undefined && !data.success) {
|
||||
console.error(data);
|
||||
@ -81,12 +80,11 @@ export class Socket extends EventHandler {
|
||||
}
|
||||
if (data.channel_uid) {
|
||||
this.emit(data.channel_uid, data.data);
|
||||
if(!data['event'])
|
||||
this.emit("channel-message", data);
|
||||
if (!data["event"]) this.emit("channel-message", data);
|
||||
}
|
||||
this.emit("data", data.data)
|
||||
if(data['event']){
|
||||
this.emit(data.event, data)
|
||||
this.emit("data", data.data);
|
||||
if (data["event"]) {
|
||||
this.emit(data.event, data.data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,27 +92,31 @@ export class Socket extends EventHandler {
|
||||
this.ws?.close();
|
||||
this.ws = null;
|
||||
|
||||
if (this.shouldReconnect) setTimeout(() => {
|
||||
if (this.shouldReconnect)
|
||||
setTimeout(() => {
|
||||
console.log("Reconnecting");
|
||||
this.emit("reconnecting");
|
||||
return this.connect();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
||||
_camelToSnake(str) {
|
||||
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
||||
return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
||||
}
|
||||
|
||||
get client() {
|
||||
const me = this;
|
||||
return new Proxy({}, {
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, prop) {
|
||||
return (...args) => {
|
||||
const functionName = me._camelToSnake(prop);
|
||||
return me.call(functionName, ...args);
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
generateCallId() {
|
||||
@ -122,7 +124,7 @@ export class Socket extends EventHandler {
|
||||
}
|
||||
|
||||
async sendJson(data) {
|
||||
await this.connect().then(api => {
|
||||
await this.connect().then((api) => {
|
||||
api.ws.send(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
@ -133,9 +135,9 @@ export class Socket extends EventHandler {
|
||||
method,
|
||||
args,
|
||||
};
|
||||
const me = this
|
||||
const me = this;
|
||||
return new Promise((resolve) => {
|
||||
me.addEventListener(call.callId, data => resolve(data));
|
||||
me.addEventListener(call.callId, (data) => resolve(data));
|
||||
me.sendJson(call);
|
||||
});
|
||||
}
|
||||
|
@ -2,18 +2,17 @@
|
||||
|
||||
// This class defines a custom HTML element for an upload button with integrated file upload functionality using XMLHttpRequest.
|
||||
|
||||
|
||||
// MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
class UploadButtonElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.attachShadow({ mode: "open" });
|
||||
}
|
||||
chatInput = null
|
||||
chatInput = null;
|
||||
async uploadFiles() {
|
||||
const fileInput = this.container.querySelector('.file-input');
|
||||
const uploadButton = this.container.querySelector('.upload-button');
|
||||
const fileInput = this.container.querySelector(".file-input");
|
||||
const uploadButton = this.container.querySelector(".upload-button");
|
||||
|
||||
if (!fileInput.files.length) {
|
||||
return;
|
||||
@ -22,12 +21,12 @@ class UploadButtonElement extends HTMLElement {
|
||||
const files = fileInput.files;
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('files[]', files[i]);
|
||||
formData.append("files[]", files[i]);
|
||||
}
|
||||
const request = new XMLHttpRequest();
|
||||
|
||||
request.responseType = 'json';
|
||||
request.open('POST', `/channel/${this.channelUid}/attachment.bin`, true);
|
||||
request.responseType = "json";
|
||||
request.open("POST", `/channel/${this.channelUid}/attachment.bin`, true);
|
||||
|
||||
request.upload.onprogress = function (event) {
|
||||
if (event.lengthComputable) {
|
||||
@ -35,27 +34,29 @@ class UploadButtonElement extends HTMLElement {
|
||||
uploadButton.innerText = `${Math.round(percentComplete)}%`;
|
||||
}
|
||||
};
|
||||
const me = this
|
||||
const me = this;
|
||||
request.onload = function () {
|
||||
if (request.status === 200) {
|
||||
me.dispatchEvent(new CustomEvent('uploaded', { detail: request.response }));
|
||||
uploadButton.innerHTML = '📤';
|
||||
me.dispatchEvent(
|
||||
new CustomEvent("uploaded", { detail: request.response }),
|
||||
);
|
||||
uploadButton.innerHTML = "📤";
|
||||
} else {
|
||||
alert('Upload failed');
|
||||
alert("Upload failed");
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = function () {
|
||||
alert('Error while uploading.');
|
||||
alert("Error while uploading.");
|
||||
};
|
||||
|
||||
request.send(formData);
|
||||
const uploadEvent = new Event('upload',{});
|
||||
const uploadEvent = new Event("upload", {});
|
||||
this.dispatchEvent(uploadEvent);
|
||||
}
|
||||
channelUid = null
|
||||
channelUid = null;
|
||||
connectedCallback() {
|
||||
this.styleElement = document.createElement('style');
|
||||
this.styleElement = document.createElement("style");
|
||||
this.styleElement.innerHTML = `
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
@ -98,7 +99,7 @@ class UploadButtonElement extends HTMLElement {
|
||||
}
|
||||
`;
|
||||
this.shadowRoot.appendChild(this.styleElement);
|
||||
this.container = document.createElement('div');
|
||||
this.container = document.createElement("div");
|
||||
this.container.innerHTML = `
|
||||
<div class="upload-container">
|
||||
<button class="upload-button">
|
||||
@ -108,16 +109,16 @@ class UploadButtonElement extends HTMLElement {
|
||||
</div>
|
||||
`;
|
||||
this.shadowRoot.appendChild(this.container);
|
||||
this.channelUid = this.getAttribute('channel');
|
||||
this.uploadButton = this.container.querySelector('.upload-button');
|
||||
this.fileInput = this.container.querySelector('.hidden-input');
|
||||
this.uploadButton.addEventListener('click', () => {
|
||||
this.channelUid = this.getAttribute("channel");
|
||||
this.uploadButton = this.container.querySelector(".upload-button");
|
||||
this.fileInput = this.container.querySelector(".hidden-input");
|
||||
this.uploadButton.addEventListener("click", () => {
|
||||
this.fileInput.click();
|
||||
});
|
||||
this.fileInput.addEventListener('change', () => {
|
||||
this.fileInput.addEventListener("change", () => {
|
||||
this.uploadFiles();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('upload-button', UploadButtonElement);
|
||||
customElements.define("upload-button", UploadButtonElement);
|
||||
|
@ -1,4 +1,4 @@
|
||||
class UserList extends HTMLElement {
|
||||
class UserList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.users = [];
|
||||
@ -18,18 +18,18 @@
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${days} day${days > 1 ? 's' : ''} ago`;
|
||||
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${days} day${days > 1 ? "s" : ""} ago`;
|
||||
} else if (hours > 0) {
|
||||
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${hours} hour${hours > 1 ? "s" : ""} ago`;
|
||||
} else {
|
||||
return `${msgTime.getHours().toString().padStart(2, '0')}:${msgTime.getMinutes().toString().padStart(2, '0')}, ${minutes} min ago`;
|
||||
return `${msgTime.getHours().toString().padStart(2, "0")}:${msgTime.getMinutes().toString().padStart(2, "0")}, ${minutes} min ago`;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = '';
|
||||
this.innerHTML = "";
|
||||
|
||||
this.users.forEach(user => {
|
||||
this.users.forEach((user) => {
|
||||
const html = `
|
||||
<div class="user-list__item"
|
||||
data-uid="${user.uid}"
|
||||
@ -54,6 +54,6 @@
|
||||
this.insertAdjacentHTML("beforeend", html);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('user-list', UserList);
|
||||
customElements.define("user-list", UserList);
|
||||
|
@ -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.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>
|
||||
|
@ -11,6 +11,7 @@
|
||||
{{ message.html }}
|
||||
{% endautoescape %}
|
||||
{% endfor %}
|
||||
<div class="message-list-bottom"></div>
|
||||
</message-list>
|
||||
<chat-input live-type="false" channel="{{ channel.uid.value }}"></chat-input>
|
||||
</section>
|
||||
@ -36,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 {
|
||||
@ -199,9 +214,8 @@
|
||||
});
|
||||
lastMessage = messagesContainer.querySelector(".message:last-child");
|
||||
if (doScrollDown) {
|
||||
lastMessage?.scrollIntoView({ block: "end", inline: "nearest" });
|
||||
messagesContainer.scrollToBottom()
|
||||
|
||||
chatInputField.scrollIntoView({ block: "end", inline: "nearest" });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import json
|
||||
import traceback
|
||||
|
||||
import asyncio
|
||||
from aiohttp import web
|
||||
|
||||
from snek.system.model import now
|
||||
@ -21,13 +21,30 @@ 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 = {}
|
||||
self._scheduled = []
|
||||
|
||||
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 +340,13 @@ class RPCView(BaseView):
|
||||
async for record in self.services.channel.get_users(channel_uid)
|
||||
]
|
||||
|
||||
async def _schedule(self, seconds, call):
|
||||
self._scheduled.append(call)
|
||||
await asyncio.sleep(seconds)
|
||||
await self.services.socket.send_to_user(self.user_uid, call)
|
||||
self._scheduled.remove(call)
|
||||
|
||||
|
||||
async def ping(self, callId, *args):
|
||||
if self.user_uid:
|
||||
user = await self.services.user.get(uid=self.user_uid)
|
||||
@ -330,7 +354,17 @@ class RPCView(BaseView):
|
||||
await self.services.user.save(user)
|
||||
return {"pong": args}
|
||||
|
||||
|
||||
|
||||
|
||||
async def get(self):
|
||||
scheduled = []
|
||||
async def schedule(uid, seconds, call):
|
||||
scheduled.append(call)
|
||||
await asyncio.sleep(seconds)
|
||||
await self.services.socket.send_to_user(uid, call)
|
||||
scheduled.remove(call)
|
||||
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(self.request)
|
||||
if self.request.session.get("logged_in"):
|
||||
@ -343,6 +377,16 @@ class RPCView(BaseView):
|
||||
await self.services.socket.subscribe(
|
||||
ws, subscription["channel_uid"], self.request.session.get("uid")
|
||||
)
|
||||
if not scheduled and self.request.app.uptime_seconds < 5:
|
||||
await schedule(self.request.session.get("uid"),0,{"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