|
14 | 14 | </template> |
15 | 15 | </template> |
16 | 16 |
|
17 | | - <!-- Decorative image, should not be aria documented --> |
18 | | - <img v-else-if="previewUrl && backgroundFailed !== true" |
19 | | - ref="previewImg" |
20 | | - alt="" |
21 | | - class="files-list__row-icon-preview" |
22 | | - :class="{'files-list__row-icon-preview--loaded': backgroundFailed === false}" |
23 | | - loading="lazy" |
24 | | - :src="previewUrl" |
25 | | - @error="onBackgroundError" |
26 | | - @load="backgroundFailed = false"> |
| 17 | + <!-- Decorative images, should not be aria documented --> |
| 18 | + <span v-else-if="previewUrl" class="files-list__row-icon-preview-container"> |
| 19 | + <canvas v-if="hasBlurhash && (backgroundFailed === true || !backgroundLoaded)" ref="canvas" class="files-list__row-icon-blurhash" /> |
| 20 | + <img v-if="backgroundFailed !== true" |
| 21 | + ref="previewImg" |
| 22 | + alt="" |
| 23 | + class="files-list__row-icon-preview" |
| 24 | + :class="{'files-list__row-icon-preview--loaded': backgroundFailed === false}" |
| 25 | + loading="lazy" |
| 26 | + :src="previewUrl" |
| 27 | + @error="onBackgroundError" |
| 28 | + @load="onBackgroundLoad"> |
| 29 | + </span> |
27 | 30 |
|
28 | 31 | <FileIcon v-else v-once /> |
29 | 32 |
|
@@ -58,6 +61,7 @@ import LinkIcon from 'vue-material-design-icons/Link.vue' |
58 | 61 | import NetworkIcon from 'vue-material-design-icons/Network.vue' |
59 | 62 | import TagIcon from 'vue-material-design-icons/Tag.vue' |
60 | 63 | import PlayCircleIcon from 'vue-material-design-icons/PlayCircle.vue' |
| 64 | +import { decode } from 'blurhash' |
61 | 65 |
|
62 | 66 | import CollectivesIcon from './CollectivesIcon.vue' |
63 | 67 | import FavoriteIcon from './FavoriteIcon.vue' |
@@ -107,6 +111,7 @@ export default Vue.extend({ |
107 | 111 | data() { |
108 | 112 | return { |
109 | 113 | backgroundFailed: undefined as boolean | undefined, |
| 114 | + backgroundLoaded: false as boolean, |
110 | 115 | } |
111 | 116 | }, |
112 | 117 |
|
@@ -206,24 +211,53 @@ export default Vue.extend({ |
206 | 211 |
|
207 | 212 | return null |
208 | 213 | }, |
| 214 | +
|
| 215 | + hasBlurhash() { |
| 216 | + return this.source.attributes['metadata-blurhash'] !== undefined |
| 217 | + }, |
| 218 | + }, |
| 219 | +
|
| 220 | + mounted() { |
| 221 | + if (this.hasBlurhash && this.$refs.canvas) { |
| 222 | + this.drawBlurhash() |
| 223 | + } |
209 | 224 | }, |
210 | 225 |
|
211 | 226 | methods: { |
212 | 227 | // Called from FileEntry |
213 | 228 | reset() { |
214 | 229 | // Reset background state to cancel any ongoing requests |
215 | 230 | this.backgroundFailed = undefined |
| 231 | + this.backgroundLoaded = false |
216 | 232 | if (this.$refs.previewImg) { |
217 | 233 | this.$refs.previewImg.src = '' |
218 | 234 | } |
219 | 235 | }, |
220 | 236 |
|
| 237 | + onBackgroundLoad(event) { |
| 238 | + this.backgroundFailed = false |
| 239 | + this.backgroundLoaded = true |
| 240 | + }, |
| 241 | +
|
221 | 242 | onBackgroundError(event) { |
222 | 243 | // Do not fail if we just reset the background |
223 | 244 | if (event.target?.src === '') { |
224 | 245 | return |
225 | 246 | } |
226 | 247 | this.backgroundFailed = true |
| 248 | + this.backgroundLoaded = false |
| 249 | + }, |
| 250 | +
|
| 251 | + drawBlurhash() { |
| 252 | + const width = this.$refs.canvas.width |
| 253 | + const height = this.$refs.canvas.height |
| 254 | +
|
| 255 | + const pixels = decode(this.source.attributes['metadata-blurhash'], width, height) |
| 256 | +
|
| 257 | + const ctx = this.$refs.canvas.getContext('2d') |
| 258 | + const imageData = ctx.createImageData(width, height) |
| 259 | + imageData.data.set(pixels) |
| 260 | + ctx.putImageData(imageData, 0, 0) |
227 | 261 | }, |
228 | 262 |
|
229 | 263 | t, |
|
0 commit comments