Compare commits

...

18 Commits
main ... main

Author SHA1 Message Date
c322d6147a Merge pull request 'bugfix/youtube-embed' () from BordedDev/snek:bugfix/youtube-embed into main
Reviewed-on: 
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-05-20 22:02:16 +02:00
87b6b3362d New avatar. 2025-05-20 04:20:10 +02:00
d261f54327 Merge branch 'main' into bugfix/youtube-embed 2025-05-20 03:34:05 +02:00
59a815f85a Update. 2025-05-19 01:35:34 +02:00
2e837f96c5 Update. 2025-05-19 01:28:22 +02:00
00fce6bd68 Update. 2025-05-19 01:23:12 +02:00
8a85cd7990 Update. 2025-05-19 01:13:18 +02:00
db5431d77d Update. 2025-05-19 01:07:17 +02:00
527b010b24 Added containers. 2025-05-19 01:07:17 +02:00
e1727caa5f Update. 2025-05-18 17:48:22 +02:00
c45b61681d Update. 2025-05-18 16:57:14 +02:00
e09652413f Added nice repo system. 2025-05-18 16:55:02 +02:00
BordedDev
0f337e569f
Fixed gif resizing 2025-05-18 14:51:38 +02:00
59a2668c8c Merge branch 'main' into bugfix/youtube-embed 2025-05-18 03:26:53 +02:00
e79abf4a26 Update stars. 2025-05-17 17:46:59 +02:00
BordedDev
53811ca9b2
Re-added webp fallback 2025-05-17 13:55:46 +02:00
BordedDev
1bed47fbf5
Re-added webp fallback 2025-05-17 13:29:25 +02:00
BordedDev
ffb22165da
Fix YouTube embed parsing and add support for start time; handle missing channel attachments 2025-05-17 13:23:32 +02:00
27 changed files with 3712 additions and 35 deletions

1489
gitlog.jsonl Normal file

File diff suppressed because one or more lines are too long

289
gitlog.py Normal file
View File

@ -0,0 +1,289 @@
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"] = urllib.parse.quote(f"{attachment['uid']}/{name}")
attachment["relative_url"] = (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)

View File

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

View File

@ -53,7 +53,8 @@ class ChatInputComponent extends HTMLElement {
this.textarea.focus();
}
connectedCallback() {
async connectedCallback() {
this.user = await app.rpc.getUser(null);
this.liveType = this.getAttribute("live-type") === "true";
this.liveTypeInterval = parseInt(this.getAttribute("live-type-interval")) || 3;
this.channelUid = this.getAttribute("channel");
@ -193,7 +194,7 @@ class ChatInputComponent extends HTMLElement {
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {
this.lastUpdateEvent = new Date();
if (typeof app !== "undefined" && app.rpc && typeof app.rpc.set_typing === "function") {
app.rpc.set_typing(this.channelUid);
app.rpc.set_typing(this.channelUid,this.user.color);
}
}
}

View File

@ -0,0 +1,42 @@
.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%);
}

View File

@ -1,4 +1,5 @@
import re
from urllib.parse import urlparse, parse_qs
from types import SimpleNamespace
import mimetypes
@ -90,16 +91,59 @@ def set_link_target_blank(text):
def embed_youtube(text):
soup = BeautifulSoup(text, "html.parser")
for element in soup.find_all("a"):
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>'
# 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>'
element.replace_with(BeautifulSoup(embed_template, "html.parser"))
return str(soup)
@ -108,35 +152,41 @@ 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"]}" type="image/webp" />
<img src="{element.attrs["src"]}" title="{element.attrs["src"]}" alt="{element.attrs["src"]}" />
<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"]}" />
</picture>'''
element.replace_with(BeautifulSoup(picture_template, "html.parser"))
return str(soup)
@ -245,7 +295,6 @@ class PythonExtension(Extension):
).set_lineno(line_number)
def _to_html(self, md_file, caller):
def fn(source):
import subprocess

View File

@ -19,6 +19,7 @@
<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">
@ -78,5 +79,6 @@ let installPrompt = null
;
</script>
{% include "sandbox.html" %}
</body>
</html>

View File

@ -3,7 +3,7 @@
{% block header_text %}Drive{% endblock %}
{% block main %}
<div class="container">
<div class="container" style="overflow-y: auto;">
<file-manager path="{{path}}" style="flex: 1"></file-manager>
</div>
{% endblock %}

View File

@ -14,7 +14,7 @@
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: 'Segoe UI',sans-serif;
background: #111;
background: #000;
color: #eee;
line-height:1.5;
}
@ -187,6 +187,7 @@
.btn { width: 100%; box-sizing: border-box; text-align:center; }
}
</style>
<link rel="stylesheet" href="/static/sandbox.css" />
</head>
<body>
@ -284,5 +285,35 @@ snek serve
<p>&copy; 2025 Snek Join our global community of developers, testers &amp; 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; // 2s5s
const delay = Math.random() * 5; // 0s5s
star.style.animationDuration = duration + 's';
star.style.animationDelay = delay + 's';
body.appendChild(star);
}
</script>
</body>
</html>

View File

@ -0,0 +1,504 @@
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,12 +29,35 @@ 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())

View File

@ -0,0 +1,871 @@
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()

View File

@ -1,5 +1,6 @@
import asyncio
import mimetypes
from os.path import isfile
from PIL import Image
import pillow_heif.HeifImagePlugin
@ -17,6 +18,9 @@ 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")
@ -75,7 +79,7 @@ class ChannelAttachmentView(BaseView):
setattr(response, "write", sync_writer)
image.save(response, format=format_)
image.save(response, format=format_, quality=100, optimize=True, save_all=True)
setattr(response, "write", naughty_steal)
@ -88,6 +92,7 @@ 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):

View File

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

View File

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