Fossil SCM
When pasting images multiple times in the same attachment slot, remove stale thumbnails. Use timestamps instead of slot numbers when generating a name for pasted content.
Commit
307213a0631ec193239c094073fd5c1684f6443bd6d7c4417d0fc1db68189706
Parent
81389347e533745…
1 file changed
+14
-10
+14
-10
| --- src/fossil.attach.js | ||
| +++ src/fossil.attach.js | ||
| @@ -167,13 +167,13 @@ | ||
| 167 | 167 | } |
| 168 | 168 | } |
| 169 | 169 | |
| 170 | 170 | #addRow(){ |
| 171 | 171 | const id = ++idCounter; |
| 172 | - const rowObj = { | |
| 172 | + const rowObj = F.nu({ | |
| 173 | 173 | id, file: null, mimeType: '' |
| 174 | - }; | |
| 174 | + }); | |
| 175 | 175 | const eRow = D.addClass(D.div(), 'attach-row'); |
| 176 | 176 | eRow.dataset.id = id; |
| 177 | 177 | const eDropzone = D.addClass(D.div(), 'attach-dropzone'); |
| 178 | 178 | const eFile = D.addClass( |
| 179 | 179 | D.input('file'), 'attach-file-input', 'hidden' |
| @@ -231,11 +231,12 @@ | ||
| 231 | 231 | this.#injestBlob(rowObj, blob); |
| 232 | 232 | break; |
| 233 | 233 | }else if( item.type === 'text/plain' ){ |
| 234 | 234 | e.preventDefault(); |
| 235 | 235 | item.getAsString((text) => { |
| 236 | - const blob = new File([text], `pasted-text-${id}.txt`, | |
| 236 | + rowObj.name = `pasted-text-${Date.now()}.txt`; | |
| 237 | + const blob = new File([text], rowObj.name, | |
| 237 | 238 | {type: 'text/plain'}); |
| 238 | 239 | this.#injestBlob(rowObj, blob); |
| 239 | 240 | }); |
| 240 | 241 | break; |
| 241 | 242 | } |
| @@ -279,12 +280,14 @@ | ||
| 279 | 280 | if( file.name === 'image.png' ){ |
| 280 | 281 | /* Workaround to attempt to avoid name collisions when pasting |
| 281 | 282 | multiple images. We cannot, at this level, unambiguously |
| 282 | 283 | distinguish a ctrl-v of bitmap data vs a ctrl-v of an image |
| 283 | 284 | file copied via a desktop file manager. */ |
| 284 | - file = new File([file], `pasted-image-${rowObj.id}.png`, | |
| 285 | - {type: file.type}); | |
| 285 | + rowObj.name = `pasted-image-${Date.now()}.png`; | |
| 286 | + } | |
| 287 | + if( rowObj.name && rowObj.name!==file.name ){ | |
| 288 | + file = new File([file], rowObj.name, {type: file.type}); | |
| 286 | 289 | } |
| 287 | 290 | /* |
| 288 | 291 | Fossil attachments treat the name as a unique-per-target key, |
| 289 | 292 | with the newest one being the primary. If a name is given |
| 290 | 293 | twice, replace the prior entry before adding the new |
| @@ -291,14 +294,10 @@ | ||
| 291 | 294 | one. There are conceivable, but also unlikely, cases where |
| 292 | 295 | this will have unintended side-effects, but that seems like a |
| 293 | 296 | lesser evil than attaching the same file N times, leading to N |
| 294 | 297 | attachment artifacts. |
| 295 | 298 | */ |
| 296 | - const old = this.#rowMatchingName(file.name); | |
| 297 | - if( old && rowObj !== old){ | |
| 298 | - this.#removeRow(old); | |
| 299 | - } | |
| 300 | 299 | rowObj.file = file; |
| 301 | 300 | rowObj.mimeType = file.type || 'application/octet-stream'; |
| 302 | 301 | |
| 303 | 302 | const lbl = file.name || 'Pasted Content'; |
| 304 | 303 | let szLbl; |
| @@ -311,13 +310,18 @@ | ||
| 311 | 310 | } |
| 312 | 311 | D.append( |
| 313 | 312 | D.clearElement(rowObj.eInfo), |
| 314 | 313 | lbl, D.br(), szLbl, ' ', rowObj.mimeType || '' |
| 315 | 314 | ); |
| 315 | + const old = this.#rowMatchingName(file.name); | |
| 316 | + if( old && rowObj !== old){ | |
| 317 | + this.#removeRow(old); | |
| 318 | + } | |
| 316 | 319 | rowObj.eDropzone.classList.add('populated'); |
| 317 | 320 | rowObj.eDesc.classList.remove('hidden'); |
| 318 | 321 | if( file.type?.startsWith?.('image/') || file.type==='BITMAP' ){ |
| 322 | + rowObj.eDropzone.querySelectorAll('img.thumbnail').forEach(e=>e.remove()); | |
| 319 | 323 | const img = D.img(); |
| 320 | 324 | img.classList.add('thumbnail'); |
| 321 | 325 | rowObj.eDropzone.insertBefore(img, rowObj.eRemove); |
| 322 | 326 | const reader = new FileReader(); |
| 323 | 327 | reader.onload = (e)=>img.setAttribute('src', e.target.result); |
| @@ -343,11 +347,11 @@ | ||
| 343 | 347 | for(let r of this.#rows){ |
| 344 | 348 | if( !r.eDropzone?.classList?.contains?.('populated') ){ |
| 345 | 349 | continue; |
| 346 | 350 | } |
| 347 | 351 | rv.push(F.nu({ |
| 348 | - name: r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`, | |
| 352 | + name: r.name || r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`, | |
| 349 | 353 | content: r.file, |
| 350 | 354 | description: r.eDesc?.value || '', |
| 351 | 355 | mimeType: r.mimeType |
| 352 | 356 | })); |
| 353 | 357 | } |
| 354 | 358 |
| --- src/fossil.attach.js | |
| +++ src/fossil.attach.js | |
| @@ -167,13 +167,13 @@ | |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | #addRow(){ |
| 171 | const id = ++idCounter; |
| 172 | const rowObj = { |
| 173 | id, file: null, mimeType: '' |
| 174 | }; |
| 175 | const eRow = D.addClass(D.div(), 'attach-row'); |
| 176 | eRow.dataset.id = id; |
| 177 | const eDropzone = D.addClass(D.div(), 'attach-dropzone'); |
| 178 | const eFile = D.addClass( |
| 179 | D.input('file'), 'attach-file-input', 'hidden' |
| @@ -231,11 +231,12 @@ | |
| 231 | this.#injestBlob(rowObj, blob); |
| 232 | break; |
| 233 | }else if( item.type === 'text/plain' ){ |
| 234 | e.preventDefault(); |
| 235 | item.getAsString((text) => { |
| 236 | const blob = new File([text], `pasted-text-${id}.txt`, |
| 237 | {type: 'text/plain'}); |
| 238 | this.#injestBlob(rowObj, blob); |
| 239 | }); |
| 240 | break; |
| 241 | } |
| @@ -279,12 +280,14 @@ | |
| 279 | if( file.name === 'image.png' ){ |
| 280 | /* Workaround to attempt to avoid name collisions when pasting |
| 281 | multiple images. We cannot, at this level, unambiguously |
| 282 | distinguish a ctrl-v of bitmap data vs a ctrl-v of an image |
| 283 | file copied via a desktop file manager. */ |
| 284 | file = new File([file], `pasted-image-${rowObj.id}.png`, |
| 285 | {type: file.type}); |
| 286 | } |
| 287 | /* |
| 288 | Fossil attachments treat the name as a unique-per-target key, |
| 289 | with the newest one being the primary. If a name is given |
| 290 | twice, replace the prior entry before adding the new |
| @@ -291,14 +294,10 @@ | |
| 291 | one. There are conceivable, but also unlikely, cases where |
| 292 | this will have unintended side-effects, but that seems like a |
| 293 | lesser evil than attaching the same file N times, leading to N |
| 294 | attachment artifacts. |
| 295 | */ |
| 296 | const old = this.#rowMatchingName(file.name); |
| 297 | if( old && rowObj !== old){ |
| 298 | this.#removeRow(old); |
| 299 | } |
| 300 | rowObj.file = file; |
| 301 | rowObj.mimeType = file.type || 'application/octet-stream'; |
| 302 | |
| 303 | const lbl = file.name || 'Pasted Content'; |
| 304 | let szLbl; |
| @@ -311,13 +310,18 @@ | |
| 311 | } |
| 312 | D.append( |
| 313 | D.clearElement(rowObj.eInfo), |
| 314 | lbl, D.br(), szLbl, ' ', rowObj.mimeType || '' |
| 315 | ); |
| 316 | rowObj.eDropzone.classList.add('populated'); |
| 317 | rowObj.eDesc.classList.remove('hidden'); |
| 318 | if( file.type?.startsWith?.('image/') || file.type==='BITMAP' ){ |
| 319 | const img = D.img(); |
| 320 | img.classList.add('thumbnail'); |
| 321 | rowObj.eDropzone.insertBefore(img, rowObj.eRemove); |
| 322 | const reader = new FileReader(); |
| 323 | reader.onload = (e)=>img.setAttribute('src', e.target.result); |
| @@ -343,11 +347,11 @@ | |
| 343 | for(let r of this.#rows){ |
| 344 | if( !r.eDropzone?.classList?.contains?.('populated') ){ |
| 345 | continue; |
| 346 | } |
| 347 | rv.push(F.nu({ |
| 348 | name: r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`, |
| 349 | content: r.file, |
| 350 | description: r.eDesc?.value || '', |
| 351 | mimeType: r.mimeType |
| 352 | })); |
| 353 | } |
| 354 |
| --- src/fossil.attach.js | |
| +++ src/fossil.attach.js | |
| @@ -167,13 +167,13 @@ | |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | #addRow(){ |
| 171 | const id = ++idCounter; |
| 172 | const rowObj = F.nu({ |
| 173 | id, file: null, mimeType: '' |
| 174 | }); |
| 175 | const eRow = D.addClass(D.div(), 'attach-row'); |
| 176 | eRow.dataset.id = id; |
| 177 | const eDropzone = D.addClass(D.div(), 'attach-dropzone'); |
| 178 | const eFile = D.addClass( |
| 179 | D.input('file'), 'attach-file-input', 'hidden' |
| @@ -231,11 +231,12 @@ | |
| 231 | this.#injestBlob(rowObj, blob); |
| 232 | break; |
| 233 | }else if( item.type === 'text/plain' ){ |
| 234 | e.preventDefault(); |
| 235 | item.getAsString((text) => { |
| 236 | rowObj.name = `pasted-text-${Date.now()}.txt`; |
| 237 | const blob = new File([text], rowObj.name, |
| 238 | {type: 'text/plain'}); |
| 239 | this.#injestBlob(rowObj, blob); |
| 240 | }); |
| 241 | break; |
| 242 | } |
| @@ -279,12 +280,14 @@ | |
| 280 | if( file.name === 'image.png' ){ |
| 281 | /* Workaround to attempt to avoid name collisions when pasting |
| 282 | multiple images. We cannot, at this level, unambiguously |
| 283 | distinguish a ctrl-v of bitmap data vs a ctrl-v of an image |
| 284 | file copied via a desktop file manager. */ |
| 285 | rowObj.name = `pasted-image-${Date.now()}.png`; |
| 286 | } |
| 287 | if( rowObj.name && rowObj.name!==file.name ){ |
| 288 | file = new File([file], rowObj.name, {type: file.type}); |
| 289 | } |
| 290 | /* |
| 291 | Fossil attachments treat the name as a unique-per-target key, |
| 292 | with the newest one being the primary. If a name is given |
| 293 | twice, replace the prior entry before adding the new |
| @@ -291,14 +294,10 @@ | |
| 294 | one. There are conceivable, but also unlikely, cases where |
| 295 | this will have unintended side-effects, but that seems like a |
| 296 | lesser evil than attaching the same file N times, leading to N |
| 297 | attachment artifacts. |
| 298 | */ |
| 299 | rowObj.file = file; |
| 300 | rowObj.mimeType = file.type || 'application/octet-stream'; |
| 301 | |
| 302 | const lbl = file.name || 'Pasted Content'; |
| 303 | let szLbl; |
| @@ -311,13 +310,18 @@ | |
| 310 | } |
| 311 | D.append( |
| 312 | D.clearElement(rowObj.eInfo), |
| 313 | lbl, D.br(), szLbl, ' ', rowObj.mimeType || '' |
| 314 | ); |
| 315 | const old = this.#rowMatchingName(file.name); |
| 316 | if( old && rowObj !== old){ |
| 317 | this.#removeRow(old); |
| 318 | } |
| 319 | rowObj.eDropzone.classList.add('populated'); |
| 320 | rowObj.eDesc.classList.remove('hidden'); |
| 321 | if( file.type?.startsWith?.('image/') || file.type==='BITMAP' ){ |
| 322 | rowObj.eDropzone.querySelectorAll('img.thumbnail').forEach(e=>e.remove()); |
| 323 | const img = D.img(); |
| 324 | img.classList.add('thumbnail'); |
| 325 | rowObj.eDropzone.insertBefore(img, rowObj.eRemove); |
| 326 | const reader = new FileReader(); |
| 327 | reader.onload = (e)=>img.setAttribute('src', e.target.result); |
| @@ -343,11 +347,11 @@ | |
| 347 | for(let r of this.#rows){ |
| 348 | if( !r.eDropzone?.classList?.contains?.('populated') ){ |
| 349 | continue; |
| 350 | } |
| 351 | rv.push(F.nu({ |
| 352 | name: r.name || r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`, |
| 353 | content: r.file, |
| 354 | description: r.eDesc?.value || '', |
| 355 | mimeType: r.mimeType |
| 356 | })); |
| 357 | } |
| 358 |