Compare commits

...

8 Commits

Author SHA1 Message Date
kolaente bfb356eb8b
Merge branch 'main' into feature/blur-hash
# Conflicts:
#	package.json
#	src/components/home/contentAuth.vue
2022-01-08 13:51:36 +01:00
kolaente b85df38245
fix: lint 2021-12-20 22:08:18 +01:00
kolaente 33158167d5
chore: make background fade in reusable 2021-12-20 22:06:45 +01:00
kolaente 0e886ba76b
feat: add blurHash loading for list cards 2021-12-20 22:04:00 +01:00
kolaente daf3212902
feat: add fade in for background images 2021-12-20 21:36:33 +01:00
kolaente af177071d6
Merge branch 'main' into feature/blur-hash
# Conflicts:
#	package.json
2021-12-20 19:42:55 +01:00
kolaente b5f693e230
feat: use blurHash when loading list backgrounds 2021-12-12 22:31:51 +01:00
kolaente 356e01cd14
feat: use BlurHash when rendering unsplash search results 2021-12-12 22:25:11 +01:00
12 changed files with 402 additions and 265 deletions

View File

@ -25,6 +25,7 @@
"@vue/compat": "3.2.26",
"@vueuse/core": "7.5.2",
"@vueuse/router": "7.5.3",
"blurhash": "^1.1.4",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"codemirror": "5.65.0",

View File

@ -4,10 +4,14 @@
<icon icon="times"/>
</a>
<div
:class="{'has-background': background}"
:style="{'background-image': background && `url(${background})`}"
:class="{'has-background': background || blurHash}"
:style="{'background-image': blurHash && `url(${blurHash})`}"
class="app-container"
>
<div
:class="{'is-visible': background}"
class="app-container-background background-fade-in"
:style="{'background-image': background && `url(${background})`}"></div>
<navigation/>
<div
:class="[
@ -53,6 +57,7 @@ import QuickActions from '@/components/quick-actions/quick-actions.vue'
const store = useStore()
const background = computed(() => store.state.background)
const blurHash = computed(() => store.state.blurHash)
const menuActive = computed(() => store.state.menuActive)
function showKeyboardShortcuts() {
@ -164,7 +169,8 @@ store.dispatch('labels/loadAllLabels')
.app-content {
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
z-index: 2;
z-index: 10;
position: relative;
@media screen and (max-width: $tablet) {
margin-left: 0;

View File

@ -2,16 +2,21 @@
<router-link
:class="{
'has-light-text': !colorIsDark(list.hexColor),
'has-background': background !== null
'has-background': blurHashUrl !== ''
}"
:style="{
'background-color': list.hexColor,
'background-image': background !== null ? `url(${background})` : false,
'background-image': blurHashUrl !== null ? `url(${blurHashUrl})` : false,
}"
:to="{ name: 'list.index', params: { listId: list.id} }"
class="list-card"
v-if="list !== null && (showArchived ? true : !list.isArchived)"
>
<div
class="list-background background-fade-in"
:class="{'is-visible': background}"
:style="{'background-image': background !== null ? `url(${background})` : false}"></div>
<div class="list-content">
<div class="is-archived-container">
<span class="is-archived" v-if="list.isArchived">
{{ $t('namespace.archived') }}
@ -24,6 +29,7 @@
</span>
</div>
<div class="title">{{ list.title }}</div>
</div>
</router-link>
</template>
@ -32,11 +38,13 @@ import {ref, watch} from 'vue'
import {useStore} from 'vuex'
import ListService from '@/services/list'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import {colorIsDark} from '@/helpers/color/colorIsDark'
const background = ref<string | null>(null)
const backgroundLoading = ref(false)
const blurHashUrl = ref('')
const props = defineProps({
list: {
@ -56,6 +64,11 @@ async function loadBackground() {
return
}
const blurHash = await getBlobFromBlurHash(props.list.backgroundBlurHash)
if (blurHash) {
blurHashUrl.value = window.URL.createObjectURL(blurHash)
}
backgroundLoading.value = true
const listService = new ListService()
@ -85,14 +98,34 @@ function toggleFavoriteList(list) {
height: $list-height;
background: var(--white);
margin: 0 $list-spacing $list-spacing 0;
padding: 1rem;
border-radius: $radius;
box-shadow: var(--shadow-sm);
transition: box-shadow $transition;
position: relative;
overflow: hidden;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
&.has-light-text .title {
color: var(--light);
}
&.has-background, .list-background {
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
&.has-background .list-content .title {
text-shadow: 0 0 10px var(--black), 1px 1px 5px var(--grey-700), -1px -1px 5px var(--grey-700);
color: var(--white);
}
.list-background {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
&:hover {
box-shadow: var(--shadow-md);
@ -140,6 +173,15 @@ function toggleFavoriteList(list) {
}
}
.list-content {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
padding: 1rem;
position: absolute;
height: 100%;
width: 100%;
.is-archived-container {
width: 100%;
text-align: right;
@ -167,21 +209,6 @@ function toggleFavoriteList(list) {
-webkit-box-orient: vertical;
}
&.has-light-text .title {
color: var(--light);
}
&.has-background {
background-size: cover;
background-repeat: no-repeat;
background-position: center;
.title {
text-shadow: 0 0 10px var(--black), 1px 1px 5px var(--grey-700), -1px -1px 5px var(--grey-700);
color: var(--white);
}
}
.favorite {
transition: opacity $transition, color $transition;
opacity: 0;
@ -204,5 +231,7 @@ function toggleFavoriteList(list) {
&:hover .favorite {
opacity: 1;
}
}
}
</style>

View 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)
})
})
}

View File

@ -7,6 +7,7 @@ export default class BackgroundImageModel extends AbstractModel {
url: '',
thumb: '',
info: {},
blurHash: '',
}
}
}

View File

@ -44,6 +44,7 @@ export default class ListModel extends AbstractModel {
isFavorite: false,
subscription: null,
position: 0,
backgroundBlurHash: '',
created: null,
updated: null,

View File

@ -1,6 +1,8 @@
import {createStore} from 'vuex'
import {getBlobFromBlurHash} from '../helpers/getBlobFromBlurHash'
import {
BACKGROUND,
BLUR_HASH,
CURRENT_LIST,
HAS_TASKS,
KEYBOARD_SHORTCUTS_ACTIVE,
@ -39,6 +41,7 @@ export const store = createStore({
// This is used to highlight the current list in menu for all list related views
currentList: {id: 0},
background: '',
blurHash: '',
hasTasks: false,
menuActive: true,
keyboardShortcutsActive: false,
@ -79,6 +82,9 @@ export const store = createStore({
[BACKGROUND](state, background) {
state.background = background
},
[BLUR_HASH](state, blurHash) {
state.blurHash = blurHash
},
vikunjaReady(state, ready) {
state.vikunjaReady = ready
},
@ -89,6 +95,7 @@ export const store = createStore({
if (currentList === null) {
commit(CURRENT_LIST, {})
commit(BACKGROUND, null)
commit(BLUR_HASH, null)
return
}
@ -121,6 +128,11 @@ 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)
@ -132,6 +144,7 @@ export const store = createStore({
if (typeof currentList.backgroundInformation === 'undefined' || currentList.backgroundInformation === null) {
commit(BACKGROUND, null)
commit(BLUR_HASH, null)
}
commit(CURRENT_LIST, currentList)

View File

@ -6,6 +6,7 @@ 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'

View File

@ -1,10 +1,14 @@
.app-container.has-background,
.link-share-container.has-background {
position: relative;
&, .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) {
@ -33,3 +37,20 @@
}
}
}
.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;
}
}

View File

@ -71,7 +71,7 @@ export default {
computed: {
// Computed property to let "listId" always have a value
listId() {
return typeof this.$route.params.listId === 'undefined' ? 0 : this.$route.params.listId
return typeof this.$route.params.listId === 'undefined' ? 0 : parseInt(this.$route.params.listId)
},
background() {
return this.$store.state.background
@ -99,9 +99,11 @@ export default {
const listData = {id: parseInt(this.$route.params.listId)}
saveListToHistory(listData)
this.setTitle(this.currentList.id ? this.getListTitle(this.currentList) : '')
const listFromStore = this.$store.getters['lists/getListById'](this.listId)
await this.$store.dispatch(CURRENT_LIST, listFromStore)
// This invalidates the loaded list at the kanban board which lets it reload its content when
// switched to it. This ensures updates done to tasks in the gantt or list views are consistently
// shown in all views while preventing reloads when closing a task popup.

View File

@ -35,16 +35,25 @@
v-model="backgroundSearchTerm"
/>
<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>
<div class="image-search-result">
<a
:key="im.id"
:style="{'background-image': `url(${backgroundThumbs[im.id]})`}"
:style="{'background-image': `url(${backgroundBlurHashes[im.id]})`}"
@click="() => setBackground(im.id)"
class="image"
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 }}
</a>
</a>
@ -65,6 +74,8 @@
<script>
import {mapState} from 'vuex'
import {getBlobFromBlurHash} from '../../../helpers/getBlobFromBlurHash'
import BackgroundUnsplashService from '../../../services/backgroundUnsplash'
import BackgroundUploadService from '../../../services/backgroundUpload'
import ListService from '@/services/list'
@ -83,6 +94,7 @@ export default {
backgroundSearchTerm: '',
backgroundSearchResult: [],
backgroundThumbs: {},
backgroundBlurHashes: {},
currentPage: 1,
// We're using debounce to not search on every keypress but with a delay.
@ -120,8 +132,16 @@ export default {
this.currentPage = page
const result = await this.backgroundService.getAll({}, {s: this.backgroundSearchTerm, p: page})
this.backgroundSearchResult = this.backgroundSearchResult.concat(result)
result.forEach(async background => {
this.backgroundThumbs[background.id] = await this.backgroundService.thumb(background)
result.forEach(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
})
})
},
@ -183,6 +203,7 @@ export default {
background-size: cover;
background-position: center;
display: flex;
position: relative;
@media screen and (min-width: $desktop) {
&:nth-child(5n) {
@ -226,6 +247,11 @@ export default {
font-weight: bold;
color: var(--white);
transition: opacity $transition;
position: absolute;
}
img {
object-fit: cover;
}
&:hover .info {

View File

@ -4128,6 +4128,11 @@ blueimp-md5@^2.10.0:
resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0"
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:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"