diff options
| author | Pinapelz <yukais@pinapelz.com> | 2025-12-29 11:15:58 -0800 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2025-12-29 11:15:58 -0800 |
| commit | 2025e3e5e56ca22a23f47005175dc8ce254024d6 (patch) | |
| tree | cd2267f53d39536f678174c68b4d44fa924f547d /src/main/resources | |
| parent | fbe9b9eb7a462b42f235d100811b377659101b3c (diff) | |
implement basic webui
Diffstat (limited to 'src/main/resources')
| -rw-r--r-- | src/main/resources/templates/index.html | 573 |
1 files changed, 573 insertions, 0 deletions
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..98bc45e --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,573 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>File Storage</title> + <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> + <script src="https://unpkg.com/htmx.org@1.9.10"></script> + <style> + /* === RESET & BASE === */ + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + background-color: #36393f; + color: #dcddde; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-size: 14px; + line-height: 1.4; + } + + /* === LAYOUT === */ + .app-container { + min-height: 100vh; + display: flex; + flex-direction: column; + } + + /* === HEADER === */ + .header { + background-color: #2f3136; + padding: 12px 20px; + border-bottom: 1px solid #202225; + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 100; + } + + .header-left { + display: flex; + align-items: center; + gap: 12px; + } + + .header-title { + font-size: 16px; + font-weight: 600; + color: #ffffff; + display: flex; + align-items: center; + gap: 8px; + } + + .header-subtitle { + font-size: 12px; + color: #72767d; + } + + .header-actions { + display: flex; + gap: 8px; + } + + /* === BUTTONS === */ + .btn { + background-color: transparent; + border: none; + color: #b9bbbe; + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 4px; + } + + .btn:hover { + background-color: #40444b; + color: #ffffff; + } + + .btn-primary { + background-color: #5865f2; + color: #ffffff; + } + + .btn-primary:hover { + background-color: #4752c4; + } + + /* === MAIN CONTENT === */ + .main-content { + flex: 1; + padding: 20px; + overflow-y: auto; + } + + /* === SEARCH BAR === */ + .search-bar { + display: flex; + gap: 12px; + margin-bottom: 20px; + flex-wrap: wrap; + } + + .search-input { + flex: 1; + min-width: 200px; + background-color: #40444b; + border: 1px solid #202225; + border-radius: 4px; + padding: 8px 12px; + color: #dcddde; + font-size: 14px; + outline: none; + } + + .search-input:focus { + border-color: #5865f2; + } + + .search-input::placeholder { + color: #72767d; + } + + .select { + background-color: #40444b; + border: 1px solid #202225; + border-radius: 4px; + padding: 8px 12px; + color: #dcddde; + font-size: 14px; + cursor: pointer; + outline: none; + } + + .select:focus { + border-color: #5865f2; + } + + /* === STATS === */ + .stats { + display: flex; + gap: 16px; + margin-bottom: 20px; + align-items: center; + } + + .stat-item { + display: flex; + align-items: center; + gap: 6px; + color: #72767d; + font-size: 12px; + } + + .stat-number { + color: #ffffff; + font-weight: 600; + } + + /* === FILE CONTAINER === */ + .file-container { + background-color: #2f3136; + border-radius: 8px; + border: 1px solid #202225; + overflow: hidden; + } + + /* === FILE TABLE === */ + .file-table { + width: 100%; + border-collapse: collapse; + } + + .file-table th { + background-color: #36393f; + padding: 12px 16px; + text-align: left; + font-size: 12px; + font-weight: 600; + color: #72767d; + text-transform: uppercase; + border-bottom: 1px solid #202225; + } + + .file-table td { + padding: 12px 16px; + border-bottom: 1px solid #202225; + vertical-align: middle; + } + + .file-table tbody tr:hover { + background-color: #36393f; + } + + .file-table tbody tr:last-child td { + border-bottom: none; + } + + /* === FILE ELEMENTS === */ + .file-link { + color: #00aff4; + text-decoration: none; + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; + } + + .file-link:hover { + color: #ffffff; + text-decoration: underline; + } + + .file-icon { + color: #72767d; + width: 16px; + text-align: center; + } + + .file-description { + color: #b9bbbe; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .file-size { + color: #72767d; + font-size: 12px; + font-family: 'Courier New', monospace; + } + + .file-type { + background-color: #5865f2; + color: #ffffff; + padding: 2px 6px; + border-radius: 12px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + } + + .file-date { + color: #72767d; + font-size: 12px; + } + + /* === EMPTY STATE === */ + .empty-state { + text-align: center; + padding: 60px 20px; + color: #72767d; + } + + .empty-state-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; + } + + .empty-state h3 { + color: #b9bbbe; + margin-bottom: 8px; + font-size: 18px; + font-weight: 600; + } + + .empty-state p { + font-size: 14px; + line-height: 1.5; + } + + /* === LOADING STATES === */ + .htmx-indicator { + display: none; + } + + .htmx-request .htmx-indicator { + display: inline; + } + + .loading-spinner { + animation: spin 1s linear infinite; + } + + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + + /* === RESPONSIVE === */ + @media (max-width: 768px) { + .search-bar { + flex-direction: column; + } + + .search-input { + min-width: 100%; + } + + .file-table th:nth-child(3), + .file-table td:nth-child(3), + .file-table th:nth-child(4), + .file-table td:nth-child(4), + .file-table th:nth-child(5), + .file-table td:nth-child(5) { + display: none; + } + + .header { + padding: 8px 16px; + } + + .main-content { + padding: 16px; + } + } + </style> +</head> +<body hx-boost="true"> + <div class="app-container"> + <!-- Header --> + <header class="header"> + <div class="header-left"> + <div class="header-title"> + <i class="fab fa-discord"></i> + file storage + </div> + <div class="header-subtitle"># root</div> + </div> + <div class="header-actions"> + <button class="btn" + hx-get="http://localhost:7070/api/files" + hx-target="#file-content" + hx-indicator="#loading-spinner"> + <i class="fas fa-sync-alt"></i> + </button> + </div> + </header> + + <!-- Main Content --> + <main class="main-content"> + <!-- Search and Filters --> + <div class="search-bar"> + <input type="text" + class="search-input" + placeholder="search files..." + hx-get="http://localhost:7070/api/files" + hx-trigger="keyup changed delay:300ms" + hx-target="#file-content" + hx-indicator="#loading-spinner" + name="search"> + + <select class="select" + hx-get="http://localhost:7070/api/files" + hx-trigger="change" + hx-target="#file-content" + hx-indicator="#loading-spinner" + name="mimeType"> + <option value="">all types</option> + <option value="image/">images</option> + <option value="video/">videos</option> + <option value="audio/">audio</option> + <option value="application/pdf">pdfs</option> + <option value="text/">text</option> + <option value="application/zip">archives</option> + </select> + + <select class="select" + hx-get="http://localhost:7070/api/files" + hx-trigger="change" + hx-target="#file-content" + hx-indicator="#loading-spinner" + name="sortBy"> + <option value="created_at">newest</option> + <option value="file_name">name</option> + <option value="size">size</option> + </select> + </div> + + <!-- Stats --> + <div class="stats"> + <div class="stat-item"> + <span id="file-count" class="stat-number"> + <span class="htmx-indicator" id="loading-spinner"> + <i class="fas fa-spinner loading-spinner"></i> + </span> + <span id="count-value">loading</span> + </span> + files + </div> + <div class="stat-item"> + <i class="fas fa-server"></i> + localhost:7070 + </div> + <div class="stat-item" id="last-updated"> + <i class="fas fa-clock"></i> + <span id="timestamp">connecting...</span> + </div> + </div> + + <!-- File Container --> + <div class="file-container" + hx-get="http://localhost:7070/api/files" + hx-trigger="load" + hx-target="#file-content" + hx-indicator="#loading-spinner"> + <div id="file-content"> + <div class="empty-state"> + <div class="empty-state-icon"> + <i class="fas fa-spinner loading-spinner"></i> + </div> + <h3>loading files...</h3> + <p>fetching your files from discord storage</p> + </div> + </div> + </div> + </main> + </div> + + <!-- Scripts --> + <script> + // Update timestamp + function updateTimestamp() { + const now = new Date(); + const timeString = now.toLocaleTimeString(); + document.getElementById('timestamp').textContent = `updated ${timeString}`; + } + + // HTMX event listeners + document.addEventListener('htmx:afterSwap', function(evt) { + if (evt.detail.target.id === 'file-content') { + updateTimestamp(); + + // Update file count + const tables = evt.detail.target.querySelectorAll('table'); + if (tables.length > 0) { + const rows = tables[0].querySelectorAll('tbody tr'); + document.getElementById('count-value').textContent = rows.length; + } else { + document.getElementById('count-value').textContent = '0'; + } + } + }); + + // Error handling + document.addEventListener('htmx:responseError', function(evt) { + document.getElementById('file-content').innerHTML = ` + <div class="empty-state"> + <div class="empty-state-icon"> + <i class="fas fa-exclamation-triangle"></i> + </div> + <h3>connection failed</h3> + <p>unable to connect to the backend server</p> + <button class="btn btn-primary" + hx-get="http://localhost:7070/api/files" + hx-target="#file-content"> + <i class="fas fa-retry"></i> try again + </button> + </div> + `; + document.getElementById('count-value').textContent = 'error'; + document.getElementById('timestamp').textContent = 'connection failed'; + }); + + // Auto-refresh every 30 seconds + setInterval(function() { + if (document.visibilityState === 'visible') { + htmx.trigger('.file-container', 'refresh'); + } + }, 30000); + + // Keyboard shortcuts + document.addEventListener('keydown', function(e) { + // Ctrl+R or F5 to refresh + if ((e.ctrlKey && e.key === 'r') || e.key === 'F5') { + e.preventDefault(); + htmx.trigger('.file-container', 'refresh'); + } + }); + + // Global clear filters function + window.clearFilters = function() { + document.querySelector('input[name="search"]').value = ''; + document.querySelector('select[name="mimeType"]').value = ''; + document.querySelector('select[name="sortBy"]').value = 'created_at'; + htmx.trigger('.file-container', 'refresh'); + }; + + // Directory creation function + function createDirectory(event) { + event.preventDefault(); + const input = document.getElementById('new-directory-name'); + const message = document.getElementById('create-directory-message'); + const path = input.value.trim(); + + // Validation + if (!path) { + showMessage(message, 'Please enter a directory name', 'error'); + return; + } + + if (path.length < 1 || path.length > 100) { + showMessage(message, 'Directory name must be 1-100 characters', 'error'); + return; + } + + // Check for invalid characters + const invalidChars = /[<>:"/\\|?*\x00-\x1f]/; + if (invalidChars.test(path)) { + showMessage(message, 'Directory name contains invalid characters', 'error'); + return; + } + + if (path === '.' || path === '..') { + showMessage(message, 'Invalid directory name', 'error'); + return; + } + + // Show loading + input.disabled = true; + showMessage(message, 'Creating directory...', 'loading'); + + fetch('http://localhost:7070/api/directories', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: 'path=' + encodeURIComponent(path) + }) + .then(response => response.json()) + .then(data => { + input.disabled = false; + if (data.success) { + showMessage(message, 'Directory created!', 'success'); + input.value = ''; + // Refresh directory list + htmx.trigger('[hx-get*="/api/directories-html"]', 'click'); + } else { + showMessage(message, data.message || 'Failed to create directory', 'error'); + } + }) + .catch(error => { + input.disabled = false; + showMessage(message, 'Network error: ' + error.message, 'error'); + }); + } + + function showMessage(element, text, type) { + element.textContent = text; + element.className = 'form-message ' + type; + if (type === 'success') { + setTimeout(() => { + element.textContent = ''; + element.className = 'form-message'; + }, 3000); + } + } + + // Initialize + updateTimestamp(); + </script> +</body> +</html>
\ No newline at end of file |
