Mercurial
comparison mrjunejune/src/notes/editor.js @ 202:b9b184b3303c
[Notes] Images get processed and it is properly fetched. Thank you.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 15 Feb 2026 09:12:57 -0800 |
| parents | 6cdee35a7ba9 |
| children |
comparison
equal
deleted
inserted
replaced
| 201:6cdee35a7ba9 | 202:b9b184b3303c |
|---|---|
| 1 console.log("june"); | |
| 2 | |
| 3 let editor = null; | 1 let editor = null; |
| 4 let currentNoteId = 'index'; | 2 let currentNoteId = 'index'; |
| 5 | 3 |
| 6 function getAuthToken() { | 4 function getAuthToken() { |
| 7 return localStorage.getItem('notes-auth-token'); | 5 return localStorage.getItem('notes-auth-token'); |
| 74 if (!token) { | 72 if (!token) { |
| 75 throw new Error('Not authenticated'); | 73 throw new Error('Not authenticated'); |
| 76 } | 74 } |
| 77 | 75 |
| 78 // 1. Create media record | 76 // 1. Create media record |
| 79 const createResp = await fetch('/api/media/create', { | 77 const createResponse = await fetch('/api/media/create', { |
| 80 method: 'POST', | 78 method: 'POST', |
| 81 headers: { | 79 headers: { |
| 82 'Authorization': 'Bearer ' + token, | 80 'Authorization': 'Bearer ' + token, |
| 83 'Content-Type': 'application/json' | 81 'Content-Type': 'application/json' |
| 84 }, | 82 }, |
| 86 filename: file.name, | 84 filename: file.name, |
| 87 content_type: file.type | 85 content_type: file.type |
| 88 }) | 86 }) |
| 89 }); | 87 }); |
| 90 | 88 |
| 91 if (!createResp.ok) { | 89 if (!createResponse.ok) { |
| 92 const error = await createResp.json(); | 90 const error = await createResponse.json().catch(() => ({})); |
| 93 throw new Error(error.error || 'Failed to create media record'); | 91 throw new Error(error.error || 'Failed to create media record'); |
| 94 } | 92 } |
| 95 | 93 |
| 96 const { media_id, upload_url } = await createResp.json(); | 94 const data = await createResponse.json(); |
| 97 | 95 |
| 98 // 2. Upload to S3 | 96 // 2. Upload file directly to S3 |
| 99 const uploadResp = await fetch(upload_url, { | 97 const uploadResponse = await fetch(data.upload_url, { |
| 100 method: 'PUT', | 98 method: 'PUT', |
| 101 headers: { 'Content-Type': file.type }, | 99 headers: { |
| 100 'Content-Type': file.type | |
| 101 }, | |
| 102 body: file | 102 body: file |
| 103 }); | 103 }); |
| 104 | 104 |
| 105 if (!uploadResp.ok) { | 105 if (!uploadResponse.ok) { |
| 106 throw new Error('S3 upload failed'); | 106 throw new Error('Failed to upload file to S3'); |
| 107 } | 107 } |
| 108 | 108 |
| 109 // 3. Mark uploaded | 109 // 3. Mark as uploaded (triggers processing for images) |
| 110 await fetch(`/api/media/${media_id}/uploaded`, { | 110 await fetch(`/api/media/${data.media_id}/uploaded`, { |
| 111 method: 'POST', | 111 method: 'POST', |
| 112 headers: { 'Authorization': 'Bearer ' + token } | 112 headers: { |
| 113 }); | 113 'Authorization': 'Bearer ' + token |
| 114 | 114 } |
| 115 // 4. Poll for images, immediate return for non-images | 115 }); |
| 116 | |
| 117 // 4. Poll for images, return immediately for non-images | |
| 116 if (file.type.startsWith('image/')) { | 118 if (file.type.startsWith('image/')) { |
| 117 return await pollForProcessedImage(media_id); | 119 return await pollForProcessedImage(data.media_id, token); |
| 118 } else { | 120 } else { |
| 119 // For non-images, return the original S3 URL | 121 // For non-images, construct the public URL |
| 120 const s3_url = upload_url.split('?')[0]; | 122 const publicUrl = data.upload_url.split('?')[0]; // Remove query params |
| 121 return { url: s3_url }; | 123 return { url: publicUrl }; |
| 122 } | 124 } |
| 123 } | 125 } |
| 124 | 126 |
| 125 async function pollForProcessedImage(mediaId) { | 127 async function pollForProcessedImage(mediaId, token) { |
| 126 const token = getAuthToken(); | 128 const maxAttempts = 60; // 2 minutes max (60 * 2 seconds) |
| 127 const maxAttempts = 60; // 2 minutes max | |
| 128 | 129 |
| 129 for (let i = 0; i < maxAttempts; i++) { | 130 for (let i = 0; i < maxAttempts; i++) { |
| 130 await new Promise(r => setTimeout(r, 2000)); // 2 sec interval | 131 await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second interval |
| 131 | 132 |
| 132 const resp = await fetch(`/api/media/${mediaId}/status`, { | 133 const statusResponse = await fetch(`/api/media/${mediaId}/status`, { |
| 133 headers: { 'Authorization': 'Bearer ' + token } | 134 headers: { |
| 135 'Authorization': 'Bearer ' + token | |
| 136 } | |
| 134 }); | 137 }); |
| 135 | 138 |
| 136 if (!resp.ok) continue; | 139 if (!statusResponse.ok) { |
| 137 | 140 console.warn('Status check failed, retrying...'); |
| 138 const { status, processed_url, error_message } = await resp.json(); | 141 continue; |
| 139 | 142 } |
| 140 if (status === 'finished') return { url: processed_url }; | 143 |
| 141 if (status === 'error') throw new Error(error_message || 'Processing failed'); | 144 const statusData = await statusResponse.json(); |
| 142 } | 145 |
| 143 | 146 if (statusData.status === 'finished') { |
| 144 throw new Error('Processing timeout'); | 147 return { url: statusData.processed_url }; |
| 148 } else if (statusData.status === 'error') { | |
| 149 throw new Error(statusData.error_message || 'Processing failed'); | |
| 150 } | |
| 151 // Status is 'uploaded' or 'processing', continue polling | |
| 152 } | |
| 153 | |
| 154 throw new Error('Processing timeout after 2 minutes'); | |
| 145 } | 155 } |
| 146 | 156 |
| 147 async function saveContent(content) { | 157 async function saveContent(content) { |
| 148 const token = getAuthToken(); | 158 const token = getAuthToken(); |
| 149 if (!token) return; | 159 if (!token) return; |
| 200 placeholder: 'Start writing... (paste images, drag files, or use /upload)\n\nTip: Click "+ New Note" to create linked notes.' | 210 placeholder: 'Start writing... (paste images, drag files, or use /upload)\n\nTip: Click "+ New Note" to create linked notes.' |
| 201 }); | 211 }); |
| 202 | 212 |
| 203 loadNote(currentNoteId); | 213 loadNote(currentNoteId); |
| 204 }); | 214 }); |
| 215 |