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