Compare commits
No commits in common. "main" and "main" have entirely different histories.
gitlog.jsonlgitlog.py
src/snek
app.py
mapper
model
service
static
system
templates
view
1489
gitlog.jsonl
1489
gitlog.jsonl
File diff suppressed because one or more lines are too long
289
gitlog.py
289
gitlog.py
@ -1,289 +0,0 @@
|
||||
import http.server
|
||||
import socketserver
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
import mimetypes
|
||||
import html
|
||||
|
||||
# --- Theme selection (choose one: "light1", "light2", "dark1", "dark2") ---
|
||||
THEME = "light1" # Change this to "light2", "dark1", or "dark2" as desired
|
||||
|
||||
THEMES = {
|
||||
"light1": """
|
||||
body { font-family: Arial, sans-serif; background: #f6f8fa; margin: 0; padding: 0; color: #222; }
|
||||
.container { max-width: 960px; margin: auto; padding: 2em; }
|
||||
.date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 1px solid #ccc; }
|
||||
.commit { margin: 1em 0; padding: 1em; background: #fff; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.07); }
|
||||
.hash { color: #555; font-family: monospace; }
|
||||
.diff { white-space: pre-wrap; background: #f1f1f1; padding: 1em; border-radius: 4px; overflow-x: auto; }
|
||||
ul { list-style: none; padding-left: 0; }
|
||||
li { margin: 0.3em 0; }
|
||||
a { text-decoration: none; color: #0366d6; }
|
||||
a:hover { text-decoration: underline; }
|
||||
""",
|
||||
"light2": """
|
||||
body { font-family: 'Segoe UI', sans-serif; background: #fdf6e3; margin: 0; padding: 0; color: #333; }
|
||||
.container { max-width: 900px; margin: auto; padding: 2em; }
|
||||
.date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 2px solid #e1c699; color: #b58900; }
|
||||
.commit { margin: 1em 0; padding: 1em; background: #fffbe6; border-radius: 6px; box-shadow: 0 1px 3px rgba(200,180,100,0.08); }
|
||||
.hash { color: #b58900; font-family: monospace; }
|
||||
.diff { white-space: pre-wrap; background: #f5e9c9; padding: 1em; border-radius: 4px; overflow-x: auto; }
|
||||
ul { list-style: none; padding-left: 0; }
|
||||
li { margin: 0.3em 0; }
|
||||
a { text-decoration: none; color: #b58900; }
|
||||
a:hover { text-decoration: underline; color: #cb4b16; }
|
||||
""",
|
||||
"dark1": """
|
||||
body { font-family: Arial, sans-serif; background: #181a1b; margin: 0; padding: 0; color: #eaeaea; }
|
||||
.container { max-width: 960px; margin: auto; padding: 2em; }
|
||||
.date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 1px solid #333; color: #8ab4f8; }
|
||||
.commit { margin: 1em 0; padding: 1em; background: #23272b; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.18); }
|
||||
.hash { color: #8ab4f8; font-family: monospace; }
|
||||
.diff { white-space: pre-wrap; background: #23272b; padding: 1em; border-radius: 4px; overflow-x: auto; }
|
||||
ul { list-style: none; padding-left: 0; }
|
||||
li { margin: 0.3em 0; }
|
||||
a { text-decoration: none; color: #8ab4f8; }
|
||||
a:hover { text-decoration: underline; color: #bb86fc; }
|
||||
""",
|
||||
"dark2": """
|
||||
body { font-family: 'Fira Sans', sans-serif; background: #121212; margin: 0; padding: 0; color: #d0d0d0; }
|
||||
.container { max-width: 900px; margin: auto; padding: 2em; }
|
||||
.date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 2px solid #444; color: #ffb86c; }
|
||||
.commit { margin: 1em 0; padding: 1em; background: #22223b; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.22); }
|
||||
.hash { color: #ffb86c; font-family: monospace; }
|
||||
.diff { white-space: pre-wrap; background: #282a36; padding: 1em; border-radius: 4px; overflow-x: auto; }
|
||||
ul { list-style: none; padding-left: 0; }
|
||||
li { margin: 0.3em 0; }
|
||||
a { text-decoration: none; color: #ffb86c; }
|
||||
a:hover { text-decoration: underline; color: #8be9fd; }
|
||||
""",
|
||||
}
|
||||
|
||||
def HTML_TEMPLATE(content, theme=THEME):
|
||||
return f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang=\"en\">
|
||||
<head>
|
||||
<meta charset=\"UTF-8\">
|
||||
<title>Git Log Viewer</title>
|
||||
<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css\">
|
||||
<style>
|
||||
{THEMES[theme]}
|
||||
</style>
|
||||
<script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js\"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class=\"container\">
|
||||
{content}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
REPO_ROOT = os.path.abspath(".")
|
||||
LOG_FILE = os.path.join(REPO_ROOT, "gitlog.jsonl")
|
||||
|
||||
PORT = 8481
|
||||
|
||||
def format_diff_to_html(diff_text: str) -> str:
|
||||
lines = diff_text.strip().splitlines()
|
||||
html_lines = ['<div style="font-family: monospace; white-space: pre;">']
|
||||
while not lines[3].startswith('diff'):
|
||||
lines.pop(3)
|
||||
lines.insert(3, "")
|
||||
for line in lines:
|
||||
escaped = html.escape(line)
|
||||
if "//" in line:
|
||||
continue
|
||||
if "#" in line:
|
||||
continue
|
||||
if "/*" in line:
|
||||
continue
|
||||
if "*/" in line:
|
||||
continue
|
||||
if line.startswith('+++') or line.startswith('---'):
|
||||
html_lines.append(f'<div style="color: #0000aa;">{escaped}</div>')
|
||||
elif line.startswith('@@'):
|
||||
html_lines.append(f'<div style="color: #005cc5;">{escaped}</div>')
|
||||
elif line.startswith('+'):
|
||||
html_lines.append(f'<div style="color: #22863a;">{escaped}</div>')
|
||||
elif line.startswith('-'):
|
||||
html_lines.append(f'<div style="color: #b31d28;">{escaped}</div>')
|
||||
elif line.startswith('\\'):
|
||||
html_lines.append(f'<div style="color: #6a737d;">{escaped}</div>')
|
||||
else:
|
||||
html_lines.append(f'<div>{escaped}</div>')
|
||||
html_lines.append('</div>')
|
||||
return '\n'.join(html_lines)
|
||||
|
||||
def parse_logs():
|
||||
logs = []
|
||||
if not os.path.exists(LOG_FILE):
|
||||
return []
|
||||
lines = []
|
||||
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.strip():
|
||||
if line.strip() not in lines:
|
||||
lines.append(line.strip())
|
||||
logs.append(json.loads(line.strip()))
|
||||
return logs
|
||||
|
||||
def group_by_date(logs):
|
||||
grouped = {}
|
||||
for entry in logs:
|
||||
date = entry["date"]
|
||||
grouped.setdefault(date, []).append(entry)
|
||||
return dict(sorted(grouped.items(), reverse=True))
|
||||
|
||||
def get_git_diff(commit_hash):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", REPO_ROOT, "show", commit_hash, "--no-color"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
return f"Error retrieving diff: {e.stderr}"
|
||||
|
||||
def list_directory(path, base_url="/browse?path="):
|
||||
try:
|
||||
entries = os.listdir(path)
|
||||
except OSError:
|
||||
return "<div>Cannot access directory.</div>"
|
||||
|
||||
entries.sort()
|
||||
content = "<ul>"
|
||||
# Parent directory link
|
||||
parent = os.path.dirname(path)
|
||||
if os.path.abspath(path) != REPO_ROOT:
|
||||
parent_rel = os.path.relpath(parent, REPO_ROOT)
|
||||
content += f"<li><a href='{base_url}{html.escape(parent_rel)}'>.. (parent directory)</a></li>"
|
||||
|
||||
for entry in entries:
|
||||
full_path = os.path.join(path, entry)
|
||||
rel_path = os.path.relpath(full_path, REPO_ROOT)
|
||||
if os.path.isdir(full_path):
|
||||
content += f"<li>📁 <a href='{base_url}{html.escape(rel_path)}'>{html.escape(entry)}/</a></li>"
|
||||
else:
|
||||
content += f"<li>📄 <a href='{base_url}{html.escape(rel_path)}'>{html.escape(entry)}</a></li>"
|
||||
content += "</ul>"
|
||||
return content
|
||||
|
||||
def read_file_content(path):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
return f"Error reading file: {e}"
|
||||
|
||||
def get_language_class(filename):
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
return {
|
||||
'.py': 'python',
|
||||
'.js': 'javascript',
|
||||
'.html': 'html',
|
||||
'.css': 'css',
|
||||
'.json': 'json',
|
||||
'.sh': 'bash',
|
||||
'.md': 'markdown',
|
||||
'.c': 'c',
|
||||
'.cpp': 'cpp',
|
||||
'.h': 'cpp',
|
||||
'.java': 'java',
|
||||
'.rb': 'ruby',
|
||||
'.go': 'go',
|
||||
'.php': 'php',
|
||||
'.rs': 'rust',
|
||||
'.ts': 'typescript',
|
||||
'.xml': 'xml',
|
||||
'.yml': 'yaml',
|
||||
'.yaml': 'yaml',
|
||||
}.get(ext, '')
|
||||
|
||||
class GitLogHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
if parsed.path == "/":
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
logs = parse_logs()
|
||||
grouped = group_by_date(logs)
|
||||
content = "<p><a href='/browse'>Browse Files</a></p>"
|
||||
for date, commits in grouped.items():
|
||||
content += f"<div class='date-header'>{date}</div>"
|
||||
for c in commits:
|
||||
commit_link = f"/diff?hash={c['commit']}"
|
||||
content += f"""
|
||||
<div class='commit'>
|
||||
<div><strong>{c['line'].splitlines()[0]}</strong></div>
|
||||
<div class='hash'><a href='{commit_link}'>{c['commit']}</a></div>
|
||||
</div>
|
||||
"""
|
||||
self.wfile.write(HTML_TEMPLATE(content).encode("utf-8"))
|
||||
elif parsed.path == "/diff":
|
||||
qs = parse_qs(parsed.query)
|
||||
commit = qs.get("hash", [""])[0]
|
||||
diff = format_diff_to_html(get_git_diff(html.escape(commit)))
|
||||
diff_html = f"<h2>Commit: {commit}</h2><div class='diff'>{diff}</div><p><a href='/'>← Back to commits</a></p>"
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(HTML_TEMPLATE(diff_html).encode("utf-8"))
|
||||
elif parsed.path == "/browse":
|
||||
qs = parse_qs(parsed.query)
|
||||
rel_path = qs.get("path", [""])[0]
|
||||
abs_path = os.path.abspath(os.path.join(REPO_ROOT, rel_path))
|
||||
# Security: prevent escaping the repo root
|
||||
if not abs_path.startswith(REPO_ROOT):
|
||||
self.send_error(403, "Forbidden")
|
||||
return
|
||||
|
||||
if os.path.isdir(abs_path):
|
||||
content = f"<h2>Browsing: /{html.escape(rel_path)}</h2>"
|
||||
content += list_directory(abs_path)
|
||||
content += "<p><a href='/'>← Back to commits</a></p>"
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(HTML_TEMPLATE(content).encode("utf-8"))
|
||||
elif os.path.isfile(abs_path):
|
||||
file_content = read_file_content(abs_path)
|
||||
lang_class = get_language_class(abs_path)
|
||||
content = f"<h2>File: /{html.escape(rel_path)}</h2>"
|
||||
content += (
|
||||
f"<pre style='background:#f1f1f1; padding:1em; border-radius:4px; overflow-x:auto;'>"
|
||||
f"<code class='{lang_class}'>{html.escape(file_content)}</code></pre>"
|
||||
)
|
||||
content += "<p><a href='{}'>← Back to directory</a></p>".format(
|
||||
f"/browse?path={html.escape(os.path.dirname(rel_path))}"
|
||||
)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
self.wfile.write(HTML_TEMPLATE(content).encode("utf-8"))
|
||||
else:
|
||||
self.send_error(404, "Not found")
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
while True:
|
||||
try:
|
||||
with socketserver.TCPServer(("", PORT), GitLogHandler) as httpd:
|
||||
print(f"Serving at http://localhost:{PORT}")
|
||||
httpd.serve_forever()
|
||||
break
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
PORT += 1
|
||||
|
||||
|
@ -55,7 +55,6 @@ 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"
|
||||
@ -209,10 +208,6 @@ 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)
|
||||
|
@ -10,12 +10,10 @@ 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(
|
||||
**{
|
||||
@ -29,7 +27,6 @@ def get_mappers(app=None):
|
||||
"user_property": UserPropertyMapper(app=app),
|
||||
"repository": RepositoryMapper(app=app),
|
||||
"channel_attachment": ChannelAttachmentMapper(app=app),
|
||||
"container": ContainerMapper(app=app),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
from snek.model.container import Container
|
||||
from snek.system.mapper import BaseMapper
|
||||
|
||||
class ContainerMapper(BaseMapper):
|
||||
model_class = Container
|
||||
table_name = "container"
|
@ -12,7 +12,6 @@ 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
|
||||
|
||||
|
||||
@ -30,7 +29,6 @@ def get_models():
|
||||
"user_property": UserPropertyModel,
|
||||
"repository": RepositoryModel,
|
||||
"channel_attachment": ChannelAttachmentModel,
|
||||
"container": Container,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -1,10 +0,0 @@
|
||||
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)
|
@ -13,7 +13,6 @@ 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
|
||||
|
||||
@ -35,7 +34,6 @@ def get_services(app):
|
||||
"repository": RepositoryService(app=app),
|
||||
"db": DBService(app=app),
|
||||
"channel_attachment": ChannelAttachmentService(app=app),
|
||||
"container": ContainerService(app=app),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -15,7 +15,7 @@ class ChannelAttachmentService(BaseService):
|
||||
attachment["mime_type"] = mimetypes.guess_type(name)[0]
|
||||
attachment['resource_type'] = "file"
|
||||
real_file_name = f"{attachment['uid']}-{name}"
|
||||
attachment["relative_url"] = (f"{attachment['uid']}-{name}")
|
||||
attachment["relative_url"] = urllib.parse.quote(f"{attachment['uid']}/{name}")
|
||||
attachment_folder = await self.services.channel.get_attachment_folder(channel_uid)
|
||||
attachment_path = attachment_folder.joinpath(real_file_name)
|
||||
attachment["path"] = str(attachment_path)
|
||||
|
@ -1,29 +0,0 @@
|
||||
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)
|
@ -53,8 +53,7 @@ class ChatInputComponent extends HTMLElement {
|
||||
this.textarea.focus();
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.user = await app.rpc.getUser(null);
|
||||
connectedCallback() {
|
||||
this.liveType = this.getAttribute("live-type") === "true";
|
||||
this.liveTypeInterval = parseInt(this.getAttribute("live-type-interval")) || 3;
|
||||
this.channelUid = this.getAttribute("channel");
|
||||
@ -194,7 +193,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,this.user.color);
|
||||
app.rpc.set_typing(this.channelUid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +0,0 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
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%);
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import re
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from types import SimpleNamespace
|
||||
|
||||
import mimetypes
|
||||
@ -91,59 +90,16 @@ def set_link_target_blank(text):
|
||||
def embed_youtube(text):
|
||||
soup = BeautifulSoup(text, "html.parser")
|
||||
for element in soup.find_all("a"):
|
||||
# Check if the link is a YouTube link
|
||||
url = urlparse(element["href"])
|
||||
if (
|
||||
url.hostname in ["www.youtu.be", "youtu.be"]
|
||||
or url.hostname
|
||||
in [
|
||||
"www.youtube.com",
|
||||
"youtube.com",
|
||||
"www.youtube-nocookie.com",
|
||||
"youtube-nocookie.com",
|
||||
]
|
||||
and any(url.path.startswith(p) for p in ["/watch", "/embed"])
|
||||
):
|
||||
queries = parse_qs(url.query)
|
||||
if "v" in queries:
|
||||
video_name = queries["v"][0]
|
||||
else:
|
||||
video_name = url.path.split("/")[-1]
|
||||
|
||||
queries.pop("v", None)
|
||||
|
||||
start_time = queries.get("t", None)
|
||||
if start_time:
|
||||
queries.pop("t", None)
|
||||
queries["start"] = []
|
||||
for t in start_time:
|
||||
if t.endswith("s"):
|
||||
t = start_time[:-1]
|
||||
if t.isdigit():
|
||||
queries["start"].append(t)
|
||||
else:
|
||||
queries["start"].append(
|
||||
str(
|
||||
sum(
|
||||
int(x) * 60**i
|
||||
for i, x in enumerate(reversed(t.split(":")))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
new_queries = "&".join(
|
||||
[f"{key}={v}" for key, value in queries.items() for v in value]
|
||||
)
|
||||
|
||||
base_url = (
|
||||
"youtube-nocookie.com"
|
||||
if "youtube-nocookie" in url.hostname
|
||||
else "youtube.com"
|
||||
)
|
||||
|
||||
embed_template = f'<iframe width="560" height="315" style="display:block" src="https://www.{base_url}/embed/{video_name}?{new_queries}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>'
|
||||
if element.attrs["href"].startswith("https://www.you"):
|
||||
video_name = element.attrs["href"].split("/")[-1]
|
||||
if "v=" in element.attrs["href"]:
|
||||
video_name = element.attrs["href"].split("?v=")[1].split("&")[0]
|
||||
# if "si=" in element.attrs["href"]:
|
||||
# video_name = "?v=" + element.attrs["href"].split("/")[-1]
|
||||
# if "t=" in element.attrs["href"]:
|
||||
# video_name += "&t=" + element.attrs["href"].split("&t=")[1].split("&")[0]
|
||||
embed_template = f'<iframe width="560" height="315" style="display:block" src="https://www.youtube.com/embed/{video_name}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>'
|
||||
element.replace_with(BeautifulSoup(embed_template, "html.parser"))
|
||||
|
||||
return str(soup)
|
||||
|
||||
|
||||
@ -152,41 +108,35 @@ def embed_image(text):
|
||||
for element in soup.find_all("a"):
|
||||
file_mime = mimetypes.guess_type(element.attrs["href"])[0]
|
||||
|
||||
if (
|
||||
file_mime
|
||||
and file_mime.startswith("image/")
|
||||
or any(
|
||||
ext in element.attrs["href"].lower()
|
||||
for ext in [
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".webp",
|
||||
".svg",
|
||||
".bmp",
|
||||
".tiff",
|
||||
".ico",
|
||||
".heif",
|
||||
".heic",
|
||||
]
|
||||
)
|
||||
if file_mime and file_mime.startswith("image/") or any(
|
||||
ext in element.attrs["href"].lower() for ext in [
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".webp",
|
||||
".svg",
|
||||
".bmp",
|
||||
".tiff",
|
||||
".ico",
|
||||
".heif",
|
||||
".heic",
|
||||
]
|
||||
):
|
||||
embed_template = f'<img src="{element.attrs["href"]}" title="{element.attrs["href"]}?width=420" alt="{element.attrs["href"]}" />'
|
||||
element.replace_with(BeautifulSoup(embed_template, "html.parser"))
|
||||
return str(soup)
|
||||
|
||||
|
||||
def enrich_image_rendering(text):
|
||||
soup = BeautifulSoup(text, "html.parser")
|
||||
for element in soup.find_all("img"):
|
||||
if element.attrs["src"].startswith("/"):
|
||||
if element.attrs["src"].startswith("/" ):
|
||||
element.attrs["src"] += "?width=240&height=240"
|
||||
picture_template = f'''
|
||||
<picture>
|
||||
<source srcset="{element.attrs["src"]}" type="{mimetypes.guess_type(element.attrs["src"])[0]}" />
|
||||
<source srcset="{element.attrs["src"]}&format=webp" type="image/webp" />
|
||||
<img src="{element.attrs["src"]}&format=png" title="{element.attrs["src"]}" alt="{element.attrs["src"]}" />
|
||||
<source srcset="{element.attrs["src"]}" type="image/webp" />
|
||||
<img src="{element.attrs["src"]}" title="{element.attrs["src"]}" alt="{element.attrs["src"]}" />
|
||||
</picture>'''
|
||||
element.replace_with(BeautifulSoup(picture_template, "html.parser"))
|
||||
return str(soup)
|
||||
@ -295,6 +245,7 @@ class PythonExtension(Extension):
|
||||
).set_lineno(line_number)
|
||||
|
||||
def _to_html(self, md_file, caller):
|
||||
|
||||
def fn(source):
|
||||
import subprocess
|
||||
|
||||
|
@ -19,7 +19,6 @@
|
||||
<script src="/user-list.js"></script>
|
||||
<script src="/message-list.js" type="module"></script>
|
||||
<script src="/chat-input.js" type="module"></script>
|
||||
<link rel="stylesheet" href="/sandbox.css">
|
||||
<link rel="stylesheet" href="/user-list.css">
|
||||
|
||||
<link rel="stylesheet" href="/base.css">
|
||||
@ -79,6 +78,5 @@ let installPrompt = null
|
||||
|
||||
;
|
||||
</script>
|
||||
{% include "sandbox.html" %}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% block header_text %}Drive{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container" style="overflow-y: auto;">
|
||||
<div class="container">
|
||||
<file-manager path="{{path}}" style="flex: 1"></file-manager>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -14,7 +14,7 @@
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI',sans-serif;
|
||||
background: #000;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
line-height:1.5;
|
||||
}
|
||||
@ -187,7 +187,6 @@
|
||||
.btn { width: 100%; box-sizing: border-box; text-align:center; }
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/static/sandbox.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -285,35 +284,5 @@ snek serve
|
||||
<p>© 2025 Snek – Join our global community of developers, testers & AI enthusiasts.</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
|
||||
// number of stars you want
|
||||
const STAR_COUNT = 200;
|
||||
const body = document.body;
|
||||
|
||||
for (let i = 0; i < STAR_COUNT; i++) {
|
||||
const star = document.createElement('div');
|
||||
star.classList.add('star');
|
||||
|
||||
// random position within the viewport
|
||||
star.style.left = Math.random() * 100 + '%';
|
||||
star.style.top = Math.random() * 100 + '%';
|
||||
|
||||
// random size (optional)
|
||||
const size = Math.random() * 2 + 1; // between 1px and 3px
|
||||
star.style.width = size + 'px';
|
||||
star.style.height = size + 'px';
|
||||
|
||||
// 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';
|
||||
|
||||
body.appendChild(star);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,504 +0,0 @@
|
||||
|
||||
<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 = {}) {
|
||||
this.container = container;
|
||||
this.stars = [];
|
||||
this.setOptions(options);
|
||||
}
|
||||
|
||||
setOptions({
|
||||
starCount = 200,
|
||||
minSize = 1,
|
||||
maxSize = 3,
|
||||
speed = 5,
|
||||
color = "white"
|
||||
}) {
|
||||
this.options = { starCount, minSize, maxSize, speed, color };
|
||||
}
|
||||
|
||||
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++) {
|
||||
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.container.appendChild(star);
|
||||
this.stars.push(star);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const starField = new StarField(document.body, {
|
||||
starCount: 200,
|
||||
minSize: 1,
|
||||
maxSize: 3,
|
||||
speed: 5,
|
||||
color: "white"
|
||||
});
|
||||
*/
|
||||
</script>
|
@ -1,39 +0,0 @@
|
||||
{% 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 %}
|
@ -1,17 +0,0 @@
|
||||
{% 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 %}
|
@ -1,28 +0,0 @@
|
||||
<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>
|
||||
|
||||
|
@ -1,96 +0,0 @@
|
||||
{% 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 %}
|
@ -1,40 +0,0 @@
|
||||
{% 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 %}
|
@ -29,35 +29,12 @@ from aiohttp import web
|
||||
from multiavatar import multiavatar
|
||||
|
||||
from snek.system.view import BaseView
|
||||
from snek.view.avatar_animal import generate_avatar_with_options
|
||||
|
||||
import functools
|
||||
|
||||
class AvatarView(BaseView):
|
||||
login_required = False
|
||||
|
||||
def __init__(self, *args,**kwargs):
|
||||
super().__init__(*args,**kwargs)
|
||||
self.avatars = {}
|
||||
|
||||
async def get(self):
|
||||
uid = self.request.match_info.get("uid")
|
||||
while True:
|
||||
try:
|
||||
return web.Response(text=self._get(uid), content_type="image/svg+xml")
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
def _get(self, uid):
|
||||
if uid in self.avatars:
|
||||
return self.avatars[uid]
|
||||
|
||||
avatar = generate_avatar_with_options(self.request.query)
|
||||
self.avatars[uid] = avatar
|
||||
return avatar
|
||||
|
||||
async def get2(self):
|
||||
uid = self.request.match_info.get("uid")
|
||||
if uid == "unique":
|
||||
uid = str(uuid.uuid4())
|
||||
|
@ -1,871 +0,0 @@
|
||||
import random
|
||||
import math
|
||||
import argparse
|
||||
import json
|
||||
from typing import Dict, List, Tuple, Optional, Union
|
||||
|
||||
class AnimalAvatarGenerator:
|
||||
"""A generator for animal-themed avatar SVGs."""
|
||||
|
||||
# Constants
|
||||
ANIMALS = [
|
||||
"cat", "dog", "fox", "wolf", "bear", "panda", "koala", "tiger", "lion",
|
||||
"rabbit", "monkey", "elephant", "giraffe", "zebra", "penguin", "owl",
|
||||
"deer", "raccoon", "squirrel", "hedgehog", "otter", "frog"
|
||||
]
|
||||
|
||||
COLOR_PALETTES = {
|
||||
"natural": {
|
||||
"cat": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#808080", "#FFFFFF", "#FFA500"],
|
||||
"dog": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#808080", "#FFFFFF", "#FFA500"],
|
||||
"fox": ["#FF6600", "#FF7F00", "#FF8C00", "#FFA500", "#FFFFFF", "#000000"],
|
||||
"wolf": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000", "#696969"],
|
||||
"bear": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#FFFFFF"],
|
||||
"panda": ["#000000", "#FFFFFF"],
|
||||
"koala": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000"],
|
||||
"tiger": ["#FF8C00", "#FF7F00", "#FFFFFF", "#000000"],
|
||||
"lion": ["#DAA520", "#B8860B", "#CD853F", "#D2B48C", "#FFFFFF", "#000000"],
|
||||
"rabbit": ["#FFFFFF", "#F5F5F5", "#D3D3D3", "#A9A9A9", "#FFC0CB", "#000000"],
|
||||
"monkey": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
|
||||
"elephant": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000"],
|
||||
"giraffe": ["#DAA520", "#B8860B", "#F5DEB3", "#FFFFFF", "#000000"],
|
||||
"zebra": ["#000000", "#FFFFFF"],
|
||||
"penguin": ["#000000", "#FFFFFF", "#FFA500"],
|
||||
"owl": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000", "#FFFFFF", "#FFC0CB"],
|
||||
"deer": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#FFFFFF", "#000000"],
|
||||
"raccoon": ["#808080", "#A9A9A9", "#D3D3D3", "#FFFFFF", "#000000"],
|
||||
"squirrel": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
|
||||
"hedgehog": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
|
||||
"otter": ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#000000"],
|
||||
"frog": ["#008000", "#00FF00", "#ADFF2F", "#7FFF00", "#000000", "#FFFFFF"]
|
||||
},
|
||||
"pastel": {
|
||||
"all": ["#FFB6C1", "#FFD700", "#FFDAB9", "#98FB98", "#ADD8E6", "#DDA0DD", "#F0E68C", "#FFFFE0"]
|
||||
},
|
||||
"vibrant": {
|
||||
"all": ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#FFA500", "#FF1493"]
|
||||
},
|
||||
"mono": {
|
||||
"all": ["#000000", "#333333", "#666666", "#999999", "#CCCCCC", "#FFFFFF"]
|
||||
}
|
||||
}
|
||||
|
||||
EYE_STYLES = ["round", "oval", "almond", "wide", "narrow", "cute"]
|
||||
|
||||
FACE_SHAPES = ["round", "oval", "square", "heart", "triangular", "diamond"]
|
||||
|
||||
EAR_STYLES = {
|
||||
"cat": ["pointed", "folded", "round", "large", "small"],
|
||||
"dog": ["floppy", "pointed", "round", "large", "small"],
|
||||
"fox": ["pointed", "large", "small"],
|
||||
"wolf": ["pointed", "large", "small"],
|
||||
"bear": ["round", "small"],
|
||||
"panda": ["round", "small"],
|
||||
"koala": ["round", "large"],
|
||||
"tiger": ["round", "small"],
|
||||
"lion": ["round", "small"],
|
||||
"rabbit": ["long", "floppy", "standing"],
|
||||
"monkey": ["round", "small"],
|
||||
"elephant": ["large", "wide"],
|
||||
"giraffe": ["small", "pointed"],
|
||||
"zebra": ["pointed", "small"],
|
||||
"penguin": ["none"],
|
||||
"owl": ["none", "tufted"],
|
||||
"deer": ["small", "pointed"],
|
||||
"raccoon": ["round", "small"],
|
||||
"squirrel": ["pointed", "small"],
|
||||
"hedgehog": ["round", "small"],
|
||||
"otter": ["round", "small"],
|
||||
"frog": ["none"]
|
||||
}
|
||||
|
||||
NOSE_STYLES = ["round", "triangular", "small", "large", "heart", "button"]
|
||||
|
||||
SPECIAL_FEATURES = {
|
||||
"cat": ["whiskers", "stripes", "spots"],
|
||||
"dog": ["spots", "patch", "whiskers"],
|
||||
"fox": ["mask", "whiskers", "brush_tail"],
|
||||
"wolf": ["mask", "whiskers", "brush_tail"],
|
||||
"bear": ["none", "patch"],
|
||||
"panda": ["eye_patches", "none"],
|
||||
"koala": ["none", "nose_patch"],
|
||||
"tiger": ["stripes", "none"],
|
||||
"lion": ["mane", "none"],
|
||||
"rabbit": ["whiskers", "nose_patch"],
|
||||
"monkey": ["none", "cheek_patches"],
|
||||
"elephant": ["tusks", "none"],
|
||||
"giraffe": ["spots", "none"],
|
||||
"zebra": ["stripes", "none"],
|
||||
"penguin": ["bib", "none"],
|
||||
"owl": ["feather_tufts", "none"],
|
||||
"deer": ["antlers", "spots", "none"],
|
||||
"raccoon": ["mask", "whiskers", "none"],
|
||||
"squirrel": ["bushy_tail", "none"],
|
||||
"hedgehog": ["spikes", "none"],
|
||||
"otter": ["whiskers", "none"],
|
||||
"frog": ["spots", "none"]
|
||||
}
|
||||
|
||||
EXPRESSIONS = ["happy", "serious", "surprised", "sleepy", "wink"]
|
||||
|
||||
def __init__(self, seed: Optional[int] = None):
|
||||
"""Initialize the avatar generator with an optional seed for reproducibility."""
|
||||
if seed is not None:
|
||||
random.seed(seed)
|
||||
|
||||
def _get_colors(self, animal: str, color_palette: str) -> List[str]:
|
||||
"""Get colors for the given animal and palette."""
|
||||
if color_palette in self.COLOR_PALETTES:
|
||||
if animal in self.COLOR_PALETTES[color_palette]:
|
||||
return self.COLOR_PALETTES[color_palette][animal]
|
||||
elif "all" in self.COLOR_PALETTES[color_palette]:
|
||||
return self.COLOR_PALETTES[color_palette]["all"]
|
||||
|
||||
# Default to natural palette for the animal or general natural colors
|
||||
if animal in self.COLOR_PALETTES["natural"]:
|
||||
return self.COLOR_PALETTES["natural"][animal]
|
||||
|
||||
# If no specific colors found, use a mix of browns and grays
|
||||
return ["#8B4513", "#A0522D", "#D2B48C", "#F5DEB3", "#808080", "#A9A9A9", "#FFFFFF"]
|
||||
|
||||
def _get_ear_style(self, animal: str) -> str:
|
||||
"""Get a random ear style appropriate for the animal."""
|
||||
if animal in self.EAR_STYLES:
|
||||
return random.choice(self.EAR_STYLES[animal])
|
||||
return "none" # Default for animals not in the list
|
||||
|
||||
def _get_special_feature(self, animal: str) -> str:
|
||||
"""Get a random special feature appropriate for the animal."""
|
||||
if animal in self.SPECIAL_FEATURES:
|
||||
return random.choice(self.SPECIAL_FEATURES[animal])
|
||||
return "none" # Default for animals not in the list
|
||||
|
||||
def _draw_circle(self, cx: float, cy: float, r: float, fill: str,
|
||||
stroke: str = "none", stroke_width: float = 1.0) -> str:
|
||||
"""Generate SVG for a circle."""
|
||||
return f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" />'
|
||||
|
||||
def _draw_ellipse(self, cx: float, cy: float, rx: float, ry: float,
|
||||
fill: str, stroke: str = "none", stroke_width: float = 1.0) -> str:
|
||||
"""Generate SVG for an ellipse."""
|
||||
return f'<ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" />'
|
||||
|
||||
def _draw_path(self, d: str, fill: str, stroke: str = "none",
|
||||
stroke_width: float = 1.0, stroke_linecap: str = "round") -> str:
|
||||
"""Generate SVG for a path."""
|
||||
return f'<path d="{d}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" stroke-linecap="{stroke_linecap}" />'
|
||||
|
||||
def _draw_polygon(self, points: str, fill: str, stroke: str = "none",
|
||||
stroke_width: float = 1.0) -> str:
|
||||
"""Generate SVG for a polygon."""
|
||||
return f'<polygon points="{points}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}" />'
|
||||
|
||||
def _draw_face(self, animal: str, face_shape: str, face_color: str,
|
||||
x: float, y: float, size: float) -> str:
|
||||
"""Draw the animal's face based on face shape."""
|
||||
elements = []
|
||||
|
||||
if face_shape == "round":
|
||||
elements.append(self._draw_circle(x, y, size * 0.4, face_color))
|
||||
elif face_shape == "oval":
|
||||
elements.append(self._draw_ellipse(x, y, size * 0.35, size * 0.45, face_color))
|
||||
elif face_shape == "square":
|
||||
points = (f"{x-size*0.35},{y-size*0.35} {x+size*0.35},{y-size*0.35} "
|
||||
f"{x+size*0.35},{y+size*0.35} {x-size*0.35},{y+size*0.35}")
|
||||
elements.append(self._draw_polygon(points, face_color))
|
||||
elif face_shape == "heart":
|
||||
# Create a heart shape using paths
|
||||
cx, cy = x, y + size * 0.05
|
||||
r = size * 0.2
|
||||
path = (f"M {cx} {cy-r*0.4} "
|
||||
f"C {cx-r*1.5} {cy-r*1.5}, {cx-r*2} {cy+r*0.5}, {cx} {cy+r} "
|
||||
f"C {cx+r*2} {cy+r*0.5}, {cx+r*1.5} {cy-r*1.5}, {cx} {cy-r*0.4} Z")
|
||||
elements.append(self._draw_path(path, face_color))
|
||||
elif face_shape == "triangular":
|
||||
points = f"{x},{y-size*0.4} {x+size*0.4},{y+size*0.3} {x-size*0.4},{y+size*0.3}"
|
||||
elements.append(self._draw_polygon(points, face_color))
|
||||
elif face_shape == "diamond":
|
||||
points = f"{x},{y-size*0.4} {x+size*0.35},{y} {x},{y+size*0.4} {x-size*0.35},{y}"
|
||||
elements.append(self._draw_polygon(points, face_color))
|
||||
|
||||
return "\n".join(elements)
|
||||
|
||||
def _draw_ears(self, animal: str, ear_style: str, face_color: str,
|
||||
inner_color: str, x: float, y: float, size: float) -> str:
|
||||
"""Draw the animal's ears based on ear style."""
|
||||
elements = []
|
||||
|
||||
if ear_style == "none":
|
||||
return ""
|
||||
|
||||
if ear_style == "pointed":
|
||||
# Left ear
|
||||
points_left = f"{x-size*0.2},{y-size*0.1} {x-size*0.35},{y-size*0.45} {x-size*0.05},{y-size*0.15}"
|
||||
elements.append(self._draw_polygon(points_left, face_color))
|
||||
|
||||
# Right ear
|
||||
points_right = f"{x+size*0.2},{y-size*0.1} {x+size*0.35},{y-size*0.45} {x+size*0.05},{y-size*0.15}"
|
||||
elements.append(self._draw_polygon(points_right, face_color))
|
||||
|
||||
# Inner ears
|
||||
points_inner_left = f"{x-size*0.2},{y-size*0.13} {x-size*0.3},{y-size*0.38} {x-size*0.1},{y-size*0.17}"
|
||||
elements.append(self._draw_polygon(points_inner_left, inner_color))
|
||||
|
||||
points_inner_right = f"{x+size*0.2},{y-size*0.13} {x+size*0.3},{y-size*0.38} {x+size*0.1},{y-size*0.17}"
|
||||
elements.append(self._draw_polygon(points_inner_right, inner_color))
|
||||
|
||||
elif ear_style == "round":
|
||||
# Left ear
|
||||
elements.append(self._draw_circle(x-size*0.25, y-size*0.25, size*0.15, face_color))
|
||||
|
||||
# Right ear
|
||||
elements.append(self._draw_circle(x+size*0.25, y-size*0.25, size*0.15, face_color))
|
||||
|
||||
# Inner ears
|
||||
elements.append(self._draw_circle(x-size*0.25, y-size*0.25, size*0.08, inner_color))
|
||||
elements.append(self._draw_circle(x+size*0.25, y-size*0.25, size*0.08, inner_color))
|
||||
|
||||
elif ear_style == "folded" or ear_style == "floppy":
|
||||
# Left ear
|
||||
path_left = (f"M {x-size*0.15} {y-size*0.15} "
|
||||
f"C {x-size*0.3} {y-size*0.4}, {x-size*0.4} {y-size*0.2}, {x-size*0.35} {y}")
|
||||
elements.append(self._draw_path(path_left, face_color, stroke="none", stroke_width=size*0.08))
|
||||
|
||||
# Right ear
|
||||
path_right = (f"M {x+size*0.15} {y-size*0.15} "
|
||||
f"C {x+size*0.3} {y-size*0.4}, {x+size*0.4} {y-size*0.2}, {x+size*0.35} {y}")
|
||||
elements.append(self._draw_path(path_right, face_color, stroke="none", stroke_width=size*0.08))
|
||||
|
||||
elif ear_style == "long" or ear_style == "standing":
|
||||
# Left ear
|
||||
points_left = f"{x-size*0.2},{y-size*0.15} {x-size*0.3},{y-size*0.6} {x-size*0.05},{y-size*0.15}"
|
||||
elements.append(self._draw_polygon(points_left, face_color))
|
||||
|
||||
# Right ear
|
||||
points_right = f"{x+size*0.2},{y-size*0.15} {x+size*0.3},{y-size*0.6} {x+size*0.05},{y-size*0.15}"
|
||||
elements.append(self._draw_polygon(points_right, face_color))
|
||||
|
||||
# Inner ears
|
||||
points_inner_left = f"{x-size*0.18},{y-size*0.18} {x-size*0.25},{y-size*0.5} {x-size*0.1},{y-size*0.18}"
|
||||
elements.append(self._draw_polygon(points_inner_left, inner_color))
|
||||
|
||||
points_inner_right = f"{x+size*0.18},{y-size*0.18} {x+size*0.25},{y-size*0.5} {x+size*0.1},{y-size*0.18}"
|
||||
elements.append(self._draw_polygon(points_inner_right, inner_color))
|
||||
|
||||
elif ear_style == "large":
|
||||
# Left ear
|
||||
elements.append(self._draw_ellipse(x-size*0.25, y-size*0.3, size*0.2, size*0.25, face_color))
|
||||
|
||||
# Right ear
|
||||
elements.append(self._draw_ellipse(x+size*0.25, y-size*0.3, size*0.2, size*0.25, face_color))
|
||||
|
||||
# Inner ears
|
||||
elements.append(self._draw_ellipse(x-size*0.25, y-size*0.3, size*0.1, size*0.15, inner_color))
|
||||
elements.append(self._draw_ellipse(x+size*0.25, y-size*0.3, size*0.1, size*0.15, inner_color))
|
||||
|
||||
elif ear_style == "small":
|
||||
# Left ear
|
||||
elements.append(self._draw_ellipse(x-size*0.22, y-size*0.22, size*0.1, size*0.12, face_color))
|
||||
|
||||
# Right ear
|
||||
elements.append(self._draw_ellipse(x+size*0.22, y-size*0.22, size*0.1, size*0.12, face_color))
|
||||
|
||||
# Inner ears
|
||||
elements.append(self._draw_ellipse(x-size*0.22, y-size*0.22, size*0.05, size*0.06, inner_color))
|
||||
elements.append(self._draw_ellipse(x+size*0.22, y-size*0.22, size*0.05, size*0.06, inner_color))
|
||||
|
||||
elif ear_style == "tufted" and animal == "owl":
|
||||
# Left ear tuft
|
||||
points_left = f"{x-size*0.2},{y-size*0.15} {x-size*0.35},{y-size*0.45} {x-size*0.05},{y-size*0.15}"
|
||||
elements.append(self._draw_polygon(points_left, face_color))
|
||||
|
||||
# Right ear tuft
|
||||
points_right = f"{x+size*0.2},{y-size*0.15} {x+size*0.35},{y-size*0.45} {x+size*0.05},{y-size*0.15}"
|
||||
elements.append(self._draw_polygon(points_right, face_color))
|
||||
|
||||
elif ear_style == "wide" and animal == "elephant":
|
||||
# Left ear
|
||||
path_left = (f"M {x-size*0.15} {y-size*0.1} "
|
||||
f"C {x-size*0.5} {y-size*0.2}, {x-size*0.6} {y+size*0.2}, {x-size*0.15} {y+size*0.2}")
|
||||
elements.append(self._draw_path(path_left, face_color, stroke=face_color, stroke_width=size*0.04))
|
||||
|
||||
# Right ear
|
||||
path_right = (f"M {x+size*0.15} {y-size*0.1} "
|
||||
f"C {x+size*0.5} {y-size*0.2}, {x+size*0.6} {y+size*0.2}, {x+size*0.15} {y+size*0.2}")
|
||||
elements.append(self._draw_path(path_right, face_color, stroke=face_color, stroke_width=size*0.04))
|
||||
|
||||
return "\n".join(elements)
|
||||
|
||||
def _draw_eyes(self, eye_style: str, expression: str, eye_color: str,
|
||||
x: float, y: float, size: float) -> str:
|
||||
"""Draw the animal's eyes based on eye style and expression."""
|
||||
elements = []
|
||||
eye_spacing = size * 0.2
|
||||
|
||||
if eye_style == "round":
|
||||
eye_size = size * 0.08
|
||||
# Left eye
|
||||
elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
|
||||
elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, eye_size * 0.6, eye_color))
|
||||
|
||||
# Right eye
|
||||
elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
|
||||
elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, eye_size * 0.6, eye_color))
|
||||
|
||||
elif eye_style == "oval":
|
||||
eye_width = size * 0.1
|
||||
eye_height = size * 0.07
|
||||
# Left eye
|
||||
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
|
||||
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width * 0.6, eye_height * 0.6, eye_color))
|
||||
|
||||
# Right eye
|
||||
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
|
||||
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width * 0.6, eye_height * 0.6, eye_color))
|
||||
|
||||
elif eye_style == "almond":
|
||||
# Left eye - almond shape
|
||||
path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.05} "
|
||||
f"C {x-eye_spacing} {y-size*0.12}, {x-eye_spacing} {y+size*0.02}, {x-eye_spacing+size*0.1} {y-size*0.05} "
|
||||
f"C {x-eye_spacing} {y+size*0.02}, {x-eye_spacing} {y-size*0.12}, {x-eye_spacing-size*0.1} {y-size*0.05} Z")
|
||||
elements.append(self._draw_path(path_left, "#FFFFFF"))
|
||||
elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, size * 0.05, eye_color))
|
||||
|
||||
# Right eye - almond shape
|
||||
path_right = (f"M {x+eye_spacing-size*0.1} {y-size*0.05} "
|
||||
f"C {x+eye_spacing} {y-size*0.12}, {x+eye_spacing} {y+size*0.02}, {x+eye_spacing+size*0.1} {y-size*0.05} "
|
||||
f"C {x+eye_spacing} {y+size*0.02}, {x+eye_spacing} {y-size*0.12}, {x+eye_spacing-size*0.1} {y-size*0.05} Z")
|
||||
elements.append(self._draw_path(path_right, "#FFFFFF"))
|
||||
elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, size * 0.05, eye_color))
|
||||
|
||||
elif eye_style == "wide":
|
||||
eye_width = size * 0.12
|
||||
eye_height = size * 0.08
|
||||
# Left eye
|
||||
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
|
||||
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width * 0.5, eye_height * 0.6, eye_color))
|
||||
|
||||
# Right eye
|
||||
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
|
||||
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width * 0.5, eye_height * 0.6, eye_color))
|
||||
|
||||
elif eye_style == "narrow":
|
||||
eye_width = size * 0.12
|
||||
eye_height = size * 0.04
|
||||
# Left eye
|
||||
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
|
||||
elements.append(self._draw_ellipse(x - eye_spacing, y - size * 0.05, eye_width * 0.4, eye_height * 0.7, eye_color))
|
||||
|
||||
# Right eye
|
||||
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width, eye_height, "#FFFFFF"))
|
||||
elements.append(self._draw_ellipse(x + eye_spacing, y - size * 0.05, eye_width * 0.4, eye_height * 0.7, eye_color))
|
||||
|
||||
elif eye_style == "cute":
|
||||
eye_size = size * 0.1
|
||||
# Left eye
|
||||
elements.append(self._draw_circle(x - eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
|
||||
elements.append(self._draw_circle(x - eye_spacing - size * 0.02, y - size * 0.07, eye_size * 0.5, eye_color))
|
||||
elements.append(self._draw_circle(x - eye_spacing - size * 0.03, y - size * 0.08, eye_size * 0.15, "#FFFFFF"))
|
||||
|
||||
# Right eye
|
||||
elements.append(self._draw_circle(x + eye_spacing, y - size * 0.05, eye_size, "#FFFFFF"))
|
||||
elements.append(self._draw_circle(x + eye_spacing - size * 0.02, y - size * 0.07, eye_size * 0.5, eye_color))
|
||||
elements.append(self._draw_circle(x + eye_spacing - size * 0.03, y - size * 0.08, eye_size * 0.15, "#FFFFFF"))
|
||||
|
||||
# Apply expression
|
||||
if expression == "happy":
|
||||
# Close bottom half of eyes slightly
|
||||
path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.03} "
|
||||
f"Q {x-eye_spacing} {y+size*0.02}, {x-eye_spacing+size*0.1} {y-size*0.03}")
|
||||
elements.append(self._draw_path(path_left, face_color, stroke="#000000", stroke_width=size*0.01))
|
||||
|
||||
path_right = (f"M {x+eye_spacing-size*0.1} {y-size*0.03} "
|
||||
f"Q {x+eye_spacing} {y+size*0.02}, {x+eye_spacing+size*0.1} {y-size*0.03}")
|
||||
elements.append(self._draw_path(path_right, face_color, stroke="#000000", stroke_width=size*0.01))
|
||||
|
||||
elif expression == "serious":
|
||||
# Serious eyebrows
|
||||
path_left = (f"M {x-eye_spacing-size*0.08} {y-size*0.12} "
|
||||
f"L {x-eye_spacing+size*0.08} {y-size*0.15}")
|
||||
elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.02))
|
||||
|
||||
path_right = (f"M {x+eye_spacing-size*0.08} {y-size*0.15} "
|
||||
f"L {x+eye_spacing+size*0.08} {y-size*0.12}")
|
||||
elements.append(self._draw_path(path_right, "none", stroke="#000000", stroke_width=size*0.02))
|
||||
|
||||
elif expression == "surprised":
|
||||
# Raise eyebrows
|
||||
path_left = (f"M {x-eye_spacing-size*0.08} {y-size*0.15} "
|
||||
f"Q {x-eye_spacing} {y-size*0.18}, {x-eye_spacing+size*0.08} {y-size*0.15}")
|
||||
elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.015))
|
||||
|
||||
path_right = (f"M {x+eye_spacing-size*0.08} {y-size*0.15} "
|
||||
f"Q {x+eye_spacing} {y-size*0.18}, {x+eye_spacing+size*0.08} {y-size*0.15}")
|
||||
elements.append(self._draw_path(path_right, "none", stroke="#000000", stroke_width=size*0.015))
|
||||
|
||||
elif expression == "sleepy":
|
||||
# Half-closed eyes
|
||||
path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.08} "
|
||||
f"Q {x-eye_spacing} {y-size*0.01}, {x-eye_spacing+size*0.1} {y-size*0.08}")
|
||||
elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.015))
|
||||
|
||||
path_right = (f"M {x+eye_spacing-size*0.1} {y-size*0.08} "
|
||||
f"Q {x+eye_spacing} {y-size*0.01}, {x+eye_spacing+size*0.1} {y-size*0.08}")
|
||||
elements.append(self._draw_path(path_right, "none", stroke="#000000", stroke_width=size*0.015))
|
||||
|
||||
elif expression == "wink":
|
||||
# Right eye normal
|
||||
# Left eye winking
|
||||
path_left = (f"M {x-eye_spacing-size*0.1} {y-size*0.05} "
|
||||
f"Q {x-eye_spacing} {y-size*0.1}, {x-eye_spacing+size*0.1} {y-size*0.05}")
|
||||
elements.append(self._draw_path(path_left, "none", stroke="#000000", stroke_width=size*0.02))
|
||||
|
||||
return "\n".join(elements)
|
||||
|
||||
def _draw_nose(self, animal: str, nose_style: str, nose_color: str,
|
||||
x: float, y: float, size: float) -> str:
|
||||
"""Draw the animal's nose based on nose style."""
|
||||
elements = []
|
||||
|
||||
if nose_style == "round":
|
||||
elements.append(self._draw_circle(x, y + size * 0.05, size * 0.08, nose_color))
|
||||
|
||||
elif nose_style == "triangular":
|
||||
points = f"{x},{y+size*0.02} {x-size*0.08},{y+size*0.12} {x+size*0.08},{y+size*0.12}"
|
||||
elements.append(self._draw_polygon(points, nose_color))
|
||||
|
||||
elif nose_style == "small":
|
||||
elements.append(self._draw_circle(x, y + size * 0.05, size * 0.05, nose_color))
|
||||
|
||||
elif nose_style == "large":
|
||||
if animal in ["dog", "bear", "panda", "koala"]:
|
||||
elements.append(self._draw_ellipse(x, y + size * 0.05, size * 0.1, size * 0.08, nose_color))
|
||||
else:
|
||||
elements.append(self._draw_circle(x, y + size * 0.05, size * 0.1, nose_color))
|
||||
|
||||
elif nose_style == "heart":
|
||||
# Heart-shaped nose
|
||||
cx, cy = x, y + size * 0.05
|
||||
r = size * 0.06
|
||||
path = (f"M {cx} {cy-r*0.2} "
|
||||
f"C {cx-r*1.5} {cy-r*1.2}, {cx-r*1.8} {cy+r*0.6}, {cx} {cy+r*0.8} "
|
||||
f"C {cx+r*1.8} {cy+r*0.6}, {cx+r*1.5} {cy-r*1.2}, {cx} {cy-r*0.2} Z")
|
||||
elements.append(self._draw_path(path, nose_color))
|
||||
|
||||
elif nose_style == "button":
|
||||
elements.append(self._draw_circle(x, y + size * 0.05, size * 0.06, nose_color))
|
||||
elements.append(self._draw_circle(x, y + size * 0.05, size * 0.04, nose_color, stroke="#000000", stroke_width=size*0.01))
|
||||
|
||||
return "\n".join(elements)
|
||||
|
||||
def _draw_mouth(self, expression: str, x: float, y: float, size: float) -> str:
|
||||
"""Draw the animal's mouth based on expression."""
|
||||
elements = []
|
||||
|
||||
if expression == "happy":
|
||||
path = (f"M {x-size*0.15} {y+size*0.12} "
|
||||
f"Q {x} {y+size*0.25}, {x+size*0.15} {y+size*0.12}")
|
||||
elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.02))
|
||||
|
||||
elif expression == "serious":
|
||||
path = (f"M {x-size*0.12} {y+size*0.15} "
|
||||
f"L {x+size*0.12} {y+size*0.15}")
|
||||
elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.02))
|
||||
|
||||
elif expression == "surprised":
|
||||
elements.append(self._draw_ellipse(x, y + size * 0.15, size * 0.06, size * 0.08, "#FFFFFF",
|
||||
stroke="#000000", stroke_width=size*0.01))
|
||||
|
||||
elif expression == "sleepy":
|
||||
path = (f"M {x-size*0.08} {y+size*0.15} "
|
||||
f"Q {x} {y+size*0.12}, {x+size*0.08} {y+size*0.15}")
|
||||
elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.015))
|
||||
|
||||
elif expression == "wink":
|
||||
path = (f"M {x-size*0.15} {y+size*0.12} "
|
||||
f"Q {x} {y+size*0.22}, {x+size*0.15} {y+size*0.12}")
|
||||
elements.append(self._draw_path(path, "none", stroke="#000000", stroke_width=size*0.02))
|
||||
|
||||
return "\n".join(elements)
|
||||
|
||||
def _draw_special_features(self, animal: str, special_feature: str, face_color: str,
|
||||
accent_color: str, x: float, y: float, size: float) -> str:
|
||||
"""Draw special features based on animal and feature type."""
|
||||
elements = []
|
||||
|
||||
if special_feature == "none":
|
||||
return ""
|
||||
|
||||
elif special_feature == "whiskers":
|
||||
# Left whiskers
|
||||
for i in range(3):
|
||||
angle = -30 + i * 30
|
||||
length = size * 0.25
|
||||
end_x = x - size * 0.15 + length * math.cos(math.radians(angle))
|
||||
end_y = y + size * 0.05 + length * math.sin(math.radians(angle))
|
||||
elements.append(self._draw_path(
|
||||
f"M {x-size*0.15} {y+size*0.05} L {end_x} {end_y}",
|
||||
"none", stroke="#000000", stroke_width=size*0.01))
|
||||
|
||||
# Right whiskers
|
||||
for i in range(3):
|
||||
angle = -150 + i * 30
|
||||
length = size * 0.25
|
||||
end_x = x + size * 0.15 + length * math.cos(math.radians(angle))
|
||||
end_y = y + size * 0.05 + length * math.sin(math.radians(angle))
|
||||
elements.append(self._draw_path(
|
||||
f"M {x+size*0.15} {y+size*0.05} L {end_x} {end_y}",
|
||||
"none", stroke="#000000", stroke_width=size*0.01))
|
||||
|
||||
elif special_feature == "stripes":
|
||||
# Vertical stripes
|
||||
if animal == "tiger":
|
||||
for i in range(3):
|
||||
offset = -size * 0.2 + i * size * 0.2
|
||||
path = (f"M {x+offset} {y-size*0.3} "
|
||||
f"Q {x+offset+size*0.1} {y}, {x+offset} {y+size*0.3}")
|
||||
elements.append(self._draw_path(path, "none", stroke=accent_color, stroke_width=size*0.04))
|
||||
# Horizontal stripes for zebra
|
||||
elif animal == "zebra":
|
||||
for i in range(3):
|
||||
offset = -size * 0.2 + i * size * 0.2
|
||||
path = (f"M {x-size*0.3} {y+offset} "
|
||||
f"Q {x} {y+offset+size*0.1}, {x+size*0.3} {y+offset}")
|
||||
elements.append(self._draw_path(path, "none", stroke=accent_color, stroke_width=size*0.04))
|
||||
|
||||
elif special_feature == "spots":
|
||||
# Random spots
|
||||
num_spots = random.randint(3, 6)
|
||||
for _ in range(num_spots):
|
||||
spot_x = x + random.uniform(-size * 0.3, size * 0.3)
|
||||
spot_y = y + random.uniform(-size * 0.3, size * 0.3)
|
||||
spot_size = random.uniform(size * 0.03, size * 0.08)
|
||||
elements.append(self._draw_circle(spot_x, spot_y, spot_size, accent_color))
|
||||
|
||||
elif special_feature == "patch":
|
||||
# Eye patch or face patch
|
||||
if animal == "dog":
|
||||
elements.append(self._draw_ellipse(x - size * 0.2, y, size * 0.2, size * 0.25, accent_color))
|
||||
else:
|
||||
# Generic face patch
|
||||
elements.append(self._draw_ellipse(x, y + size * 0.2, size * 0.2, size * 0.15, accent_color))
|
||||
|
||||
elif special_feature == "mask":
|
||||
if animal == "raccoon":
|
||||
# Raccoon mask
|
||||
path = (f"M {x-size*0.3} {y-size*0.1} "
|
||||
f"Q {x} {y-size*0.3}, {x+size*0.3} {y-size*0.1} "
|
||||
f"Q {x+size*0.2} {y+size*0.1}, {x} {y+size*0.15} "
|
||||
f"Q {x-size*0.2} {y+size*0.1}, {x-size*0.3} {y-size*0.1} Z")
|
||||
elements.append(self._draw_path(path, accent_color))
|
||||
elif animal in ["fox", "wolf"]:
|
||||
# Fox/wolf mask
|
||||
path = (f"M {x-size*0.3} {y-size*0.1} "
|
||||
f"L {x} {y+size*0.1} "
|
||||
f"L {x+size*0.3} {y-size*0.1} "
|
||||
f"Q {x+size*0.15} {y-size*0.05}, {x} {y-size*0.1} "
|
||||
f"Q {x-size*0.15} {y-size*0.05}, {x-size*0.3} {y-size*0.1} Z")
|
||||
elements.append(self._draw_path(path, accent_color))
|
||||
|
||||
elif special_feature == "eye_patches" and animal == "panda":
|
||||
# Panda eye patches
|
||||
elements.append(self._draw_ellipse(x - size * 0.2, y - size * 0.05, size * 0.15, size * 0.2, accent_color))
|
||||
elements.append(self._draw_ellipse(x + size * 0.2, y - size * 0.05, size * 0.15, size * 0.2, accent_color))
|
||||
|
||||
elif special_feature == "nose_patch":
|
||||
elements.append(self._draw_ellipse(x, y + size * 0.05, size * 0.12, size * 0.1, accent_color))
|
||||
|
||||
elif special_feature == "mane" and animal == "lion":
|
||||
# Lion mane
|
||||
for i in range(12):
|
||||
angle = i * 30
|
||||
outer_x = x + size * 0.5 * math.cos(math.radians(angle))
|
||||
outer_y = y + size * 0.5 * math.sin(math.radians(angle))
|
||||
inner_x = x + size * 0.3 * math.cos(math.radians(angle))
|
||||
inner_y = y + size * 0.3 * math.sin(math.radians(angle))
|
||||
|
||||
# Draw mane sections
|
||||
path = (f"M {inner_x} {inner_y} "
|
||||
f"L {outer_x} {outer_y} "
|
||||
f"A {size*0.5} {size*0.5} 0 0 1 "
|
||||
f"{x + size * 0.5 * math.cos(math.radians(angle + 30))} "
|
||||
f"{y + size * 0.5 * math.sin(math.radians(angle + 30))} "
|
||||
f"L {x + size * 0.3 * math.cos(math.radians(angle + 30))} "
|
||||
f"{y + size * 0.3 * math.sin(math.radians(angle + 30))} Z")
|
||||
elements.append(self._draw_path(path, accent_color))
|
||||
|
||||
elif special_feature == "tusks" and animal == "elephant":
|
||||
# Elephant tusks
|
||||
path_left = (f"M {x-size*0.15} {y+size*0.1} "
|
||||
f"Q {x-size*0.3} {y+size*0.3}, {x-size*0.35} {y+size*0.5}")
|
||||
elements.append(self._draw_path(path_left, "#FFFFF0", stroke="#F5F5DC", stroke_width=size*0.04))
|
||||
|
||||
path_right = (f"M {x+size*0.15} {y+size*0.1} "
|
||||
f"Q {x+size*0.3} {y+size*0.3}, {x+size*0.35} {y+size*0.5}")
|
||||
elements.append(self._draw_path(path_right, "#FFFFF0", stroke="#F5F5DC", stroke_width=size*0.04))
|
||||
|
||||
elif special_feature == "antlers" and animal == "deer":
|
||||
# Deer antlers
|
||||
# Left antler
|
||||
path_left = (f"M {x-size*0.15} {y-size*0.2} "
|
||||
f"L {x-size*0.3} {y-size*0.45} "
|
||||
f"L {x-size*0.4} {y-size*0.4} "
|
||||
f"M {x-size*0.3} {y-size*0.45} "
|
||||
f"L {x-size*0.2} {y-size*0.5}")
|
||||
elements.append(self._draw_path(path_left, "none", stroke=accent_color, stroke_width=size*0.03))
|
||||
|
||||
# Right antler
|
||||
path_right = (f"M {x+size*0.15} {y-size*0.2} "
|
||||
f"L {x+size*0.3} {y-size*0.45} "
|
||||
f"L {x+size*0.4} {y-size*0.4} "
|
||||
f"M {x+size*0.3} {y-size*0.45} "
|
||||
f"L {x+size*0.2} {y-size*0.5}")
|
||||
elements.append(self._draw_path(path_right, "none", stroke=accent_color, stroke_width=size*0.03))
|
||||
|
||||
elif special_feature == "bushy_tail" and animal == "squirrel":
|
||||
# Squirrel bushy tail
|
||||
path = (f"M {x+size*0.1} {y+size*0.2} "
|
||||
f"Q {x+size*0.5} {y}, {x+size*0.3} {y-size*0.3} "
|
||||
f"Q {x+size*0.4} {y-size*0.4}, {x+size*0.5} {y-size*0.35} "
|
||||
f"Q {x+size*0.45} {y-size*0.25}, {x+size*0.6} {y-size*0.3} "
|
||||
f"Q {x+size*0.55} {y-size*0.1}, {x+size*0.4} {y+size*0.1} "
|
||||
f"Q {x+size*0.3} {y+size*0.3}, {x+size*0.1} {y+size*0.2} Z")
|
||||
elements.append(self._draw_path(path, accent_color))
|
||||
|
||||
elif special_feature == "brush_tail" and animal in ["fox", "wolf"]:
|
||||
# Fox/wolf brush tail
|
||||
path = (f"M {x+size*0.1} {y+size*0.2} "
|
||||
f"Q {x+size*0.4} {y+size*0.1}, {x+size*0.5} {y-size*0.1} "
|
||||
f"Q {x+size*0.6} {y-size*0.2}, {x+size*0.7} {y-size*0.1} "
|
||||
f"Q {x+size*0.65} {y}, {x+size*0.6} {y+size*0.1} "
|
||||
f"Q {x+size*0.5} {y+size*0.2}, {x+size*0.3} {y+size*0.3} Z")
|
||||
elements.append(self._draw_path(path, face_color))
|
||||
# Tail tip
|
||||
elements.append(self._draw_ellipse(x + size * 0.6, y - size * 0.05, size * 0.12, size * 0.08, accent_color))
|
||||
|
||||
elif special_feature == "bib" and animal == "penguin":
|
||||
# Penguin bib/chest
|
||||
path = (f"M {x-size*0.2} {y} "
|
||||
f"Q {x} {y+size*0.4}, {x+size*0.2} {y} "
|
||||
f"Q {x} {y+size*0.1}, {x-size*0.2} {y} Z")
|
||||
elements.append(self._draw_path(path, "#FFFFFF"))
|
||||
|
||||
elif special_feature == "feather_tufts" and animal == "owl":
|
||||
# Owl feather tufts
|
||||
path_left = (f"M {x-size*0.1} {y-size*0.3} "
|
||||
f"Q {x-size*0.15} {y-size*0.45}, {x-size*0.05} {y-size*0.5}")
|
||||
elements.append(self._draw_path(path_left, face_color, stroke="#000000", stroke_width=size*0.01))
|
||||
|
||||
path_right = (f"M {x+size*0.1} {y-size*0.3} "
|
||||
f"Q {x+size*0.15} {y-size*0.45}, {x+size*0.05} {y-size*0.5}")
|
||||
elements.append(self._draw_path(path_right, face_color, stroke="#000000", stroke_width=size*0.01))
|
||||
|
||||
elif special_feature == "spikes" and animal == "hedgehog":
|
||||
# Hedgehog spikes
|
||||
for i in range(12):
|
||||
angle = i * 30
|
||||
inner_x = x + size * 0.3 * math.cos(math.radians(angle))
|
||||
inner_y = y + size * 0.3 * math.sin(math.radians(angle))
|
||||
outer_x = x + size * 0.5 * math.cos(math.radians(angle))
|
||||
outer_y = y + size * 0.5 * math.sin(math.radians(angle))
|
||||
|
||||
path = f"M {inner_x} {inner_y} L {outer_x} {outer_y}"
|
||||
elements.append(self._draw_path(path, "none", stroke=accent_color, stroke_width=size*0.03))
|
||||
|
||||
elif special_feature == "cheek_patches" and animal == "monkey":
|
||||
# Monkey cheek patches
|
||||
elements.append(self._draw_circle(x - size * 0.25, y + size * 0.1, size * 0.12, accent_color))
|
||||
elements.append(self._draw_circle(x + size * 0.25, y + size * 0.1, size * 0.12, accent_color))
|
||||
|
||||
return "\n".join(elements)
|
||||
|
||||
def generate_avatar(self, animal: Optional[str] = None, color_palette: str = "natural",
|
||||
face_shape: Optional[str] = None, eye_style: Optional[str] = None,
|
||||
ear_style: Optional[str] = None, nose_style: Optional[str] = None,
|
||||
expression: Optional[str] = None, special_feature: Optional[str] = None,
|
||||
size: int = 500) -> str:
|
||||
"""
|
||||
Generate an animal avatar with the specified parameters.
|
||||
|
||||
Args:
|
||||
animal: Animal type (e.g., "cat", "dog"). If None, a random animal is selected.
|
||||
color_palette: Color palette to use (e.g., "natural", "pastel", "vibrant", "mono").
|
||||
face_shape: Shape of the face. If None, a random shape is selected.
|
||||
eye_style: Style of the eyes. If None, a random style is selected.
|
||||
ear_style: Style of the ears. If None, a random style is selected for the animal.
|
||||
nose_style: Style of the nose. If None, a random style is selected.
|
||||
expression: Facial expression. If None, a random expression is selected.
|
||||
special_feature: Special feature to add. If None, a random feature is selected for the animal.
|
||||
size: Size of the avatar in pixels.
|
||||
|
||||
Returns:
|
||||
SVG string representation of the generated avatar.
|
||||
"""
|
||||
# Select random animal if not specified
|
||||
if animal is None or animal not in self.ANIMALS:
|
||||
animal = random.choice(self.ANIMALS)
|
||||
|
||||
# Select random options if not specified
|
||||
if face_shape is None or face_shape not in self.FACE_SHAPES:
|
||||
face_shape = random.choice(self.FACE_SHAPES)
|
||||
|
||||
if eye_style is None or eye_style not in self.EYE_STYLES:
|
||||
eye_style = random.choice(self.EYE_STYLES)
|
||||
|
||||
if ear_style is None:
|
||||
ear_style = self._get_ear_style(animal)
|
||||
|
||||
if nose_style is None or nose_style not in self.NOSE_STYLES:
|
||||
nose_style = random.choice(self.NOSE_STYLES)
|
||||
|
||||
if expression is None or expression not in self.EXPRESSIONS:
|
||||
expression = random.choice(self.EXPRESSIONS)
|
||||
|
||||
if special_feature is None:
|
||||
special_feature = self._get_special_feature(animal)
|
||||
|
||||
# Get colors
|
||||
colors = self._get_colors(animal, color_palette)
|
||||
face_color = random.choice(colors)
|
||||
|
||||
# Make sure accent color is different from face color
|
||||
remaining_colors = [c for c in colors if c != face_color]
|
||||
if not remaining_colors:
|
||||
remaining_colors = ["#000000", "#FFFFFF"]
|
||||
accent_color = random.choice(remaining_colors)
|
||||
|
||||
# Ensure inner ear color is different from face color
|
||||
inner_ear_color = random.choice(remaining_colors)
|
||||
|
||||
# Eye color options
|
||||
eye_colors = ["#000000", "#331800", "#0000FF", "#008000", "#FFA500", "#800080"]
|
||||
eye_color = random.choice(eye_colors)
|
||||
|
||||
# Nose color options based on animal
|
||||
if animal in ["dog", "cat", "fox", "wolf", "bear", "panda", "koala", "tiger", "lion"]:
|
||||
nose_color = "#000000"
|
||||
else:
|
||||
nose_color = accent_color
|
||||
|
||||
# Center coordinates
|
||||
x, y = size / 2, size / 2
|
||||
|
||||
# Generate SVG elements
|
||||
elements = []
|
||||
|
||||
# Draw the face first
|
||||
elements.append(self._draw_face(animal, face_shape, face_color, x, y, size))
|
||||
|
||||
# Draw special features behind the face if needed
|
||||
if special_feature in ["mane", "spikes"]:
|
||||
elements.append(self._draw_special_features(animal, special_feature, face_color,
|
||||
accent_color, x, y, size))
|
||||
|
||||
# Draw ears
|
||||
elements.append(self._draw_ears(animal, ear_style, face_color, inner_ear_color, x, y, size))
|
||||
|
||||
# Draw eyes
|
||||
elements.append(self._draw_eyes(eye_style, expression, eye_color, x, y, size))
|
||||
|
||||
# Draw nose
|
||||
elements.append(self._draw_nose(animal, nose_style, nose_color, x, y, size))
|
||||
|
||||
# Draw mouth
|
||||
elements.append(self._draw_mouth(expression, x, y, size))
|
||||
|
||||
# Draw special features that should be in front
|
||||
if special_feature not in ["mane", "spikes"]:
|
||||
elements.append(self._draw_special_features(animal, special_feature, face_color,
|
||||
accent_color, x, y, size))
|
||||
|
||||
# Assemble SVG
|
||||
svg_content = '\n'.join(elements)
|
||||
svg = (f'<svg viewBox="0 0 {size} {size}" xmlns="http://www.w3.org/2000/svg">\n'
|
||||
f'{svg_content}\n'
|
||||
f'</svg>')
|
||||
|
||||
return svg
|
||||
|
||||
def get_avatar_options(self) -> Dict[str, List[str]]:
|
||||
"""Return all available avatar options."""
|
||||
return {
|
||||
"animals": self.ANIMALS,
|
||||
"color_palettes": list(self.COLOR_PALETTES.keys()),
|
||||
"face_shapes": self.FACE_SHAPES,
|
||||
"eye_styles": self.EYE_STYLES,
|
||||
"ear_styles": {animal: styles for animal, styles in self.EAR_STYLES.items()},
|
||||
"nose_styles": self.NOSE_STYLES,
|
||||
"expressions": self.EXPRESSIONS,
|
||||
"special_features": {animal: features for animal, features in self.SPECIAL_FEATURES.items()}
|
||||
}
|
||||
|
||||
def generate_random_avatar(self, size: int = 500) -> str:
|
||||
"""Generate a completely random avatar."""
|
||||
return self.generate_avatar(size=size)
|
||||
|
||||
def generate_avatar_with_options(options: Dict) -> str:
|
||||
"""Generate an avatar with the given options."""
|
||||
generator = AnimalAvatarGenerator(seed=options.get("seed"))
|
||||
|
||||
return generator.generate_avatar(
|
||||
animal=options.get("animal"),
|
||||
color_palette=options.get("color_palette", "natural"),
|
||||
face_shape=options.get("face_shape"),
|
||||
eye_style=options.get("eye_style"),
|
||||
ear_style=options.get("ear_style"),
|
||||
nose_style=options.get("nose_style"),
|
||||
expression=options.get("expression"),
|
||||
special_feature=options.get("special_feature"),
|
||||
size=options.get("size", 500)
|
||||
)
|
||||
|
||||
def list_avatar_options() -> Dict[str, List[str]]:
|
||||
"""Return all available avatar options."""
|
||||
generator = AnimalAvatarGenerator()
|
||||
return generator.get_avatar_options()
|
||||
|
||||
def create_avatar_app():
|
||||
"""Command-line interface for the avatar generator."""
|
||||
parser = argparse.ArgumentParser(description="Generate animal avatars")
|
||||
parser.add_argument("--animal", help="Animal type", choices=AnimalAvatarGenerator.ANIMALS)
|
||||
parser.add_argument("--color-palette", help="Color palette", default="natural",
|
||||
choices=["natural", "pastel", "vibrant", "mono"])
|
||||
parser.add_argument("--face-shape", help="Face shape", choices=AnimalAvatarGenerator.FACE_SHAPES)
|
||||
parser.add_argument("--eye-style", help="Eye style", choices=AnimalAvatarGenerator.EYE_STYLES)
|
||||
parser.add_argument("--ear-style", help="Ear style")
|
||||
parser.add_argument("--nose-style", help="Nose style", choices=AnimalAvatarGenerator.NOSE_STYLES)
|
||||
parser.add_argument("--expression", help="Expression", choices=AnimalAvatarGenerator.EXPRESSIONS)
|
||||
parser.add_argument("--special-feature", help="Special feature")
|
||||
parser.add_argument("--size", help="Size in pixels", type=int, default=500)
|
||||
parser.add_argument("--seed", help="Random seed for reproducibility", type=int)
|
||||
parser.add_argument("--output", help="Output file path", default="avatar.svg")
|
||||
parser.add_argument("--list-options", help="List all available options", action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list_options:
|
||||
options = list_avatar_options()
|
||||
print(json.dumps(options, indent=2))
|
||||
return
|
||||
|
||||
generator = AnimalAvatarGenerator(seed=args.seed)
|
||||
|
||||
svg = generator.generate_avatar(
|
||||
animal=args.animal,
|
||||
color_palette=args.color_palette,
|
||||
face_shape=args.face_shape,
|
||||
eye_style=args.eye_style,
|
||||
ear_style=args.ear_style,
|
||||
nose_style=args.nose_style,
|
||||
expression=args.expression,
|
||||
special_feature=args.special_feature,
|
||||
size=args.size
|
||||
)
|
||||
|
||||
with open(args.output, "w") as f:
|
||||
f.write(svg)
|
||||
|
||||
print(f"Avatar saved to {args.output}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_avatar_app()
|
@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import mimetypes
|
||||
from os.path import isfile
|
||||
|
||||
from PIL import Image
|
||||
import pillow_heif.HeifImagePlugin
|
||||
@ -18,9 +17,6 @@ class ChannelAttachmentView(BaseView):
|
||||
relative_url=relative_path
|
||||
)
|
||||
|
||||
if not channel_attachment or not isfile(channel_attachment["path"]):
|
||||
return web.HTTPNotFound()
|
||||
|
||||
original_format = mimetypes.guess_type(channel_attachment["path"])[0]
|
||||
format_ = self.request.query.get("format")
|
||||
width = self.request.query.get("width")
|
||||
@ -79,7 +75,7 @@ class ChannelAttachmentView(BaseView):
|
||||
|
||||
setattr(response, "write", sync_writer)
|
||||
|
||||
image.save(response, format=format_, quality=100, optimize=True, save_all=True)
|
||||
image.save(response, format=format_)
|
||||
|
||||
setattr(response, "write", naughty_steal)
|
||||
|
||||
@ -92,7 +88,6 @@ class ChannelAttachmentView(BaseView):
|
||||
response.headers["Content-Disposition"] = (
|
||||
f'attachment; filename="{channel_attachment["name"]}"'
|
||||
)
|
||||
response.headers["Content-Type"] = original_format
|
||||
return response
|
||||
|
||||
async def post(self):
|
||||
|
@ -36,11 +36,9 @@ 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,color=None):
|
||||
async def set_typing(self,channel_uid):
|
||||
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",
|
||||
@ -49,8 +47,7 @@ class RPCView(BaseView):
|
||||
"user_uid": user['uid'],
|
||||
"username": user["username"],
|
||||
"nick": user["nick"],
|
||||
"channel_uid": channel_uid,
|
||||
"color": color
|
||||
"channel_uid": channel_uid
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1,91 +0,0 @@
|
||||
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")
|
Loading…
Reference in New Issue
Block a user