Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
c322d6147a | |||
87b6b3362d | |||
d261f54327 | |||
59a815f85a | |||
2e837f96c5 | |||
00fce6bd68 | |||
8a85cd7990 | |||
db5431d77d | |||
527b010b24 | |||
e1727caa5f | |||
c45b61681d | |||
e09652413f | |||
![]() |
0f337e569f | ||
59a2668c8c | |||
e79abf4a26 | |||
![]() |
53811ca9b2 | ||
![]() |
1bed47fbf5 | ||
![]() |
ffb22165da |
gitlog.jsonlgitlog.py
src/snek
app.py
mapper
model
service
static
system
templates
view
1489
gitlog.jsonl
Normal file
1489
gitlog.jsonl
Normal file
File diff suppressed because one or more lines are too long
289
gitlog.py
Normal file
289
gitlog.py
Normal 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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
}
|
||||
)
|
||||
|
||||
|
6
src/snek/mapper/container.py
Normal file
6
src/snek/mapper/container.py
Normal 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"
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
10
src/snek/model/container.py
Normal file
10
src/snek/model/container.py
Normal 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)
|
@ -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),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
29
src/snek/service/container.py
Normal file
29
src/snek/service/container.py
Normal 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)
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
42
src/snek/static/sandbox.css
Normal file
42
src/snek/static/sandbox.css
Normal 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%);
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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>© 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>
|
||||
|
504
src/snek/templates/sandbox.html
Normal file
504
src/snek/templates/sandbox.html
Normal 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>
|
39
src/snek/templates/settings/containers/create.html
Normal file
39
src/snek/templates/settings/containers/create.html
Normal 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 %}
|
17
src/snek/templates/settings/containers/delete.html
Normal file
17
src/snek/templates/settings/containers/delete.html
Normal 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 %}
|
28
src/snek/templates/settings/containers/form.html
Normal file
28
src/snek/templates/settings/containers/form.html
Normal 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>
|
||||
|
||||
|
96
src/snek/templates/settings/containers/index.html
Normal file
96
src/snek/templates/settings/containers/index.html
Normal 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 %}
|
40
src/snek/templates/settings/containers/update.html
Normal file
40
src/snek/templates/settings/containers/update.html
Normal 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 %}
|
@ -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())
|
||||
|
871
src/snek/view/avatar_animal.py
Normal file
871
src/snek/view/avatar_animal.py
Normal 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()
|
@ -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):
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
91
src/snek/view/settings/containers.py
Normal file
91
src/snek/view/settings/containers.py
Normal 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")
|
Loading…
Reference in New Issue
Block a user