aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/resources
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2025-12-29 11:15:58 -0800
committerPinapelz <yukais@pinapelz.com>2025-12-29 11:15:58 -0800
commit2025e3e5e56ca22a23f47005175dc8ce254024d6 (patch)
treecd2267f53d39536f678174c68b4d44fa924f547d /src/main/resources
parentfbe9b9eb7a462b42f235d100811b377659101b3c (diff)
implement basic webui
Diffstat (limited to 'src/main/resources')
-rw-r--r--src/main/resources/templates/index.html573
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
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage