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/stores/lists.ts

492 lines
12 KiB
TypeScript

import {watch, shallowReactive, unref, toRefs, readonly, ref, computed} from 'vue'
import type {MaybeRef} from '@vueuse/core'
import {acceptHMRUpdate, defineStore} from 'pinia'
import {useI18n} from 'vue-i18n'
import {useRouter} from 'vue-router'
import router from '@/router'
import type {IList} from '@/modelTypes/IList'
import type {INamespace} from '@/modelTypes/INamespace'
import type {IBackgroundImage} from '@/modelTypes/IBackgroundImage'
import {LIST_ID} from '@/constants/lists'
import {NAMESPACE_ID} from '@/constants/namespaces'
import ListService from '@/services/list'
import ListDuplicateService from '@/services/listDuplicateService'
import BackgroundUnsplashService from '@/services/backgroundUnsplash'
import ListModel from '@/models/list'
import ListDuplicateModel from '@/models/listDuplicateModel'
import {removeListFromHistory} from '@/modules/listHistory'
import {setModuleLoading} from '@/stores/helper'
import {useBaseStore} from './base'
import {useNamespaceStore} from './namespaces'
import {createNewIndexer} from '@/indexes'
import {success} from '@/message'
import {i18n} from '@/i18n'
import BackgroundUploadService from '@/services/backgroundUpload'
import {getBlurHash} from '@/helpers/blurhash'
import cloneDeep from 'lodash.clonedeep'
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
export interface ListState {
[id: IList['id']]: IList
}
export const useListStore = defineStore('list', () => {
const baseStore = useBaseStore()
const namespaceStore = useNamespaceStore()
const isLoading = ref(false)
// The lists are stored as an object which has the list ids as keys.
const lists = ref<ListState>({})
const getListById = computed(() => {
return (id: IList['id']) => typeof lists.value[id] !== 'undefined'
? lists.value[id]
: null
})
const findListByExactname = computed(() => {
return (name: string) => {
const list = Object.values(lists.value).find(l => {
return l.title.toLowerCase() === name.toLowerCase()
})
return typeof list === 'undefined' ? null : list
}
})
const searchList = computed(() => {
return (query: string, includeArchived = false) => {
return search(query)
?.filter(value => value > 0)
.map(id => lists.value[id])
.filter(list => list.isArchived === includeArchived)
|| [] as IList[]
}
})
function setIsLoading(newIsLoading: boolean) {
isLoading.value = newIsLoading
}
function setList(list: IList) {
lists.value[list.id] = list
update(list)
if (baseStore.currentList?.id === list.id) {
baseStore.setCurrentList(list)
}
}
function setLists(newLists: IList[]) {
newLists.forEach(l => {
lists.value[l.id] = l
add(l)
})
}
function toggleListFavorite(list: IList) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === LIST_ID.FAVORITES || list.isArchived) {
return
}
return updateList({
...list,
isFavorite: !list.isFavorite,
})
}
async function loadList(listId: List['id']) {
const cancel = setModuleLoading(this, setIsLoading)
const listService = new ListService()
try {
const list = await listService.get(new ListModel({id: listId}))
setList(list)
namespaceStore.setListInNamespaceById(list)
return list
} finally {
cancel()
}
}
async function createList(list: IList) {
const cancel = setModuleLoading(setIsLoading)
const listService = new ListService()
try {
const createdList = await listService.create(list)
createdList.namespaceId = list.namespaceId
namespaceStore.addListToNamespace(createdList)
setList(createdList)
return createdList
} finally {
cancel()
}
}
async function updateList(list: IList) {
const cancel = setModuleLoading(setIsLoading)
const listService = new ListService()
const oldList = cloneDeep(getListById.value(list.id) as IList)
try {
await listService.update(list)
setList(list)
namespaceStore.setListInNamespaceById(list)
// the returned list from listService.update is the same!
// in order to not create a manipulation in pinia store we have to create a new copy
const newList = {...list}
if (list.isFavorite) {
namespaceStore.addListToNamespace(newList, NAMESPACE_ID.FAVORITES)
} else {
namespaceStore.removeListFromNamespaceById(newList, NAMESPACE_ID.FAVORITES)
}
namespaceStore.loadNamespacesIfFavoritesDontExist()
namespaceStore.removeFavoritesNamespaceIfEmpty()
return newList
} catch (e) {
// Reset the list state to the initial one to avoid confusion for the user
setList(oldList)
throw e
} finally {
cancel()
}
}
async function deleteList(list: IList) {
const cancel = setModuleLoading(setIsLoading)
const listService = new ListService()
try {
const response = await listService.delete(list)
remove(list)
delete lists.value[list.id]
namespaceStore.removeListFromNamespaceById(list)
removeListFromHistory({id: list.id})
return response
} finally {
cancel()
}
}
async function toggleArchiveList(list: IList) {
try {
const newList = await updateList({
...list,
isArchived: !list.isArchived,
})
baseStore.setCurrentList(newList)
success({message: newList.isArchived
? i18n.global.t('list.archive.successArchived')
: i18n.global.t('list.archive.successUnarchived'),
})
} finally {
router.back()
}
}
const listDuplicateService = shallowReactive(new ListDuplicateService())
const isDuplicatingList = computed(() => listDuplicateService.loading)
async function duplicateList(listId: IList['id'], namespaceId: INamespace['id'] | undefined) {
if (namespaceId === undefined) {
throw new Error()
}
const listDuplicate = new ListDuplicateModel({
listId,
namespaceId: namespaceId,
})
const duplicate = await listDuplicateService.create(listDuplicate)
namespaceStore.addListToNamespace(duplicate.list)
setList(duplicate.list)
success({message: i18n.global.t('list.duplicate.success')})
router.push({name: 'list.index', params: {listId: duplicate.list.id}})
}
const backgroundService = shallowReactive(new BackgroundUnsplashService())
async function setUnsplashBackground(listId: IList['id'], backgroundId: IBackgroundImage['id']) {
// Don't set a background if we're in the process of setting one
if (backgroundService.loading || listId === undefined) {
return
}
const list: IList = await backgroundService.update({
id: backgroundId,
listId,
})
baseStore.handleSetCurrentList({list})
namespaceStore.setListInNamespaceById(list)
setList(list)
success({message: i18n.global.t('list.background.success')})
}
const backgroundUploadService = shallowReactive(new BackgroundUploadService())
const isUploadingBackground = computed(() => backgroundUploadService.loading)
async function uploadListBackground(listId: IList['id'], file: File | undefined) {
if (listId === undefined || file === undefined) {
return
}
const list = await backgroundUploadService.create(listId, file)
baseStore.handleSetCurrentList({list})
namespaceStore.setListInNamespaceById(list)
setList(list)
success({message: i18n.global.t('list.background.success')})
}
async function removeListBackground(listId: IList['id']) {
const listService = new ListService()
const listWithBackground = getListById.value(listId)
if (listWithBackground === null) {
return
}
const list = await listService.removeBackground(listWithBackground)
baseStore.handleSetCurrentList({list})
namespaceStore.setListInNamespaceById(list)
setList(list)
success({message: i18n.global.t('list.background.removeSuccess')})
router.back()
}
async function loadListBackground(
list: IList,
blurhashSetter: (blurhash: string) => void,
backgroundSetter: (background: string) => void,
) {
if (
list === null ||
!list.backgroundInformation
) {
blurhashSetter('')
backgroundSetter('')
return
}
try {
const listService = new ListService()
const blurHashPromise = getBlurHash(list.backgroundBlurHash).then(blurHash => {
blurhashSetter(blurHash || '')
})
const backgroundPromise = listService.loadBackground(list).then(background => {
if (background === undefined) {
throw new Error()
}
backgroundSetter(background)
})
await Promise.all([
blurHashPromise,
backgroundPromise,
])
} catch (e) {
console.error('Error getting background image for list', list.id, e)
}
}
return {
isLoading: readonly(isLoading),
lists: readonly(lists),
getListById,
findListByExactname,
searchList,
setList,
setLists,
toggleListFavorite,
// crud
loadList,
createList,
updateList,
deleteList,
toggleArchiveList,
//duplciate
isDuplicatingList,
duplicateList,
isUploadingBackground,
uploadListBackground,
removeListBackground,
setUnsplashBackground,
loadListBackground,
}
})
// support hot reloading
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useListStore, import.meta.hot))
}
export function useListBase(listId?: MaybeRef<IList['id']>) {
const listStore = useListStore()
const listService = shallowReactive(new ListService())
const {loading: isLoading} = toRefs(listService)
const list = ref<IList>(new ListModel())
const currentListId = computed(() => unref(listId))
// load list
watch(
currentListId,
async (listId, oldListId) => {
if (listId === oldListId) {
return
}
if (listId === undefined) {
list.value = new ListModel()
return
}
list.value = listStore.getListById(listId) || list.value
// FIXME: does this also make sense for a newly created list?
list.value = await listService.get(new ListModel({id: listId}))
},
{immediate: true},
)
return {
listStore,
listService,
isLoading,
currentListId,
list,
}
}
export function useList(listId?: MaybeRef<IList['id']>) {
const {
listStore,
listService,
isLoading,
list,
} = useListBase(listId)
const {t} = useI18n({useScope: 'global'})
const router = useRouter()
const baseStore = useBaseStore()
const namespaceStore = useNamespaceStore()
const errors = ref<{ [key: string]: string | boolean }>({})
async function createList(newList?: Partial<IList>) {
if (newList !== undefined) {
list.value = new ListModel(newList)
}
if (list.value.title === '') {
errors.value.createList = true
return
}
errors.value.createList = false
list.value = await listStore.createList(list.value)
await router.push({
name: 'list.index',
params: { listId: list.value.id },
})
success({message: t('list.create.createdSuccess') })
}
async function saveList() {
await listStore.updateList(list.value)
success({message: t('list.edit.success')})
baseStore.handleSetCurrentList({list: list.value})
router.back()
}
async function deleteList() {
if (!list.value) {
return
}
await listStore.deleteList(list.value)
success({message: t('list.delete.success')})
router.push({name: 'home'})
}
async function removeListBackground() {
list.value = await listService.removeBackground(list.value)
useBaseStore().handleSetCurrentList({list: list.value})
namespaceStore.setListInNamespaceById(list.value)
listStore.setList(list.value)
success({message: t('list.background.removeSuccess')})
router.back()
}
return {
isLoading: readonly(isLoading),
errors: readonly(errors),
list,
createList,
saveList,
deleteList,
removeListBackground,
}
}
export function useListBackground(list: MaybeRef<IList>) {
const listStore = useListStore()
const listVal = computed(() => unref(list))
const backgroundUrl = ref<string | null>(null)
const backgroundLoading = ref(false)
const blurHashUrl = ref('')
watch(
() => [listVal.value.id, listVal.value.backgroundBlurHash] as [IList['id'], IList['backgroundBlurHash']],
async ([listId, blurHash], oldValue) => {
if (backgroundLoading.value) {
return
}
const [oldListId, oldBlurHash] = oldValue || []
if (
oldValue !== undefined &&
listId === oldListId && blurHash === oldBlurHash
) {
// list hasn't changed
return
}
backgroundLoading.value = true
await listStore.loadListBackground(
listVal.value,
(value) => {
blurHashUrl.value = value
},
(value) => {
backgroundUrl.value = value
},
)
backgroundLoading.value = false
},
{ immediate: true },
)
return {
backgroundUrl,
blurHashUrl,
backgroundLoading,
}
}