Compare commits

...

2 Commits

Author SHA1 Message Date
db5431d77d Update. 2025-05-19 01:07:17 +02:00
527b010b24 Added containers. 2025-05-19 01:07:17 +02:00
17 changed files with 464 additions and 53 deletions

View File

@ -55,6 +55,7 @@ from snek.view.upload import UploadView
from snek.view.user import UserView
from snek.view.web import WebView
from snek.view.channel import ChannelAttachmentView
from snek.view.settings.containers import ContainersIndexView, ContainersCreateView, ContainersUpdateView, ContainersDeleteView
from snek.webdav import WebdavApplication
from snek.sgit import GitApplication
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
@ -208,6 +209,10 @@ class Application(BaseApplication):
self.router.add_view("/settings/repositories/create.html", RepositoriesCreateView)
self.router.add_view("/settings/repositories/repository/{name}/update.html", RepositoriesUpdateView)
self.router.add_view("/settings/repositories/repository/{name}/delete.html", RepositoriesDeleteView)
self.router.add_view("/settings/containers/index.html", ContainersIndexView)
self.router.add_view("/settings/containers/create.html", ContainersCreateView)
self.router.add_view("/settings/containers/container/{uid}/update.html", ContainersUpdateView)
self.router.add_view("/settings/containers/container/{uid}/delete.html", ContainersDeleteView)
self.webdav = WebdavApplication(self)
self.git = GitApplication(self)
self.add_subapp("/webdav", self.webdav)

View File

@ -10,10 +10,12 @@ from snek.mapper.user import UserMapper
from snek.mapper.user_property import UserPropertyMapper
from snek.mapper.repository import RepositoryMapper
from snek.mapper.channel_attachment import ChannelAttachmentMapper
from snek.mapper.container import ContainerMapper
from snek.system.object import Object
@functools.cache
def get_mappers(app=None):
return Object(
**{
@ -27,6 +29,7 @@ def get_mappers(app=None):
"user_property": UserPropertyMapper(app=app),
"repository": RepositoryMapper(app=app),
"channel_attachment": ChannelAttachmentMapper(app=app),
"container": ContainerMapper(app=app),
}
)

View File

@ -0,0 +1,6 @@
from snek.model.container import Container
from snek.system.mapper import BaseMapper
class ContainerMapper(BaseMapper):
model_class = Container
table_name = "container"

View File

@ -12,6 +12,7 @@ from snek.model.user import UserModel
from snek.model.user_property import UserPropertyModel
from snek.model.repository import RepositoryModel
from snek.model.channel_attachment import ChannelAttachmentModel
from snek.model.container import Container
from snek.system.object import Object
@ -29,6 +30,7 @@ def get_models():
"user_property": UserPropertyModel,
"repository": RepositoryModel,
"channel_attachment": ChannelAttachmentModel,
"container": Container,
}
)

View File

@ -0,0 +1,10 @@
from snek.system.model import BaseModel, ModelField
class Container(BaseModel):
id = ModelField(name="id", required=True, kind=str)
name = ModelField(name="name", required=True, kind=str)
status = ModelField(name="status", required=True, kind=str)
resources = ModelField(name="resources", required=False, kind=str)
user_uid = ModelField(name="user_uid", required=False, kind=str)
path = ModelField(name="path", required=False, kind=str)
readonly = ModelField(name="readonly", required=False, kind=bool, default=False)

View File

@ -13,6 +13,7 @@ from snek.service.user_property import UserPropertyService
from snek.service.util import UtilService
from snek.service.repository import RepositoryService
from snek.service.channel_attachment import ChannelAttachmentService
from snek.service.container import ContainerService
from snek.system.object import Object
from snek.service.db import DBService
@ -34,6 +35,7 @@ def get_services(app):
"repository": RepositoryService(app=app),
"db": DBService(app=app),
"channel_attachment": ChannelAttachmentService(app=app),
"container": ContainerService(app=app),
}
)

View File

@ -0,0 +1,29 @@
from snek.system.service import BaseService
class ContainerService(BaseService):
mapper_name = "container"
async def create(self, id, name, status, resources=None, user_uid=None, path=None, readonly=False):
model = await self.new()
model["id"] = id
model["name"] = name
model["status"] = status
if resources:
model["resources"] = resources
if user_uid:
model["user_uid"] = user_uid
if path:
model["path"] = path
model["readonly"] = readonly
if await super().save(model):
return model
raise Exception(f"Failed to create container: {model.errors}")
async def get(self, id):
return await self.mapper.get(id)
async def update(self, model):
return await self.mapper.update(model)
async def delete(self, id):
return await self.mapper.delete(id)

View File

@ -53,7 +53,8 @@ class ChatInputComponent extends HTMLElement {
this.textarea.focus();
}
connectedCallback() {
async connectedCallback() {
this.user = await app.rpc.getUser(null);
this.liveType = this.getAttribute("live-type") === "true";
this.liveTypeInterval = parseInt(this.getAttribute("live-type-interval")) || 3;
this.channelUid = this.getAttribute("channel");
@ -193,7 +194,7 @@ 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);
app.rpc.set_typing(this.channelUid,this.user.color);
}
}
}

View File

@ -1,28 +1,42 @@
/* each star */
.star {
position: absolute;
width: 2px;
height: 2px;
background: #fff;
border-radius: 50%;
opacity: 0;
/* flicker animation */
animation: twinkle ease-in-out infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
.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;
}
/* optional page content */
.content {
position: relative;
z-index: 1;
color: #eee;
font-family: sans-serif;
text-align: center;
top: 40%;
transform: translateY(-40%);
}
@keyframes twinkle {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
@keyframes star-glow-frames {
0% {
box-shadow: 0 0 5px --star-color;
}
50% {
box-shadow: 0 0 20px --star-color, 0 0 30px --star-color;
}
100% {
box-shadow: 0 0 5px --star-color;
}
}
.star-glow {
animation: star-glow-frames 1s;
}
.content {
position: relative;
z-index: 1;
color: var(--star-content-color, #eee);
font-family: sans-serif;
text-align: center;
top: 40%;
transform: translateY(-40%);
}

View File

@ -1,31 +1,56 @@
<script>
// number of stars you want
const STAR_COUNT = 200;
const body = document.body;
<script type="module">
import { app } from "/app.js";
for (let i = 0; i < STAR_COUNT; i++) {
const star = document.createElement('div');
star.classList.add('star');
const STAR_COUNT = 200;
const body = document.body;
// random position within the viewport
star.style.left = Math.random() * 100 + '%';
star.style.top = Math.random() * 100 + '%';
function createStar() {
const star = document.createElement('div');
star.classList.add('star');
star.style.left = `${Math.random() * 100}%`;
star.style.top = `${Math.random() * 100}%`;
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);
}
// random size (optional)
const size = Math.random() * 2 + 1; // between 1px and 3px
star.style.width = size + 'px';
star.style.height = size + 'px';
Array.from({ length: STAR_COUNT }, createStar);
// random animation timing for natural flicker
const duration = Math.random() * 3 + 2; // 2s–5s
const delay = Math.random() * 5; // 0s–5s
star.style.animationDuration = duration + 's';
star.style.animationDelay = delay + 's';
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)}`;
}
body.appendChild(star);
}
const originalColor = document.documentElement.style.getPropertyValue("--star-color").trim();
</script>
function glowCSSVariable(varName, glowColor, duration = 500) {
const root = document.documentElement;
//igetComputedStyle(root).getPropertyValue(varName).trim();
glowColor = lightenColor(glowColor, 20);
root.style.setProperty(varName, glowColor);
setTimeout(() => {
root.style.setProperty(varName, originalColor);
}, duration);
}
function updateStarColorDelayed(color) {
glowCSSVariable('--star-color', color, 2500);
}
app.ws.addEventListener("set_typing", (data) => {
updateStarColorDelayed(data.data.color);
});
</script>

View File

@ -0,0 +1,39 @@
{% extends 'settings/index.html' %}
{% block header_text %}<h1><i class="fa-solid fa-plus"></i> Create Container</h1>{% endblock %}
{% block main %}
{% include 'settings/containers/form.html' %}
<div class="container">
<form action="/settings/containers/create.html" method="post">
<div>
<label for="name"><i class="fa-solid fa-box"></i> Name</label>
<input type="text" id="name" name="name" required placeholder="Container name">
</div>
<div>
<label for="status"><i class="fa-solid fa-info-circle"></i> Status</label>
<input type="text" id="status" name="status" required placeholder="Container status">
</div>
<div>
<label for="resources"><i class="fa-solid fa-memory"></i> Resources</label>
<input type="text" id="resources" name="resources" placeholder="Resource details">
</div>
<div>
<label for="user_uid"><i class="fa-solid fa-user"></i> User UID</label>
<input type="text" id="user_uid" name="user_uid" placeholder="User UID">
</div>
<div>
<label for="path"><i class="fa-solid fa-folder"></i> Path</label>
<input type="text" id="path" name="path" placeholder="Container path">
</div>
<div>
<label>
<input type="checkbox" name="readonly" value="1">
<i class="fa-solid fa-lock"></i> Readonly
</label>
</div>
<button type="submit"><i class="fa-solid fa-plus"></i> Create</button>
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i>Cancel</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends 'settings/index.html' %}
{% block header_text %}<h1><i class="fa-solid fa-trash-can"></i> Delete Container</h1>{% endblock %}
{% block main %}
<div class="container">
<p>Are you sure you want to <strong>delete</strong> the following container?</p>
<div class="container-name"><i class="fa-solid fa-box"></i> {{ container.name }}</div>
<form method="post" style="margin-top:1.5rem;">
<input type="hidden" name="id" value="{{ container.id }}">
<div class="actions">
<button type="submit"><i class="fa-solid fa-trash"></i> Yes, delete</button>
<button type="button" onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i> Cancel</button>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,28 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<style>
form {
padding: 2rem;
border-radius: 10px;
div {
padding: 10px;
padding-bottom: 15px
}
}
label { font-weight: bold; display: flex; align-items: center; gap: 0.5rem;}
button {
background: #0d6efd; color: #fff;
border: none; border-radius: 5px; padding: 0.6rem 1rem;
cursor: pointer;
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
}
.cancel {
background: #6c757d;
}
@media (max-width: 600px) {
.container { max-width: 98vw; }
form { padding: 1rem; }
}
</style>

View File

@ -0,0 +1,96 @@
{% extends 'settings/index.html' %}
{% block header_text %}<h1><i class="fa-solid fa-database"></i> Containers</h1>{% endblock %}
{% block main %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Containers - List</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<style>
.actions {
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
}
.container-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.container-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-radius: 8px;
flex-wrap: wrap;
}
.container-info {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
min-width: 220px;
}
.container-name {
font-size: 1.1rem;
font-weight: 600;
}
@media (max-width: 600px) {
.container-row { flex-direction: column; align-items: stretch; }
.actions { justify-content: flex-start; }
}
.topbar {
display: flex;
margin-bottom: 1rem;
}
button, a.button {
background: #198754; color: #fff; border: none; border-radius: 5px;
padding: 0.4rem 0.8rem; text-decoration: none; cursor: pointer;
transition: background 0.2s;
font-size: 1rem; display: inline-flex; align-items: center; gap: 0.4rem;
}
.button.delete { background: #dc3545; }
.button.edit { background: #0d6efd; }
.button.clone { background: #6c757d; }
.button.browse { background: #ffc107; color: #212529; }
.button.create { background: #20c997; margin-left: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="topbar">
<a class="button create" href="/settings/containers/create.html">
<i class="fa-solid fa-plus"></i> New Container
</a>
</div>
<section class="container-list">
{% for container in containers %}
<div class="container-row">
<div class="container-info">
<span class="container-name"><i class="fa-solid fa-box"></i> {{ container.name }}</span>
<span title="Status"><i class="fa-solid fa-info-circle"></i> {{ container.status }}</span>
<span title="Readonly">
<i class="fa-solid {% if container.readonly %}fa-lock{% else %}fa-lock-open{% endif %}"></i>
{% if container.readonly %}Readonly{% else %}Writable{% endif %}
</span>
</div>
<div class="actions">
<a class="button edit" href="/settings/containers/container/{{ container.id }}/update.html">
<i class="fa-solid fa-pen"></i> Edit
</a>
<a class="button delete" href="/settings/containers/container/{{ container.id }}/delete.html">
<i class="fa-solid fa-trash"></i> Delete
</a>
</div>
</div>
{% endfor %}
</section>
</div>
</body>
</html>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "settings/index.html" %}
{% block header_text %}<h1><i class="fa-solid fa-pen"></i> Update Container</h1>{% endblock %}
{% block main %}
{% include "settings/containers/form.html" %}
<div class="container">
<form method="post">
<input type="hidden" name="id" value="{{ container.id }}">
<div>
<label for="name"><i class="fa-solid fa-box"></i> Name</label>
<input type="text" id="name" name="name" value="{{ container.name }}" required>
</div>
<div>
<label for="status"><i class="fa-solid fa-info-circle"></i> Status</label>
<input type="text" id="status" name="status" value="{{ container.status }}" required>
</div>
<div>
<label for="resources"><i class="fa-solid fa-memory"></i> Resources</label>
<input type="text" id="resources" name="resources" value="{{ container.resources }}">
</div>
<div>
<label for="user_uid"><i class="fa-solid fa-user"></i> User UID</label>
<input type="text" id="user_uid" name="user_uid" value="{{ container.user_uid }}">
</div>
<div>
<label for="path"><i class="fa-solid fa-folder"></i> Path</label>
<input type="text" id="path" name="path" value="{{ container.path }}">
</div>
<div>
<label>
<input type="checkbox" name="readonly" value="1" {% if container.readonly %}checked{% endif %}>
<i class="fa-solid fa-lock"></i> Readonly
</label>
</div>
<button type="submit"><i class="fa-solid fa-pen"></i> Update</button>
<button onclick="history.back()" class="cancel"><i class="fa-solid fa-arrow-left"></i>Cancel</button>
</form>
</div>
{% endblock %}

View File

@ -36,9 +36,11 @@ class RPCView(BaseView):
async def db_update(self, table_name, record):
self._require_login()
return await self.services.db.update(self.user_uid, table_name, record)
async def set_typing(self,channel_uid):
async def set_typing(self,channel_uid,color=None):
self._require_login()
user = await self.services.user.get(self.user_uid)
if not color:
color = user["color"]
return await self.services.socket.broadcast(channel_uid, {
"channel_uid": "293ecf12-08c9-494b-b423-48ba1a2d12c2",
"event": "set_typing",
@ -47,7 +49,8 @@ class RPCView(BaseView):
"user_uid": user['uid'],
"username": user["username"],
"nick": user["nick"],
"channel_uid": channel_uid
"channel_uid": channel_uid,
"color": color
}
})

View File

@ -0,0 +1,91 @@
import asyncio
from aiohttp import web
from snek.system.view import BaseFormView
import pathlib
class ContainersIndexView(BaseFormView):
login_required = True
async def get(self):
user_uid = self.session.get("uid")
containers = []
async for container in self.services.container.find(user_uid=user_uid):
containers.append(container.record)
user = await self.services.user.get(uid=self.session.get("uid"))
return await self.render_template("settings/containers/index.html", {"containers": containers, "user": user})
class ContainersCreateView(BaseFormView):
login_required = True
async def get(self):
return await self.render_template("settings/containers/create.html")
async def post(self):
data = await self.request.post()
container = await self.services.container.create(
user_uid=self.session.get("uid"),
name=data['name'],
status=data['status'],
resources=data.get('resources', ''),
path=data.get('path', ''),
readonly=bool(data.get('readonly', False))
)
return web.HTTPFound("/settings/containers/index.html")
class ContainersUpdateView(BaseFormView):
login_required = True
async def get(self):
container = await self.services.container.get(
user_uid=self.session.get("uid"), uid=self.request.match_info["uid"]
)
if not container:
return web.HTTPNotFound()
return await self.render_template("settings/containers/update.html", {"container": container.record})
async def post(self):
data = await self.request.post()
container = await self.services.container.get(
user_uid=self.session.get("uid"), uid=self.request.match_info["uid"]
)
container['status'] = data['status']
container['resources'] = data.get('resources', '')
container['path'] = data.get('path', '')
container['readonly'] = bool(data.get('readonly', False))
await self.services.container.save(container)
return web.HTTPFound("/settings/containers/index.html")
class ContainersDeleteView(BaseFormView):
login_required = True
async def get(self):
container = await self.services.container.get(
user_uid=self.session.get("uid"), uid=self.request.match_info["uid"]
)
if not container:
return web.HTTPNotFound()
return await self.render_template("settings/containers/delete.html", {"container": container.record})
async def post(self):
user_uid = self.session.get("uid")
uid = self.request.match_info["uid"]
container = await self.services.container.get(
user_uid=user_uid, uid=uid
)
if not container:
return web.HTTPNotFound()
await self.services.container.delete(user_uid=user_uid, uid=uid)
return web.HTTPFound("/settings/containers/index.html")