Skip to main content

max / makenotwork

8.1 KB · 189 lines History Blame Raw
1 <div class="content-section">
2 <div class="section-header user-media-header">
3 <h2 class="subsection-title">Media Library</h2>
4 <span class="form-hint">{{ storage_display }} used</span>
5 </div>
6
7 <p class="form-hint user-media-intro">
8 Upload images and videos to embed in your item content, sections, and blog posts.
9 Use <code>![alt text](folder/filename.ext)</code> in your markdown to reference uploaded files.
10 </p>
11
12 <!-- Upload area -->
13 <div id="media-upload-area" class="user-media-upload"
14 ondragover="event.preventDefault(); this.classList.add('user-media-upload--dragover');"
15 ondragleave="this.classList.remove('user-media-upload--dragover');"
16 ondrop="mediaHandleDrop(event); this.classList.remove('user-media-upload--dragover');">
17 <div class="user-media-upload-row">
18 <label class="user-media-upload-label">
19 Folder:
20 <input type="text" id="media-folder-input" placeholder="(root)" class="user-media-upload-folder-input">
21 </label>
22 <input type="file" id="media-file-input" multiple accept="image/*,video/*" class="sr-only"
23 onchange="mediaUploadFiles(this.files)">
24 <button class="btn" onclick="document.getElementById('media-file-input').click()">Choose Files</button>
25 </div>
26 <p class="form-hint">or drag and drop files here</p>
27 <div id="media-upload-progress" class="user-media-upload-progress"></div>
28 </div>
29
30 <!-- Folder filter -->
31 {% if !folders.is_empty() %}
32 <div class="user-media-folders">
33 <span class="user-media-folders-label">Folders:</span>
34 <button class="btn-small user-media-folders-btn{% if folders.is_empty() %} is-selected{% endif %}" onclick="mediaFilterFolder(null, this)">All</button>
35 <button class="btn-small user-media-folders-btn" onclick="mediaFilterFolder('', this)">(root)</button>
36 {% for folder in folders %}
37 {% if !folder.is_empty() %}
38 <button class="btn-small user-media-folders-btn" onclick="mediaFilterFolder('{{ folder }}', this)">{{ folder }}</button>
39 {% endif %}
40 {% endfor %}
41 </div>
42 {% endif %}
43
44 <!-- File grid -->
45 {% if files.is_empty() %}
46 <p class="muted">No media files yet. Upload images or videos to get started.</p>
47 {% else %}
48 <div id="media-grid" class="user-media-grid">
49 {% for file in files %}
50 <div class="media-card user-media-card" data-folder="{{ file.folder }}">
51 {% if file.media_type == "image" %}
52 <div class="user-media-card-thumb">
53 <img src="{{ file.cdn_url }}" alt="{{ file.filename }}" class="user-media-card-img" loading="lazy">
54 </div>
55 {% else %}
56 <div class="user-media-card-thumb user-media-card-thumb--video">
57 &#9654;
58 </div>
59 {% endif %}
60 <div class="user-media-card-name" title="{{ file.filename }}">{{ file.filename }}</div>
61 <div class="user-media-card-meta">
62 {% if !file.folder.is_empty() %}{{ file.folder }} &middot; {% endif %}{{ file.file_size }} &middot; {{ file.created_at }}
63 </div>
64 <div class="user-media-card-actions">
65 <button class="btn-small user-media-card-btn" onclick="mediaCopyRef('{{ file.markdown_ref }}')">Copy ref</button>
66 <button class="btn-small btn-danger user-media-card-btn" onclick="mediaDeleteFile('{{ file.id }}', '{{ file.filename }}')">Delete</button>
67 </div>
68 </div>
69 {% endfor %}
70 </div>
71 {% endif %}
72 </div>
73
74 <script>
75 function mediaCopyRef(ref) {
76 var text = '![](' + ref + ')';
77 navigator.clipboard.writeText(text).then(function() {
78 showToast('Copied: ' + text);
79 });
80 }
81
82 function mediaFilterFolder(folder, btn) {
83 var cards = document.querySelectorAll('.media-card');
84 cards.forEach(function(card) {
85 if (folder === null) {
86 card.classList.remove('hidden');
87 } else {
88 card.classList.toggle('hidden', card.dataset.folder !== folder);
89 }
90 });
91 // Update selected button
92 var buttons = btn.parentElement.querySelectorAll('.btn-small');
93 buttons.forEach(function(b) { b.classList.remove('is-selected'); });
94 btn.classList.add('is-selected');
95 }
96
97 function mediaDeleteFile(id, name) {
98 if (!confirm('Delete "' + name + '"? This cannot be undone. Existing references in your content will break.')) return;
99 fetch('/api/media/' + id, {
100 method: 'DELETE',
101 credentials: 'same-origin',
102 headers: csrfHeaders()
103 }).then(function(res) {
104 if (res.ok) {
105 document.getElementById('tab-media').click();
106 } else {
107 res.text().then(function(t) { showToast(t || 'Failed to delete file.'); });
108 }
109 }).catch(function() {
110 showToast('Network error. Please check your connection and try again.');
111 });
112 }
113
114 function mediaHandleDrop(e) {
115 e.preventDefault();
116 if (e.dataTransfer && e.dataTransfer.files.length > 0) {
117 mediaUploadFiles(e.dataTransfer.files);
118 }
119 }
120
121 function mediaUploadFiles(fileList) {
122 var folder = (document.getElementById('media-folder-input').value || '').trim();
123 var progress = document.getElementById('media-upload-progress');
124 progress.innerHTML = '';
125
126 for (var i = 0; i < fileList.length; i++) {
127 (function(file) {
128 var statusEl = document.createElement('div');
129 statusEl.className = 'user-media-upload-status';
130 statusEl.textContent = 'Uploading ' + file.name + '...';
131 progress.appendChild(statusEl);
132
133 // 1. Presign
134 fetch('/api/media/presign', {
135 method: 'POST',
136 credentials: 'same-origin',
137 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
138 body: JSON.stringify({
139 file_name: file.name,
140 content_type: file.type,
141 folder: folder
142 })
143 }).then(function(res) {
144 if (!res.ok) return res.text().then(function(t) { throw new Error(t); });
145 return res.json();
146 }).then(function(data) {
147 // 2. Upload to S3
148 var headers = { 'Content-Type': file.type };
149 if (data.cache_control) headers['Cache-Control'] = data.cache_control;
150 return fetch(data.upload_url, {
151 method: 'PUT',
152 headers: headers,
153 body: file
154 }).then(function(putRes) {
155 if (!putRes.ok) throw new Error('S3 upload failed');
156 return data;
157 });
158 }).then(function(data) {
159 // 3. Confirm
160 return fetch('/api/media/confirm', {
161 method: 'POST',
162 credentials: 'same-origin',
163 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
164 body: JSON.stringify({
165 s3_key: data.s3_key,
166 file_name: file.name,
167 content_type: file.type,
168 folder: folder
169 })
170 }).then(function(res) {
171 if (!res.ok) return res.text().then(function(t) { throw new Error(t); });
172 statusEl.textContent = file.name + ' uploaded.';
173 statusEl.classList.add('user-media-upload-status--ok');
174 });
175 }).catch(function(err) {
176 statusEl.textContent = file.name + ': ' + (err.message || 'Upload failed');
177 statusEl.classList.add('user-media-upload-status--err');
178 }).finally(function() {
179 // Refresh tab after last file
180 var remaining = progress.querySelectorAll('.user-media-upload-status:not(.user-media-upload-status--ok):not(.user-media-upload-status--err)');
181 if (remaining.length === 0) {
182 setTimeout(function() { document.getElementById('tab-media').click(); }, 1000);
183 }
184 });
185 })(fileList[i]);
186 }
187 }
188 </script>
189