Merge branch 'main' into bugfix/youtube-embed
This commit is contained in:
commit
d261f54327
gitlog.jsonlgitlog.py
src/snek
992
gitlog.jsonl
Normal file
992
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,42 @@
|
||||
/* each star */
|
||||
.star {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
/* flicker animation */
|
||||
animation: twinkle ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
.star {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: var(--star-color, #fff);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: background 0.5s ease;
|
||||
animation: twinkle ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* optional page content */
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #eee;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
top: 40%;
|
||||
transform: translateY(-40%);
|
||||
}
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes star-glow-frames {
|
||||
0% {
|
||||
box-shadow: 0 0 5px --star-color;
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px --star-color, 0 0 30px --star-color;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 5px --star-color;
|
||||
}
|
||||
}
|
||||
|
||||
.star-glow {
|
||||
animation: star-glow-frames 1s;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--star-content-color, #eee);
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
top: 40%;
|
||||
transform: translateY(-40%);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -1,31 +1,115 @@
|
||||
|
||||
<script>
|
||||
|
||||
// number of stars you want
|
||||
const STAR_COUNT = 200;
|
||||
const body = document.body;
|
||||
<script type="module">
|
||||
import { app } from "/app.js";
|
||||
|
||||
for (let i = 0; i < STAR_COUNT; i++) {
|
||||
const star = document.createElement('div');
|
||||
star.classList.add('star');
|
||||
const STAR_COUNT = 200;
|
||||
const body = document.body;
|
||||
|
||||
// random position within the viewport
|
||||
star.style.left = Math.random() * 100 + '%';
|
||||
star.style.top = Math.random() * 100 + '%';
|
||||
function createStar() {
|
||||
const star = document.createElement('div');
|
||||
star.classList.add('star');
|
||||
star.style.left = `${Math.random() * 100}%`;
|
||||
star.style.top = `${Math.random() * 100}%`;
|
||||
const size = Math.random() * 2 + 1;
|
||||
star.style.width = `${size}px`;
|
||||
star.style.height = `${size}px`;
|
||||
const duration = Math.random() * 3 + 2;
|
||||
const delay = Math.random() * 5;
|
||||
star.style.animationDuration = `${duration}s`;
|
||||
star.style.animationDelay = `${delay}s`;
|
||||
body.appendChild(star);
|
||||
}
|
||||
|
||||
// random size (optional)
|
||||
const size = Math.random() * 2 + 1; // between 1px and 3px
|
||||
star.style.width = size + 'px';
|
||||
star.style.height = size + 'px';
|
||||
Array.from({ length: STAR_COUNT }, createStar);
|
||||
|
||||
// random animation timing for natural flicker
|
||||
const duration = Math.random() * 3 + 2; // 2s–5s
|
||||
const delay = Math.random() * 5; // 0s–5s
|
||||
star.style.animationDuration = duration + 's';
|
||||
star.style.animationDelay = delay + 's';
|
||||
function lightenColor(hex, percent) {
|
||||
const num = parseInt(hex.replace("#", ""), 16);
|
||||
let r = (num >> 16) + Math.round(255 * percent / 100);
|
||||
let g = ((num >> 8) & 0x00FF) + Math.round(255 * percent / 100);
|
||||
let b = (num & 0x0000FF) + Math.round(255 * percent / 100);
|
||||
r = Math.min(255, r);
|
||||
g = Math.min(255, g);
|
||||
b = Math.min(255, b);
|
||||
return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}`;
|
||||
}
|
||||
|
||||
body.appendChild(star);
|
||||
const originalColor = document.documentElement.style.getPropertyValue("--star-color").trim();
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
/*
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
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 %}
|
@ -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