Merge branch 'main' into bugfix/youtube-embed
This commit is contained in:
		
						commit
						d261f54327
					
				
							
								
								
									
										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,12 +1,12 @@ | ||||
| /* each star */ | ||||
| 
 | ||||
| .star { | ||||
|   position: absolute; | ||||
|   width: 2px; | ||||
|   height: 2px; | ||||
|       background: #fff; | ||||
|   background: var(--star-color, #fff); | ||||
|   border-radius: 50%; | ||||
|   opacity: 0; | ||||
|       /* flicker animation */ | ||||
|   transition: background 0.5s ease; | ||||
|   animation: twinkle ease-in-out infinite; | ||||
| } | ||||
| 
 | ||||
| @ -15,14 +15,28 @@ | ||||
|   50%      { opacity: 1; } | ||||
| } | ||||
| 
 | ||||
|     /* optional page content */ | ||||
| @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: #eee; | ||||
|   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> | ||||
| <script type="module"> | ||||
| import { app } from "/app.js"; | ||||
| 
 | ||||
|     // number of stars you want | ||||
| const STAR_COUNT = 200; | ||||
| const body = document.body; | ||||
| 
 | ||||
|     for (let i = 0; i < STAR_COUNT; i++) { | ||||
| function createStar() { | ||||
|   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'; | ||||
| 
 | ||||
|   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); | ||||
| } | ||||
| 
 | ||||
| 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); | ||||
| }); | ||||
| 
 | ||||
| /* | ||||
| 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 %} | ||||
| @ -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