...
This commit is contained in:
		
							
								
								
									
										206
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,206 @@ | ||||
| # Markdown Editor | ||||
|  | ||||
| A full-featured markdown editor with live preview, built with modern web technologies. | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - **Real-time Preview**: See your markdown rendered as you type | ||||
| - **Syntax Highlighting**: Powered by CodeMirror with support for multiple languages | ||||
| - **File Management**: Create, save, load, and delete markdown files | ||||
| - **Mermaid Diagrams**: Render beautiful diagrams directly in your markdown | ||||
| - **HTML Support**: Embed HTML snippets in your markdown | ||||
| - **Synchronized Scrolling**: Editor and preview scroll together | ||||
| - **Responsive Design**: Works on desktop and mobile devices | ||||
|  | ||||
| ## Supported Languages | ||||
|  | ||||
| The editor provides syntax highlighting for: | ||||
|  | ||||
| - Markdown | ||||
| - JavaScript | ||||
| - Python | ||||
| - HTML/CSS | ||||
| - Go | ||||
| - Rust | ||||
| - JSON | ||||
| - TOML | ||||
| - YAML | ||||
| - And more! | ||||
|  | ||||
| ## Technology Stack | ||||
|  | ||||
| ### Frontend | ||||
| - **Bootstrap 5**: Modern, responsive UI framework | ||||
| - **CodeMirror 5**: Powerful code editor component | ||||
| - **Marked.js**: Fast markdown parser | ||||
| - **Mermaid**: Diagram and flowchart rendering | ||||
| - **Unpoly**: Progressive enhancement library | ||||
|  | ||||
| ### Backend | ||||
| - **FastAPI**: Modern Python web framework | ||||
| - **Uvicorn**: ASGI server | ||||
| - **uv (Astral)**: Fast Python package installer and environment manager | ||||
|  | ||||
| ## Installation & Usage | ||||
|  | ||||
| ### Quick Start | ||||
|  | ||||
| 1. Navigate to the project directory: | ||||
| ```bash | ||||
| cd markdown-editor | ||||
| ``` | ||||
|  | ||||
| 2. Run the startup script: | ||||
| ```bash | ||||
| ./start.sh | ||||
| ``` | ||||
|  | ||||
| The script will: | ||||
| - Install uv if not present | ||||
| - Create a virtual environment | ||||
| - Install all Python dependencies | ||||
| - Create a sample markdown file | ||||
| - Start the development server | ||||
|  | ||||
| 3. Open your browser and navigate to: | ||||
| ``` | ||||
| http://localhost:8000 | ||||
| ``` | ||||
|  | ||||
| ### Manual Installation | ||||
|  | ||||
| If you prefer to install manually: | ||||
|  | ||||
| ```bash | ||||
| # Install dependencies with uv | ||||
| uv venv | ||||
| uv pip install -e . | ||||
|  | ||||
| # Start the server | ||||
| uv run uvicorn server:app --host 0.0.0.0 --port 8000 --reload | ||||
| ``` | ||||
|  | ||||
| ## Project Structure | ||||
|  | ||||
| ``` | ||||
| markdown-editor/ | ||||
| ├── data/                  # Markdown files storage | ||||
| ├── static/               # Static assets | ||||
| │   ├── app.js           # Main JavaScript application | ||||
| │   └── style.css        # Custom styles | ||||
| ├── templates/           # HTML templates | ||||
| │   └── index.html       # Main editor page | ||||
| ├── server.py            # FastAPI backend server | ||||
| ├── pyproject.toml       # Python project configuration | ||||
| ├── start.sh             # Installation and startup script | ||||
| └── README.md            # This file | ||||
| ``` | ||||
|  | ||||
| ## API Endpoints | ||||
|  | ||||
| The backend provides the following REST API endpoints: | ||||
|  | ||||
| - `GET /` - Serve the main editor page | ||||
| - `GET /api/files` - List all markdown files | ||||
| - `GET /api/files/{filename}` - Get content of a specific file | ||||
| - `POST /api/files` - Create or update a file | ||||
| - `DELETE /api/files/{filename}` - Delete a file | ||||
|  | ||||
| ## Usage Guide | ||||
|  | ||||
| ### Creating a New File | ||||
|  | ||||
| 1. Click the **New** button in the navbar | ||||
| 2. Enter a filename in the input field (e.g., `my-document.md`) | ||||
| 3. Start typing your markdown content | ||||
| 4. Click **Save** to save the file | ||||
|  | ||||
| ### Loading an Existing File | ||||
|  | ||||
| - Click on any file in the sidebar to load it into the editor | ||||
|  | ||||
| ### Saving Changes | ||||
|  | ||||
| - Click the **Save** button or press `Ctrl+S` (Windows/Linux) or `Cmd+S` (Mac) | ||||
|  | ||||
| ### Deleting a File | ||||
|  | ||||
| 1. Load the file you want to delete | ||||
| 2. Click the **Delete** button | ||||
| 3. Confirm the deletion | ||||
|  | ||||
| ### Using Mermaid Diagrams | ||||
|  | ||||
| Create a code block with the `mermaid` language identifier: | ||||
|  | ||||
| ````markdown | ||||
| ```mermaid | ||||
| graph TD | ||||
|     A[Start] --> B[Process] | ||||
|     B --> C[End] | ||||
| ``` | ||||
| ```` | ||||
|  | ||||
| ### Embedding HTML | ||||
|  | ||||
| You can embed HTML directly in your markdown: | ||||
|  | ||||
| ```markdown | ||||
| <div style="color: red;"> | ||||
|     This is red text! | ||||
| </div> | ||||
| ``` | ||||
|  | ||||
| ## Keyboard Shortcuts | ||||
|  | ||||
| - `Ctrl+S` / `Cmd+S` - Save current file | ||||
|  | ||||
| ## Development | ||||
|  | ||||
| ### Requirements | ||||
|  | ||||
| - Python 3.11+ | ||||
| - uv (Astral) - Will be installed automatically by start.sh | ||||
|  | ||||
| ### Running in Development Mode | ||||
|  | ||||
| The server runs with auto-reload enabled by default, so any changes to the Python code will automatically restart the server. | ||||
|  | ||||
| ### Adding New Features | ||||
|  | ||||
| - Frontend code: Edit `static/app.js` and `static/style.css` | ||||
| - Backend API: Edit `server.py` | ||||
| - UI layout: Edit `templates/index.html` | ||||
|  | ||||
| ## Troubleshooting | ||||
|  | ||||
| ### Port Already in Use | ||||
|  | ||||
| If port 8000 is already in use, you can change it in `start.sh` or run manually: | ||||
|  | ||||
| ```bash | ||||
| uv run uvicorn server:app --host 0.0.0.0 --port 8080 --reload | ||||
| ``` | ||||
|  | ||||
| ### Dependencies Not Installing | ||||
|  | ||||
| Make sure uv is properly installed: | ||||
|  | ||||
| ```bash | ||||
| curl -LsSf https://astral.sh/uv/install.sh | sh | ||||
| ``` | ||||
|  | ||||
| Then try running `./start.sh` again. | ||||
|  | ||||
| ## License | ||||
|  | ||||
| MIT License - Feel free to use this project for any purpose. | ||||
|  | ||||
| ## Contributing | ||||
|  | ||||
| Contributions are welcome! Feel free to submit issues or pull requests. | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Happy writing!** 📝✨ | ||||
|  | ||||
							
								
								
									
										58
									
								
								collections/documents/images/welcome2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								collections/documents/images/welcome2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| # Welcome to Markdown Editor | ||||
|  | ||||
| This is a **WebDAV-based** markdown editor with modular architecture. | ||||
|  | ||||
| ```mermaid | ||||
| %%{init: {'theme':'dark'}}%% | ||||
| graph TD | ||||
|  | ||||
|     %% User side | ||||
|     H1[Human A] --> PA1[Personal Agent A] | ||||
|     H2[Human B] --> PA2[Personal Agent B] | ||||
|  | ||||
|     %% Local mail nodes | ||||
|     PA1 --> M1[MyMail Node A] | ||||
|     PA2 --> M2[MyMail Node B] | ||||
|  | ||||
|     %% Proxy coordination layer | ||||
|     M1 --> Proxy1A[Proxy Agent L1] | ||||
|     Proxy1A --> Proxy2A[Proxy Agent L2] | ||||
|     Proxy2A --> Proxy2B[Proxy Agent L2] | ||||
|     Proxy2B --> Proxy1B[Proxy Agent L1] | ||||
|     Proxy1B --> M2 | ||||
|  | ||||
|     %% Blockchain anchoring | ||||
|     M1 --> Chain[Dynamic Blockchain] | ||||
|     M2 --> Chain | ||||
| ``` | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - ✅ Standards-compliant WebDAV backend | ||||
| - ✅ Multiple document collections | ||||
| - ✅ Modular JavaScript/CSS | ||||
| - ✅ Live preview | ||||
| - ✅ Syntax highlighting | ||||
| - ✅ Mermaid diagrams | ||||
| - ✅ Dark mode | ||||
|  | ||||
| ## WebDAV Methods | ||||
|  | ||||
| This editor uses standard WebDAV methods: | ||||
|  | ||||
| - `PROPFIND` - List files | ||||
| - `GET` - Read files | ||||
| - `PUT` - Create/update files | ||||
| - `DELETE` - Delete files | ||||
| - `COPY` - Copy files | ||||
| - `MOVE` - Move/rename files | ||||
| - `MKCOL` - Create directories | ||||
|  | ||||
| ## Try It Out | ||||
|  | ||||
| 1. Create a new file | ||||
| 2. Edit markdown | ||||
| 3. See live preview | ||||
| 4. Save with WebDAV PUT | ||||
|  | ||||
| Enjoy! | ||||
							
								
								
									
										58
									
								
								collections/documents/welcome2.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								collections/documents/welcome2.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| # Welcome to Markdown Editor | ||||
|  | ||||
| This is a **WebDAV-based** markdown editor with modular architecture. | ||||
|  | ||||
| ```mermaid | ||||
| %%{init: {'theme':'dark'}}%% | ||||
| graph TD | ||||
|  | ||||
|     %% User side | ||||
|     H1[Human A] --> PA1[Personal Agent A] | ||||
|     H2[Human B] --> PA2[Personal Agent B] | ||||
|  | ||||
|     %% Local mail nodes | ||||
|     PA1 --> M1[MyMail Node A] | ||||
|     PA2 --> M2[MyMail Node B] | ||||
|  | ||||
|     %% Proxy coordination layer | ||||
|     M1 --> Proxy1A[Proxy Agent L1] | ||||
|     Proxy1A --> Proxy2A[Proxy Agent L2] | ||||
|     Proxy2A --> Proxy2B[Proxy Agent L2] | ||||
|     Proxy2B --> Proxy1B[Proxy Agent L1] | ||||
|     Proxy1B --> M2 | ||||
|  | ||||
|     %% Blockchain anchoring | ||||
|     M1 --> Chain[Dynamic Blockchain] | ||||
|     M2 --> Chain | ||||
| ``` | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - ✅ Standards-compliant WebDAV backend | ||||
| - ✅ Multiple document collections | ||||
| - ✅ Modular JavaScript/CSS | ||||
| - ✅ Live preview | ||||
| - ✅ Syntax highlighting | ||||
| - ✅ Mermaid diagrams | ||||
| - ✅ Dark mode | ||||
|  | ||||
| ## WebDAV Methods | ||||
|  | ||||
| This editor uses standard WebDAV methods: | ||||
|  | ||||
| - `PROPFIND` - List files | ||||
| - `GET` - Read files | ||||
| - `PUT` - Create/update files | ||||
| - `DELETE` - Delete files | ||||
| - `COPY` - Copy files | ||||
| - `MOVE` - Move/rename files | ||||
| - `MKCOL` - Create directories | ||||
|  | ||||
| ## Try It Out | ||||
|  | ||||
| 1. Create a new file | ||||
| 2. Edit markdown | ||||
| 3. See live preview | ||||
| 4. Save with WebDAV PUT | ||||
|  | ||||
| Enjoy! | ||||
							
								
								
									
										4
									
								
								collections/notes/ttt/test.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								collections/notes/ttt/test.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
|  | ||||
| test | ||||
|  | ||||
|  | ||||
							
								
								
									
										27
									
								
								config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								config.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| # WsgiDAV Configuration | ||||
| # Collections define WebDAV-accessible directories | ||||
|  | ||||
| collections: | ||||
|   documents: | ||||
|     path: "./collections/documents" | ||||
|     description: "General documents and notes" | ||||
|  | ||||
|   notes: | ||||
|     path: "./collections/notes" | ||||
|     description: "Personal notes and drafts" | ||||
|  | ||||
|   projects: | ||||
|     path: "./collections/projects" | ||||
|     description: "Project documentation" | ||||
|  | ||||
| # Server settings | ||||
| server: | ||||
|   host: "0.0.0.0" | ||||
|   port: 8004 | ||||
|  | ||||
| # WebDAV settings | ||||
| webdav: | ||||
|   verbose: 1 | ||||
|   enable_loggers: [] | ||||
|   property_manager: true | ||||
|   lock_manager: true | ||||
							
								
								
									
										18
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| [project] | ||||
| name = "markdown-editor" | ||||
| version = "3.0.0" | ||||
| description = "WebDAV-based Markdown Editor with modular architecture" | ||||
| requires-python = ">=3.11" | ||||
| dependencies = [ | ||||
|     "wsgidav>=4.3.0", | ||||
|     "cheroot>=10.0.0", | ||||
|     "pyyaml>=6.0", | ||||
| ] | ||||
|  | ||||
| [build-system] | ||||
| requires = ["hatchling"] | ||||
| build-backend = "hatchling.build" | ||||
|  | ||||
| [tool.hatch.build.targets.wheel] | ||||
| packages = [] | ||||
|  | ||||
							
								
								
									
										197
									
								
								server_webdav.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										197
									
								
								server_webdav.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,197 @@ | ||||
| #!/usr/bin/env python3 | ||||
| """ | ||||
| WebDAV-based Markdown Editor Server | ||||
| Uses WsgiDAV for standards-compliant file operations | ||||
| """ | ||||
|  | ||||
| import os | ||||
| import sys | ||||
| import yaml | ||||
| import json | ||||
| from pathlib import Path | ||||
| from wsgidav.wsgidav_app import WsgiDAVApp | ||||
| from wsgidav.fs_dav_provider import FilesystemProvider | ||||
| from cheroot import wsgi | ||||
| from cheroot.ssl.builtin import BuiltinSSLAdapter | ||||
|  | ||||
|  | ||||
| class MarkdownEditorApp: | ||||
|     """Main application that wraps WsgiDAV and adds custom endpoints""" | ||||
|      | ||||
|     def __init__(self, config_path="config.yaml"): | ||||
|         self.config = self.load_config(config_path) | ||||
|         self.collections = self.config.get('collections', {}) | ||||
|         self.setup_collections() | ||||
|         self.webdav_app = self.create_webdav_app() | ||||
|      | ||||
|     def load_config(self, config_path): | ||||
|         """Load configuration from YAML file""" | ||||
|         with open(config_path, 'r') as f: | ||||
|             return yaml.safe_load(f) | ||||
|      | ||||
|     def setup_collections(self): | ||||
|         """Create collection directories if they don't exist""" | ||||
|         for name, config in self.collections.items(): | ||||
|             path = Path(config['path']) | ||||
|             path.mkdir(parents=True, exist_ok=True) | ||||
|              | ||||
|             # Create images subdirectory | ||||
|             images_path = path / 'images' | ||||
|             images_path.mkdir(exist_ok=True) | ||||
|              | ||||
|             print(f"Collection '{name}' -> {path.absolute()}") | ||||
|      | ||||
|     def create_webdav_app(self): | ||||
|         """Create WsgiDAV application with configured collections""" | ||||
|         provider_mapping = {} | ||||
|          | ||||
|         for name, config in self.collections.items(): | ||||
|             path = os.path.abspath(config['path']) | ||||
|             provider_mapping[f'/fs/{name}'] = FilesystemProvider(path) | ||||
|          | ||||
|         config = { | ||||
|             'host': self.config['server']['host'], | ||||
|             'port': self.config['server']['port'], | ||||
|             'provider_mapping': provider_mapping, | ||||
|             'verbose': self.config['webdav'].get('verbose', 1), | ||||
|             'logging': { | ||||
|                 'enable_loggers': [] | ||||
|             }, | ||||
|             'property_manager': True, | ||||
|             'lock_storage': True, | ||||
|             'simple_dc': { | ||||
|                 'user_mapping': { | ||||
|                     '*': True  # Allow anonymous access for development | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         return WsgiDAVApp(config) | ||||
|      | ||||
|     def __call__(self, environ, start_response): | ||||
|         """WSGI application entry point""" | ||||
|         path = environ.get('PATH_INFO', '') | ||||
|         method = environ.get('REQUEST_METHOD', '') | ||||
|          | ||||
|         # Handle collection list endpoint | ||||
|         if path == '/fs/' and method == 'GET': | ||||
|             return self.handle_collections_list(environ, start_response) | ||||
|          | ||||
|         # Handle static files | ||||
|         if path.startswith('/static/'): | ||||
|             return self.handle_static(environ, start_response) | ||||
|          | ||||
|         # Handle root - serve index.html | ||||
|         if path == '/' or path == '/index.html': | ||||
|             return self.handle_index(environ, start_response) | ||||
|          | ||||
|         # All other requests go to WebDAV | ||||
|         return self.webdav_app(environ, start_response) | ||||
|      | ||||
|     def handle_collections_list(self, environ, start_response): | ||||
|         """Return list of available collections""" | ||||
|         collections = list(self.collections.keys()) | ||||
|         response_body = json.dumps(collections).encode('utf-8') | ||||
|          | ||||
|         start_response('200 OK', [ | ||||
|             ('Content-Type', 'application/json'), | ||||
|             ('Content-Length', str(len(response_body))), | ||||
|             ('Access-Control-Allow-Origin', '*') | ||||
|         ]) | ||||
|          | ||||
|         return [response_body] | ||||
|      | ||||
|     def handle_static(self, environ, start_response): | ||||
|         """Serve static files""" | ||||
|         path = environ.get('PATH_INFO', '')[1:]  # Remove leading / | ||||
|         file_path = Path(path) | ||||
|          | ||||
|         if not file_path.exists() or not file_path.is_file(): | ||||
|             start_response('404 Not Found', [('Content-Type', 'text/plain')]) | ||||
|             return [b'File not found'] | ||||
|          | ||||
|         # Determine content type | ||||
|         content_types = { | ||||
|             '.html': 'text/html', | ||||
|             '.css': 'text/css', | ||||
|             '.js': 'application/javascript', | ||||
|             '.json': 'application/json', | ||||
|             '.png': 'image/png', | ||||
|             '.jpg': 'image/jpeg', | ||||
|             '.jpeg': 'image/jpeg', | ||||
|             '.gif': 'image/gif', | ||||
|             '.svg': 'image/svg+xml', | ||||
|             '.ico': 'image/x-icon' | ||||
|         } | ||||
|          | ||||
|         ext = file_path.suffix.lower() | ||||
|         content_type = content_types.get(ext, 'application/octet-stream') | ||||
|          | ||||
|         with open(file_path, 'rb') as f: | ||||
|             content = f.read() | ||||
|          | ||||
|         start_response('200 OK', [ | ||||
|             ('Content-Type', content_type), | ||||
|             ('Content-Length', str(len(content))) | ||||
|         ]) | ||||
|          | ||||
|         return [content] | ||||
|      | ||||
|     def handle_index(self, environ, start_response): | ||||
|         """Serve index.html""" | ||||
|         index_path = Path('templates/index.html') | ||||
|          | ||||
|         if not index_path.exists(): | ||||
|             start_response('404 Not Found', [('Content-Type', 'text/plain')]) | ||||
|             return [b'index.html not found'] | ||||
|          | ||||
|         with open(index_path, 'r', encoding='utf-8') as f: | ||||
|             content = f.read().encode('utf-8') | ||||
|          | ||||
|         start_response('200 OK', [ | ||||
|             ('Content-Type', 'text/html; charset=utf-8'), | ||||
|             ('Content-Length', str(len(content))) | ||||
|         ]) | ||||
|          | ||||
|         return [content] | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     """Start the server""" | ||||
|     print("=" * 60) | ||||
|     print("Markdown Editor with WebDAV Backend") | ||||
|     print("=" * 60) | ||||
|      | ||||
|     # Create application | ||||
|     app = MarkdownEditorApp() | ||||
|      | ||||
|     # Get server config | ||||
|     host = app.config['server']['host'] | ||||
|     port = app.config['server']['port'] | ||||
|      | ||||
|     print(f"\nServer starting on http://{host}:{port}") | ||||
|     print(f"\nAvailable collections:") | ||||
|     for name, config in app.collections.items(): | ||||
|         print(f"  - {name}: {config['description']}") | ||||
|         print(f"    WebDAV: http://{host}:{port}/fs/{name}/") | ||||
|      | ||||
|     print(f"\nWeb UI: http://{host}:{port}/") | ||||
|     print("\nPress Ctrl+C to stop the server") | ||||
|     print("=" * 60) | ||||
|      | ||||
|     # Create and start server | ||||
|     server = wsgi.Server( | ||||
|         bind_addr=(host, port), | ||||
|         wsgi_app=app | ||||
|     ) | ||||
|      | ||||
|     try: | ||||
|         server.start() | ||||
|     except KeyboardInterrupt: | ||||
|         print("\n\nShutting down...") | ||||
|         server.stop() | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
|  | ||||
							
								
								
									
										20
									
								
								start.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										20
									
								
								start.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| #!/bin/bash | ||||
| set -e | ||||
| echo "==============================================" | ||||
| echo "Markdown Editor v3.0 - WebDAV Server" | ||||
| echo "==============================================" | ||||
| if ! command -v uv &> /dev/null; then | ||||
|     echo "Installing uv..." | ||||
|     curl -LsSf https://astral.sh/uv/install.sh | sh | ||||
|     export PATH="$HOME/.cargo/bin:$PATH" | ||||
| fi | ||||
| if [ ! -d ".venv" ]; then | ||||
|     echo "Creating virtual environment..." | ||||
|     uv venv | ||||
| fi | ||||
| echo "Activating virtual environment..." | ||||
| source .venv/bin/activate | ||||
| echo "Installing dependencies..." | ||||
| uv pip install wsgidav cheroot pyyaml | ||||
| echo "Starting WebDAV server..." | ||||
| python server_webdav.py | ||||
							
								
								
									
										866
									
								
								static/app-tree.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										866
									
								
								static/app-tree.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,866 @@ | ||||
| // Markdown Editor Application with File Tree | ||||
| (function() { | ||||
|     'use strict'; | ||||
|  | ||||
|     // State management | ||||
|     let currentFile = null; | ||||
|     let currentFilePath = null; | ||||
|     let editor = null; | ||||
|     let isScrollingSynced = true; | ||||
|     let scrollTimeout = null; | ||||
|     let isDarkMode = false; | ||||
|     let fileTree = []; | ||||
|     let contextMenuTarget = null; | ||||
|     let clipboard = null; // For copy/move operations | ||||
|  | ||||
|     // Dark mode management | ||||
|     function initDarkMode() { | ||||
|         const savedMode = localStorage.getItem('darkMode'); | ||||
|         if (savedMode === 'true') { | ||||
|             enableDarkMode(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function enableDarkMode() { | ||||
|         isDarkMode = true; | ||||
|         document.body.classList.add('dark-mode'); | ||||
|         document.getElementById('darkModeIcon').textContent = '☀️'; | ||||
|         localStorage.setItem('darkMode', 'true'); | ||||
|          | ||||
|         mermaid.initialize({  | ||||
|             startOnLoad: false, | ||||
|             theme: 'dark', | ||||
|             securityLevel: 'loose' | ||||
|         }); | ||||
|          | ||||
|         if (editor && editor.getValue()) { | ||||
|             updatePreview(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function disableDarkMode() { | ||||
|         isDarkMode = false; | ||||
|         document.body.classList.remove('dark-mode'); | ||||
|         document.getElementById('darkModeIcon').textContent = '🌙'; | ||||
|         localStorage.setItem('darkMode', 'false'); | ||||
|          | ||||
|         mermaid.initialize({  | ||||
|             startOnLoad: false, | ||||
|             theme: 'default', | ||||
|             securityLevel: 'loose' | ||||
|         }); | ||||
|          | ||||
|         if (editor && editor.getValue()) { | ||||
|             updatePreview(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function toggleDarkMode() { | ||||
|         if (isDarkMode) { | ||||
|             disableDarkMode(); | ||||
|         } else { | ||||
|             enableDarkMode(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Initialize Mermaid | ||||
|     mermaid.initialize({  | ||||
|         startOnLoad: false, | ||||
|         theme: 'default', | ||||
|         securityLevel: 'loose' | ||||
|     }); | ||||
|  | ||||
|     // Configure marked.js for markdown parsing | ||||
|     marked.setOptions({ | ||||
|         breaks: true, | ||||
|         gfm: true, | ||||
|         headerIds: true, | ||||
|         mangle: false, | ||||
|         sanitize: false, | ||||
|         smartLists: true, | ||||
|         smartypants: true, | ||||
|         xhtml: false | ||||
|     }); | ||||
|  | ||||
|     // Handle image upload | ||||
|     async function uploadImage(file) { | ||||
|         const formData = new FormData(); | ||||
|         formData.append('file', file); | ||||
|          | ||||
|         try { | ||||
|             const response = await fetch('/api/upload-image', { | ||||
|                 method: 'POST', | ||||
|                 body: formData | ||||
|             }); | ||||
|              | ||||
|             if (!response.ok) throw new Error('Upload failed'); | ||||
|              | ||||
|             const result = await response.json(); | ||||
|             return result.url; | ||||
|         } catch (error) { | ||||
|             console.error('Error uploading image:', error); | ||||
|             showNotification('Error uploading image', 'danger'); | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Handle drag and drop for images | ||||
|     function setupDragAndDrop() { | ||||
|         const editorElement = document.querySelector('.CodeMirror'); | ||||
|          | ||||
|         ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | ||||
|             editorElement.addEventListener(eventName, preventDefaults, false); | ||||
|         }); | ||||
|          | ||||
|         function preventDefaults(e) { | ||||
|             e.preventDefault(); | ||||
|             e.stopPropagation(); | ||||
|         } | ||||
|          | ||||
|         ['dragenter', 'dragover'].forEach(eventName => { | ||||
|             editorElement.addEventListener(eventName, () => { | ||||
|                 editorElement.classList.add('drag-over'); | ||||
|             }, false); | ||||
|         }); | ||||
|          | ||||
|         ['dragleave', 'drop'].forEach(eventName => { | ||||
|             editorElement.addEventListener(eventName, () => { | ||||
|                 editorElement.classList.remove('drag-over'); | ||||
|             }, false); | ||||
|         }); | ||||
|          | ||||
|         editorElement.addEventListener('drop', async (e) => { | ||||
|             const files = e.dataTransfer.files; | ||||
|              | ||||
|             if (files.length === 0) return; | ||||
|              | ||||
|             const imageFiles = Array.from(files).filter(file =>  | ||||
|                 file.type.startsWith('image/') | ||||
|             ); | ||||
|              | ||||
|             if (imageFiles.length === 0) { | ||||
|                 showNotification('Please drop image files only', 'warning'); | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             showNotification(`Uploading ${imageFiles.length} image(s)...`, 'info'); | ||||
|              | ||||
|             for (const file of imageFiles) { | ||||
|                 const url = await uploadImage(file); | ||||
|                 if (url) { | ||||
|                     const cursor = editor.getCursor(); | ||||
|                     const imageMarkdown = ``; | ||||
|                     editor.replaceRange(imageMarkdown, cursor); | ||||
|                     editor.setCursor(cursor.line, cursor.ch + imageMarkdown.length); | ||||
|                     showNotification(`Image uploaded: ${file.name}`, 'success'); | ||||
|                 } | ||||
|             } | ||||
|         }, false); | ||||
|          | ||||
|         editorElement.addEventListener('paste', async (e) => { | ||||
|             const items = e.clipboardData?.items; | ||||
|             if (!items) return; | ||||
|              | ||||
|             for (const item of items) { | ||||
|                 if (item.type.startsWith('image/')) { | ||||
|                     e.preventDefault(); | ||||
|                     const file = item.getAsFile(); | ||||
|                     if (file) { | ||||
|                         showNotification('Uploading pasted image...', 'info'); | ||||
|                         const url = await uploadImage(file); | ||||
|                         if (url) { | ||||
|                             const cursor = editor.getCursor(); | ||||
|                             const imageMarkdown = ``; | ||||
|                             editor.replaceRange(imageMarkdown, cursor); | ||||
|                             showNotification('Image uploaded successfully', 'success'); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Initialize CodeMirror editor | ||||
|     function initEditor() { | ||||
|         editor = CodeMirror.fromTextArea(document.getElementById('editor'), { | ||||
|             mode: 'markdown', | ||||
|             theme: 'monokai', | ||||
|             lineNumbers: true, | ||||
|             lineWrapping: true, | ||||
|             autofocus: true, | ||||
|             extraKeys: { | ||||
|                 'Ctrl-S': function() { saveFile(); }, | ||||
|                 'Cmd-S': function() { saveFile(); } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         editor.on('change', debounce(updatePreview, 300)); | ||||
|          | ||||
|         setTimeout(setupDragAndDrop, 100); | ||||
|          | ||||
|         setupScrollSync(); | ||||
|     } | ||||
|  | ||||
|     // Debounce function | ||||
|     function debounce(func, wait) { | ||||
|         let timeout; | ||||
|         return function executedFunction(...args) { | ||||
|             const later = () => { | ||||
|                 clearTimeout(timeout); | ||||
|                 func(...args); | ||||
|             }; | ||||
|             clearTimeout(timeout); | ||||
|             timeout = setTimeout(later, wait); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     // Setup synchronized scrolling | ||||
|     function setupScrollSync() { | ||||
|         const previewDiv = document.getElementById('preview'); | ||||
|          | ||||
|         editor.on('scroll', () => { | ||||
|             if (!isScrollingSynced) return; | ||||
|              | ||||
|             const scrollInfo = editor.getScrollInfo(); | ||||
|             const scrollPercentage = scrollInfo.top / (scrollInfo.height - scrollInfo.clientHeight); | ||||
|              | ||||
|             const previewScrollHeight = previewDiv.scrollHeight - previewDiv.clientHeight; | ||||
|             previewDiv.scrollTop = previewScrollHeight * scrollPercentage; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Update preview | ||||
|     async function updatePreview() { | ||||
|         const markdown = editor.getValue(); | ||||
|         const previewDiv = document.getElementById('preview'); | ||||
|          | ||||
|         if (!markdown.trim()) { | ||||
|             previewDiv.innerHTML = ` | ||||
|                 <div class="text-muted text-center mt-5"> | ||||
|                     <h4>Preview</h4> | ||||
|                     <p>Start typing in the editor to see the preview</p> | ||||
|                 </div> | ||||
|             `; | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         try { | ||||
|             let html = marked.parse(markdown); | ||||
|              | ||||
|             html = html.replace( | ||||
|                 /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, | ||||
|                 '<div class="mermaid">$1</div>' | ||||
|             ); | ||||
|              | ||||
|             previewDiv.innerHTML = html; | ||||
|              | ||||
|             const codeBlocks = previewDiv.querySelectorAll('pre code'); | ||||
|             codeBlocks.forEach(block => { | ||||
|                 const languageClass = Array.from(block.classList).find(cls => cls.startsWith('language-')); | ||||
|                 if (languageClass && languageClass !== 'language-mermaid') { | ||||
|                     Prism.highlightElement(block); | ||||
|                 } | ||||
|             }); | ||||
|              | ||||
|             const mermaidElements = previewDiv.querySelectorAll('.mermaid'); | ||||
|             if (mermaidElements.length > 0) { | ||||
|                 try { | ||||
|                     await mermaid.run({ | ||||
|                         nodes: mermaidElements, | ||||
|                         suppressErrors: false | ||||
|                     }); | ||||
|                 } catch (error) { | ||||
|                     console.error('Mermaid rendering error:', error); | ||||
|                 } | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.error('Preview rendering error:', error); | ||||
|             previewDiv.innerHTML = ` | ||||
|                 <div class="alert alert-danger" role="alert"> | ||||
|                     <strong>Error rendering preview:</strong> ${error.message} | ||||
|                 </div> | ||||
|             `; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // ======================================================================== | ||||
|     // File Tree Management | ||||
|     // ======================================================================== | ||||
|  | ||||
|     async function loadFileTree() { | ||||
|         try { | ||||
|             const response = await fetch('/api/tree'); | ||||
|             if (!response.ok) throw new Error('Failed to load file tree'); | ||||
|              | ||||
|             fileTree = await response.json(); | ||||
|             renderFileTree(); | ||||
|         } catch (error) { | ||||
|             console.error('Error loading file tree:', error); | ||||
|             showNotification('Error loading files', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function renderFileTree() { | ||||
|         const container = document.getElementById('fileTree'); | ||||
|         container.innerHTML = ''; | ||||
|          | ||||
|         if (fileTree.length === 0) { | ||||
|             container.innerHTML = '<div class="text-muted text-center p-3">No files yet</div>'; | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         fileTree.forEach(node => { | ||||
|             container.appendChild(createTreeNode(node)); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function createTreeNode(node, level = 0) { | ||||
|         const nodeDiv = document.createElement('div'); | ||||
|         nodeDiv.className = 'tree-node-wrapper'; | ||||
|          | ||||
|         const nodeContent = document.createElement('div'); | ||||
|         nodeContent.className = 'tree-node'; | ||||
|         nodeContent.dataset.path = node.path; | ||||
|         nodeContent.dataset.type = node.type; | ||||
|         nodeContent.dataset.name = node.name; | ||||
|          | ||||
|         // Make draggable | ||||
|         nodeContent.draggable = true; | ||||
|         nodeContent.addEventListener('dragstart', handleDragStart); | ||||
|         nodeContent.addEventListener('dragend', handleDragEnd); | ||||
|         nodeContent.addEventListener('dragover', handleDragOver); | ||||
|         nodeContent.addEventListener('dragleave', handleDragLeave); | ||||
|         nodeContent.addEventListener('drop', handleDrop); | ||||
|          | ||||
|         const contentWrapper = document.createElement('div'); | ||||
|         contentWrapper.className = 'tree-node-content'; | ||||
|          | ||||
|         if (node.type === 'directory') { | ||||
|             const toggle = document.createElement('span'); | ||||
|             toggle.className = 'tree-node-toggle'; | ||||
|             toggle.innerHTML = '▶'; | ||||
|             toggle.addEventListener('click', (e) => { | ||||
|                 e.stopPropagation(); | ||||
|                 toggleNode(nodeDiv); | ||||
|             }); | ||||
|             contentWrapper.appendChild(toggle); | ||||
|         } else { | ||||
|             const spacer = document.createElement('span'); | ||||
|             spacer.style.width = '16px'; | ||||
|             contentWrapper.appendChild(spacer); | ||||
|         } | ||||
|          | ||||
|         const icon = document.createElement('i'); | ||||
|         icon.className = node.type === 'directory' ? 'bi bi-folder tree-node-icon' : 'bi bi-file-earmark-text tree-node-icon'; | ||||
|         contentWrapper.appendChild(icon); | ||||
|          | ||||
|         const name = document.createElement('span'); | ||||
|         name.className = 'tree-node-name'; | ||||
|         name.textContent = node.name; | ||||
|         contentWrapper.appendChild(name); | ||||
|          | ||||
|         if (node.type === 'file' && node.size) { | ||||
|             const size = document.createElement('span'); | ||||
|             size.className = 'file-size-badge'; | ||||
|             size.textContent = formatFileSize(node.size); | ||||
|             contentWrapper.appendChild(size); | ||||
|         } | ||||
|          | ||||
|         nodeContent.appendChild(contentWrapper); | ||||
|          | ||||
|         nodeContent.addEventListener('click', (e) => { | ||||
|             if (node.type === 'file') { | ||||
|                 loadFile(node.path); | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         nodeContent.addEventListener('contextmenu', (e) => { | ||||
|             e.preventDefault(); | ||||
|             showContextMenu(e, node); | ||||
|         }); | ||||
|          | ||||
|         nodeDiv.appendChild(nodeContent); | ||||
|          | ||||
|         if (node.children && node.children.length > 0) { | ||||
|             const childrenDiv = document.createElement('div'); | ||||
|             childrenDiv.className = 'tree-children collapsed'; | ||||
|              | ||||
|             node.children.forEach(child => { | ||||
|                 childrenDiv.appendChild(createTreeNode(child, level + 1)); | ||||
|             }); | ||||
|              | ||||
|             nodeDiv.appendChild(childrenDiv); | ||||
|         } | ||||
|          | ||||
|         return nodeDiv; | ||||
|     } | ||||
|  | ||||
|     function toggleNode(nodeWrapper) { | ||||
|         const toggle = nodeWrapper.querySelector('.tree-node-toggle'); | ||||
|         const children = nodeWrapper.querySelector('.tree-children'); | ||||
|          | ||||
|         if (children) { | ||||
|             children.classList.toggle('collapsed'); | ||||
|             toggle.classList.toggle('expanded'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function formatFileSize(bytes) { | ||||
|         if (bytes < 1024) return bytes + ' B'; | ||||
|         if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; | ||||
|         return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; | ||||
|     } | ||||
|  | ||||
|     // ======================================================================== | ||||
|     // Drag and Drop for Files | ||||
|     // ======================================================================== | ||||
|  | ||||
|     let draggedNode = null; | ||||
|  | ||||
|     function handleDragStart(e) { | ||||
|         draggedNode = { | ||||
|             path: e.currentTarget.dataset.path, | ||||
|             type: e.currentTarget.dataset.type, | ||||
|             name: e.currentTarget.dataset.name | ||||
|         }; | ||||
|         e.currentTarget.classList.add('dragging'); | ||||
|         e.dataTransfer.effectAllowed = 'move'; | ||||
|         e.dataTransfer.setData('text/plain', draggedNode.path); | ||||
|     } | ||||
|  | ||||
|     function handleDragEnd(e) { | ||||
|         e.currentTarget.classList.remove('dragging'); | ||||
|         document.querySelectorAll('.drag-over').forEach(el => { | ||||
|             el.classList.remove('drag-over'); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function handleDragOver(e) { | ||||
|         if (!draggedNode) return; | ||||
|          | ||||
|         e.preventDefault(); | ||||
|         e.dataTransfer.dropEffect = 'move'; | ||||
|          | ||||
|         const targetType = e.currentTarget.dataset.type; | ||||
|         if (targetType === 'directory') { | ||||
|             e.currentTarget.classList.add('drag-over'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function handleDragLeave(e) { | ||||
|         e.currentTarget.classList.remove('drag-over'); | ||||
|     } | ||||
|  | ||||
|     async function handleDrop(e) { | ||||
|         e.preventDefault(); | ||||
|         e.currentTarget.classList.remove('drag-over'); | ||||
|          | ||||
|         if (!draggedNode) return; | ||||
|          | ||||
|         const targetPath = e.currentTarget.dataset.path; | ||||
|         const targetType = e.currentTarget.dataset.type; | ||||
|          | ||||
|         if (targetType !== 'directory') return; | ||||
|         if (draggedNode.path === targetPath) return; | ||||
|          | ||||
|         const sourcePath = draggedNode.path; | ||||
|         const destPath = targetPath + '/' + draggedNode.name; | ||||
|          | ||||
|         try { | ||||
|             const response = await fetch('/api/file/move', { | ||||
|                 method: 'POST', | ||||
|                 headers: { 'Content-Type': 'application/json' }, | ||||
|                 body: JSON.stringify({ | ||||
|                     source: sourcePath, | ||||
|                     destination: destPath | ||||
|                 }) | ||||
|             }); | ||||
|              | ||||
|             if (!response.ok) throw new Error('Move failed'); | ||||
|              | ||||
|             showNotification(`Moved ${draggedNode.name}`, 'success'); | ||||
|             loadFileTree(); | ||||
|         } catch (error) { | ||||
|             console.error('Error moving file:', error); | ||||
|             showNotification('Error moving file', 'danger'); | ||||
|         } | ||||
|          | ||||
|         draggedNode = null; | ||||
|     } | ||||
|  | ||||
|     // ======================================================================== | ||||
|     // Context Menu | ||||
|     // ======================================================================== | ||||
|  | ||||
|     function showContextMenu(e, node) { | ||||
|         contextMenuTarget = node; | ||||
|         const menu = document.getElementById('contextMenu'); | ||||
|         const pasteItem = document.getElementById('pasteMenuItem'); | ||||
|          | ||||
|         // Show paste option only if clipboard has something and target is a directory | ||||
|         if (clipboard && node.type === 'directory') { | ||||
|             pasteItem.style.display = 'flex'; | ||||
|         } else { | ||||
|             pasteItem.style.display = 'none'; | ||||
|         } | ||||
|          | ||||
|         menu.style.display = 'block'; | ||||
|         menu.style.left = e.pageX + 'px'; | ||||
|         menu.style.top = e.pageY + 'px'; | ||||
|          | ||||
|         document.addEventListener('click', hideContextMenu); | ||||
|     } | ||||
|  | ||||
|     function hideContextMenu() { | ||||
|         const menu = document.getElementById('contextMenu'); | ||||
|         menu.style.display = 'none'; | ||||
|         document.removeEventListener('click', hideContextMenu); | ||||
|     } | ||||
|  | ||||
|     // ======================================================================== | ||||
|     // File Operations | ||||
|     // ======================================================================== | ||||
|  | ||||
|     async function loadFile(path) { | ||||
|         try { | ||||
|             const response = await fetch(`/api/file?path=${encodeURIComponent(path)}`); | ||||
|             if (!response.ok) throw new Error('Failed to load file'); | ||||
|              | ||||
|             const data = await response.json(); | ||||
|             currentFile = data.filename; | ||||
|             currentFilePath = path; | ||||
|              | ||||
|             document.getElementById('filenameInput').value = path; | ||||
|             editor.setValue(data.content); | ||||
|             updatePreview(); | ||||
|              | ||||
|             document.querySelectorAll('.tree-node').forEach(node => { | ||||
|                 node.classList.remove('active'); | ||||
|             }); | ||||
|             document.querySelector(`[data-path="${path}"]`)?.classList.add('active'); | ||||
|              | ||||
|             showNotification(`Loaded ${data.filename}`, 'info'); | ||||
|         } catch (error) { | ||||
|             console.error('Error loading file:', error); | ||||
|             showNotification('Error loading file', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async function saveFile() { | ||||
|         const path = document.getElementById('filenameInput').value.trim(); | ||||
|          | ||||
|         if (!path) { | ||||
|             showNotification('Please enter a filename', 'warning'); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         const content = editor.getValue(); | ||||
|          | ||||
|         try { | ||||
|             const response = await fetch('/api/file', { | ||||
|                 method: 'POST', | ||||
|                 headers: { 'Content-Type': 'application/json' }, | ||||
|                 body: JSON.stringify({ path, content }) | ||||
|             }); | ||||
|              | ||||
|             if (!response.ok) throw new Error('Failed to save file'); | ||||
|              | ||||
|             const result = await response.json(); | ||||
|             currentFile = path.split('/').pop(); | ||||
|             currentFilePath = result.path; | ||||
|              | ||||
|             showNotification(`Saved ${currentFile}`, 'success'); | ||||
|             loadFileTree(); | ||||
|         } catch (error) { | ||||
|             console.error('Error saving file:', error); | ||||
|             showNotification('Error saving file', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async function deleteFile() { | ||||
|         if (!currentFilePath) { | ||||
|             showNotification('No file selected', 'warning'); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         if (!confirm(`Are you sure you want to delete ${currentFile}?`)) { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         try { | ||||
|             const response = await fetch(`/api/file?path=${encodeURIComponent(currentFilePath)}`, { | ||||
|                 method: 'DELETE' | ||||
|             }); | ||||
|              | ||||
|             if (!response.ok) throw new Error('Failed to delete file'); | ||||
|              | ||||
|             showNotification(`Deleted ${currentFile}`, 'success'); | ||||
|              | ||||
|             currentFile = null; | ||||
|             currentFilePath = null; | ||||
|             document.getElementById('filenameInput').value = ''; | ||||
|             editor.setValue(''); | ||||
|             updatePreview(); | ||||
|              | ||||
|             loadFileTree(); | ||||
|         } catch (error) { | ||||
|             console.error('Error deleting file:', error); | ||||
|             showNotification('Error deleting file', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function newFile() { | ||||
|         // Clear editor for new file | ||||
|         currentFile = null; | ||||
|         currentFilePath = null; | ||||
|         document.getElementById('filenameInput').value = ''; | ||||
|         document.getElementById('filenameInput').focus(); | ||||
|         editor.setValue(''); | ||||
|         updatePreview(); | ||||
|          | ||||
|         document.querySelectorAll('.tree-node').forEach(node => { | ||||
|             node.classList.remove('active'); | ||||
|         }); | ||||
|          | ||||
|         showNotification('Enter filename and start typing', 'info'); | ||||
|     } | ||||
|  | ||||
|     async function createFolder() { | ||||
|         const folderName = prompt('Enter folder name:'); | ||||
|         if (!folderName) return; | ||||
|          | ||||
|         try { | ||||
|             const response = await fetch('/api/directory', { | ||||
|                 method: 'POST', | ||||
|                 headers: { 'Content-Type': 'application/json' }, | ||||
|                 body: JSON.stringify({ path: folderName }) | ||||
|             }); | ||||
|              | ||||
|             if (!response.ok) throw new Error('Failed to create folder'); | ||||
|              | ||||
|             showNotification(`Created folder ${folderName}`, 'success'); | ||||
|             loadFileTree(); | ||||
|         } catch (error) { | ||||
|             console.error('Error creating folder:', error); | ||||
|             showNotification('Error creating folder', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // ======================================================================== | ||||
|     // Context Menu Actions | ||||
|     // ======================================================================== | ||||
|  | ||||
|     async function handleContextMenuAction(action) { | ||||
|         if (!contextMenuTarget) return; | ||||
|          | ||||
|         switch (action) { | ||||
|             case 'open': | ||||
|                 if (contextMenuTarget.type === 'file') { | ||||
|                     loadFile(contextMenuTarget.path); | ||||
|                 } | ||||
|                 break; | ||||
|                  | ||||
|             case 'rename': | ||||
|                 await renameItem(); | ||||
|                 break; | ||||
|                  | ||||
|             case 'copy': | ||||
|                 clipboard = { ...contextMenuTarget, operation: 'copy' }; | ||||
|                 showNotification(`Copied ${contextMenuTarget.name}`, 'info'); | ||||
|                 break; | ||||
|                  | ||||
|             case 'move': | ||||
|                 clipboard = { ...contextMenuTarget, operation: 'move' }; | ||||
|                 showNotification(`Cut ${contextMenuTarget.name}`, 'info'); | ||||
|                 break; | ||||
|                  | ||||
|             case 'paste': | ||||
|                 await pasteItem(); | ||||
|                 break; | ||||
|                  | ||||
|             case 'delete': | ||||
|                 await deleteItem(); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async function renameItem() { | ||||
|         const newName = prompt(`Rename ${contextMenuTarget.name}:`, contextMenuTarget.name); | ||||
|         if (!newName || newName === contextMenuTarget.name) return; | ||||
|          | ||||
|         const oldPath = contextMenuTarget.path; | ||||
|         const newPath = oldPath.substring(0, oldPath.lastIndexOf('/') + 1) + newName; | ||||
|          | ||||
|         try { | ||||
|             const endpoint = contextMenuTarget.type === 'directory' ? '/api/directory/rename' : '/api/file/rename'; | ||||
|             const response = await fetch(endpoint, { | ||||
|                 method: 'POST', | ||||
|                 headers: { 'Content-Type': 'application/json' }, | ||||
|                 body: JSON.stringify({ | ||||
|                     old_path: oldPath, | ||||
|                     new_path: newPath | ||||
|                 }) | ||||
|             }); | ||||
|              | ||||
|             if (!response.ok) throw new Error('Rename failed'); | ||||
|              | ||||
|             showNotification(`Renamed to ${newName}`, 'success'); | ||||
|             loadFileTree(); | ||||
|         } catch (error) { | ||||
|             console.error('Error renaming:', error); | ||||
|             showNotification('Error renaming', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async function pasteItem() { | ||||
|         if (!clipboard) return; | ||||
|          | ||||
|         const destDir = contextMenuTarget.path; | ||||
|         const sourcePath = clipboard.path; | ||||
|         const fileName = clipboard.name; | ||||
|         const destPath = destDir + '/' + fileName; | ||||
|          | ||||
|         try { | ||||
|             if (clipboard.operation === 'copy') { | ||||
|                 // Copy operation | ||||
|                 const response = await fetch('/api/file/copy', { | ||||
|                     method: 'POST', | ||||
|                     headers: { 'Content-Type': 'application/json' }, | ||||
|                     body: JSON.stringify({ | ||||
|                         source: sourcePath, | ||||
|                         destination: destPath | ||||
|                     }) | ||||
|                 }); | ||||
|                  | ||||
|                 if (!response.ok) throw new Error('Copy failed'); | ||||
|                 showNotification(`Copied ${fileName} to ${contextMenuTarget.name}`, 'success'); | ||||
|             } else if (clipboard.operation === 'move') { | ||||
|                 // Move operation | ||||
|                 const response = await fetch('/api/file/move', { | ||||
|                     method: 'PUT', | ||||
|                     headers: { 'Content-Type': 'application/json' }, | ||||
|                     body: JSON.stringify({ | ||||
|                         source: sourcePath, | ||||
|                         destination: destPath | ||||
|                     }) | ||||
|                 }); | ||||
|                  | ||||
|                 if (!response.ok) throw new Error('Move failed'); | ||||
|                 showNotification(`Moved ${fileName} to ${contextMenuTarget.name}`, 'success'); | ||||
|                 clipboard = null; // Clear clipboard after move | ||||
|             } | ||||
|              | ||||
|             loadFileTree(); | ||||
|         } catch (error) { | ||||
|             console.error('Error pasting:', error); | ||||
|             showNotification('Error pasting file', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async function deleteItem() { | ||||
|         if (!confirm(`Are you sure you want to delete ${contextMenuTarget.name}?`)) { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         try { | ||||
|             let response; | ||||
|             if (contextMenuTarget.type === 'directory') { | ||||
|                 response = await fetch(`/api/directory?path=${encodeURIComponent(contextMenuTarget.path)}&recursive=true`, { | ||||
|                     method: 'DELETE' | ||||
|                 }); | ||||
|             } else { | ||||
|                 response = await fetch(`/api/file?path=${encodeURIComponent(contextMenuTarget.path)}`, { | ||||
|                     method: 'DELETE' | ||||
|                 }); | ||||
|             } | ||||
|              | ||||
|             if (!response.ok) throw new Error('Delete failed'); | ||||
|              | ||||
|             showNotification(`Deleted ${contextMenuTarget.name}`, 'success'); | ||||
|             loadFileTree(); | ||||
|         } catch (error) { | ||||
|             console.error('Error deleting:', error); | ||||
|             showNotification('Error deleting', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // ======================================================================== | ||||
|     // Notifications | ||||
|     // ======================================================================== | ||||
|  | ||||
|     function showNotification(message, type = 'info') { | ||||
|         let toastContainer = document.getElementById('toastContainer'); | ||||
|         if (!toastContainer) { | ||||
|             toastContainer = createToastContainer(); | ||||
|         } | ||||
|          | ||||
|         const toast = document.createElement('div'); | ||||
|         toast.className = `toast align-items-center text-white bg-${type} border-0`; | ||||
|         toast.setAttribute('role', 'alert'); | ||||
|         toast.innerHTML = ` | ||||
|             <div class="d-flex"> | ||||
|                 <div class="toast-body">${message}</div> | ||||
|                 <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button> | ||||
|             </div> | ||||
|         `; | ||||
|          | ||||
|         toastContainer.appendChild(toast); | ||||
|          | ||||
|         const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); | ||||
|         bsToast.show(); | ||||
|          | ||||
|         toast.addEventListener('hidden.bs.toast', () => { | ||||
|             toast.remove(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function createToastContainer() { | ||||
|         const container = document.createElement('div'); | ||||
|         container.id = 'toastContainer'; | ||||
|         container.className = 'toast-container position-fixed top-0 end-0 p-3'; | ||||
|         container.style.zIndex = '9999'; | ||||
|         document.body.appendChild(container); | ||||
|         return container; | ||||
|     } | ||||
|  | ||||
|     // ======================================================================== | ||||
|     // Initialization | ||||
|     // ======================================================================== | ||||
|  | ||||
|     function init() { | ||||
|         initDarkMode(); | ||||
|         initEditor(); | ||||
|         loadFileTree(); | ||||
|          | ||||
|         document.getElementById('saveBtn').addEventListener('click', saveFile); | ||||
|         document.getElementById('deleteBtn').addEventListener('click', deleteFile); | ||||
|         document.getElementById('newFileBtn').addEventListener('click', newFile); | ||||
|         document.getElementById('newFolderBtn').addEventListener('click', createFolder); | ||||
|         document.getElementById('darkModeToggle').addEventListener('click', toggleDarkMode); | ||||
|          | ||||
|         // Context menu actions | ||||
|         document.querySelectorAll('.context-menu-item').forEach(item => { | ||||
|             item.addEventListener('click', () => { | ||||
|                 const action = item.dataset.action; | ||||
|                 handleContextMenuAction(action); | ||||
|                 hideContextMenu(); | ||||
|             }); | ||||
|         }); | ||||
|          | ||||
|         document.addEventListener('keydown', (e) => { | ||||
|             if ((e.ctrlKey || e.metaKey) && e.key === 's') { | ||||
|                 e.preventDefault(); | ||||
|                 saveFile(); | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         console.log('Markdown Editor with File Tree initialized'); | ||||
|     } | ||||
|  | ||||
|     if (document.readyState === 'loading') { | ||||
|         document.addEventListener('DOMContentLoaded', init); | ||||
|     } else { | ||||
|         init(); | ||||
|     } | ||||
| })(); | ||||
|  | ||||
							
								
								
									
										527
									
								
								static/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										527
									
								
								static/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,527 @@ | ||||
| // Markdown Editor Application | ||||
| (function() { | ||||
|     'use strict'; | ||||
|  | ||||
|     // State management | ||||
|     let currentFile = null; | ||||
|     let editor = null; | ||||
|     let isScrollingSynced = true; | ||||
|     let scrollTimeout = null; | ||||
|     let isDarkMode = false; | ||||
|  | ||||
|     // Dark mode management | ||||
|     function initDarkMode() { | ||||
|         // Check for saved preference | ||||
|         const savedMode = localStorage.getItem('darkMode'); | ||||
|         if (savedMode === 'true') { | ||||
|             enableDarkMode(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function enableDarkMode() { | ||||
|         isDarkMode = true; | ||||
|         document.body.classList.add('dark-mode'); | ||||
|         document.getElementById('darkModeIcon').textContent = '☀️'; | ||||
|         localStorage.setItem('darkMode', 'true'); | ||||
|          | ||||
|         // Update mermaid theme | ||||
|         mermaid.initialize({  | ||||
|             startOnLoad: false, | ||||
|             theme: 'dark', | ||||
|             securityLevel: 'loose' | ||||
|         }); | ||||
|          | ||||
|         // Re-render preview if there's content | ||||
|         if (editor && editor.getValue()) { | ||||
|             updatePreview(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function disableDarkMode() { | ||||
|         isDarkMode = false; | ||||
|         document.body.classList.remove('dark-mode'); | ||||
|         document.getElementById('darkModeIcon').textContent = '🌙'; | ||||
|         localStorage.setItem('darkMode', 'false'); | ||||
|          | ||||
|         // Update mermaid theme | ||||
|         mermaid.initialize({  | ||||
|             startOnLoad: false, | ||||
|             theme: 'default', | ||||
|             securityLevel: 'loose' | ||||
|         }); | ||||
|          | ||||
|         // Re-render preview if there's content | ||||
|         if (editor && editor.getValue()) { | ||||
|             updatePreview(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function toggleDarkMode() { | ||||
|         if (isDarkMode) { | ||||
|             disableDarkMode(); | ||||
|         } else { | ||||
|             enableDarkMode(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Initialize Mermaid | ||||
|     mermaid.initialize({  | ||||
|         startOnLoad: false, | ||||
|         theme: 'default', | ||||
|         securityLevel: 'loose' | ||||
|     }); | ||||
|  | ||||
|     // Configure marked.js for markdown parsing | ||||
|     marked.setOptions({ | ||||
|         breaks: true, | ||||
|         gfm: true, | ||||
|         headerIds: true, | ||||
|         mangle: false, | ||||
|         sanitize: false, // Allow HTML in markdown | ||||
|         smartLists: true, | ||||
|         smartypants: true, | ||||
|         xhtml: false | ||||
|     }); | ||||
|  | ||||
|     // Handle image upload | ||||
|     async function uploadImage(file) { | ||||
|         const formData = new FormData(); | ||||
|         formData.append('file', file); | ||||
|          | ||||
|         try { | ||||
|             const response = await fetch('/api/upload-image', { | ||||
|                 method: 'POST', | ||||
|                 body: formData | ||||
|             }); | ||||
|              | ||||
|             if (!response.ok) throw new Error('Upload failed'); | ||||
|              | ||||
|             const result = await response.json(); | ||||
|             return result.url; | ||||
|         } catch (error) { | ||||
|             console.error('Error uploading image:', error); | ||||
|             showNotification('Error uploading image', 'danger'); | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Handle drag and drop | ||||
|     function setupDragAndDrop() { | ||||
|         const editorElement = document.querySelector('.CodeMirror'); | ||||
|          | ||||
|         // Prevent default drag behavior | ||||
|         ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | ||||
|             editorElement.addEventListener(eventName, preventDefaults, false); | ||||
|         }); | ||||
|          | ||||
|         function preventDefaults(e) { | ||||
|             e.preventDefault(); | ||||
|             e.stopPropagation(); | ||||
|         } | ||||
|          | ||||
|         // Highlight drop zone | ||||
|         ['dragenter', 'dragover'].forEach(eventName => { | ||||
|             editorElement.addEventListener(eventName, () => { | ||||
|                 editorElement.classList.add('drag-over'); | ||||
|             }, false); | ||||
|         }); | ||||
|          | ||||
|         ['dragleave', 'drop'].forEach(eventName => { | ||||
|             editorElement.addEventListener(eventName, () => { | ||||
|                 editorElement.classList.remove('drag-over'); | ||||
|             }, false); | ||||
|         }); | ||||
|          | ||||
|         // Handle drop | ||||
|         editorElement.addEventListener('drop', async (e) => { | ||||
|             const files = e.dataTransfer.files; | ||||
|              | ||||
|             if (files.length === 0) return; | ||||
|              | ||||
|             // Filter for images only | ||||
|             const imageFiles = Array.from(files).filter(file =>  | ||||
|                 file.type.startsWith('image/') | ||||
|             ); | ||||
|              | ||||
|             if (imageFiles.length === 0) { | ||||
|                 showNotification('Please drop image files only', 'warning'); | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             showNotification(`Uploading ${imageFiles.length} image(s)...`, 'info'); | ||||
|              | ||||
|             // Upload images | ||||
|             for (const file of imageFiles) { | ||||
|                 const url = await uploadImage(file); | ||||
|                 if (url) { | ||||
|                     // Insert markdown image syntax at cursor | ||||
|                     const cursor = editor.getCursor(); | ||||
|                     const imageMarkdown = ``; | ||||
|                     editor.replaceRange(imageMarkdown, cursor); | ||||
|                     editor.setCursor(cursor.line, cursor.ch + imageMarkdown.length); | ||||
|                     showNotification(`Image uploaded: ${file.name}`, 'success'); | ||||
|                 } | ||||
|             } | ||||
|         }, false); | ||||
|          | ||||
|         // Also handle paste events for images | ||||
|         editorElement.addEventListener('paste', async (e) => { | ||||
|             const items = e.clipboardData?.items; | ||||
|             if (!items) return; | ||||
|              | ||||
|             for (const item of items) { | ||||
|                 if (item.type.startsWith('image/')) { | ||||
|                     e.preventDefault(); | ||||
|                     const file = item.getAsFile(); | ||||
|                     if (file) { | ||||
|                         showNotification('Uploading pasted image...', 'info'); | ||||
|                         const url = await uploadImage(file); | ||||
|                         if (url) { | ||||
|                             const cursor = editor.getCursor(); | ||||
|                             const imageMarkdown = ``; | ||||
|                             editor.replaceRange(imageMarkdown, cursor); | ||||
|                             showNotification('Image uploaded successfully', 'success'); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Initialize CodeMirror editor | ||||
|     function initEditor() { | ||||
|         const textarea = document.getElementById('editor'); | ||||
|         editor = CodeMirror.fromTextArea(textarea, { | ||||
|             mode: 'markdown', | ||||
|             theme: 'monokai', | ||||
|             lineNumbers: true, | ||||
|             lineWrapping: true, | ||||
|             autofocus: true, | ||||
|             extraKeys: { | ||||
|                 'Ctrl-S': function() { saveFile(); }, | ||||
|                 'Cmd-S': function() { saveFile(); } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Update preview on change | ||||
|         editor.on('change', debounce(updatePreview, 300)); | ||||
|          | ||||
|         // Setup drag and drop after editor is ready | ||||
|         setTimeout(setupDragAndDrop, 100); | ||||
|          | ||||
|         // Sync scroll | ||||
|         editor.on('scroll', handleEditorScroll); | ||||
|     } | ||||
|  | ||||
|     // Debounce function to limit update frequency | ||||
|     function debounce(func, wait) { | ||||
|         let timeout; | ||||
|         return function executedFunction(...args) { | ||||
|             const later = () => { | ||||
|                 clearTimeout(timeout); | ||||
|                 func(...args); | ||||
|             }; | ||||
|             clearTimeout(timeout); | ||||
|             timeout = setTimeout(later, wait); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     // Update preview with markdown content | ||||
|     async function updatePreview() { | ||||
|         const content = editor.getValue(); | ||||
|         const previewDiv = document.getElementById('preview'); | ||||
|          | ||||
|         if (!content.trim()) { | ||||
|             previewDiv.innerHTML = ` | ||||
|                 <div class="text-muted text-center mt-5"> | ||||
|                     <h4>Preview</h4> | ||||
|                     <p>Start typing in the editor to see the preview</p> | ||||
|                 </div> | ||||
|             `; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             // Parse markdown to HTML | ||||
|             let html = marked.parse(content); | ||||
|              | ||||
|             // Replace mermaid code blocks with div containers | ||||
|             html = html.replace( | ||||
|                 /<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, | ||||
|                 '<div class="mermaid">$1</div>' | ||||
|             ); | ||||
|              | ||||
|             previewDiv.innerHTML = html; | ||||
|              | ||||
|             // Apply syntax highlighting to code blocks | ||||
|             const codeBlocks = previewDiv.querySelectorAll('pre code'); | ||||
|             codeBlocks.forEach(block => { | ||||
|                 // Detect language from class name | ||||
|                 const languageClass = Array.from(block.classList).find(cls => cls.startsWith('language-')); | ||||
|                 if (languageClass && languageClass !== 'language-mermaid') { | ||||
|                     Prism.highlightElement(block); | ||||
|                 } | ||||
|             }); | ||||
|              | ||||
|             // Render mermaid diagrams | ||||
|             const mermaidElements = previewDiv.querySelectorAll('.mermaid'); | ||||
|             if (mermaidElements.length > 0) { | ||||
|                 try { | ||||
|                     await mermaid.run({ | ||||
|                         nodes: mermaidElements, | ||||
|                         suppressErrors: false | ||||
|                     }); | ||||
|                 } catch (error) { | ||||
|                     console.error('Mermaid rendering error:', error); | ||||
|                 } | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.error('Preview rendering error:', error); | ||||
|             previewDiv.innerHTML = ` | ||||
|                 <div class="alert alert-danger" role="alert"> | ||||
|                     <strong>Error rendering preview:</strong> ${error.message} | ||||
|                 </div> | ||||
|             `; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Handle editor scroll for synchronized scrolling | ||||
|     function handleEditorScroll() { | ||||
|         if (!isScrollingSynced) return; | ||||
|          | ||||
|         clearTimeout(scrollTimeout); | ||||
|         scrollTimeout = setTimeout(() => { | ||||
|             const editorScrollInfo = editor.getScrollInfo(); | ||||
|             const editorScrollPercentage = editorScrollInfo.top / (editorScrollInfo.height - editorScrollInfo.clientHeight); | ||||
|              | ||||
|             const previewPane = document.querySelector('.preview-pane'); | ||||
|             const previewScrollHeight = previewPane.scrollHeight - previewPane.clientHeight; | ||||
|              | ||||
|             if (previewScrollHeight > 0) { | ||||
|                 previewPane.scrollTop = editorScrollPercentage * previewScrollHeight; | ||||
|             } | ||||
|         }, 10); | ||||
|     } | ||||
|  | ||||
|     // Load file list from server | ||||
|     async function loadFileList() { | ||||
|         try { | ||||
|             const response = await fetch('/api/files'); | ||||
|             if (!response.ok) throw new Error('Failed to load file list'); | ||||
|              | ||||
|             const files = await response.json(); | ||||
|             const fileListDiv = document.getElementById('fileList'); | ||||
|              | ||||
|             if (files.length === 0) { | ||||
|                 fileListDiv.innerHTML = '<div class="text-muted p-2 small">No files yet</div>'; | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             fileListDiv.innerHTML = files.map(file => ` | ||||
|                 <a href="#" class="list-group-item list-group-item-action file-item" data-filename="${file.filename}"> | ||||
|                     <span class="file-name">${file.filename}</span> | ||||
|                     <span class="file-size">${formatFileSize(file.size)}</span> | ||||
|                 </a> | ||||
|             `).join(''); | ||||
|              | ||||
|             // Add click handlers | ||||
|             document.querySelectorAll('.file-item').forEach(item => { | ||||
|                 item.addEventListener('click', (e) => { | ||||
|                     e.preventDefault(); | ||||
|                     const filename = item.dataset.filename; | ||||
|                     loadFile(filename); | ||||
|                 }); | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             console.error('Error loading file list:', error); | ||||
|             showNotification('Error loading file list', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Load a specific file | ||||
|     async function loadFile(filename) { | ||||
|         try { | ||||
|             const response = await fetch(`/api/files/${filename}`); | ||||
|             if (!response.ok) throw new Error('Failed to load file'); | ||||
|              | ||||
|             const data = await response.json(); | ||||
|             currentFile = data.filename; | ||||
|              | ||||
|             // Update UI | ||||
|             document.getElementById('filenameInput').value = data.filename; | ||||
|             editor.setValue(data.content); | ||||
|              | ||||
|             // Update active state in file list | ||||
|             document.querySelectorAll('.file-item').forEach(item => { | ||||
|                 item.classList.toggle('active', item.dataset.filename === filename); | ||||
|             }); | ||||
|              | ||||
|             updatePreview(); | ||||
|             showNotification(`Loaded ${filename}`, 'success'); | ||||
|         } catch (error) { | ||||
|             console.error('Error loading file:', error); | ||||
|             showNotification('Error loading file', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Save current file | ||||
|     async function saveFile() { | ||||
|         const filename = document.getElementById('filenameInput').value.trim(); | ||||
|          | ||||
|         if (!filename) { | ||||
|             showNotification('Please enter a filename', 'warning'); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         const content = editor.getValue(); | ||||
|          | ||||
|         try { | ||||
|             const response = await fetch('/api/files', { | ||||
|                 method: 'POST', | ||||
|                 headers: { | ||||
|                     'Content-Type': 'application/json' | ||||
|                 }, | ||||
|                 body: JSON.stringify({ filename, content }) | ||||
|             }); | ||||
|              | ||||
|             if (!response.ok) throw new Error('Failed to save file'); | ||||
|              | ||||
|             const result = await response.json(); | ||||
|             currentFile = result.filename; | ||||
|              | ||||
|             showNotification(`Saved ${result.filename}`, 'success'); | ||||
|             loadFileList(); | ||||
|         } catch (error) { | ||||
|             console.error('Error saving file:', error); | ||||
|             showNotification('Error saving file', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Delete current file | ||||
|     async function deleteFile() { | ||||
|         const filename = document.getElementById('filenameInput').value.trim(); | ||||
|          | ||||
|         if (!filename) { | ||||
|             showNotification('No file selected', 'warning'); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         if (!confirm(`Are you sure you want to delete ${filename}?`)) { | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         try { | ||||
|             const response = await fetch(`/api/files/${filename}`, { | ||||
|                 method: 'DELETE' | ||||
|             }); | ||||
|              | ||||
|             if (!response.ok) throw new Error('Failed to delete file'); | ||||
|              | ||||
|             showNotification(`Deleted ${filename}`, 'success'); | ||||
|              | ||||
|             // Clear editor | ||||
|             currentFile = null; | ||||
|             document.getElementById('filenameInput').value = ''; | ||||
|             editor.setValue(''); | ||||
|             updatePreview(); | ||||
|              | ||||
|             loadFileList(); | ||||
|         } catch (error) { | ||||
|             console.error('Error deleting file:', error); | ||||
|             showNotification('Error deleting file', 'danger'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Create new file | ||||
|     function newFile() { | ||||
|         currentFile = null; | ||||
|         document.getElementById('filenameInput').value = ''; | ||||
|         editor.setValue(''); | ||||
|         updatePreview(); | ||||
|          | ||||
|         // Remove active state from all file items | ||||
|         document.querySelectorAll('.file-item').forEach(item => { | ||||
|             item.classList.remove('active'); | ||||
|         }); | ||||
|          | ||||
|         showNotification('New file created', 'info'); | ||||
|     } | ||||
|  | ||||
|     // Format file size for display | ||||
|     function formatFileSize(bytes) { | ||||
|         if (bytes === 0) return '0 B'; | ||||
|         const k = 1024; | ||||
|         const sizes = ['B', 'KB', 'MB', 'GB']; | ||||
|         const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||||
|         return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; | ||||
|     } | ||||
|  | ||||
|     // Show notification | ||||
|     function showNotification(message, type = 'info') { | ||||
|         // Create toast notification | ||||
|         const toastContainer = document.getElementById('toastContainer') || createToastContainer(); | ||||
|          | ||||
|         const toast = document.createElement('div'); | ||||
|         toast.className = `toast align-items-center text-white bg-${type} border-0`; | ||||
|         toast.setAttribute('role', 'alert'); | ||||
|         toast.setAttribute('aria-live', 'assertive'); | ||||
|         toast.setAttribute('aria-atomic', 'true'); | ||||
|          | ||||
|         toast.innerHTML = ` | ||||
|             <div class="d-flex"> | ||||
|                 <div class="toast-body">${message}</div> | ||||
|                 <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button> | ||||
|             </div> | ||||
|         `; | ||||
|          | ||||
|         toastContainer.appendChild(toast); | ||||
|          | ||||
|         const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); | ||||
|         bsToast.show(); | ||||
|          | ||||
|         toast.addEventListener('hidden.bs.toast', () => { | ||||
|             toast.remove(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // Create toast container if it doesn't exist | ||||
|     function createToastContainer() { | ||||
|         const container = document.createElement('div'); | ||||
|         container.id = 'toastContainer'; | ||||
|         container.className = 'toast-container position-fixed top-0 end-0 p-3'; | ||||
|         container.style.zIndex = '9999'; | ||||
|         document.body.appendChild(container); | ||||
|         return container; | ||||
|     } | ||||
|  | ||||
|     // Initialize application | ||||
|     function init() { | ||||
|         initDarkMode(); | ||||
|         initEditor(); | ||||
|         loadFileList(); | ||||
|          | ||||
|         // Set up event listeners | ||||
|         document.getElementById('saveBtn').addEventListener('click', saveFile); | ||||
|         document.getElementById('deleteBtn').addEventListener('click', deleteFile); | ||||
|         document.getElementById('newFileBtn').addEventListener('click', newFile); | ||||
|         document.getElementById('darkModeToggle').addEventListener('click', toggleDarkMode); | ||||
|          | ||||
|         // Keyboard shortcuts | ||||
|         document.addEventListener('keydown', (e) => { | ||||
|             if ((e.ctrlKey || e.metaKey) && e.key === 's') { | ||||
|                 e.preventDefault(); | ||||
|                 saveFile(); | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         console.log('Markdown Editor initialized'); | ||||
|     } | ||||
|  | ||||
|     // Start application when DOM is ready | ||||
|     if (document.readyState === 'loading') { | ||||
|         document.addEventListener('DOMContentLoaded', init); | ||||
|     } else { | ||||
|         init(); | ||||
|     } | ||||
| })(); | ||||
|  | ||||
							
								
								
									
										160
									
								
								static/css/components.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								static/css/components.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,160 @@ | ||||
| /* Preview pane styles */ | ||||
| .preview-pane { | ||||
|     font-size: 16px; | ||||
|     line-height: 1.6; | ||||
| } | ||||
|  | ||||
| .preview-pane h1, .preview-pane h2, .preview-pane h3, | ||||
| .preview-pane h4, .preview-pane h5, .preview-pane h6 { | ||||
|     margin-top: 24px; | ||||
|     margin-bottom: 16px; | ||||
|     font-weight: 600; | ||||
|     line-height: 1.25; | ||||
|     color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| .preview-pane a { | ||||
|     color: var(--link-color); | ||||
|     text-decoration: none; | ||||
| } | ||||
|  | ||||
| .preview-pane a:hover { | ||||
|     text-decoration: underline; | ||||
| } | ||||
|  | ||||
| .preview-pane code { | ||||
|     background-color: var(--bg-tertiary); | ||||
|     padding: 2px 6px; | ||||
|     border-radius: 3px; | ||||
|     font-size: 85%; | ||||
| } | ||||
|  | ||||
| .preview-pane pre { | ||||
|     background-color: var(--bg-tertiary); | ||||
|     padding: 16px; | ||||
|     border-radius: 6px; | ||||
|     overflow-x: auto; | ||||
| } | ||||
|  | ||||
| .preview-pane pre code { | ||||
|     background-color: transparent; | ||||
|     padding: 0; | ||||
| } | ||||
|  | ||||
| .preview-pane table { | ||||
|     border-collapse: collapse; | ||||
|     width: 100%; | ||||
|     margin: 16px 0; | ||||
| } | ||||
|  | ||||
| .preview-pane table th, | ||||
| .preview-pane table td { | ||||
|     border: 1px solid var(--border-color); | ||||
|     padding: 8px 12px; | ||||
| } | ||||
|  | ||||
| .preview-pane table th { | ||||
|     background-color: var(--bg-secondary); | ||||
|     font-weight: 600; | ||||
| } | ||||
|  | ||||
| .preview-pane blockquote { | ||||
|     border-left: 4px solid var(--border-color); | ||||
|     padding-left: 16px; | ||||
|     margin-left: 0; | ||||
|     color: var(--text-secondary); | ||||
| } | ||||
|  | ||||
| .preview-pane img { | ||||
|     max-width: 100%; | ||||
|     height: auto; | ||||
| } | ||||
|  | ||||
| /* Context Menu */ | ||||
| .context-menu { | ||||
|     position: fixed; | ||||
|     background-color: var(--bg-primary); | ||||
|     border: 1px solid var(--border-color); | ||||
|     border-radius: 6px; | ||||
|     box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||||
|     z-index: 10000; | ||||
|     min-width: 180px; | ||||
|     max-width: 200px; | ||||
|     width: auto; | ||||
|     padding: 4px 0; | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| body.dark-mode .context-menu { | ||||
|     box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); | ||||
| } | ||||
|  | ||||
| .context-menu-item { | ||||
|     padding: 8px 16px; | ||||
|     cursor: pointer; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 10px; | ||||
|     color: var(--text-primary); | ||||
|     transition: background-color 0.15s ease; | ||||
| } | ||||
|  | ||||
| .context-menu-item:hover { | ||||
|     background-color: var(--bg-tertiary); | ||||
| } | ||||
|  | ||||
| .context-menu-item i { | ||||
|     width: 16px; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .context-menu-divider { | ||||
|     height: 1px; | ||||
|     background-color: var(--border-color); | ||||
|     margin: 4px 0; | ||||
| } | ||||
|  | ||||
| /* Toast Notifications */ | ||||
| .toast-container { | ||||
|     position: fixed; | ||||
|     top: 70px; | ||||
|     right: 20px; | ||||
|     z-index: 9999; | ||||
| } | ||||
|  | ||||
| .toast { | ||||
|     min-width: 250px; | ||||
|     margin-bottom: 10px; | ||||
|     background-color: var(--bg-primary); | ||||
|     border: 1px solid var(--border-color); | ||||
|     border-radius: 6px; | ||||
|     box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||||
|     animation: slideIn 0.3s ease; | ||||
| } | ||||
|  | ||||
| @keyframes slideIn { | ||||
|     from { | ||||
|         transform: translateX(400px); | ||||
|         opacity: 0; | ||||
|     } | ||||
|     to { | ||||
|         transform: translateX(0); | ||||
|         opacity: 1; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .toast.hiding { | ||||
|     animation: slideOut 0.3s ease; | ||||
| } | ||||
|  | ||||
| @keyframes slideOut { | ||||
|     from { | ||||
|         transform: translateX(0); | ||||
|         opacity: 1; | ||||
|     } | ||||
|     to { | ||||
|         transform: translateX(400px); | ||||
|         opacity: 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										75
									
								
								static/css/editor.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								static/css/editor.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| /* Editor styles */ | ||||
| .editor-header { | ||||
|     padding: 10px; | ||||
|     background-color: var(--bg-secondary); | ||||
|     border-bottom: 1px solid var(--border-color); | ||||
|     display: flex; | ||||
|     gap: 10px; | ||||
|     align-items: center; | ||||
| } | ||||
|  | ||||
| .editor-header input { | ||||
|     flex: 1; | ||||
|     padding: 6px 12px; | ||||
|     background-color: var(--bg-primary); | ||||
|     color: var(--text-primary); | ||||
|     border: 1px solid var(--border-color); | ||||
|     border-radius: 4px; | ||||
| } | ||||
|  | ||||
| .editor-container { | ||||
|     flex: 1; | ||||
|     overflow: hidden; | ||||
| } | ||||
|  | ||||
| /* CodeMirror customization */ | ||||
| .CodeMirror { | ||||
|     height: 100%; | ||||
|     font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | ||||
|     font-size: 14px; | ||||
|     background-color: var(--bg-primary); | ||||
|     color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| body.dark-mode .CodeMirror { | ||||
|     background-color: #1c2128; | ||||
|     color: #e6edf3; | ||||
| } | ||||
|  | ||||
| .CodeMirror-gutters { | ||||
|     background-color: var(--bg-secondary); | ||||
|     border-right: 1px solid var(--border-color); | ||||
| } | ||||
|  | ||||
| body.dark-mode .CodeMirror-gutters { | ||||
|     background-color: #161b22; | ||||
|     border-right-color: #30363d; | ||||
| } | ||||
|  | ||||
| .CodeMirror-linenumber { | ||||
|     color: var(--text-secondary); | ||||
| } | ||||
|  | ||||
| .CodeMirror-cursor { | ||||
|     border-left-color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| /* Drag and drop overlay */ | ||||
| .editor-container.drag-over::after { | ||||
|     content: 'Drop images here'; | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     bottom: 0; | ||||
|     background-color: rgba(9, 105, 218, 0.1); | ||||
|     border: 2px dashed var(--info-color); | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     font-size: 24px; | ||||
|     color: var(--info-color); | ||||
|     pointer-events: none; | ||||
|     z-index: 1000; | ||||
| } | ||||
|  | ||||
							
								
								
									
										88
									
								
								static/css/file-tree.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								static/css/file-tree.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| /* File tree styles */ | ||||
| .file-tree { | ||||
|     font-size: 14px; | ||||
|     user-select: none; | ||||
| } | ||||
|  | ||||
| .tree-node { | ||||
|     padding: 6px 8px; | ||||
|     cursor: pointer; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 8px; | ||||
|     border-radius: 4px; | ||||
|     margin: 2px 0; | ||||
|     color: var(--text-primary); | ||||
|     transition: background-color 0.15s ease; | ||||
| } | ||||
|  | ||||
| .tree-node:hover { | ||||
|     background-color: var(--bg-tertiary); | ||||
| } | ||||
|  | ||||
| .tree-node.active { | ||||
|     background-color: #0969da; | ||||
|     color: white; | ||||
| } | ||||
|  | ||||
| body.dark-mode .tree-node.active { | ||||
|     background-color: #1f6feb; | ||||
| } | ||||
|  | ||||
| .tree-node-icon { | ||||
|     width: 16px; | ||||
|     text-align: center; | ||||
|     flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| .tree-node-content { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 6px; | ||||
|     flex: 1; | ||||
|     min-width: 0; | ||||
| } | ||||
|  | ||||
| .tree-node-name { | ||||
|     flex: 1; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     white-space: nowrap; | ||||
| } | ||||
|  | ||||
| .tree-node-size { | ||||
|     font-size: 11px; | ||||
|     color: var(--text-secondary); | ||||
|     flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| .tree-children { | ||||
|     margin-left: 16px; | ||||
| } | ||||
|  | ||||
| .tree-node.dragging { | ||||
|     opacity: 0.5; | ||||
| } | ||||
|  | ||||
| .tree-node.drag-over { | ||||
|     background-color: var(--info-color); | ||||
|     color: white; | ||||
| } | ||||
|  | ||||
| /* Collection selector */ | ||||
| .collection-selector { | ||||
|     margin-bottom: 10px; | ||||
|     padding: 8px; | ||||
|     background-color: var(--bg-tertiary); | ||||
|     border-radius: 4px; | ||||
| } | ||||
|  | ||||
| .collection-selector select { | ||||
|     width: 100%; | ||||
|     padding: 6px; | ||||
|     background-color: var(--bg-primary); | ||||
|     color: var(--text-primary); | ||||
|     border: 1px solid var(--border-color); | ||||
|     border-radius: 4px; | ||||
| } | ||||
|  | ||||
							
								
								
									
										69
									
								
								static/css/layout.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								static/css/layout.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| /* Base layout styles */ | ||||
| html, body { | ||||
|     height: 100%; | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
|     font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; | ||||
|     background-color: var(--bg-primary); | ||||
|     color: var(--text-primary); | ||||
|     transition: background-color 0.3s ease, color 0.3s ease; | ||||
| } | ||||
|  | ||||
| .container-fluid { | ||||
|     height: calc(100% - 56px); | ||||
| } | ||||
|  | ||||
| .sidebar { | ||||
|     background-color: var(--bg-secondary); | ||||
|     border-right: 1px solid var(--border-color); | ||||
|     overflow-y: auto; | ||||
|     height: 100%; | ||||
|     transition: background-color 0.3s ease; | ||||
| } | ||||
|  | ||||
| .editor-pane { | ||||
|     background-color: var(--bg-primary); | ||||
|     height: 100%; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     border-right: 1px solid var(--border-color); | ||||
| } | ||||
|  | ||||
| .preview-pane { | ||||
|     background-color: var(--bg-primary); | ||||
|     height: 100%; | ||||
|     overflow-y: auto; | ||||
|     padding: 20px; | ||||
| } | ||||
|  | ||||
| /* Navbar */ | ||||
| .navbar { | ||||
|     background-color: var(--bg-secondary); | ||||
|     border-bottom: 1px solid var(--border-color); | ||||
|     transition: background-color 0.3s ease; | ||||
| } | ||||
|  | ||||
| .navbar-brand { | ||||
|     color: var(--text-primary) !important; | ||||
|     font-weight: 600; | ||||
| } | ||||
|  | ||||
| /* Scrollbar styling */ | ||||
| ::-webkit-scrollbar { | ||||
|     width: 10px; | ||||
|     height: 10px; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-track { | ||||
|     background: var(--bg-secondary); | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-thumb { | ||||
|     background: var(--border-color); | ||||
|     border-radius: 5px; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-thumb:hover { | ||||
|     background: var(--text-secondary); | ||||
| } | ||||
|  | ||||
							
								
								
									
										31
									
								
								static/css/variables.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								static/css/variables.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| /* CSS Variables for theming */ | ||||
| :root { | ||||
|     /* Light mode colors */ | ||||
|     --bg-primary: #ffffff; | ||||
|     --bg-secondary: #f6f8fa; | ||||
|     --bg-tertiary: #f0f0f0; | ||||
|     --text-primary: #24292f; | ||||
|     --text-secondary: #57606a; | ||||
|     --border-color: #d0d7de; | ||||
|     --link-color: #0969da; | ||||
|     --success-color: #2da44e; | ||||
|     --danger-color: #cf222e; | ||||
|     --warning-color: #bf8700; | ||||
|     --info-color: #0969da; | ||||
| } | ||||
|  | ||||
| body.dark-mode { | ||||
|     /* Dark mode colors - GitHub style */ | ||||
|     --bg-primary: #0d1117; | ||||
|     --bg-secondary: #161b22; | ||||
|     --bg-tertiary: #1c2128; | ||||
|     --text-primary: #e6edf3; | ||||
|     --text-secondary: #8d96a0; | ||||
|     --border-color: #30363d; | ||||
|     --link-color: #4fc3f7; | ||||
|     --success-color: #3fb950; | ||||
|     --danger-color: #f85149; | ||||
|     --warning-color: #d29922; | ||||
|     --info-color: #58a6ff; | ||||
| } | ||||
|  | ||||
							
								
								
									
										302
									
								
								static/js/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								static/js/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,302 @@ | ||||
| /** | ||||
|  * Main Application | ||||
|  * Coordinates all modules and handles user interactions | ||||
|  */ | ||||
|  | ||||
| // Global state | ||||
| let webdavClient; | ||||
| let fileTree; | ||||
| let editor; | ||||
| let darkMode; | ||||
| let collectionSelector; | ||||
| let clipboard = null; | ||||
| let currentFilePath = null; | ||||
|  | ||||
| // Initialize application | ||||
| document.addEventListener('DOMContentLoaded', async () => { | ||||
|     // Initialize WebDAV client | ||||
|     webdavClient = new WebDAVClient('/fs/'); | ||||
|      | ||||
|     // Initialize dark mode | ||||
|     darkMode = new DarkMode(); | ||||
|     document.getElementById('darkModeBtn').addEventListener('click', () => { | ||||
|         darkMode.toggle(); | ||||
|     }); | ||||
|      | ||||
|     // Initialize collection selector | ||||
|     collectionSelector = new CollectionSelector('collectionSelect', webdavClient); | ||||
|     collectionSelector.onChange = async (collection) => { | ||||
|         await fileTree.load(); | ||||
|     }; | ||||
|     await collectionSelector.load(); | ||||
|      | ||||
|     // Initialize file tree | ||||
|     fileTree = new FileTree('fileTree', webdavClient); | ||||
|     fileTree.onFileSelect = async (item) => { | ||||
|         await loadFile(item.path); | ||||
|     }; | ||||
|     await fileTree.load(); | ||||
|      | ||||
|     // Initialize editor | ||||
|     editor = new MarkdownEditor('editor', 'preview'); | ||||
|      | ||||
|     // Setup editor drop handler | ||||
|     const editorDropHandler = new EditorDropHandler( | ||||
|         document.querySelector('.editor-container'), | ||||
|         async (file) => { | ||||
|             await handleEditorFileDrop(file); | ||||
|         } | ||||
|     ); | ||||
|      | ||||
|     // Setup button handlers | ||||
|     document.getElementById('newBtn').addEventListener('click', () => { | ||||
|         newFile(); | ||||
|     }); | ||||
|      | ||||
|     document.getElementById('saveBtn').addEventListener('click', async () => { | ||||
|         await saveFile(); | ||||
|     }); | ||||
|      | ||||
|     document.getElementById('deleteBtn').addEventListener('click', async () => { | ||||
|         await deleteCurrentFile(); | ||||
|     }); | ||||
|      | ||||
|     // Setup context menu handlers | ||||
|     setupContextMenuHandlers(); | ||||
|      | ||||
|     // Initialize mermaid | ||||
|     mermaid.initialize({ startOnLoad: true, theme: darkMode.isDark ? 'dark' : 'default' }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * File Operations | ||||
|  */ | ||||
| async function loadFile(path) { | ||||
|     try { | ||||
|         const content = await webdavClient.get(path); | ||||
|         editor.setValue(content); | ||||
|         document.getElementById('filenameInput').value = path; | ||||
|         currentFilePath = path; | ||||
|         showNotification('File loaded', 'success'); | ||||
|     } catch (error) { | ||||
|         console.error('Failed to load file:', error); | ||||
|         showNotification('Failed to load file', 'error'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function newFile() { | ||||
|     editor.setValue('# New File\n\nStart typing...\n'); | ||||
|     document.getElementById('filenameInput').value = ''; | ||||
|     document.getElementById('filenameInput').focus(); | ||||
|     currentFilePath = null; | ||||
|     showNotification('New file', 'info'); | ||||
| } | ||||
|  | ||||
| async function saveFile() { | ||||
|     const filename = document.getElementById('filenameInput').value.trim(); | ||||
|     if (!filename) { | ||||
|         showNotification('Please enter a filename', 'warning'); | ||||
|         return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|         const content = editor.getValue(); | ||||
|         await webdavClient.put(filename, content); | ||||
|         currentFilePath = filename; | ||||
|         await fileTree.load(); | ||||
|         showNotification('Saved', 'success'); | ||||
|     } catch (error) { | ||||
|         console.error('Failed to save file:', error); | ||||
|         showNotification('Failed to save file', 'error'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function deleteCurrentFile() { | ||||
|     if (!currentFilePath) { | ||||
|         showNotification('No file selected', 'warning'); | ||||
|         return; | ||||
|     } | ||||
|      | ||||
|     if (!confirm(`Delete ${currentFilePath}?`)) { | ||||
|         return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|         await webdavClient.delete(currentFilePath); | ||||
|         await fileTree.load(); | ||||
|         newFile(); | ||||
|         showNotification('Deleted', 'success'); | ||||
|     } catch (error) { | ||||
|         console.error('Failed to delete file:', error); | ||||
|         showNotification('Failed to delete file', 'error'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Context Menu Handlers | ||||
|  */ | ||||
| function setupContextMenuHandlers() { | ||||
|     const menu = document.getElementById('contextMenu'); | ||||
|      | ||||
|     menu.addEventListener('click', async (e) => { | ||||
|         const item = e.target.closest('.context-menu-item'); | ||||
|         if (!item) return; | ||||
|          | ||||
|         const action = item.dataset.action; | ||||
|         const targetPath = menu.dataset.targetPath; | ||||
|         const isDir = menu.dataset.targetIsDir === 'true'; | ||||
|          | ||||
|         hideContextMenu(); | ||||
|          | ||||
|         await handleContextAction(action, targetPath, isDir); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| async function handleContextAction(action, targetPath, isDir) { | ||||
|     switch (action) { | ||||
|         case 'open': | ||||
|             if (!isDir) { | ||||
|                 await loadFile(targetPath); | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'new-file': | ||||
|             if (isDir) { | ||||
|                 const filename = prompt('Enter filename:'); | ||||
|                 if (filename) { | ||||
|                     await fileTree.createFile(targetPath, filename); | ||||
|                 } | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'new-folder': | ||||
|             if (isDir) { | ||||
|                 const foldername = prompt('Enter folder name:'); | ||||
|                 if (foldername) { | ||||
|                     await fileTree.createFolder(targetPath, foldername); | ||||
|                 } | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'upload': | ||||
|             if (isDir) { | ||||
|                 showFileUploadDialog(targetPath, async (path, file) => { | ||||
|                     await fileTree.uploadFile(path, file); | ||||
|                 }); | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'download': | ||||
|             if (isDir) { | ||||
|                 await fileTree.downloadFolder(targetPath); | ||||
|             } else { | ||||
|                 await fileTree.downloadFile(targetPath); | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'rename': | ||||
|             const newName = prompt('Enter new name:', targetPath.split('/').pop()); | ||||
|             if (newName) { | ||||
|                 const parentPath = targetPath.split('/').slice(0, -1).join('/'); | ||||
|                 const newPath = parentPath ? `${parentPath}/${newName}` : newName; | ||||
|                 try { | ||||
|                     await webdavClient.move(targetPath, newPath); | ||||
|                     await fileTree.load(); | ||||
|                     showNotification('Renamed', 'success'); | ||||
|                 } catch (error) { | ||||
|                     console.error('Failed to rename:', error); | ||||
|                     showNotification('Failed to rename', 'error'); | ||||
|                 } | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'copy': | ||||
|             clipboard = { path: targetPath, operation: 'copy' }; | ||||
|             showNotification('Copied to clipboard', 'info'); | ||||
|             updatePasteVisibility(); | ||||
|             break; | ||||
|              | ||||
|         case 'cut': | ||||
|             clipboard = { path: targetPath, operation: 'cut' }; | ||||
|             showNotification('Cut to clipboard', 'info'); | ||||
|             updatePasteVisibility(); | ||||
|             break; | ||||
|              | ||||
|         case 'paste': | ||||
|             if (clipboard && isDir) { | ||||
|                 const filename = clipboard.path.split('/').pop(); | ||||
|                 const destPath = `${targetPath}/${filename}`; | ||||
|                  | ||||
|                 try { | ||||
|                     if (clipboard.operation === 'copy') { | ||||
|                         await webdavClient.copy(clipboard.path, destPath); | ||||
|                         showNotification('Copied', 'success'); | ||||
|                     } else { | ||||
|                         await webdavClient.move(clipboard.path, destPath); | ||||
|                         showNotification('Moved', 'success'); | ||||
|                         clipboard = null; | ||||
|                         updatePasteVisibility(); | ||||
|                     } | ||||
|                     await fileTree.load(); | ||||
|                 } catch (error) { | ||||
|                     console.error('Failed to paste:', error); | ||||
|                     showNotification('Failed to paste', 'error'); | ||||
|                 } | ||||
|             } | ||||
|             break; | ||||
|              | ||||
|         case 'delete': | ||||
|             if (confirm(`Delete ${targetPath}?`)) { | ||||
|                 try { | ||||
|                     await webdavClient.delete(targetPath); | ||||
|                     await fileTree.load(); | ||||
|                     showNotification('Deleted', 'success'); | ||||
|                 } catch (error) { | ||||
|                     console.error('Failed to delete:', error); | ||||
|                     showNotification('Failed to delete', 'error'); | ||||
|                 } | ||||
|             } | ||||
|             break; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function updatePasteVisibility() { | ||||
|     const pasteItem = document.getElementById('pasteMenuItem'); | ||||
|     if (pasteItem) { | ||||
|         pasteItem.style.display = clipboard ? 'block' : 'none'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Editor File Drop Handler | ||||
|  */ | ||||
| async function handleEditorFileDrop(file) { | ||||
|     try { | ||||
|         // Get current file's directory | ||||
|         let targetDir = ''; | ||||
|         if (currentFilePath) { | ||||
|             const parts = currentFilePath.split('/'); | ||||
|             parts.pop(); // Remove filename | ||||
|             targetDir = parts.join('/'); | ||||
|         } | ||||
|          | ||||
|         // Upload file | ||||
|         const uploadedPath = await fileTree.uploadFile(targetDir, file); | ||||
|          | ||||
|         // Insert markdown link at cursor | ||||
|         const isImage = file.type.startsWith('image/'); | ||||
|         const link = isImage  | ||||
|             ? `` | ||||
|             : `[${file.name}](/${webdavClient.currentCollection}/${uploadedPath})`; | ||||
|          | ||||
|         editor.insertAtCursor(link); | ||||
|         showNotification(`Uploaded and inserted link`, 'success'); | ||||
|     } catch (error) { | ||||
|         console.error('Failed to handle file drop:', error); | ||||
|         showNotification('Failed to upload file', 'error'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Make showContextMenu global | ||||
| window.showContextMenu = showContextMenu; | ||||
|  | ||||
							
								
								
									
										273
									
								
								static/js/editor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								static/js/editor.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,273 @@ | ||||
| /** | ||||
|  * Editor Module | ||||
|  * Handles CodeMirror editor and markdown preview | ||||
|  */ | ||||
|  | ||||
| class MarkdownEditor { | ||||
|     constructor(editorId, previewId, filenameInputId) { | ||||
|         this.editorElement = document.getElementById(editorId); | ||||
|         this.previewElement = document.getElementById(previewId); | ||||
|         this.filenameInput = document.getElementById(filenameInputId); | ||||
|         this.currentFile = null; | ||||
|         this.webdavClient = null; | ||||
|          | ||||
|         this.initCodeMirror(); | ||||
|         this.initMarkdown(); | ||||
|         this.initMermaid(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initialize CodeMirror | ||||
|      */ | ||||
|     initCodeMirror() { | ||||
|         this.editor = CodeMirror(this.editorElement, { | ||||
|             mode: 'markdown', | ||||
|             theme: 'monokai', | ||||
|             lineNumbers: true, | ||||
|             lineWrapping: true, | ||||
|             autofocus: true, | ||||
|             extraKeys: { | ||||
|                 'Ctrl-S': () => this.save(), | ||||
|                 'Cmd-S': () => this.save() | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Update preview on change | ||||
|         this.editor.on('change', () => { | ||||
|             this.updatePreview(); | ||||
|         }); | ||||
|  | ||||
|         // Sync scroll | ||||
|         this.editor.on('scroll', () => { | ||||
|             this.syncScroll(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initialize markdown parser | ||||
|      */ | ||||
|     initMarkdown() { | ||||
|         this.marked = window.marked; | ||||
|         this.marked.setOptions({ | ||||
|             breaks: true, | ||||
|             gfm: true, | ||||
|             highlight: (code, lang) => { | ||||
|                 if (lang && window.Prism.languages[lang]) { | ||||
|                     return window.Prism.highlight(code, window.Prism.languages[lang], lang); | ||||
|                 } | ||||
|                 return code; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initialize Mermaid | ||||
|      */ | ||||
|     initMermaid() { | ||||
|         if (window.mermaid) { | ||||
|             window.mermaid.initialize({ | ||||
|                 startOnLoad: false, | ||||
|                 theme: document.body.classList.contains('dark-mode') ? 'dark' : 'default' | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set WebDAV client | ||||
|      */ | ||||
|     setWebDAVClient(client) { | ||||
|         this.webdavClient = client; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Load file | ||||
|      */ | ||||
|     async loadFile(path) { | ||||
|         try { | ||||
|             const content = await this.webdavClient.get(path); | ||||
|             this.currentFile = path; | ||||
|             this.filenameInput.value = path; | ||||
|             this.editor.setValue(content); | ||||
|             this.updatePreview(); | ||||
|              | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification(`Loaded ${path}`, 'info'); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.error('Failed to load file:', error); | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification('Failed to load file', 'danger'); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Save file | ||||
|      */ | ||||
|     async save() { | ||||
|         const path = this.filenameInput.value.trim(); | ||||
|         if (!path) { | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification('Please enter a filename', 'warning'); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const content = this.editor.getValue(); | ||||
|  | ||||
|         try { | ||||
|             await this.webdavClient.put(path, content); | ||||
|             this.currentFile = path; | ||||
|              | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification('✅ Saved', 'success'); | ||||
|             } | ||||
|  | ||||
|             // Trigger file tree reload | ||||
|             if (window.fileTree) { | ||||
|                 await window.fileTree.load(); | ||||
|                 window.fileTree.selectNode(path); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.error('Failed to save file:', error); | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification('Failed to save file', 'danger'); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create new file | ||||
|      */ | ||||
|     newFile() { | ||||
|         this.currentFile = null; | ||||
|         this.filenameInput.value = ''; | ||||
|         this.filenameInput.focus(); | ||||
|         this.editor.setValue(''); | ||||
|         this.updatePreview(); | ||||
|  | ||||
|         if (window.showNotification) { | ||||
|             window.showNotification('Enter filename and start typing', 'info'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Delete current file | ||||
|      */ | ||||
|     async deleteFile() { | ||||
|         if (!this.currentFile) { | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification('No file selected', 'warning'); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!confirm(`Delete ${this.currentFile}?`)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             await this.webdavClient.delete(this.currentFile); | ||||
|              | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification(`Deleted ${this.currentFile}`, 'success'); | ||||
|             } | ||||
|  | ||||
|             this.newFile(); | ||||
|  | ||||
|             // Trigger file tree reload | ||||
|             if (window.fileTree) { | ||||
|                 await window.fileTree.load(); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.error('Failed to delete file:', error); | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification('Failed to delete file', 'danger'); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update preview | ||||
|      */ | ||||
|     updatePreview() { | ||||
|         const markdown = this.editor.getValue(); | ||||
|         let html = this.marked.parse(markdown); | ||||
|  | ||||
|         // Process mermaid diagrams | ||||
|         html = html.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, (match, code) => { | ||||
|             const id = 'mermaid-' + Math.random().toString(36).substr(2, 9); | ||||
|             return `<div class="mermaid" id="${id}">${code}</div>`; | ||||
|         }); | ||||
|  | ||||
|         this.previewElement.innerHTML = html; | ||||
|  | ||||
|         // Render mermaid diagrams | ||||
|         if (window.mermaid) { | ||||
|             window.mermaid.init(undefined, this.previewElement.querySelectorAll('.mermaid')); | ||||
|         } | ||||
|  | ||||
|         // Highlight code blocks | ||||
|         if (window.Prism) { | ||||
|             window.Prism.highlightAllUnder(this.previewElement); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sync scroll between editor and preview | ||||
|      */ | ||||
|     syncScroll() { | ||||
|         const scrollInfo = this.editor.getScrollInfo(); | ||||
|         const scrollPercent = scrollInfo.top / (scrollInfo.height - scrollInfo.clientHeight); | ||||
|          | ||||
|         const previewHeight = this.previewElement.scrollHeight - this.previewElement.clientHeight; | ||||
|         this.previewElement.scrollTop = previewHeight * scrollPercent; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Handle image upload | ||||
|      */ | ||||
|     async uploadImage(file) { | ||||
|         try { | ||||
|             const filename = await this.webdavClient.uploadImage(file); | ||||
|             const imageUrl = `/fs/${this.webdavClient.currentCollection}/images/${filename}`; | ||||
|             const markdown = ``; | ||||
|              | ||||
|             // Insert at cursor | ||||
|             this.editor.replaceSelection(markdown); | ||||
|              | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification('Image uploaded', 'success'); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.error('Failed to upload image:', error); | ||||
|             if (window.showNotification) { | ||||
|                 window.showNotification('Failed to upload image', 'danger'); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get editor content | ||||
|      */ | ||||
|     getValue() { | ||||
|         return this.editor.getValue(); | ||||
|     } | ||||
|      | ||||
|     insertAtCursor(text) { | ||||
|         const doc = this.editor.getDoc(); | ||||
|         const cursor = doc.getCursor(); | ||||
|         doc.replaceRange(text, cursor); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set editor content | ||||
|      */ | ||||
|     setValue(content) { | ||||
|         this.editor.setValue(content); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Export for use in other modules | ||||
| window.MarkdownEditor = MarkdownEditor; | ||||
|  | ||||
							
								
								
									
										290
									
								
								static/js/file-tree.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								static/js/file-tree.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,290 @@ | ||||
| /** | ||||
|  * File Tree Component | ||||
|  * Manages the hierarchical file tree display and interactions | ||||
|  */ | ||||
|  | ||||
| class FileTree { | ||||
|     constructor(containerId, webdavClient) { | ||||
|         this.container = document.getElementById(containerId); | ||||
|         this.webdavClient = webdavClient; | ||||
|         this.tree = []; | ||||
|         this.selectedPath = null; | ||||
|         this.onFileSelect = null; | ||||
|         this.onFolderSelect = null; | ||||
|          | ||||
|         this.setupEventListeners(); | ||||
|     } | ||||
|      | ||||
|     setupEventListeners() { | ||||
|         // Click handler for tree nodes | ||||
|         this.container.addEventListener('click', (e) => { | ||||
|             const node = e.target.closest('.tree-node'); | ||||
|             if (!node) return; | ||||
|              | ||||
|             const path = node.dataset.path; | ||||
|             const isDir = node.dataset.isdir === 'true'; | ||||
|              | ||||
|             // Toggle folder | ||||
|             if (e.target.closest('.tree-toggle')) { | ||||
|                 this.toggleFolder(node); | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             // Select node | ||||
|             if (isDir) { | ||||
|                 this.selectFolder(path); | ||||
|             } else { | ||||
|                 this.selectFile(path); | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         // Context menu | ||||
|         this.container.addEventListener('contextmenu', (e) => { | ||||
|             const node = e.target.closest('.tree-node'); | ||||
|             if (!node) return; | ||||
|              | ||||
|             e.preventDefault(); | ||||
|             const path = node.dataset.path; | ||||
|             const isDir = node.dataset.isdir === 'true'; | ||||
|              | ||||
|             window.showContextMenu(e.clientX, e.clientY, { path, isDir }); | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     async load() { | ||||
|         try { | ||||
|             const items = await this.webdavClient.propfind('', 'infinity'); | ||||
|             this.tree = this.webdavClient.buildTree(items); | ||||
|             this.render(); | ||||
|         } catch (error) { | ||||
|             console.error('Failed to load file tree:', error); | ||||
|             showNotification('Failed to load files', 'error'); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     render() { | ||||
|         this.container.innerHTML = ''; | ||||
|         this.renderNodes(this.tree, this.container, 0); | ||||
|     } | ||||
|      | ||||
|     renderNodes(nodes, parentElement, level) { | ||||
|         nodes.forEach(node => { | ||||
|             const nodeElement = this.createNodeElement(node, level); | ||||
|             parentElement.appendChild(nodeElement); | ||||
|              | ||||
|             if (node.children && node.children.length > 0) { | ||||
|                 const childContainer = document.createElement('div'); | ||||
|                 childContainer.className = 'tree-children'; | ||||
|                 childContainer.style.display = 'none'; | ||||
|                 nodeElement.appendChild(childContainer); | ||||
|                 this.renderNodes(node.children, childContainer, level + 1); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     createNodeElement(node, level) { | ||||
|         const div = document.createElement('div'); | ||||
|         div.className = 'tree-node'; | ||||
|         div.dataset.path = node.path; | ||||
|         div.dataset.isdir = node.isDirectory; | ||||
|         div.style.paddingLeft = `${level * 20 + 10}px`; | ||||
|          | ||||
|         // Toggle arrow for folders | ||||
|         if (node.isDirectory) { | ||||
|             const toggle = document.createElement('span'); | ||||
|             toggle.className = 'tree-toggle'; | ||||
|             toggle.innerHTML = '<i class="bi bi-chevron-right"></i>'; | ||||
|             div.appendChild(toggle); | ||||
|         } else { | ||||
|             const spacer = document.createElement('span'); | ||||
|             spacer.className = 'tree-spacer'; | ||||
|             spacer.style.width = '16px'; | ||||
|             spacer.style.display = 'inline-block'; | ||||
|             div.appendChild(spacer); | ||||
|         } | ||||
|          | ||||
|         // Icon | ||||
|         const icon = document.createElement('i'); | ||||
|         if (node.isDirectory) { | ||||
|             icon.className = 'bi bi-folder-fill'; | ||||
|             icon.style.color = '#dcb67a'; | ||||
|         } else { | ||||
|             icon.className = 'bi bi-file-earmark-text'; | ||||
|             icon.style.color = '#6a9fb5'; | ||||
|         } | ||||
|         div.appendChild(icon); | ||||
|          | ||||
|         // Name | ||||
|         const name = document.createElement('span'); | ||||
|         name.className = 'tree-name'; | ||||
|         name.textContent = node.name; | ||||
|         div.appendChild(name); | ||||
|          | ||||
|         // Size for files | ||||
|         if (!node.isDirectory && node.size) { | ||||
|             const size = document.createElement('span'); | ||||
|             size.className = 'tree-size'; | ||||
|             size.textContent = this.formatSize(node.size); | ||||
|             div.appendChild(size); | ||||
|         } | ||||
|          | ||||
|         return div; | ||||
|     } | ||||
|      | ||||
|     toggleFolder(nodeElement) { | ||||
|         const childContainer = nodeElement.querySelector('.tree-children'); | ||||
|         if (!childContainer) return; | ||||
|          | ||||
|         const toggle = nodeElement.querySelector('.tree-toggle i'); | ||||
|         const isExpanded = childContainer.style.display !== 'none'; | ||||
|          | ||||
|         if (isExpanded) { | ||||
|             childContainer.style.display = 'none'; | ||||
|             toggle.className = 'bi bi-chevron-right'; | ||||
|         } else { | ||||
|             childContainer.style.display = 'block'; | ||||
|             toggle.className = 'bi bi-chevron-down'; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     selectFile(path) { | ||||
|         this.selectedPath = path; | ||||
|         this.updateSelection(); | ||||
|         if (this.onFileSelect) { | ||||
|             this.onFileSelect({ path, isDirectory: false }); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     selectFolder(path) { | ||||
|         this.selectedPath = path; | ||||
|         this.updateSelection(); | ||||
|         if (this.onFolderSelect) { | ||||
|             this.onFolderSelect({ path, isDirectory: true }); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     updateSelection() { | ||||
|         // Remove previous selection | ||||
|         this.container.querySelectorAll('.tree-node').forEach(node => { | ||||
|             node.classList.remove('selected'); | ||||
|         }); | ||||
|          | ||||
|         // Add selection to current | ||||
|         if (this.selectedPath) { | ||||
|             const node = this.container.querySelector(`[data-path="${this.selectedPath}"]`); | ||||
|             if (node) { | ||||
|                 node.classList.add('selected'); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     formatSize(bytes) { | ||||
|         if (bytes === 0) return '0 B'; | ||||
|         const k = 1024; | ||||
|         const sizes = ['B', 'KB', 'MB', 'GB']; | ||||
|         const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||||
|         return Math.round(bytes / Math.pow(k, i) * 10) / 10 + ' ' + sizes[i]; | ||||
|     } | ||||
|      | ||||
|     async createFile(parentPath, filename) { | ||||
|         try { | ||||
|             const fullPath = parentPath ? `${parentPath}/${filename}` : filename; | ||||
|             await this.webdavClient.put(fullPath, '# New File\n\nStart typing...\n'); | ||||
|             await this.load(); | ||||
|             showNotification('File created', 'success'); | ||||
|             return fullPath; | ||||
|         } catch (error) { | ||||
|             console.error('Failed to create file:', error); | ||||
|             showNotification('Failed to create file', 'error'); | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     async createFolder(parentPath, foldername) { | ||||
|         try { | ||||
|             const fullPath = parentPath ? `${parentPath}/${foldername}` : foldername; | ||||
|             await this.webdavClient.mkcol(fullPath); | ||||
|             await this.load(); | ||||
|             showNotification('Folder created', 'success'); | ||||
|             return fullPath; | ||||
|         } catch (error) { | ||||
|             console.error('Failed to create folder:', error); | ||||
|             showNotification('Failed to create folder', 'error'); | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     async uploadFile(parentPath, file) { | ||||
|         try { | ||||
|             const fullPath = parentPath ? `${parentPath}/${file.name}` : file.name; | ||||
|             const content = await file.arrayBuffer(); | ||||
|             await this.webdavClient.putBinary(fullPath, content); | ||||
|             await this.load(); | ||||
|             showNotification(`Uploaded ${file.name}`, 'success'); | ||||
|             return fullPath; | ||||
|         } catch (error) { | ||||
|             console.error('Failed to upload file:', error); | ||||
|             showNotification('Failed to upload file', 'error'); | ||||
|             throw error; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     async downloadFile(path) { | ||||
|         try { | ||||
|             const content = await this.webdavClient.get(path); | ||||
|             const filename = path.split('/').pop(); | ||||
|             this.triggerDownload(content, filename); | ||||
|             showNotification('Downloaded', 'success'); | ||||
|         } catch (error) { | ||||
|             console.error('Failed to download file:', error); | ||||
|             showNotification('Failed to download file', 'error'); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     async downloadFolder(path) { | ||||
|         try { | ||||
|             showNotification('Creating zip...', 'info'); | ||||
|             // Get all files in folder | ||||
|             const items = await this.webdavClient.propfind(path, 'infinity'); | ||||
|             const files = items.filter(item => !item.isDirectory); | ||||
|              | ||||
|             // Use JSZip to create zip file | ||||
|             const JSZip = window.JSZip; | ||||
|             if (!JSZip) { | ||||
|                 throw new Error('JSZip not loaded'); | ||||
|             } | ||||
|              | ||||
|             const zip = new JSZip(); | ||||
|             const folder = zip.folder(path.split('/').pop() || 'download'); | ||||
|              | ||||
|             // Add all files to zip | ||||
|             for (const file of files) { | ||||
|                 const content = await this.webdavClient.get(file.path); | ||||
|                 const relativePath = file.path.replace(path + '/', ''); | ||||
|                 folder.file(relativePath, content); | ||||
|             } | ||||
|              | ||||
|             // Generate zip | ||||
|             const zipBlob = await zip.generateAsync({ type: 'blob' }); | ||||
|             const zipFilename = `${path.split('/').pop() || 'download'}.zip`; | ||||
|             this.triggerDownload(zipBlob, zipFilename); | ||||
|             showNotification('Downloaded', 'success'); | ||||
|         } catch (error) { | ||||
|             console.error('Failed to download folder:', error); | ||||
|             showNotification('Failed to download folder', 'error'); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     triggerDownload(content, filename) { | ||||
|         const blob = content instanceof Blob ? content : new Blob([content]); | ||||
|         const url = URL.createObjectURL(blob); | ||||
|         const a = document.createElement('a'); | ||||
|         a.href = url; | ||||
|         a.download = filename; | ||||
|         document.body.appendChild(a); | ||||
|         a.click(); | ||||
|         document.body.removeChild(a); | ||||
|         URL.revokeObjectURL(url); | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										256
									
								
								static/js/ui-utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								static/js/ui-utils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,256 @@ | ||||
| /** | ||||
|  * UI Utilities Module | ||||
|  * Toast notifications, context menu, dark mode, file upload dialog | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Show toast notification | ||||
|  */ | ||||
| function showNotification(message, type = 'info') { | ||||
|     const container = document.getElementById('toastContainer') || createToastContainer(); | ||||
|      | ||||
|     const toast = document.createElement('div'); | ||||
|     const bgClass = type === 'error' ? 'danger' : type === 'success' ? 'success' : type === 'warning' ? 'warning' : 'primary'; | ||||
|     toast.className = `toast align-items-center text-white bg-${bgClass} border-0`; | ||||
|     toast.setAttribute('role', 'alert'); | ||||
|      | ||||
|     toast.innerHTML = ` | ||||
|         <div class="d-flex"> | ||||
|             <div class="toast-body">${message}</div> | ||||
|             <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button> | ||||
|         </div> | ||||
|     `; | ||||
|      | ||||
|     container.appendChild(toast); | ||||
|      | ||||
|     const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); | ||||
|     bsToast.show(); | ||||
|      | ||||
|     toast.addEventListener('hidden.bs.toast', () => { | ||||
|         toast.remove(); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function createToastContainer() { | ||||
|     const container = document.createElement('div'); | ||||
|     container.id = 'toastContainer'; | ||||
|     container.className = 'toast-container position-fixed top-0 end-0 p-3'; | ||||
|     container.style.zIndex = '9999'; | ||||
|     document.body.appendChild(container); | ||||
|     return container; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Enhanced Context Menu | ||||
|  */ | ||||
| function showContextMenu(x, y, target) { | ||||
|     const menu = document.getElementById('contextMenu'); | ||||
|     if (!menu) return; | ||||
|      | ||||
|     // Store target | ||||
|     menu.dataset.targetPath = target.path; | ||||
|     menu.dataset.targetIsDir = target.isDir; | ||||
|      | ||||
|     // Show/hide menu items based on target type | ||||
|     const newFileItem = menu.querySelector('[data-action="new-file"]'); | ||||
|     const newFolderItem = menu.querySelector('[data-action="new-folder"]'); | ||||
|     const uploadItem = menu.querySelector('[data-action="upload"]'); | ||||
|     const downloadItem = menu.querySelector('[data-action="download"]'); | ||||
|      | ||||
|     if (target.isDir) { | ||||
|         // Folder context menu | ||||
|         if (newFileItem) newFileItem.style.display = 'block'; | ||||
|         if (newFolderItem) newFolderItem.style.display = 'block'; | ||||
|         if (uploadItem) uploadItem.style.display = 'block'; | ||||
|         if (downloadItem) downloadItem.style.display = 'block'; | ||||
|     } else { | ||||
|         // File context menu | ||||
|         if (newFileItem) newFileItem.style.display = 'none'; | ||||
|         if (newFolderItem) newFolderItem.style.display = 'none'; | ||||
|         if (uploadItem) uploadItem.style.display = 'none'; | ||||
|         if (downloadItem) downloadItem.style.display = 'block'; | ||||
|     } | ||||
|      | ||||
|     // Position menu | ||||
|     menu.style.display = 'block'; | ||||
|     menu.style.left = x + 'px'; | ||||
|     menu.style.top = y + 'px'; | ||||
|      | ||||
|     // Adjust if off-screen | ||||
|     const rect = menu.getBoundingClientRect(); | ||||
|     if (rect.right > window.innerWidth) { | ||||
|         menu.style.left = (window.innerWidth - rect.width - 10) + 'px'; | ||||
|     } | ||||
|     if (rect.bottom > window.innerHeight) { | ||||
|         menu.style.top = (window.innerHeight - rect.height - 10) + 'px'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function hideContextMenu() { | ||||
|     const menu = document.getElementById('contextMenu'); | ||||
|     if (menu) { | ||||
|         menu.style.display = 'none'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Hide context menu on click outside | ||||
| document.addEventListener('click', (e) => { | ||||
|     if (!e.target.closest('#contextMenu')) { | ||||
|         hideContextMenu(); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * File Upload Dialog | ||||
|  */ | ||||
| function showFileUploadDialog(targetPath, onUpload) { | ||||
|     const input = document.createElement('input'); | ||||
|     input.type = 'file'; | ||||
|     input.multiple = true; | ||||
|      | ||||
|     input.addEventListener('change', async (e) => { | ||||
|         const files = Array.from(e.target.files); | ||||
|         if (files.length === 0) return; | ||||
|          | ||||
|         for (const file of files) { | ||||
|             try { | ||||
|                 await onUpload(targetPath, file); | ||||
|             } catch (error) { | ||||
|                 console.error('Upload failed:', error); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     input.click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Dark Mode Manager | ||||
|  */ | ||||
| class DarkMode { | ||||
|     constructor() { | ||||
|         this.isDark = localStorage.getItem('darkMode') === 'true'; | ||||
|         this.apply(); | ||||
|     } | ||||
|      | ||||
|     toggle() { | ||||
|         this.isDark = !this.isDark; | ||||
|         localStorage.setItem('darkMode', this.isDark); | ||||
|         this.apply(); | ||||
|     } | ||||
|      | ||||
|     apply() { | ||||
|         if (this.isDark) { | ||||
|             document.body.classList.add('dark-mode'); | ||||
|             const btn = document.getElementById('darkModeBtn'); | ||||
|             if (btn) btn.textContent = '☀️'; | ||||
|              | ||||
|             // Update mermaid theme | ||||
|             if (window.mermaid) { | ||||
|                 mermaid.initialize({ theme: 'dark' }); | ||||
|             } | ||||
|         } else { | ||||
|             document.body.classList.remove('dark-mode'); | ||||
|             const btn = document.getElementById('darkModeBtn'); | ||||
|             if (btn) btn.textContent = '🌙'; | ||||
|              | ||||
|             // Update mermaid theme | ||||
|             if (window.mermaid) { | ||||
|                 mermaid.initialize({ theme: 'default' }); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Collection Selector | ||||
|  */ | ||||
| class CollectionSelector { | ||||
|     constructor(selectId, webdavClient) { | ||||
|         this.select = document.getElementById(selectId); | ||||
|         this.webdavClient = webdavClient; | ||||
|         this.onChange = null; | ||||
|     } | ||||
|      | ||||
|     async load() { | ||||
|         try { | ||||
|             const collections = await this.webdavClient.getCollections(); | ||||
|             this.select.innerHTML = ''; | ||||
|              | ||||
|             collections.forEach(collection => { | ||||
|                 const option = document.createElement('option'); | ||||
|                 option.value = collection; | ||||
|                 option.textContent = collection; | ||||
|                 this.select.appendChild(option); | ||||
|             }); | ||||
|              | ||||
|             // Select first collection | ||||
|             if (collections.length > 0) { | ||||
|                 this.select.value = collections[0]; | ||||
|                 this.webdavClient.setCollection(collections[0]); | ||||
|                 if (this.onChange) { | ||||
|                     this.onChange(collections[0]); | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             // Add change listener | ||||
|             this.select.addEventListener('change', () => { | ||||
|                 const collection = this.select.value; | ||||
|                 this.webdavClient.setCollection(collection); | ||||
|                 if (this.onChange) { | ||||
|                     this.onChange(collection); | ||||
|                 } | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             console.error('Failed to load collections:', error); | ||||
|             showNotification('Failed to load collections', 'error'); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Editor Drop Handler | ||||
|  * Handles file drops into the editor | ||||
|  */ | ||||
| class EditorDropHandler { | ||||
|     constructor(editorElement, onFileDrop) { | ||||
|         this.editorElement = editorElement; | ||||
|         this.onFileDrop = onFileDrop; | ||||
|         this.setupHandlers(); | ||||
|     } | ||||
|      | ||||
|     setupHandlers() { | ||||
|         this.editorElement.addEventListener('dragover', (e) => { | ||||
|             e.preventDefault(); | ||||
|             e.stopPropagation(); | ||||
|             this.editorElement.classList.add('drag-over'); | ||||
|         }); | ||||
|          | ||||
|         this.editorElement.addEventListener('dragleave', (e) => { | ||||
|             e.preventDefault(); | ||||
|             e.stopPropagation(); | ||||
|             this.editorElement.classList.remove('drag-over'); | ||||
|         }); | ||||
|          | ||||
|         this.editorElement.addEventListener('drop', async (e) => { | ||||
|             e.preventDefault(); | ||||
|             e.stopPropagation(); | ||||
|             this.editorElement.classList.remove('drag-over'); | ||||
|              | ||||
|             const files = Array.from(e.dataTransfer.files); | ||||
|             if (files.length === 0) return; | ||||
|              | ||||
|             for (const file of files) { | ||||
|                 try { | ||||
|                     if (this.onFileDrop) { | ||||
|                         await this.onFileDrop(file); | ||||
|                     } | ||||
|                 } catch (error) { | ||||
|                     console.error('Drop failed:', error); | ||||
|                     showNotification(`Failed to upload ${file.name}`, 'error'); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										239
									
								
								static/js/webdav-client.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								static/js/webdav-client.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,239 @@ | ||||
| /** | ||||
|  * WebDAV Client | ||||
|  * Handles all WebDAV protocol operations | ||||
|  */ | ||||
|  | ||||
| class WebDAVClient { | ||||
|     constructor(baseUrl) { | ||||
|         this.baseUrl = baseUrl; | ||||
|         this.currentCollection = null; | ||||
|     } | ||||
|      | ||||
|     setCollection(collection) { | ||||
|         this.currentCollection = collection; | ||||
|     } | ||||
|      | ||||
|     getFullUrl(path) { | ||||
|         if (!this.currentCollection) { | ||||
|             throw new Error('No collection selected'); | ||||
|         } | ||||
|         const cleanPath = path.startsWith('/') ? path.slice(1) : path; | ||||
|         return `${this.baseUrl}${this.currentCollection}/${cleanPath}`; | ||||
|     } | ||||
|      | ||||
|     async getCollections() { | ||||
|         const response = await fetch(this.baseUrl); | ||||
|         if (!response.ok) { | ||||
|             throw new Error('Failed to get collections'); | ||||
|         } | ||||
|         return await response.json(); | ||||
|     } | ||||
|      | ||||
|     async propfind(path = '', depth = '1') { | ||||
|         const url = this.getFullUrl(path); | ||||
|         const response = await fetch(url, { | ||||
|             method: 'PROPFIND', | ||||
|             headers: { | ||||
|                 'Depth': depth, | ||||
|                 'Content-Type': 'application/xml' | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`PROPFIND failed: ${response.statusText}`); | ||||
|         } | ||||
|          | ||||
|         const xml = await response.text(); | ||||
|         return this.parseMultiStatus(xml); | ||||
|     } | ||||
|      | ||||
|     async get(path) { | ||||
|         const url = this.getFullUrl(path); | ||||
|         const response = await fetch(url); | ||||
|          | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`GET failed: ${response.statusText}`); | ||||
|         } | ||||
|          | ||||
|         return await response.text(); | ||||
|     } | ||||
|      | ||||
|     async getBinary(path) { | ||||
|         const url = this.getFullUrl(path); | ||||
|         const response = await fetch(url); | ||||
|          | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`GET failed: ${response.statusText}`); | ||||
|         } | ||||
|          | ||||
|         return await response.blob(); | ||||
|     } | ||||
|      | ||||
|     async put(path, content) { | ||||
|         const url = this.getFullUrl(path); | ||||
|         const response = await fetch(url, { | ||||
|             method: 'PUT', | ||||
|             headers: { | ||||
|                 'Content-Type': 'text/plain' | ||||
|             }, | ||||
|             body: content | ||||
|         }); | ||||
|          | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`PUT failed: ${response.statusText}`); | ||||
|         } | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     async putBinary(path, content) { | ||||
|         const url = this.getFullUrl(path); | ||||
|         const response = await fetch(url, { | ||||
|             method: 'PUT', | ||||
|             body: content | ||||
|         }); | ||||
|          | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`PUT failed: ${response.statusText}`); | ||||
|         } | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     async delete(path) { | ||||
|         const url = this.getFullUrl(path); | ||||
|         const response = await fetch(url, { | ||||
|             method: 'DELETE' | ||||
|         }); | ||||
|          | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`DELETE failed: ${response.statusText}`); | ||||
|         } | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     async copy(sourcePath, destPath) { | ||||
|         const sourceUrl = this.getFullUrl(sourcePath); | ||||
|         const destUrl = this.getFullUrl(destPath); | ||||
|          | ||||
|         const response = await fetch(sourceUrl, { | ||||
|             method: 'COPY', | ||||
|             headers: { | ||||
|                 'Destination': destUrl | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`COPY failed: ${response.statusText}`); | ||||
|         } | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     async move(sourcePath, destPath) { | ||||
|         const sourceUrl = this.getFullUrl(sourcePath); | ||||
|         const destUrl = this.getFullUrl(destPath); | ||||
|          | ||||
|         const response = await fetch(sourceUrl, { | ||||
|             method: 'MOVE', | ||||
|             headers: { | ||||
|                 'Destination': destUrl | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`MOVE failed: ${response.statusText}`); | ||||
|         } | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     async mkcol(path) { | ||||
|         const url = this.getFullUrl(path); | ||||
|         const response = await fetch(url, { | ||||
|             method: 'MKCOL' | ||||
|         }); | ||||
|          | ||||
|         if (!response.ok && response.status !== 405) { // 405 means already exists | ||||
|             throw new Error(`MKCOL failed: ${response.statusText}`); | ||||
|         } | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     parseMultiStatus(xml) { | ||||
|         const parser = new DOMParser(); | ||||
|         const doc = parser.parseFromString(xml, 'text/xml'); | ||||
|         const responses = doc.getElementsByTagNameNS('DAV:', 'response'); | ||||
|          | ||||
|         const items = []; | ||||
|         for (let i = 0; i < responses.length; i++) { | ||||
|             const response = responses[i]; | ||||
|             const href = response.getElementsByTagNameNS('DAV:', 'href')[0].textContent; | ||||
|             const propstat = response.getElementsByTagNameNS('DAV:', 'propstat')[0]; | ||||
|             const prop = propstat.getElementsByTagNameNS('DAV:', 'prop')[0]; | ||||
|              | ||||
|             // Check if it's a collection (directory) | ||||
|             const resourcetype = prop.getElementsByTagNameNS('DAV:', 'resourcetype')[0]; | ||||
|             const isDirectory = resourcetype.getElementsByTagNameNS('DAV:', 'collection').length > 0; | ||||
|              | ||||
|             // Get size | ||||
|             const contentlengthEl = prop.getElementsByTagNameNS('DAV:', 'getcontentlength')[0]; | ||||
|             const size = contentlengthEl ? parseInt(contentlengthEl.textContent) : 0; | ||||
|              | ||||
|             // Extract path relative to collection | ||||
|             const pathParts = href.split(`/${this.currentCollection}/`); | ||||
|             const relativePath = pathParts.length > 1 ? pathParts[1] : ''; | ||||
|              | ||||
|             // Skip the collection root itself | ||||
|             if (!relativePath) continue; | ||||
|              | ||||
|             // Remove trailing slash from directories | ||||
|             const cleanPath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath; | ||||
|              | ||||
|             items.push({ | ||||
|                 path: cleanPath, | ||||
|                 name: cleanPath.split('/').pop(), | ||||
|                 isDirectory, | ||||
|                 size | ||||
|             }); | ||||
|         } | ||||
|          | ||||
|         return items; | ||||
|     } | ||||
|      | ||||
|     buildTree(items) { | ||||
|         const root = []; | ||||
|         const map = {}; | ||||
|          | ||||
|         // Sort items by path depth and name | ||||
|         items.sort((a, b) => { | ||||
|             const depthA = a.path.split('/').length; | ||||
|             const depthB = b.path.split('/').length; | ||||
|             if (depthA !== depthB) return depthA - depthB; | ||||
|             return a.path.localeCompare(b.path); | ||||
|         }); | ||||
|          | ||||
|         items.forEach(item => { | ||||
|             const parts = item.path.split('/'); | ||||
|             const parentPath = parts.slice(0, -1).join('/'); | ||||
|              | ||||
|             const node = { | ||||
|                 ...item, | ||||
|                 children: [] | ||||
|             }; | ||||
|              | ||||
|             map[item.path] = node; | ||||
|              | ||||
|             if (parentPath && map[parentPath]) { | ||||
|                 map[parentPath].children.push(node); | ||||
|             } else { | ||||
|                 root.push(node); | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         return root; | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										594
									
								
								static/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										594
									
								
								static/style.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,594 @@ | ||||
| /* CSS Variables for theming */ | ||||
| :root { | ||||
|     --bg-primary: #ffffff; | ||||
|     --bg-secondary: #f8f9fa; | ||||
|     --bg-tertiary: #f6f8fa; | ||||
|     --text-primary: #24292e; | ||||
|     --text-secondary: #6a737d; | ||||
|     --border-color: #dee2e6; | ||||
|     --border-light: #eaecef; | ||||
|     --link-color: #0366d6; | ||||
|     --accent-color: #0d6efd; | ||||
|     --code-bg: rgba(27, 31, 35, 0.05); | ||||
|     --scrollbar-track: #f1f1f1; | ||||
|     --scrollbar-thumb: #888; | ||||
|     --scrollbar-thumb-hover: #555; | ||||
| } | ||||
|  | ||||
| /* Dark mode variables */ | ||||
| body.dark-mode { | ||||
|     --bg-primary: #0d1117; | ||||
|     --bg-secondary: #161b22; | ||||
|     --bg-tertiary: #1c2128; | ||||
|     --text-primary: #e6edf3; | ||||
|     --text-secondary: #8b949e; | ||||
|     --border-color: #30363d; | ||||
|     --border-light: #21262d; | ||||
|     --link-color: #58a6ff; | ||||
|     --accent-color: #1f6feb; | ||||
|     --code-bg: rgba(110, 118, 129, 0.15); | ||||
|     --scrollbar-track: #161b22; | ||||
|     --scrollbar-thumb: #484f58; | ||||
|     --scrollbar-thumb-hover: #6e7681; | ||||
| } | ||||
|  | ||||
| /* Global styles */ | ||||
| html, body { | ||||
|     height: 100%; | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
|     overflow: hidden; | ||||
| } | ||||
|  | ||||
| body { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     background-color: var(--bg-primary); | ||||
|     color: var(--text-primary); | ||||
|     transition: background-color 0.3s ease, color 0.3s ease; | ||||
| } | ||||
|  | ||||
| .container-fluid { | ||||
|     flex: 1; | ||||
|     padding: 0; | ||||
|     overflow: hidden; | ||||
| } | ||||
|  | ||||
| .row { | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| /* Navbar */ | ||||
| .navbar { | ||||
|     z-index: 1000; | ||||
|     background-color: var(--bg-secondary) !important; | ||||
|     border-bottom: 1px solid var(--border-color); | ||||
|     transition: background-color 0.3s ease; | ||||
| } | ||||
|  | ||||
| .navbar-brand { | ||||
|     color: var(--text-primary) !important; | ||||
| } | ||||
|  | ||||
| .btn { | ||||
|     transition: all 0.2s ease; | ||||
| } | ||||
|  | ||||
| /* Dark mode toggle button */ | ||||
| .dark-mode-toggle { | ||||
|     background: none; | ||||
|     border: 1px solid var(--border-color); | ||||
|     color: var(--text-primary); | ||||
|     padding: 0.375rem 0.75rem; | ||||
|     border-radius: 0.25rem; | ||||
|     cursor: pointer; | ||||
|     font-size: 1.2rem; | ||||
|     transition: all 0.2s ease; | ||||
| } | ||||
|  | ||||
| .dark-mode-toggle:hover { | ||||
|     background-color: var(--bg-tertiary); | ||||
| } | ||||
|  | ||||
| /* Sidebar */ | ||||
| .sidebar { | ||||
|     height: calc(100vh - 56px); | ||||
|     overflow-y: auto; | ||||
|     padding: 0; | ||||
|     background-color: var(--bg-secondary); | ||||
|     border-right: 1px solid var(--border-color); | ||||
|     transition: background-color 0.3s ease; | ||||
| } | ||||
|  | ||||
| .sidebar h6 { | ||||
|     color: var(--text-primary); | ||||
|     background-color: var(--bg-tertiary); | ||||
|     transition: background-color 0.3s ease; | ||||
| } | ||||
|  | ||||
| .list-group-item { | ||||
|     cursor: pointer; | ||||
|     border-radius: 0; | ||||
|     border-left: 0; | ||||
|     border-right: 0; | ||||
|     background-color: var(--bg-secondary); | ||||
|     color: var(--text-primary); | ||||
|     border-color: var(--border-color); | ||||
|     transition: background-color 0.2s ease, color 0.2s ease; | ||||
| } | ||||
|  | ||||
| .list-group-item:first-child { | ||||
|     border-top: 0; | ||||
| } | ||||
|  | ||||
| .list-group-item.active { | ||||
|     background-color: var(--accent-color); | ||||
|     border-color: var(--accent-color); | ||||
|     color: #ffffff; | ||||
| } | ||||
|  | ||||
| .list-group-item:hover:not(.active) { | ||||
|     background-color: var(--bg-tertiary); | ||||
| } | ||||
|  | ||||
| /* Editor pane */ | ||||
| .editor-pane { | ||||
|     height: calc(100vh - 56px); | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     padding: 0; | ||||
|     border-right: 1px solid var(--border-color); | ||||
|     background-color: var(--bg-primary); | ||||
|     transition: background-color 0.3s ease; | ||||
| } | ||||
|  | ||||
| .editor-pane input[type="text"] { | ||||
|     background-color: var(--bg-primary); | ||||
|     color: var(--text-primary); | ||||
|     border-color: var(--border-color); | ||||
|     transition: background-color 0.3s ease, color 0.3s ease; | ||||
| } | ||||
|  | ||||
| .editor-pane input[type="text"]:focus { | ||||
|     background-color: var(--bg-primary); | ||||
|     color: var(--text-primary); | ||||
|     border-color: var(--accent-color); | ||||
| } | ||||
|  | ||||
| #editorContainer { | ||||
|     flex: 1; | ||||
|     overflow: hidden; | ||||
| } | ||||
|  | ||||
| .CodeMirror { | ||||
|     height: 100%; | ||||
|     font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | ||||
|     font-size: 14px; | ||||
|     line-height: 1.6; | ||||
|     transition: all 0.2s ease; | ||||
| } | ||||
|  | ||||
| .CodeMirror.drag-over { | ||||
|     border: 3px dashed var(--accent-color); | ||||
|     background-color: rgba(13, 110, 253, 0.05); | ||||
| } | ||||
|  | ||||
| /* Dark mode CodeMirror adjustments */ | ||||
| body.dark-mode .CodeMirror { | ||||
|     background-color: #1c2128; | ||||
|     color: #e6edf3; | ||||
| } | ||||
|  | ||||
| body.dark-mode .CodeMirror-gutters { | ||||
|     background-color: #161b22; | ||||
|     border-right: 1px solid #30363d; | ||||
| } | ||||
|  | ||||
| body.dark-mode .CodeMirror-linenumber { | ||||
|     color: #8b949e; | ||||
| } | ||||
|  | ||||
| /* Preview pane */ | ||||
| .preview-pane { | ||||
|     height: calc(100vh - 56px); | ||||
|     overflow-y: auto; | ||||
|     background-color: var(--bg-primary); | ||||
|     padding: 0; | ||||
|     transition: background-color 0.3s ease; | ||||
| } | ||||
|  | ||||
| #preview { | ||||
|     padding: 20px; | ||||
|     max-width: 100%; | ||||
|     word-wrap: break-word; | ||||
|     color: var(--text-primary); | ||||
|     transition: color 0.3s ease; | ||||
| } | ||||
|  | ||||
| /* Markdown preview styles */ | ||||
| #preview h1, #preview h2, #preview h3, #preview h4, #preview h5, #preview h6 { | ||||
|     margin-top: 24px; | ||||
|     margin-bottom: 16px; | ||||
|     font-weight: 600; | ||||
|     line-height: 1.25; | ||||
|     color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| #preview h1 { | ||||
|     font-size: 2em; | ||||
|     border-bottom: 1px solid var(--border-light); | ||||
|     padding-bottom: 0.3em; | ||||
| } | ||||
|  | ||||
| #preview h2 { | ||||
|     font-size: 1.5em; | ||||
|     border-bottom: 1px solid var(--border-light); | ||||
|     padding-bottom: 0.3em; | ||||
| } | ||||
|  | ||||
| #preview h3 { | ||||
|     font-size: 1.25em; | ||||
| } | ||||
|  | ||||
| #preview p { | ||||
|     margin-bottom: 16px; | ||||
|     line-height: 1.6; | ||||
|     color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| #preview code { | ||||
|     background-color: var(--code-bg); | ||||
|     border-radius: 3px; | ||||
|     font-size: 85%; | ||||
|     margin: 0; | ||||
|     padding: 0.2em 0.4em; | ||||
|     font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | ||||
|     color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| #preview pre { | ||||
|     background-color: #2d2d2d !important; | ||||
|     border-radius: 6px; | ||||
|     padding: 16px; | ||||
|     overflow: auto; | ||||
|     line-height: 1.45; | ||||
|     margin-bottom: 16px; | ||||
| } | ||||
|  | ||||
| #preview pre code { | ||||
|     background-color: transparent !important; | ||||
|     border: 0; | ||||
|     display: block; | ||||
|     line-height: inherit; | ||||
|     margin: 0; | ||||
|     overflow: visible; | ||||
|     padding: 0 !important; | ||||
|     word-wrap: normal; | ||||
|     font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | ||||
|     font-size: 14px; | ||||
| } | ||||
|  | ||||
| /* Prism theme override for better visibility */ | ||||
| #preview pre[class*="language-"] { | ||||
|     background-color: #2d2d2d !important; | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| #preview code[class*="language-"] { | ||||
|     background-color: transparent !important; | ||||
| } | ||||
|  | ||||
| #preview blockquote { | ||||
|     border-left: 4px solid var(--border-light); | ||||
|     color: var(--text-secondary); | ||||
|     padding-left: 16px; | ||||
|     margin-left: 0; | ||||
|     margin-bottom: 16px; | ||||
| } | ||||
|  | ||||
| #preview ul, #preview ol { | ||||
|     margin-bottom: 16px; | ||||
|     padding-left: 2em; | ||||
| } | ||||
|  | ||||
| #preview li { | ||||
|     margin-bottom: 4px; | ||||
| } | ||||
|  | ||||
| #preview table { | ||||
|     border-collapse: collapse; | ||||
|     width: 100%; | ||||
|     margin-bottom: 16px; | ||||
| } | ||||
|  | ||||
| #preview table th, | ||||
| #preview table td { | ||||
|     border: 1px solid var(--border-light); | ||||
|     padding: 6px 13px; | ||||
| } | ||||
|  | ||||
| #preview table th { | ||||
|     background-color: var(--bg-tertiary); | ||||
|     font-weight: 600; | ||||
| } | ||||
|  | ||||
| #preview table tr:nth-child(2n) { | ||||
|     background-color: var(--bg-tertiary); | ||||
| } | ||||
|  | ||||
| #preview img { | ||||
|     max-width: 100%; | ||||
|     height: auto; | ||||
|     margin: 16px 0; | ||||
| } | ||||
|  | ||||
| #preview hr { | ||||
|     height: 4px; | ||||
|     padding: 0; | ||||
|     margin: 24px 0; | ||||
|     background-color: var(--border-light); | ||||
|     border: 0; | ||||
| } | ||||
|  | ||||
| #preview a { | ||||
|     color: var(--link-color); | ||||
|     text-decoration: none; | ||||
| } | ||||
|  | ||||
| #preview a:hover { | ||||
|     text-decoration: underline; | ||||
| } | ||||
|  | ||||
| /* Mermaid diagrams */ | ||||
| .mermaid { | ||||
|     text-align: center; | ||||
|     margin: 20px 0; | ||||
| } | ||||
|  | ||||
| /* Dark mode mermaid adjustments */ | ||||
| body.dark-mode .mermaid { | ||||
|     filter: invert(0.9) hue-rotate(180deg); | ||||
| } | ||||
|  | ||||
| body.dark-mode .mermaid svg { | ||||
|     background-color: transparent !important; | ||||
| } | ||||
|  | ||||
| /* Scrollbar styling */ | ||||
| ::-webkit-scrollbar { | ||||
|     width: 10px; | ||||
|     height: 10px; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-track { | ||||
|     background: var(--scrollbar-track); | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-thumb { | ||||
|     background: var(--scrollbar-thumb); | ||||
|     border-radius: 5px; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-thumb:hover { | ||||
|     background: var(--scrollbar-thumb-hover); | ||||
| } | ||||
|  | ||||
| /* Responsive adjustments */ | ||||
| @media (max-width: 768px) { | ||||
|     .sidebar { | ||||
|         display: none; | ||||
|     } | ||||
|      | ||||
|     .editor-pane, | ||||
|     .preview-pane { | ||||
|         height: 50vh; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* File list item styling */ | ||||
| .file-item { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
| } | ||||
|  | ||||
| .file-name { | ||||
|     flex: 1; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     white-space: nowrap; | ||||
| } | ||||
|  | ||||
| .file-size { | ||||
|     font-size: 0.75rem; | ||||
|     color: var(--text-secondary); | ||||
|     margin-left: 8px; | ||||
| } | ||||
|  | ||||
| /* Toast notifications dark mode */ | ||||
| body.dark-mode .toast { | ||||
|     background-color: var(--bg-secondary); | ||||
|     color: var(--text-primary); | ||||
|     border: 1px solid var(--border-color); | ||||
| } | ||||
|  | ||||
| body.dark-mode .toast-header { | ||||
|     background-color: var(--bg-tertiary); | ||||
|     color: var(--text-primary); | ||||
|     border-bottom: 1px solid var(--border-color); | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| /* File Tree Styles */ | ||||
| .file-tree { | ||||
|     user-select: none; | ||||
|     font-size: 14px; | ||||
| } | ||||
|  | ||||
| .tree-node { | ||||
|     padding: 4px 8px; | ||||
|     cursor: pointer; | ||||
|     border-radius: 4px; | ||||
|     transition: background-color 0.15s ease; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 6px; | ||||
| } | ||||
|  | ||||
| .tree-node:hover { | ||||
|     background-color: var(--bg-tertiary); | ||||
| } | ||||
|  | ||||
| .tree-node.active { | ||||
|     background-color: var(--accent-color); | ||||
|     color: white; | ||||
| } | ||||
|  | ||||
| .tree-node.drag-over { | ||||
|     background-color: rgba(13, 110, 253, 0.2); | ||||
|     border: 2px dashed var(--accent-color); | ||||
| } | ||||
|  | ||||
| .tree-node-content { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 6px; | ||||
|     flex: 1; | ||||
| } | ||||
|  | ||||
| .tree-node-icon { | ||||
|     font-size: 16px; | ||||
|     width: 16px; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .tree-node-name { | ||||
|     flex: 1; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     white-space: nowrap; | ||||
| } | ||||
|  | ||||
| .tree-node-toggle { | ||||
|     width: 16px; | ||||
|     height: 16px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     font-size: 12px; | ||||
|     transition: transform 0.2s ease; | ||||
| } | ||||
|  | ||||
| .tree-node-toggle.expanded { | ||||
|     transform: rotate(90deg); | ||||
| } | ||||
|  | ||||
| .tree-children { | ||||
|     margin-left: 16px; | ||||
|     border-left: 1px solid var(--border-color); | ||||
|     padding-left: 8px; | ||||
| } | ||||
|  | ||||
| .tree-children.collapsed { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| /* Context Menu */ | ||||
| .context-menu { | ||||
|     position: fixed; | ||||
|     background-color: var(--bg-primary); | ||||
|     border: 1px solid var(--border-color); | ||||
|     border-radius: 6px; | ||||
|     box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||||
|     z-index: 10000; | ||||
|     min-width: 180px; | ||||
|     max-width: 200px; | ||||
|     width: auto; | ||||
|     padding: 4px 0; | ||||
| } | ||||
|  | ||||
| body.dark-mode .context-menu { | ||||
|     box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); | ||||
| } | ||||
|  | ||||
| .context-menu-item { | ||||
|     padding: 8px 16px; | ||||
|     cursor: pointer; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 10px; | ||||
|     color: var(--text-primary); | ||||
|     transition: background-color 0.15s ease; | ||||
| } | ||||
|  | ||||
| .context-menu-item:hover { | ||||
|     background-color: var(--bg-tertiary); | ||||
| } | ||||
|  | ||||
| .context-menu-item i { | ||||
|     width: 16px; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .context-menu-divider { | ||||
|     height: 1px; | ||||
|     background-color: var(--border-color); | ||||
|     margin: 4px 0; | ||||
| } | ||||
|  | ||||
| /* Drag and Drop */ | ||||
| .dragging { | ||||
|     opacity: 0.5; | ||||
| } | ||||
|  | ||||
| .drop-indicator { | ||||
|     height: 2px; | ||||
|     background-color: var(--accent-color); | ||||
|     margin: 2px 0; | ||||
| } | ||||
|  | ||||
| /* Modal for rename/new folder */ | ||||
| .modal-backdrop.show { | ||||
|     opacity: 0.5; | ||||
| } | ||||
|  | ||||
| /* File size badge */ | ||||
| .file-size-badge { | ||||
|     font-size: 10px; | ||||
|     color: var(--text-secondary); | ||||
|     margin-left: auto; | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| /* Dark mode tree and sidebar fixes */ | ||||
| body.dark-mode .sidebar { | ||||
|     background-color: var(--bg-secondary); | ||||
| } | ||||
|  | ||||
| body.dark-mode .tree-node { | ||||
|     color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| body.dark-mode .tree-node:hover { | ||||
|     background-color: var(--bg-tertiary); | ||||
| } | ||||
|  | ||||
| body.dark-mode .tree-node-name { | ||||
|     color: var(--text-primary); | ||||
| } | ||||
|  | ||||
| body.dark-mode .tree-node-size { | ||||
|     color: var(--text-secondary); | ||||
| } | ||||
|  | ||||
| body.dark-mode .sidebar h6 { | ||||
|     color: var(--text-primary); | ||||
|     background-color: var(--bg-tertiary); | ||||
| } | ||||
|  | ||||
| body.dark-mode .tree-children { | ||||
|     border-left-color: var(--border-color); | ||||
| } | ||||
|  | ||||
							
								
								
									
										164
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|  | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>Markdown Editor</title> | ||||
|  | ||||
|     <!-- Bootstrap CSS --> | ||||
|     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> | ||||
|     <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet"> | ||||
|  | ||||
|     <!-- CodeMirror CSS --> | ||||
|     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.20/codemirror.min.css"> | ||||
|     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.20/theme/monokai.min.css"> | ||||
|  | ||||
|     <!-- Prism CSS for syntax highlighting --> | ||||
|     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css"> | ||||
|  | ||||
|     <!-- Modular CSS --> | ||||
|     <link rel="stylesheet" href="/static/css/variables.css"> | ||||
|     <link rel="stylesheet" href="/static/css/layout.css"> | ||||
|     <link rel="stylesheet" href="/static/css/file-tree.css"> | ||||
|     <link rel="stylesheet" href="/static/css/editor.css"> | ||||
|     <link rel="stylesheet" href="/static/css/components.css"> | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|     <!-- Navbar --> | ||||
|     <nav class="navbar navbar-expand-lg"> | ||||
|         <div class="container-fluid"> | ||||
|             <span class="navbar-brand"> | ||||
|                 <i class="bi bi-markdown"></i> Markdown Editor | ||||
|             </span> | ||||
|             <div class="d-flex gap-2"> | ||||
|                 <button id="newBtn" class="btn btn-success btn-sm"> | ||||
|                     <i class="bi bi-file-plus"></i> New | ||||
|                 </button> | ||||
|                 <button id="saveBtn" class="btn btn-primary btn-sm"> | ||||
|                     <i class="bi bi-save"></i> Save | ||||
|                 </button> | ||||
|                 <button id="deleteBtn" class="btn btn-danger btn-sm"> | ||||
|                     <i class="bi bi-trash"></i> Delete | ||||
|                 </button> | ||||
|                 <button id="darkModeBtn" class="btn btn-secondary btn-sm">🌙</button> | ||||
|             </div> | ||||
|         </div> | ||||
|     </nav> | ||||
|  | ||||
|     <!-- Main Content --> | ||||
|     <div class="container-fluid"> | ||||
|         <div class="row h-100"> | ||||
|             <!-- Sidebar --> | ||||
|             <div class="col-md-2 sidebar"> | ||||
|                 <!-- Collection Selector --> | ||||
|                 <div class="collection-selector"> | ||||
|                     <label class="form-label small">Collection:</label> | ||||
|                     <select id="collectionSelect" class="form-select form-select-sm"></select> | ||||
|                 </div> | ||||
|  | ||||
|                 <!-- File Tree --> | ||||
|                 <div id="fileTree" class="file-tree"></div> | ||||
|             </div> | ||||
|  | ||||
|             <!-- Editor Pane --> | ||||
|             <div class="col-md-5 editor-pane"> | ||||
|                 <div class="editor-header"> | ||||
|                     <input type="text" id="filenameInput" placeholder="filename.md" | ||||
|                         class="form-control form-control-sm"> | ||||
|                 </div> | ||||
|                 <div class="editor-container"> | ||||
|                     <div id="editor"></div> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <!-- Preview Pane --> | ||||
|             <div class="col-md-5 preview-pane"> | ||||
|                 <h3>Preview</h3> | ||||
|                 <div id="preview"> | ||||
|                     <p class="text-muted">Start typing in the editor to see the preview</p> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Context Menu --> | ||||
|     <div id="contextMenu" class="context-menu"> | ||||
|         <div class="context-menu-item" data-action="open"> | ||||
|             <i class="bi bi-folder2-open"></i> Open | ||||
|         </div> | ||||
|         <div class="context-menu-divider"></div> | ||||
|         <div class="context-menu-item" data-action="new-file"> | ||||
|             <i class="bi bi-file-plus"></i> New File | ||||
|         </div> | ||||
|         <div class="context-menu-item" data-action="new-folder"> | ||||
|             <i class="bi bi-folder-plus"></i> New Folder | ||||
|         </div> | ||||
|         <div class="context-menu-item" data-action="upload"> | ||||
|             <i class="bi bi-upload"></i> Upload File | ||||
|         </div> | ||||
|         <div class="context-menu-divider"></div> | ||||
|         <div class="context-menu-item" data-action="download"> | ||||
|             <i class="bi bi-download"></i> Download | ||||
|         </div> | ||||
|         <div class="context-menu-item" data-action="rename"> | ||||
|             <i class="bi bi-pencil"></i> Rename | ||||
|         </div> | ||||
|         <div class="context-menu-item" data-action="copy"> | ||||
|             <i class="bi bi-files"></i> Copy | ||||
|         </div> | ||||
|         <div class="context-menu-item" data-action="cut"> | ||||
|             <i class="bi bi-scissors"></i> Cut | ||||
|         </div> | ||||
|         <div class="context-menu-item" id="pasteMenuItem" data-action="paste" style="display: none;"> | ||||
|             <i class="bi bi-clipboard"></i> Paste | ||||
|         </div> | ||||
|         <div class="context-menu-divider"></div> | ||||
|         <div class="context-menu-item text-danger" data-action="delete"> | ||||
|             <i class="bi bi-trash"></i> Delete | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Bootstrap JS --> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> | ||||
|  | ||||
|     <!-- JSZip for folder downloads --> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> | ||||
|  | ||||
|     <!-- CodeMirror JS --> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/markdown/markdown.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/python/python.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/htmlmixed/htmlmixed.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/css/css.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/yaml/yaml.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/toml/toml.min.js"></script> | ||||
|  | ||||
|     <!-- Marked.js for markdown parsing --> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | ||||
|  | ||||
|     <!-- Prism.js for syntax highlighting --> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-toml.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markdown.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-go.min.js"></script> | ||||
|  | ||||
|     <!-- Mermaid for diagrams --> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script> | ||||
|  | ||||
|     <!-- Modular JavaScript --> | ||||
|     <script src="/static/js/webdav-client.js"></script> | ||||
|     <script src="/static/js/file-tree.js"></script> | ||||
|     <script src="/static/js/editor.js"></script> | ||||
|     <script src="/static/js/ui-utils.js"></script> | ||||
|     <script src="/static/js/app.js"></script> | ||||
| </body> | ||||
|  | ||||
| </html> | ||||
							
								
								
									
										548
									
								
								uv.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										548
									
								
								uv.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,548 @@ | ||||
| version = 1 | ||||
| revision = 3 | ||||
| requires-python = ">=3.11" | ||||
|  | ||||
| [[package]] | ||||
| name = "annotated-doc" | ||||
| version = "0.0.3" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535, upload-time = "2025-10-24T14:57:10.718Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488, upload-time = "2025-10-24T14:57:09.462Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "annotated-types" | ||||
| version = "0.7.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "anyio" | ||||
| version = "4.11.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "idna" }, | ||||
|     { name = "sniffio" }, | ||||
|     { name = "typing-extensions", marker = "python_full_version < '3.13'" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "click" | ||||
| version = "8.3.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "colorama", marker = "sys_platform == 'win32'" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "colorama" | ||||
| version = "0.4.6" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "fastapi" | ||||
| version = "0.120.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "annotated-doc" }, | ||||
|     { name = "pydantic" }, | ||||
|     { name = "starlette" }, | ||||
|     { name = "typing-extensions" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/f7/0e/7f29e8f7219e4526747db182e1afb5a4b6abc3201768fb38d81fa2536241/fastapi-0.120.0.tar.gz", hash = "sha256:6ce2c1cfb7000ac14ffd8ddb2bc12e62d023a36c20ec3710d09d8e36fab177a0", size = 337603, upload-time = "2025-10-23T20:56:34.743Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/1d/60/7a639ceaba54aec4e1d5676498c568abc654b95762d456095b6cb529b1ca/fastapi-0.120.0-py3-none-any.whl", hash = "sha256:84009182e530c47648da2f07eb380b44b69889a4acfd9e9035ee4605c5cfc469", size = 108243, upload-time = "2025-10-23T20:56:33.281Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "h11" | ||||
| version = "0.16.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "httptools" | ||||
| version = "0.7.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "idna" | ||||
| version = "3.11" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "markdown-editor" | ||||
| version = "0.1.0" | ||||
| source = { editable = "." } | ||||
| dependencies = [ | ||||
|     { name = "fastapi" }, | ||||
|     { name = "pydantic" }, | ||||
|     { name = "uvicorn", extra = ["standard"] }, | ||||
| ] | ||||
|  | ||||
| [package.metadata] | ||||
| requires-dist = [ | ||||
|     { name = "fastapi", specifier = ">=0.104.0" }, | ||||
|     { name = "pydantic", specifier = ">=2.5.0" }, | ||||
|     { name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "pydantic" | ||||
| version = "2.12.3" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "annotated-types" }, | ||||
|     { name = "pydantic-core" }, | ||||
|     { name = "typing-extensions" }, | ||||
|     { name = "typing-inspection" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "pydantic-core" | ||||
| version = "2.41.4" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "typing-extensions" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062, upload-time = "2025-10-14T10:20:04.486Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301, upload-time = "2025-10-14T10:20:06.857Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728, upload-time = "2025-10-14T10:20:08.353Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238, upload-time = "2025-10-14T10:20:09.766Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424, upload-time = "2025-10-14T10:20:11.732Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047, upload-time = "2025-10-14T10:20:13.647Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163, upload-time = "2025-10-14T10:20:15.307Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585, upload-time = "2025-10-14T10:20:17.3Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109, upload-time = "2025-10-14T10:20:19.143Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078, upload-time = "2025-10-14T10:20:20.742Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737, upload-time = "2025-10-14T10:20:22.306Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160, upload-time = "2025-10-14T10:20:23.817Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883, upload-time = "2025-10-14T10:20:25.48Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026, upload-time = "2025-10-14T10:20:27.039Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b", size = 2122139, upload-time = "2025-10-14T10:22:47.288Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42", size = 1936674, upload-time = "2025-10-14T10:22:49.555Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee", size = 1956398, upload-time = "2025-10-14T10:22:52.19Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c", size = 2138674, upload-time = "2025-10-14T10:22:54.499Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721, upload-time = "2025-10-14T10:23:26.906Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608, upload-time = "2025-10-14T10:23:29.306Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986, upload-time = "2025-10-14T10:23:32.057Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516, upload-time = "2025-10-14T10:23:34.871Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146, upload-time = "2025-10-14T10:23:37.342Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296, upload-time = "2025-10-14T10:23:40.145Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386, upload-time = "2025-10-14T10:23:42.624Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775, upload-time = "2025-10-14T10:23:45.406Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "python-dotenv" | ||||
| version = "1.1.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "pyyaml" | ||||
| version = "6.0.3" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "sniffio" | ||||
| version = "1.3.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "starlette" | ||||
| version = "0.48.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "anyio" }, | ||||
|     { name = "typing-extensions", marker = "python_full_version < '3.13'" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "typing-extensions" | ||||
| version = "4.15.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "typing-inspection" | ||||
| version = "0.4.2" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "typing-extensions" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "uvicorn" | ||||
| version = "0.38.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "click" }, | ||||
|     { name = "h11" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, | ||||
| ] | ||||
|  | ||||
| [package.optional-dependencies] | ||||
| standard = [ | ||||
|     { name = "colorama", marker = "sys_platform == 'win32'" }, | ||||
|     { name = "httptools" }, | ||||
|     { name = "python-dotenv" }, | ||||
|     { name = "pyyaml" }, | ||||
|     { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, | ||||
|     { name = "watchfiles" }, | ||||
|     { name = "websockets" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "uvloop" | ||||
| version = "0.22.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "watchfiles" | ||||
| version = "1.1.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "anyio" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "websockets" | ||||
| version = "15.0.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, | ||||
| ] | ||||
		Reference in New Issue
	
	Block a user