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 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' const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description']) export interface ListState { [id: IList['id']]: IList } export const useListStore = defineStore('list', () => { const router = useRouter() 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({}) 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() 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, namespaceId: NAMESPACE_ID.FAVORITES, } if (list.isFavorite) { namespaceStore.addListToNamespace(newList) } else { namespaceStore.removeListFromNamespaceById(newList) } namespaceStore.loadNamespacesIfFavoritesDontExist() namespaceStore.removeFavoritesNamespaceIfEmpty() return newList } catch (e) { // Reset the list state to the initial one to avoid confusion for the user setList({ ...list, isFavorite: !list.isFavorite, }) 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 archiveList(list: IList) { try { const newList = await updateList({ ...list, isArchived: !list.isArchived, }) baseStore.setCurrentList(newList) success({message: i18n.global.t('list.archive.success')}) } 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, archiveList, //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) { const listStore = useListStore() const listService = shallowReactive(new ListService()) const {loading: isLoading} = toRefs(listService) const list = ref(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) { 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) { 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) { const listStore = useListStore() const listVal = computed(() => unref(list)) const backgroundUrl = ref(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, } }