From 4cff3ebee138289c915d8e70e89e009f3be28fea Mon Sep 17 00:00:00 2001 From: konrad Date: Sat, 2 Apr 2022 15:05:30 +0000 Subject: [PATCH] feat: use blurHash when loading list backgrounds (#1188) Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/1188 --- package.json | 1 + src/components/home/contentAuth.vue | 98 ++++---- src/components/list/partials/list-card.vue | 255 ++++++++++++--------- src/components/modal/modal.vue | 4 + src/helpers/getBlobFromBlurHash.ts | 31 +++ src/models/backgroundImage.js | 1 + src/models/list.js | 3 +- src/store/index.js | 19 +- src/store/mutation-types.js | 2 +- src/styles/theme/background.scss | 35 ++- src/views/list/ListWrapper.vue | 3 +- src/views/list/settings/background.vue | 164 +++++++------ yarn.lock | 5 + 13 files changed, 387 insertions(+), 234 deletions(-) create mode 100644 src/helpers/getBlobFromBlurHash.ts diff --git a/package.json b/package.json index 5fe64d85..f543b0f5 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@vue/compat": "3.2.31", "@vueuse/core": "8.2.3", "@vueuse/router": "8.2.3", + "blurhash": "^1.1.4", "bulma-css-variables": "0.9.33", "camel-case": "4.1.2", "codemirror": "5.65.2", diff --git a/src/components/home/contentAuth.vue b/src/components/home/contentAuth.vue index d6883635..30e38182 100644 --- a/src/components/home/contentAuth.vue +++ b/src/components/home/contentAuth.vue @@ -1,17 +1,21 @@ @@ -32,12 +38,14 @@ import {PropType, ref, watch} from 'vue' import {useStore} from 'vuex' import ListService from '@/services/list' +import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash' import {colorIsDark} from '@/helpers/color/colorIsDark' import ListModel from '@/models/list' const background = ref(null) const backgroundLoading = ref(false) +const blurHashUrl = ref('') const props = defineProps({ list: { @@ -50,13 +58,18 @@ const props = defineProps({ }, }) -watch(props.list, loadBackground, { immediate: true }) +watch(props.list, loadBackground, {immediate: true}) async function loadBackground() { if (props.list === null || !props.list.backgroundInformation || backgroundLoading.value) { return } + const blurHash = await getBlobFromBlurHash(props.list.backgroundBlurHash) + if (blurHash) { + blurHashUrl.value = window.URL.createObjectURL(blurHash) + } + backgroundLoading.value = true const listService = new ListService() @@ -81,129 +94,145 @@ function toggleFavoriteList(list: ListModel) { \ No newline at end of file diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue index e7803add..38d4dccf 100644 --- a/src/components/modal/modal.vue +++ b/src/components/modal/modal.vue @@ -64,6 +64,9 @@ import BaseButton from '@/components/base/BaseButton.vue' import {ref, watch} from 'vue' import {useScrollLock} from '@vueuse/core' +import {useStore} from 'vuex' + +const store = useStore() const props = withDefaults(defineProps<{ enabled?: boolean, @@ -86,6 +89,7 @@ watch( () => props.enabled, enabled => { scrollLock.value = enabled + store.commit('modalActive', enabled) }, { immediate: true, diff --git a/src/helpers/getBlobFromBlurHash.ts b/src/helpers/getBlobFromBlurHash.ts new file mode 100644 index 00000000..d8134730 --- /dev/null +++ b/src/helpers/getBlobFromBlurHash.ts @@ -0,0 +1,31 @@ +import {decode} from 'blurhash' + +export async function getBlobFromBlurHash(blurHash: string): Promise { + 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((resolve, reject) => { + canvas.toBlob(b => { + if (b === null) { + reject(b) + return + } + + resolve(b) + }) + }) +} diff --git a/src/models/backgroundImage.js b/src/models/backgroundImage.js index 78bc29c3..70e44122 100644 --- a/src/models/backgroundImage.js +++ b/src/models/backgroundImage.js @@ -7,6 +7,7 @@ export default class BackgroundImageModel extends AbstractModel { url: '', thumb: '', info: {}, + blurHash: '', } } } \ No newline at end of file diff --git a/src/models/list.js b/src/models/list.js index b5a08e81..bd170aad 100644 --- a/src/models/list.js +++ b/src/models/list.js @@ -20,7 +20,7 @@ export default class ListModel extends AbstractModel { this.owner = new UserModel(this.owner) - if(typeof this.subscription !== 'undefined' && this.subscription !== null) { + if (typeof this.subscription !== 'undefined' && this.subscription !== null) { this.subscription = new SubscriptionModel(this.subscription) } @@ -44,6 +44,7 @@ export default class ListModel extends AbstractModel { isFavorite: false, subscription: null, position: 0, + backgroundBlurHash: '', created: null, updated: null, diff --git a/src/store/index.js b/src/store/index.js index 0833e3f6..0a5a060e 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,6 +1,8 @@ import {createStore} from 'vuex' +import {getBlobFromBlurHash} from '../helpers/getBlobFromBlurHash' import { BACKGROUND, + BLUR_HASH, CURRENT_LIST, HAS_TASKS, KEYBOARD_SHORTCUTS_ACTIVE, @@ -44,10 +46,12 @@ export const store = createStore({ isArchived: false, }), background: '', + blurHash: '', hasTasks: false, menuActive: true, keyboardShortcutsActive: false, quickActionsActive: false, + modalActive: false, }, mutations: { [LOADING](state, loading) { @@ -83,6 +87,12 @@ export const store = createStore({ [BACKGROUND](state, background) { state.background = background }, + [BLUR_HASH](state, blurHash) { + state.blurHash = blurHash + }, + modalActive(state, active) { + state.modalActive = active + }, }, actions: { async [CURRENT_LIST]({state, commit}, currentList) { @@ -90,6 +100,7 @@ export const store = createStore({ if (currentList === null) { commit(CURRENT_LIST, {}) commit(BACKGROUND, null) + commit(BLUR_HASH, null) return } @@ -122,10 +133,15 @@ export const store = createStore({ ) { if (currentList.backgroundInformation) { try { + const blurHash = await getBlobFromBlurHash(currentList.backgroundBlurHash) + if (blurHash) { + commit(BLUR_HASH, window.URL.createObjectURL(blurHash)) + } + const listService = new ListService() const background = await listService.background(currentList) commit(BACKGROUND, background) - } catch(e) { + } catch (e) { console.error('Error getting background image for list', currentList.id, e) } } @@ -133,6 +149,7 @@ export const store = createStore({ if (typeof currentList.backgroundInformation === 'undefined' || currentList.backgroundInformation === null) { commit(BACKGROUND, null) + commit(BLUR_HASH, null) } commit(CURRENT_LIST, currentList) diff --git a/src/store/mutation-types.js b/src/store/mutation-types.js index ee3a993f..95078927 100644 --- a/src/store/mutation-types.js +++ b/src/store/mutation-types.js @@ -6,6 +6,6 @@ export const MENU_ACTIVE = 'menuActive' export const KEYBOARD_SHORTCUTS_ACTIVE = 'keyboardShortcutsActive' export const QUICK_ACTIONS_ACTIVE = 'quickActionsActive' export const BACKGROUND = 'background' +export const BLUR_HASH = 'blurHash' export const CONFIG = 'config' -export const AUTH = 'auth' diff --git a/src/styles/theme/background.scss b/src/styles/theme/background.scss index d478aac4..150cb754 100644 --- a/src/styles/theme/background.scss +++ b/src/styles/theme/background.scss @@ -1,12 +1,16 @@ .app-container.has-background, .link-share-container.has-background { - background-position: center; - background-size: cover; - background-repeat: no-repeat; - background-attachment: fixed; - min-height: 100vh; + position: relative; - // FIXME: move to pagination component + &, .app-container-background { + background-position: center; + background-size: cover; + background-repeat: no-repeat; + background-attachment: fixed; + min-height: 100vh; + } + + // FIXME: move to pagination component .pagination-link:not(.is-current) { background: var(--grey-100); } @@ -32,4 +36,21 @@ border-radius: $radius !important; } } -} \ No newline at end of file +} + +.app-container-background { + width: 100vw; + height: 100vh; + position: fixed; + z-index: 0; +} + +.background-fade-in { + opacity: 0; + transition: opacity $transition; + transition-delay: $transition-duration * 2; // To fake an appearing background + + &.is-visible { + opacity: 1; + } +} diff --git a/src/views/list/ListWrapper.vue b/src/views/list/ListWrapper.vue index 1153dc7e..22837242 100644 --- a/src/views/list/ListWrapper.vue +++ b/src/views/list/ListWrapper.vue @@ -55,7 +55,7 @@ import Message from '@/components/misc/message.vue' import ListModel from '@/models/list' import ListService from '@/services/list' -import {BACKGROUND, CURRENT_LIST} from '@/store/mutation-types' +import {BACKGROUND, BLUR_HASH, CURRENT_LIST} from '@/store/mutation-types' import {getListTitle} from '@/helpers/getListTitle' import {saveListToHistory} from '@/modules/listHistory' @@ -145,6 +145,7 @@ async function loadList(listIdToLoad: number) { const listFromStore = store.getters['lists/getListById'](listData.id) if (listFromStore !== null) { store.commit(BACKGROUND, null) + store.commit(BLUR_HASH, null) store.commit(CURRENT_LIST, listFromStore) } diff --git a/src/views/list/settings/background.vue b/src/views/list/settings/background.vue index ff8a2389..83c98d87 100644 --- a/src/views/list/settings/background.vue +++ b/src/views/list/settings/background.vue @@ -35,16 +35,25 @@ v-model="backgroundSearchTerm" />
- + + + + {{ im.info.authorName }} @@ -65,6 +74,8 @@