This repository has been archived on 2024-02-08. You can view files and clone it, but cannot push or open issues or pull requests.
frontend/src/views/list/settings/background.vue

288 lines
7.9 KiB
Vue

<template>
<create-edit
v-if="uploadBackgroundEnabled || unsplashBackgroundEnabled"
:title="$t('list.background.title')"
:loading="backgroundService.loading"
class="list-background-setting"
:wide="true"
>
<div class="mb-4" v-if="uploadBackgroundEnabled">
<input
@change="uploadBackground"
accept="image/*"
class="is-hidden"
ref="backgroundUploadInput"
type="file"
/>
<x-button
:loading="backgroundUploadService.loading"
@click="backgroundUploadInput?.click()"
variant="primary"
>
{{ $t('list.background.upload') }}
</x-button>
</div>
<template v-if="unsplashBackgroundEnabled">
<input
:class="{'is-loading': backgroundService.loading}"
@keyup="debounceNewBackgroundSearch()"
class="input is-expanded"
:placeholder="$t('list.background.searchPlaceholder')"
type="text"
v-model="backgroundSearchTerm"
/>
<p class="unsplash-credit">
<BaseButton class="unsplash-credit__link" href="https://unsplash.com">{{ $t('list.background.poweredByUnsplash') }}</BaseButton>
</p>
<ul class="image-search__result-list">
<li
v-for="im in backgroundSearchResult"
class="image-search__result-item"
:key="im.id"
:style="{'background-image': `url(${backgroundBlurHashes[im.id]})`}"
>
<transition name="fade">
<BaseButton
v-if="backgroundThumbs[im.id]"
class="image-search__image-button"
@click="setBackground(im.id)"
>
<img class="image-search__image" :src="backgroundThumbs[im.id]" alt="" />
</BaseButton>
</transition>
<BaseButton
:href="`https://unsplash.com/@${im.info.author}`"
class="image-search__info"
>
{{ im.info.authorName }}
</BaseButton>
</li>
</ul>
<x-button
v-if="backgroundSearchResult.length > 0"
:disabled="backgroundService.loading"
@click="searchBackgrounds(currentPage + 1)"
class="is-load-more-button mt-4"
:shadow="false"
variant="secondary"
>
{{ backgroundService.loading ? $t('misc.loading') : $t('list.background.loadMore') }}
</x-button>
</template>
<template #footer>
<x-button
v-if="hasBackground"
:shadow="false"
variant="tertiary"
class="is-danger"
@click.prevent.stop="removeBackground"
>
{{ $t('list.background.remove') }}
</x-button>
<x-button
variant="secondary"
@click.prevent.stop="$router.back()"
>
{{ $t('misc.close') }}
</x-button>
</template>
</create-edit>
</template>
<script lang="ts">
export default { name: 'list-setting-background' }
</script>
<script setup lang="ts">
import {ref, computed, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n'
import {useStore} from '@/store'
import {useRoute, useRouter} from 'vue-router'
import debounce from 'lodash.debounce'
import BaseButton from '@/components/base/BaseButton.vue'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'
import BackgroundUnsplashService from '@/services/backgroundUnsplash'
import BackgroundUploadService from '@/services/backgroundUpload'
import ListService from '@/services/list'
import type BackgroundImageModel from '@/models/backgroundImage'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import {useTitle} from '@/composables/useTitle'
import {CURRENT_LIST} from '@/store/mutation-types'
import CreateEdit from '@/components/misc/create-edit.vue'
import {success} from '@/message'
const SEARCH_DEBOUNCE = 300
const {t} = useI18n({useScope: 'global'})
const store = useStore()
const route = useRoute()
const router = useRouter()
useTitle(() => t('list.background.title'))
const backgroundService = shallowReactive(new BackgroundUnsplashService())
const backgroundSearchTerm = ref('')
const backgroundSearchResult = ref([])
const backgroundThumbs = ref<Record<string, string>>({})
const backgroundBlurHashes = ref<Record<string, string>>({})
const currentPage = ref(1)
// We're using debounce to not search on every keypress but with a delay.
const debounceNewBackgroundSearch = debounce(newBackgroundSearch, SEARCH_DEBOUNCE, {
trailing: true,
})
const backgroundUploadService = ref(new BackgroundUploadService())
const listService = ref(new ListService())
const listStore = useListStore()
const namespaceStore = useNamespaceStore()
const unsplashBackgroundEnabled = computed(() => store.state.config.enabledBackgroundProviders.includes('unsplash'))
const uploadBackgroundEnabled = computed(() => store.state.config.enabledBackgroundProviders.includes('upload'))
const currentList = computed(() => store.state.currentList)
const hasBackground = computed(() => store.state.background !== null)
// Show the default collection of backgrounds
newBackgroundSearch()
function newBackgroundSearch() {
if (!unsplashBackgroundEnabled.value) {
return
}
// This is an extra method to reset a few things when searching to not break loading more photos.
backgroundSearchResult.value = []
backgroundThumbs.value = {}
searchBackgrounds()
}
async function searchBackgrounds(page = 1) {
currentPage.value = page
const result = await backgroundService.getAll({}, {s: backgroundSearchTerm.value, p: page})
backgroundSearchResult.value = backgroundSearchResult.value.concat(result)
result.forEach((background: BackgroundImageModel) => {
getBlobFromBlurHash(background.blurHash)
.then((b) => {
backgroundBlurHashes.value[background.id] = window.URL.createObjectURL(b)
})
backgroundService.thumb(background).then(b => {
backgroundThumbs.value[background.id] = b
})
})
}
async function setBackground(backgroundId: string) {
// Don't set a background if we're in the process of setting one
if (backgroundService.loading) {
return
}
const list = await backgroundService.update({id: backgroundId, listId: route.params.listId})
await store.dispatch(CURRENT_LIST, {list, forceUpdate: true})
namespaceStore.setListInNamespaceById(list)
listStore.setList(list)
success({message: t('list.background.success')})
}
const backgroundUploadInput = ref<HTMLInputElement | null>(null)
async function uploadBackground() {
if (backgroundUploadInput.value?.files?.length === 0) {
return
}
const list = await backgroundUploadService.value.create(route.params.listId, backgroundUploadInput.value?.files[0])
await store.dispatch(CURRENT_LIST, {list, forceUpdate: true})
namespaceStore.setListInNamespaceById(list)
listStore.setList(list)
success({message: t('list.background.success')})
}
async function removeBackground() {
const list = await listService.value.removeBackground(currentList.value)
await store.dispatch(CURRENT_LIST, {list, forceUpdate: true})
namespaceStore.setListInNamespaceById(list)
listStore.setList(list)
success({message: t('list.background.removeSuccess')})
router.back()
}
</script>
<style lang="scss" scoped>
.unsplash-credit {
text-align: right;
font-size: .8rem;
}
.unsplash-credit__link {
color: var(--grey-800);
}
.image-search__result-list {
--items-per-row: 1;
margin: 1rem 0 0;
display: grid;
gap: 1rem;
grid-template-columns: repeat(var(--items-per-row), 1fr);
@media screen and (min-width: $mobile) {
--items-per-row: 2;
}
@media screen and (min-width: $tablet) {
--items-per-row: 4;
}
@media screen and (min-width: $tablet) {
--items-per-row: 5;
}
}
.image-search__result-item {
margin-top: 0; // FIXME: removes padding from .content
aspect-ratio: 16 / 10;
background-size: cover;
background-position: center;
display: flex;
position: relative;
}
.image-search__image-button {
width: 100%;
}
.image-search__image {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-search__info {
position: absolute;
bottom: 0;
width: 100%;
padding: .25rem 0;
opacity: 0;
text-align: center;
background: rgba(0, 0, 0, 0.5);
font-size: .75rem;
font-weight: bold;
color: var(--white);
transition: opacity $transition;
}
.image-search__result-item:hover .image-search__info {
opacity: 1;
}
.is-load-more-button {
margin: 1rem auto 0 !important;
display: block;
width: 200px;
}
</style>