feat: use BlurHash when rendering unsplash search results
This commit is contained in:
parent
ed332f5dd3
commit
356e01cd14
|
@ -24,6 +24,7 @@
|
||||||
"@vue/compat": "3.2.26",
|
"@vue/compat": "3.2.26",
|
||||||
"@vueuse/core": "7.3.0",
|
"@vueuse/core": "7.3.0",
|
||||||
"@vueuse/router": "7.3.0",
|
"@vueuse/router": "7.3.0",
|
||||||
|
"blurhash": "^1.1.4",
|
||||||
"bulma-css-variables": "0.9.33",
|
"bulma-css-variables": "0.9.33",
|
||||||
"camel-case": "4.1.2",
|
"camel-case": "4.1.2",
|
||||||
"codemirror": "5.64.0",
|
"codemirror": "5.64.0",
|
||||||
|
|
31
src/helpers/getBlobFromBlurHash.ts
Normal file
31
src/helpers/getBlobFromBlurHash.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import {decode} from 'blurhash'
|
||||||
|
|
||||||
|
export async function getBlobFromBlurHash(blurHash: string): Promise<Blob | null> {
|
||||||
|
if (blurHash === '') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const pixels = decode(blurHash, 32, 32)
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = 32
|
||||||
|
canvas.height = 32
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (ctx === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageData = ctx.createImageData(32, 32)
|
||||||
|
imageData.data.set(pixels)
|
||||||
|
ctx.putImageData(imageData, 0, 0)
|
||||||
|
|
||||||
|
return new Promise<Blob>((resolve, reject) => {
|
||||||
|
canvas.toBlob(b => {
|
||||||
|
if (b === null) {
|
||||||
|
reject(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(b)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ export default class BackgroundImageModel extends AbstractModel {
|
||||||
url: '',
|
url: '',
|
||||||
thumb: '',
|
thumb: '',
|
||||||
info: {},
|
info: {},
|
||||||
|
blurHash: '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -35,16 +35,21 @@
|
||||||
v-model="backgroundSearchTerm"
|
v-model="backgroundSearchTerm"
|
||||||
/>
|
/>
|
||||||
<p class="unsplash-link">
|
<p class="unsplash-link">
|
||||||
<a href="https://unsplash.com" rel="noreferrer noopener nofollow" target="_blank">{{ $t('list.background.poweredByUnsplash') }}</a>
|
<a href="https://unsplash.com" rel="noreferrer noopener nofollow"
|
||||||
|
target="_blank">{{ $t('list.background.poweredByUnsplash') }}</a>
|
||||||
</p>
|
</p>
|
||||||
<div class="image-search-result">
|
<div class="image-search-result">
|
||||||
<a
|
<a
|
||||||
:key="im.id"
|
:key="im.id"
|
||||||
:style="{'background-image': `url(${backgroundThumbs[im.id]})`}"
|
:style="{'background-image': `url(${backgroundBlurHashes[im.id]})`}"
|
||||||
@click="() => setBackground(im.id)"
|
@click="() => setBackground(im.id)"
|
||||||
class="image"
|
class="image"
|
||||||
v-for="im in backgroundSearchResult">
|
v-for="im in backgroundSearchResult">
|
||||||
<a :href="`https://unsplash.com/@${im.info.author}`" rel="noreferrer noopener nofollow" target="_blank" class="info">
|
<transition name="fade">
|
||||||
|
<img :src="backgroundThumbs[im.id]" alt="" v-if="backgroundThumbs[im.id]"/>
|
||||||
|
</transition>
|
||||||
|
<a :href="`https://unsplash.com/@${im.info.author}`" rel="noreferrer noopener nofollow"
|
||||||
|
target="_blank" class="info">
|
||||||
{{ im.info.authorName }}
|
{{ im.info.authorName }}
|
||||||
</a>
|
</a>
|
||||||
</a>
|
</a>
|
||||||
|
@ -65,6 +70,9 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
|
import {decode} from 'blurhash'
|
||||||
|
import {getBlobFromBlurHash} from '../../../helpers/getBlobFromBlurHash'
|
||||||
|
|
||||||
import BackgroundUnsplashService from '../../../services/backgroundUnsplash'
|
import BackgroundUnsplashService from '../../../services/backgroundUnsplash'
|
||||||
import BackgroundUploadService from '../../../services/backgroundUpload'
|
import BackgroundUploadService from '../../../services/backgroundUpload'
|
||||||
import ListService from '@/services/list'
|
import ListService from '@/services/list'
|
||||||
|
@ -83,6 +91,7 @@ export default {
|
||||||
backgroundSearchTerm: '',
|
backgroundSearchTerm: '',
|
||||||
backgroundSearchResult: [],
|
backgroundSearchResult: [],
|
||||||
backgroundThumbs: {},
|
backgroundThumbs: {},
|
||||||
|
backgroundBlurHashes: {},
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
|
||||||
// We're using debounce to not search on every keypress but with a delay.
|
// We're using debounce to not search on every keypress but with a delay.
|
||||||
|
@ -120,8 +129,16 @@ export default {
|
||||||
this.currentPage = page
|
this.currentPage = page
|
||||||
const result = await this.backgroundService.getAll({}, {s: this.backgroundSearchTerm, p: page})
|
const result = await this.backgroundService.getAll({}, {s: this.backgroundSearchTerm, p: page})
|
||||||
this.backgroundSearchResult = this.backgroundSearchResult.concat(result)
|
this.backgroundSearchResult = this.backgroundSearchResult.concat(result)
|
||||||
result.forEach(async background => {
|
result.forEach(background => {
|
||||||
this.backgroundThumbs[background.id] = await this.backgroundService.thumb(background)
|
getBlobFromBlurHash(background.blurHash)
|
||||||
|
.then(b => {
|
||||||
|
this.backgroundBlurHashes[background.id] = window.URL.createObjectURL(b)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.backgroundService.thumb(background)
|
||||||
|
.then(b => {
|
||||||
|
this.backgroundThumbs[background.id] = b
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -162,82 +179,88 @@ export default {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.list-background-setting {
|
.list-background-setting {
|
||||||
|
|
||||||
.unsplash-link {
|
.unsplash-link {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--grey-800);
|
color: var(--grey-800);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-search-result {
|
.image-search-result {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
width: calc(100% / 5 - 1rem);
|
width: calc(100% / 5 - 1rem);
|
||||||
height: 120px;
|
height: 120px;
|
||||||
margin: .5rem;
|
margin: .5rem;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
@media screen and (min-width: $desktop) {
|
@media screen and (min-width: $desktop) {
|
||||||
&:nth-child(5n) {
|
&:nth-child(5n) {
|
||||||
break-after: always;
|
break-after: always;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $desktop) {
|
@media screen and (max-width: $desktop) {
|
||||||
width: calc(100% / 4 - 1rem);
|
width: calc(100% / 4 - 1rem);
|
||||||
|
|
||||||
&:nth-child(4n) {
|
&:nth-child(4n) {
|
||||||
break-after: always;
|
break-after: always;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $tablet) {
|
@media screen and (max-width: $tablet) {
|
||||||
width: calc(100% / 2 - 1rem);
|
width: calc(100% / 2 - 1rem);
|
||||||
|
|
||||||
&:nth-child(2n) {
|
&:nth-child(2n) {
|
||||||
break-after: always;
|
break-after: always;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: ($mobile)) {
|
@media screen and (max-width: ($mobile)) {
|
||||||
width: calc(100% - 1rem);
|
width: calc(100% - 1rem);
|
||||||
|
|
||||||
&:nth-child(1n) {
|
&:nth-child(1n) {
|
||||||
break-after: always;
|
break-after: always;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
display: block;
|
display: block;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: .25rem 0;
|
padding: .25rem 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
transition: opacity $transition;
|
transition: opacity $transition;
|
||||||
}
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover .info {
|
img {
|
||||||
opacity: 1;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-load-more-button {
|
&:hover .info {
|
||||||
margin: 1rem auto 0 !important;
|
opacity: 1;
|
||||||
display: block;
|
}
|
||||||
width: 200px;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-load-more-button {
|
||||||
|
margin: 1rem auto 0 !important;
|
||||||
|
display: block;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -4512,6 +4512,11 @@ blueimp-md5@^2.10.0:
|
||||||
resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0"
|
resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0"
|
||||||
integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==
|
integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==
|
||||||
|
|
||||||
|
blurhash@^1.1.4:
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.4.tgz#a7010ceb3019cd2c9809b17c910ebf6175d29244"
|
||||||
|
integrity sha512-MXIPz6zwYUKayju+Uidf83KhH0vodZfeRl6Ich8Gu+KGl0JgKiFq9LsfqV7cVU5fKD/AotmduZqvOfrGKOfTaA==
|
||||||
|
|
||||||
body-parser@1.19.0:
|
body-parser@1.19.0:
|
||||||
version "1.19.0"
|
version "1.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
||||||
|
|
Reference in New Issue
Block a user