feature/feat-useList-composable #2589
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<header
|
||||
:class="{'has-background': background, 'menu-active': menuActive}"
|
||||
:class="{'has-background': hasBackground, 'menu-active': menuActive}"
|
||||
aria-label="main navigation"
|
||||
class="navbar d-print-none"
|
||||
>
|
||||
|
@ -21,7 +21,7 @@
|
|||
</BaseButton>
|
||||
|
||||
<list-settings-dropdown
|
||||
v-if="canWriteCurrentList && currentList.id !== -1"
|
||||
v-if="canWriteCurrentList && currentList.id !== LIST_ID.FAVORITES"
|
||||
class="list-title-dropdown"
|
||||
:list="currentList"
|
||||
>
|
||||
|
@ -88,6 +88,7 @@
|
|||
import {computed} from 'vue'
|
||||
|
||||
import {RIGHTS as Rights} from '@/constants/rights'
|
||||
import {LIST_ID} from '@/constants/lists'
|
||||
|
||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
|
@ -105,7 +106,7 @@ import {useAuthStore} from '@/stores/auth'
|
|||
|
||||
const baseStore = useBaseStore()
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const background = computed(() => baseStore.background)
|
||||
const hasBackground = computed(() => Boolean(baseStore.currentList?.backgroundInformation))
|
||||
const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Rights.READ)
|
||||
const menuActive = computed(() => baseStore.menuActive)
|
||||
|
||||
|
|
|
@ -9,13 +9,14 @@
|
|||
</BaseButton>
|
||||
<div
|
||||
class="app-container"
|
||||
:class="{'has-background': background || blurHash}"
|
||||
:style="{'background-image': blurHash && `url(${blurHash})`}"
|
||||
:class="{'has-background': backgroundUrl || blurHashUrl}"
|
||||
:style="{'background-image': blurHashUrl ? `url(${blurHashUrl})` : undefined}"
|
||||
>
|
||||
<div
|
||||
:class="{'is-visible': background}"
|
||||
:class="{'is-visible': backgroundUrl}"
|
||||
class="app-container-background background-fade-in d-print-none"
|
||||
:style="{'background-image': background && `url(${background})`}"></div>
|
||||
:style="{'background-image': backgroundUrl ? `url(${backgroundUrl})` : undefined}">
|
||||
</div>
|
||||
<navigation class="d-print-none"/>
|
||||
<main
|
||||
class="app-content"
|
||||
|
@ -72,13 +73,15 @@ import {useLabelStore} from '@/stores/labels'
|
|||
|
||||
import {useRouteWithModal} from '@/composables/useRouteWithModal'
|
||||
import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus'
|
||||
import {useListBackground} from '@/stores/lists'
|
||||
|
||||
const {routeWithModal, currentModal, closeModal} = useRouteWithModal()
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const background = computed(() => baseStore.background)
|
||||
const blurHash = computed(() => baseStore.blurHash)
|
||||
const menuActive = computed(() => baseStore.menuActive)
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
|
||||
const {backgroundUrl, blurHashUrl} = useListBackground(currentList)
|
||||
|
||||
function showKeyboardShortcuts() {
|
||||
baseStore.setKeyboardShortcutsActive(true)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[background ? 'has-background' : '', $route.name as string +'-view']"
|
||||
:style="{'background-image': `url(${background})`}"
|
||||
:class="[backgroundUrl ? 'has-background' : '', $route.name as string +'-view']"
|
||||
:style="{'background-image': backgroundUrl ? `url(${backgroundUrl})`: undefined}"
|
||||
class="link-share-container"
|
||||
>
|
||||
<div class="container has-text-centered link-share-view">
|
||||
|
@ -26,14 +26,18 @@
|
|||
import {computed} from 'vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListBackground} from '@/stores/lists'
|
||||
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
import PoweredByLink from './PoweredByLink.vue'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const background = computed(() => baseStore.background)
|
||||
const logoVisible = computed(() => baseStore.logoVisible)
|
||||
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
|
||||
// TODO: use blurhash here aswell
|
||||
const{backgroundUrl} = useListBackground(currentList)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -132,8 +132,6 @@ watch(
|
|||
loadedListId.value = 0
|
||||
const listFromStore = listStore.getListById(listData.id)
|
||||
if (listFromStore !== null) {
|
||||
baseStore.setBackground(null)
|
||||
baseStore.setBlurHash(null)
|
||||
baseStore.handleSetCurrentList({list: listFromStore})
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,13 @@
|
|||
>
|
||||
{{ $t('menu.unarchive') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.delete', params: { listId: list.id } }"
|
||||
icon="trash-alt"
|
||||
class="has-text-danger"
|
||||
>
|
||||
{{ $t('menu.delete') }}
|
||||
</dropdown-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<dropdown-item
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<div
|
||||
class="list-card"
|
||||
:class="{
|
||||
'has-light-text': background !== null,
|
||||
'has-background': blurHashUrl !== '' || background !== null
|
||||
'has-light-text': backgroundUrl !== null,
|
||||
'has-background': blurHashUrl !== '' || backgroundUrl !== null
|
||||
}"
|
||||
:style="{
|
||||
'border-left': list.hexColor ? `0.25rem solid ${list.hexColor}` : undefined,
|
||||
|
@ -12,8 +12,8 @@
|
|||
>
|
||||
<div
|
||||
class="list-background background-fade-in"
|
||||
:class="{'is-visible': background}"
|
||||
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
|
||||
:class="{'is-visible': backgroundUrl}"
|
||||
:style="{'background-image': backgroundUrl !== null ? `url(${backgroundUrl})` : undefined}"
|
||||
/>
|
||||
<span v-if="list.isArchived" class="is-archived" >{{ $t('namespace.archived') }}</span>
|
||||
|
||||
|
@ -45,8 +45,7 @@ import type {IList} from '@/modelTypes/IList'
|
|||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import {useListBackground} from './useListBackground'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useListBackground, useListStore} from '@/stores/lists'
|
||||
|
||||
const props = defineProps({
|
||||
list: {
|
||||
|
@ -55,7 +54,7 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const {background, blurHashUrl} = useListBackground(toRef(props, 'list'))
|
||||
const {backgroundUrl, blurHashUrl} = useListBackground(toRef(props, 'list'))
|
||||
|
||||
const listStore = useListStore()
|
||||
</script>
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
import {ref, watch, type Ref} from 'vue'
|
||||
import ListService from '@/services/list'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||
|
||||
export function useListBackground(list: Ref<IList>) {
|
||||
const background = ref<string | null>(null)
|
||||
const backgroundLoading = ref(false)
|
||||
const blurHashUrl = ref('')
|
||||
|
||||
watch(
|
||||
() => [list.value.id, list.value.backgroundBlurHash] as [IList['id'], IList['backgroundBlurHash']],
|
||||
async ([listId, blurHash], oldValue) => {
|
||||
if (
|
||||
list.value === null ||
|
||||
!list.value.backgroundInformation ||
|
||||
backgroundLoading.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const [oldListId, oldBlurHash] = oldValue || []
|
||||
if (
|
||||
oldValue !== undefined &&
|
||||
listId === oldListId && blurHash === oldBlurHash
|
||||
) {
|
||||
// list hasn't changed
|
||||
return
|
||||
}
|
||||
|
||||
backgroundLoading.value = true
|
||||
|
||||
try {
|
||||
const blurHashPromise = getBlobFromBlurHash(blurHash).then((blurHash) => {
|
||||
blurHashUrl.value = blurHash ? window.URL.createObjectURL(blurHash) : ''
|
||||
})
|
||||
|
||||
const listService = new ListService()
|
||||
const backgroundPromise = listService.background(list.value).then((result) => {
|
||||
background.value = result
|
||||
})
|
||||
await Promise.all([blurHashPromise, backgroundPromise])
|
||||
} finally {
|
||||
backgroundLoading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
return {
|
||||
background,
|
||||
blurHashUrl,
|
||||
backgroundLoading,
|
||||
}
|
||||
}
|
|
@ -69,7 +69,7 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
|||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useList, useListStore} from '@/stores/lists'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
@ -493,18 +493,16 @@ async function newTask() {
|
|||
await router.push({ name: 'task.detail', params: { id: task.id } })
|
||||
}
|
||||
|
||||
const {createList} = useList()
|
||||
|
||||
async function newList() {
|
||||
if (currentList.value === null) {
|
||||
return
|
||||
}
|
||||
const newList = await listStore.createList(new ListModel({
|
||||
|
||||
await createList({
|
||||
title: query.value,
|
||||
namespaceId: currentList.value.namespaceId,
|
||||
}))
|
||||
success({ message: t('list.create.createdSuccess')})
|
||||
await router.push({
|
||||
name: 'list.index',
|
||||
params: { listId: newList.id },
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export const LIST_ID = {
|
||||
FAVORITES: -1,
|
||||
} as const
|
|
@ -0,0 +1,5 @@
|
|||
export const NAMESPACE_ID = {
|
||||
SHARED_LIST: -1,
|
||||
FAVORITES: -2,
|
||||
FILTERS: -3,
|
||||
} as const
|
|
@ -29,3 +29,14 @@ export async function getBlobFromBlurHash(blurHash: string): Promise<Blob | null
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function getBlurHash(blurHashString: string) {
|
||||
if (!blurHashString) {
|
||||
return
|
||||
}
|
||||
const blurHashBlob = await getBlobFromBlurHash(blurHashString)
|
||||
if (!blurHashBlob) {
|
||||
return
|
||||
}
|
||||
return URL.createObjectURL(blurHashBlob)
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import {i18n} from '@/i18n'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
import {LIST_ID} from '@/constants/lists'
|
||||
|
||||
export function getListTitle(l: IList) {
|
||||
if (l.id === -1) {
|
||||
if (l.id === LIST_ID.FAVORITES) {
|
||||
return i18n.global.t('list.pseudo.favorites.title')
|
||||
}
|
||||
return l.title
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import {i18n} from '@/i18n'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
import {NAMESPACE_ID} from '@/constants/namespaces'
|
||||
|
||||
export const getNamespaceTitle = (n: INamespace) => {
|
||||
if (n.id === -1) {
|
||||
if (n.id === NAMESPACE_ID.SHARED_LIST) {
|
||||
return i18n.global.t('namespace.pseudo.sharedLists.title')
|
||||
}
|
||||
if (n.id === -2) {
|
||||
if (n.id === NAMESPACE_ID.FAVORITES) {
|
||||
return i18n.global.t('namespace.pseudo.favorites.title')
|
||||
}
|
||||
if (n.id === -3) {
|
||||
if (n.id === NAMESPACE_ID.FILTERS) {
|
||||
return i18n.global.t('namespace.pseudo.savedFilters.title')
|
||||
}
|
||||
return n.title
|
||||
|
|
|
@ -169,7 +169,6 @@
|
|||
"title": "List Title",
|
||||
"color": "Color",
|
||||
"lists": "Lists",
|
||||
"list": "List",
|
||||
"search": "Type to search for a list…",
|
||||
"searchSelect": "Click or press enter to select this list",
|
||||
"shared": "Shared Lists",
|
||||
|
@ -187,7 +186,8 @@
|
|||
"unarchive": "Un-Archive this list",
|
||||
"unarchiveText": "You will be able to create new tasks or edit it.",
|
||||
"archiveText": "You won't be able to edit this list or create new tasks until you un-archive it.",
|
||||
"success": "The list was successfully archived."
|
||||
"successArchived": "The list has been successfully archived.",
|
||||
"successUnarchived": "The list has been successfully unarchived."
|
||||
},
|
||||
"background": {
|
||||
"title": "Set list background",
|
||||
|
|
|
@ -16,10 +16,10 @@ export interface IList extends IAbstract {
|
|||
hexColor: string
|
||||
identifier: string
|
||||
backgroundInformation: unknown | null // FIXME: improve type
|
||||
backgroundBlurHash: string
|
||||
isFavorite: boolean
|
||||
subscription: ISubscription
|
||||
position: number
|
||||
backgroundBlurHash: string
|
||||
|
||||
created: Date
|
||||
updated: Date
|
||||
|
|
|
@ -275,6 +275,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ namespaceId: Number(route.params.namespaceId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
|
@ -292,6 +293,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/duplicate',
|
||||
|
@ -300,6 +302,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/share',
|
||||
|
@ -308,6 +311,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/delete',
|
||||
|
@ -316,6 +320,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/archive',
|
||||
|
@ -324,6 +329,7 @@ const router = createRouter({
|
|||
meta: {
|
||||
showAsModal: true,
|
||||
},
|
||||
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||
},
|
||||
{
|
||||
path: '/lists/:listId/settings/edit',
|
||||
|
|
|
@ -2,7 +2,6 @@ import {AuthenticatedHTTPFactory} from '@/helpers/fetcher'
|
|||
import type {Method} from 'axios'
|
||||
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
import AbstractModel from '@/models/abstractModel'
|
||||
import type {IAbstract} from '@/modelTypes/IAbstract'
|
||||
import type {Right} from '@/constants/rights'
|
||||
|
||||
|
@ -15,6 +14,11 @@ interface Paths {
|
|||
reset?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The replacement pattern for url paths, can be overwritten by implementations.
|
||||
*/
|
||||
const ROUTE_PARAMETER_PATTERN = /{([^}]+)}/
|
||||
|
||||
function convertObject(o: Record<string, unknown>) {
|
||||
if (o instanceof Date) {
|
||||
return o.toISOString()
|
||||
|
@ -75,10 +79,8 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
this.http.interceptors.request.use((config) => {
|
||||
switch (config.method) {
|
||||
case 'post':
|
||||
if (this.useUpdateInterceptor()) {
|
||||
config.data = this.beforeUpdate(config.data)
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
}
|
||||
config.data = this.beforeUpdate(config.data)
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
break
|
||||
case 'put':
|
||||
if (this.useCreateInterceptor()) {
|
||||
|
@ -87,10 +89,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
}
|
||||
break
|
||||
case 'delete':
|
||||
if (this.useDeleteInterceptor()) {
|
||||
config.data = this.beforeDelete(config.data)
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
}
|
||||
config.data = objectToSnakeCase(config.data)
|
||||
break
|
||||
}
|
||||
return config
|
||||
|
@ -106,20 +105,6 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not to use the update interceptor which processes a request payload into json
|
||||
*/
|
||||
useUpdateInterceptor(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not to use the delete interceptor which processes a request payload into json
|
||||
*/
|
||||
useDeleteInterceptor(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Helper functions
|
||||
///////////////
|
||||
|
@ -127,10 +112,9 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
/**
|
||||
* Returns an object with all route parameters and their values.
|
||||
*/
|
||||
getRouteReplacements(route : string, parameters : Record<string, unknown> = {}) {
|
||||
private getRouteReplacements(route: string, parameters: {[key: string]: unknown} = {}) {
|
||||
const replace$$1: Record<string, unknown> = {}
|
||||
let pattern = this.getRouteParameterPattern()
|
||||
pattern = new RegExp(pattern instanceof RegExp ? pattern.source : pattern, 'g')
|
||||
const pattern = new RegExp(ROUTE_PARAMETER_PATTERN, 'g')
|
||||
|
||||
for (let parameter; (parameter = pattern.exec(route)) !== null;) {
|
||||
replace$$1[parameter[0]] = parameters[parameter[1]]
|
||||
|
@ -139,17 +123,10 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
return replace$$1
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the replacement pattern for url paths, can be overwritten by implementations.
|
||||
*/
|
||||
getRouteParameterPattern(): RegExp {
|
||||
return /{([^}]+)}/
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fully-ready-ready-to-make-a-request-to route with replaced parameters.
|
||||
*/
|
||||
getReplacedRoute(path : string, pathparams : Record<string, unknown>) : string {
|
||||
getReplacedRoute<M extends {[key: string]: unknown}>(path : string, pathparams : M) : string {
|
||||
const replacements = this.getRouteReplacements(path, pathparams)
|
||||
return Object.entries(replacements).reduce(
|
||||
(result, [parameter, value]) => result.replace(parameter, value as string),
|
||||
|
@ -158,8 +135,8 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
}
|
||||
|
||||
/**
|
||||
* setLoading is a method which sets the loading variable to true, after a timeout of 100ms.
|
||||
* It has the timeout to prevent the loading indicator from showing for only a blink of an eye in the
|
||||
* Sets the loading variable to true, after a timeout of 100ms.
|
||||
* The timeout prevents the loading indicator from showing for only a blink of an eye in the
|
||||
* case the api returns a response in < 100ms.
|
||||
* But because the timeout is created using setTimeout, it will still trigger even if the request is
|
||||
* already finished, so we return a method to call in that case.
|
||||
|
@ -185,36 +162,36 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
* The modelFactory returns a model from an object.
|
||||
* This one here is the default one, usually the service definitions for a model will override this.
|
||||
*/
|
||||
modelFactory(data : Partial<Model>) {
|
||||
return data as Model
|
||||
modelFactory<DefaultModel extends {[key: string]: unknown} = Partial<Model>>(data: DefaultModel = {}) {
|
||||
return {...data} as DefaultModel
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for get requests.
|
||||
*/
|
||||
modelGetFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
modelGetFactory<GetModel extends {[key: string]: unknown} = Partial<Model>>(data: GetModel = {}) {
|
||||
return this.modelFactory<GetModel>(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for get all requests.
|
||||
*/
|
||||
modelGetAllFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
modelGetAllFactory<GetAllModel extends {[key: string]: unknown} = Partial<Model>>(data: GetAllModel = {}) {
|
||||
return this.modelFactory<GetAllModel>(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for create requests.
|
||||
*/
|
||||
modelCreateFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
modelCreateFactory<CreateModel extends {[key: string]: unknown} = Partial<Model>>(data: CreateModel = {}) {
|
||||
return this.modelFactory<CreateModel>(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the model factory for update requests.
|
||||
*/
|
||||
modelUpdateFactory(data : Partial<Model>) {
|
||||
return this.modelFactory(data)
|
||||
modelUpdateFactory<UpdateModel extends {[key: string]: unknown} = Partial<Model>>(data: UpdateModel = {}) {
|
||||
return this.modelFactory<UpdateModel>(data)
|
||||
}
|
||||
|
||||
//////////////
|
||||
|
@ -224,28 +201,21 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
/**
|
||||
* Default preprocessor for get requests
|
||||
*/
|
||||
beforeGet(model : Model) {
|
||||
beforeGet(model: Model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for create requests
|
||||
*/
|
||||
beforeCreate(model : Model) {
|
||||
beforeCreate(model: Model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for update requests
|
||||
*/
|
||||
beforeUpdate(model : Model) {
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preprocessor for delete requests
|
||||
*/
|
||||
beforeDelete(model : Model) {
|
||||
beforeUpdate(model: Model) {
|
||||
return model
|
||||
}
|
||||
|
||||
|
@ -258,7 +228,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
* @param model The model to use. The request path is built using the values from the model.
|
||||
* @param params Optional query parameters
|
||||
*/
|
||||
get(model : Model, params = {}) {
|
||||
get(model: Model, params = {}) {
|
||||
if (this.paths.get === '') {
|
||||
throw new Error('This model is not able to get data.')
|
||||
}
|
||||
|
@ -270,14 +240,14 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
* This is a more abstract implementation which only does a get request.
|
||||
* Services which need more flexibility can use this.
|
||||
*/
|
||||
async getM(url : string, model : Model = new AbstractModel({}), params: Record<string, unknown> = {}) {
|
||||
async getM(url : string, model: Model = this.modelGetFactory(), params: Record<string, unknown> = {}) {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
model = this.beforeGet(model)
|
||||
const finalUrl = this.getReplacedRoute(url, model)
|
||||
|
||||
try {
|
||||
const response = await this.http.get(finalUrl, {params: prepareParams(params)})
|
||||
const response = await this.http.get<Model>(finalUrl, {params: prepareParams(params)})
|
||||
const result = this.modelGetFactory(response.data)
|
||||
result.maxRight = Number(response.headers['x-max-right']) as Right
|
||||
return result
|
||||
|
@ -293,7 +263,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
responseType: 'blob',
|
||||
data,
|
||||
})
|
||||
return window.URL.createObjectURL(new Blob([response.data]))
|
||||
return URL.createObjectURL(new Blob([response.data]))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -303,7 +273,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
* @param params Optional query parameters
|
||||
* @param page The page to get
|
||||
*/
|
||||
async getAll(model : Model = new AbstractModel({}), params = {}, page = 1) {
|
||||
async getAll(model : Model = this.modelFactory(), params = {}, page = 1) {
|
||||
if (this.paths.getAll === '') {
|
||||
throw new Error('This model is not able to get data.')
|
||||
}
|
||||
|
@ -315,7 +285,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
const finalUrl = this.getReplacedRoute(this.paths.getAll, model)
|
||||
|
||||
try {
|
||||
const response = await this.http.get(finalUrl, {params: prepareParams(params)})
|
||||
const response = await this.http.get<Model>(finalUrl, {params: prepareParams(params)})
|
||||
this.resultCount = Number(response.headers['x-pagination-result-count'])
|
||||
this.totalPages = Number(response.headers['x-pagination-total-pages'])
|
||||
|
||||
|
@ -326,7 +296,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
if (Array.isArray(response.data)) {
|
||||
return response.data.map(entry => this.modelGetAllFactory(entry))
|
||||
}
|
||||
return this.modelGetAllFactory(response.data)
|
||||
return [this.modelGetAllFactory(response.data)]
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
|
@ -334,9 +304,8 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
|
||||
/**
|
||||
* Performs a put request to the url specified before
|
||||
* @returns {Promise<any | never>}
|
||||
*/
|
||||
async create(model : Model) {
|
||||
async create<M = Model>(model: M) {
|
||||
if (this.paths.create === '') {
|
||||
throw new Error('This model is not able to create data.')
|
||||
}
|
||||
|
@ -345,7 +314,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
const finalUrl = this.getReplacedRoute(this.paths.create, model)
|
||||
|
||||
try {
|
||||
const response = await this.http.put(finalUrl, model)
|
||||
const response = await this.http.put<M>(finalUrl, model)
|
||||
const result = this.modelCreateFactory(response.data)
|
||||
if (typeof model.maxRight !== 'undefined') {
|
||||
result.maxRight = model.maxRight
|
||||
|
@ -360,11 +329,11 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
* An abstract implementation to send post requests.
|
||||
* Services can use this to implement functions to do post requests other than using the update method.
|
||||
*/
|
||||
async post(url : string, model : Model) {
|
||||
async post<PostModel extends {[key: string]: unknown} = Model>(url : string, model: PostModel) {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
const response = await this.http.post(url, model)
|
||||
const response = await this.http.post<PostModel>(url, model)
|
||||
const result = this.modelUpdateFactory(response.data)
|
||||
if (typeof model.maxRight !== 'undefined') {
|
||||
result.maxRight = model.maxRight
|
||||
|
@ -378,7 +347,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
|||
/**
|
||||
* Performs a post request to the update url
|
||||
*/
|
||||
update(model : Model) {
|
||||
update<M = Model>(model: M) {
|
||||
if (this.paths.update === '') {
|
||||
throw new Error('This model is not able to update data.')
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import AbstractService from './abstractService'
|
||||
import BackgroundImageModel from '../models/backgroundImage'
|
||||
import type {IBackgroundImage} from '@/modelTypes/IBackgroundImage'
|
||||
|
||||
import AbstractService from '@/services/abstractService'
|
||||
|
||||
import BackgroundImageModel from '@/models/backgroundImage'
|
||||
import ListModel from '@/models/list'
|
||||
import type { IBackgroundImage } from '@/modelTypes/IBackgroundImage'
|
||||
|
||||
export default class BackgroundUnsplashService extends AbstractService<IBackgroundImage> {
|
||||
constructor() {
|
||||
|
@ -11,7 +13,7 @@ export default class BackgroundUnsplashService extends AbstractService<IBackgrou
|
|||
})
|
||||
}
|
||||
|
||||
modelFactory(data: Partial<IBackgroundImage>) {
|
||||
modelFactory(data) {
|
||||
return new BackgroundImageModel(data)
|
||||
}
|
||||
|
||||
|
@ -25,6 +27,6 @@ export default class BackgroundUnsplashService extends AbstractService<IBackgrou
|
|||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
})
|
||||
return window.URL.createObjectURL(new Blob([response.data]))
|
||||
return URL.createObjectURL(new Blob([response.data]))
|
||||
}
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
import AbstractService from './abstractService'
|
||||
import ListModel from '@/models/list'
|
||||
|
||||
import type { IList } from '@/modelTypes/IList'
|
||||
import type { IFile } from '@/modelTypes/IFile'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
export default class BackgroundUploadService extends AbstractService {
|
||||
export default class BackgroundUploadService extends AbstractService<IList> {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/lists/{listId}/backgrounds/upload',
|
||||
|
@ -20,9 +19,9 @@ export default class BackgroundUploadService extends AbstractService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to the server
|
||||
*/
|
||||
create(listId: IList['id'], file: IFile) {
|
||||
* Uploads a file to the server
|
||||
*/
|
||||
create(listId: IList['id'], file: File) {
|
||||
return this.uploadFile(
|
||||
this.getReplacedRoute(this.paths.create, {listId}),
|
||||
file,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import AbstractService from './abstractService'
|
||||
import ListModel from '@/models/list'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import TaskService from './task'
|
||||
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import TaskService from '@/services/task'
|
||||
|
||||
import ListModel from '@/models/list'
|
||||
|
||||
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
||||
|
||||
export default class ListService extends AbstractService<IList> {
|
||||
|
@ -34,29 +37,35 @@ export default class ListService extends AbstractService<IList> {
|
|||
return model
|
||||
}
|
||||
|
||||
beforeCreate(list) {
|
||||
beforeCreate(list: IList) {
|
||||
list.hexColor = colorFromHex(list.hexColor)
|
||||
return list
|
||||
}
|
||||
|
||||
async background(list: Pick<IList, 'id' | 'backgroundInformation'>) {
|
||||
if (list.backgroundInformation === null) {
|
||||
return ''
|
||||
async loadBackground(list: Pick<IList, 'id' | 'backgroundInformation'>) {
|
||||
if (list === null || !list.backgroundInformation) {
|
||||
return
|
||||
}
|
||||
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
const response = await this.http({
|
||||
url: `/lists/${list.id}/background`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
})
|
||||
return URL.createObjectURL(new Blob([response.data]))
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
|
||||
const response = await this.http({
|
||||
url: `/lists/${list.id}/background`,
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
})
|
||||
return window.URL.createObjectURL(new Blob([response.data]))
|
||||
}
|
||||
|
||||
async removeBackground(list: Pick<IList, 'id'>) {
|
||||
const cancel = this.setLoading()
|
||||
|
||||
try {
|
||||
const response = await this.http.delete(`/lists/${list.id}/background`, list)
|
||||
const response = await this.http.delete<IList>(`/lists/${list.id}/background`)
|
||||
return response.data
|
||||
} finally {
|
||||
cancel()
|
||||
|
|
|
@ -100,7 +100,7 @@ export default class TaskService extends AbstractService<ITask> {
|
|||
})
|
||||
})
|
||||
|
||||
// Process all attachments to preven parsing errors
|
||||
// Process all attachments to prevent parsing errors
|
||||
if (model.attachments.length > 0) {
|
||||
const attachmentService = new AttachmentService()
|
||||
model.attachments.map(a => {
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { readonly, ref} from 'vue'
|
||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
|
||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||
|
||||
import ListModel from '@/models/list'
|
||||
import ListService from '../services/list'
|
||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||
|
||||
import {useMenuActive} from '@/composables/useMenuActive'
|
||||
|
@ -21,8 +18,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
id: 0,
|
||||
isArchived: false,
|
||||
}))
|
||||
const background = ref('')
|
||||
const blurHash = ref('')
|
||||
|
||||
const hasTasks = ref(false)
|
||||
const keyboardShortcutsActive = ref(false)
|
||||
|
@ -62,14 +57,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
quickActionsActive.value = value
|
||||
}
|
||||
|
||||
function setBackground(newBackground: string) {
|
||||
background.value = newBackground
|
||||
}
|
||||
|
||||
function setBlurHash(newBlurHash: string) {
|
||||
blurHash.value = newBlurHash
|
||||
}
|
||||
|
||||
function setLogoVisible(visible: boolean) {
|
||||
logoVisible.value = visible
|
||||
}
|
||||
|
@ -78,43 +65,14 @@ export const useBaseStore = defineStore('base', () => {
|
|||
ready.value = value
|
||||
}
|
||||
|
||||
async function handleSetCurrentList(
|
||||
{list, forceUpdate = false}: {list: IList | null, forceUpdate?: boolean},
|
||||
function handleSetCurrentList(
|
||||
{list}: {list: IList | null},
|
||||
) {
|
||||
if (list === null) {
|
||||
setCurrentList({})
|
||||
setBackground('')
|
||||
setBlurHash('')
|
||||
return
|
||||
}
|
||||
|
||||
// The forceUpdate parameter is used only when updating a list background directly because in that case
|
||||
// the current list stays the same, but we want to show the new background right away.
|
||||
if (list.id !== currentList.value?.id || forceUpdate) {
|
||||
if (list.backgroundInformation) {
|
||||
try {
|
||||
const blurHash = await getBlobFromBlurHash(list.backgroundBlurHash)
|
||||
if (blurHash) {
|
||||
setBlurHash(window.URL.createObjectURL(blurHash))
|
||||
}
|
||||
|
||||
const listService = new ListService()
|
||||
const background = await listService.background(list)
|
||||
setBackground(background)
|
||||
} catch (e) {
|
||||
console.error('Error getting background image for list', list.id, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof list.backgroundInformation === 'undefined' ||
|
||||
list.backgroundInformation === null
|
||||
) {
|
||||
setBackground('')
|
||||
setBlurHash('')
|
||||
}
|
||||
|
||||
setCurrentList(list)
|
||||
}
|
||||
|
||||
|
@ -129,8 +87,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
loading: readonly(loading),
|
||||
ready: readonly(ready),
|
||||
currentList: readonly(currentList),
|
||||
background: readonly(background),
|
||||
blurHash: readonly(blurHash),
|
||||
hasTasks: readonly(hasTasks),
|
||||
keyboardShortcutsActive: readonly(keyboardShortcutsActive),
|
||||
quickActionsActive: readonly(quickActionsActive),
|
||||
|
@ -142,8 +98,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
setHasTasks,
|
||||
setKeyboardShortcutsActive,
|
||||
setQuickActionsActive,
|
||||
setBackground,
|
||||
setBlurHash,
|
||||
setLogoVisible,
|
||||
|
||||
handleSetCurrentList,
|
||||
|
|
|
@ -1,25 +1,38 @@
|
|||
import {watch, reactive, shallowReactive, unref, toRefs, readonly, ref, computed} from 'vue'
|
||||
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 ListService from '@/services/list'
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
import {removeListFromHistory} from '@/modules/listHistory'
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
import {useNamespaceStore} from './namespaces'
|
||||
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 type {MaybeRef} from '@vueuse/core'
|
||||
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 {useBaseStore} from '@/stores/base'
|
||||
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'])
|
||||
|
||||
const FavoriteListsNamespace = -2
|
||||
|
||||
export interface ListState {
|
||||
[id: IList['id']]: IList
|
||||
}
|
||||
|
@ -35,7 +48,9 @@ export const useListStore = defineStore('list', () => {
|
|||
|
||||
|
||||
const getListById = computed(() => {
|
||||
return (id: IList['id']) => typeof lists.value[id] !== 'undefined' ? lists.value[id] : null
|
||||
return (id: IList['id']) => typeof lists.value[id] !== 'undefined'
|
||||
? lists.value[id]
|
||||
: null
|
||||
})
|
||||
|
||||
const findListByExactname = computed(() => {
|
||||
|
@ -53,7 +68,7 @@ export const useListStore = defineStore('list', () => {
|
|||
?.filter(value => value > 0)
|
||||
.map(id => lists.value[id])
|
||||
.filter(list => list.isArchived === includeArchived)
|
||||
|| []
|
||||
|| [] as IList[]
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -77,15 +92,10 @@ export const useListStore = defineStore('list', () => {
|
|||
})
|
||||
}
|
||||
|
||||
function removeListById(list: IList) {
|
||||
remove(list)
|
||||
delete lists.value[list.id]
|
||||
}
|
||||
|
||||
function toggleListFavorite(list: IList) {
|
||||
// The favorites pseudo list is always favorite
|
||||
// Archived lists cannot be marked favorite
|
||||
if (list.id === -1 || list.isArchived) {
|
||||
if (list.id === LIST_ID.FAVORITES || list.isArchived) {
|
||||
return
|
||||
}
|
||||
return updateList({
|
||||
|
@ -94,6 +104,20 @@ export const useListStore = defineStore('list', () => {
|
|||
})
|
||||
}
|
||||
|
||||
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()
|
||||
|
@ -113,6 +137,8 @@ export const useListStore = defineStore('list', () => {
|
|||
const cancel = setModuleLoading(setIsLoading)
|
||||
const listService = new ListService()
|
||||
|
||||
const oldList = cloneDeep(getListById.value(list.id) as IList)
|
||||
|
||||
try {
|
||||
await listService.update(list)
|
||||
setList(list)
|
||||
|
@ -120,24 +146,18 @@ export const useListStore = defineStore('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: FavoriteListsNamespace,
|
||||
}
|
||||
|
||||
namespaceStore.removeListFromNamespaceById(newList)
|
||||
const newList = {...list}
|
||||
if (list.isFavorite) {
|
||||
namespaceStore.addListToNamespace(newList)
|
||||
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({
|
||||
...list,
|
||||
isFavorite: !list.isFavorite,
|
||||
})
|
||||
setList(oldList)
|
||||
throw e
|
||||
} finally {
|
||||
cancel()
|
||||
|
@ -150,7 +170,8 @@ export const useListStore = defineStore('list', () => {
|
|||
|
||||
try {
|
||||
const response = await listService.delete(list)
|
||||
removeListById(list)
|
||||
remove(list)
|
||||
delete lists.value[list.id]
|
||||
namespaceStore.removeListFromNamespaceById(list)
|
||||
removeListFromHistory({id: list.id})
|
||||
return response
|
||||
|
@ -159,6 +180,124 @@ export const useListStore = defineStore('list', () => {
|
|||
}
|
||||
}
|
||||
|
||||
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),
|
||||
|
@ -169,43 +308,185 @@ export const useListStore = defineStore('list', () => {
|
|||
|
||||
setList,
|
||||
setLists,
|
||||
removeListById,
|
||||
toggleListFavorite,
|
||||
|
||||
// crud
|
||||
loadList,
|
||||
createList,
|
||||
updateList,
|
||||
deleteList,
|
||||
|
||||
toggleArchiveList,
|
||||
|
||||
//duplciate
|
||||
isDuplicatingList,
|
||||
duplicateList,
|
||||
|
||||
isUploadingBackground,
|
||||
uploadListBackground,
|
||||
removeListBackground,
|
||||
setUnsplashBackground,
|
||||
loadListBackground,
|
||||
}
|
||||
})
|
||||
|
||||
export function useList(listId: MaybeRef<IList['id']>) {
|
||||
const listService = shallowReactive(new ListService())
|
||||
const {loading: isLoading} = toRefs(listService)
|
||||
const list: IList = reactive(new ListModel())
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
watch(
|
||||
() => unref(listId),
|
||||
async (listId) => {
|
||||
const loadedList = await listService.get(new ListModel({id: listId}))
|
||||
Object.assign(list, loadedList)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const listStore = useListStore()
|
||||
async function save() {
|
||||
await listStore.updateList(list)
|
||||
success({message: t('list.edit.success')})
|
||||
}
|
||||
|
||||
return {
|
||||
isLoading: readonly(isLoading),
|
||||
list,
|
||||
save,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
import {computed, readonly, ref} from 'vue'
|
||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
|
||||
import NamespaceService from '../services/namespace'
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
import {NAMESPACE_ID} from '@/constants/namespaces'
|
||||
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
|
||||
import {createNewIndexer} from '@/indexes'
|
||||
import NamespaceService from '@/services/namespace'
|
||||
|
||||
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
|
||||
|
||||
export const useNamespaceStore = defineStore('namespace', () => {
|
||||
|
@ -123,20 +127,20 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
|||
}
|
||||
}
|
||||
|
||||
function addListToNamespace(list: IList) {
|
||||
function addListToNamespace(list: IList, namespaceId?: INamespace['id']) {
|
||||
for (const n in namespaces.value) {
|
||||
if (namespaces.value[n].id === list.namespaceId) {
|
||||
if (namespaces.value[n].id === (namespaceId ?? list.namespaceId)) {
|
||||
namespaces.value[n].lists.push(list)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeListFromNamespaceById(list: IList) {
|
||||
function removeListFromNamespaceById(list: IList, namespaceId?: INamespace['id']) {
|
||||
for (const n in namespaces.value) {
|
||||
// We don't have the namespace id on the list which means we need to loop over all lists until we find it.
|
||||
// FIXME: Not ideal at all - we should fix that at the api level.
|
||||
if (namespaces.value[n].id === list.namespaceId) {
|
||||
if (namespaces.value[n].id === (namespaceId ?? list.namespaceId)) {
|
||||
for (const l in namespaces.value[n].lists) {
|
||||
if (namespaces.value[n].lists[l].id === list.id) {
|
||||
namespaces.value[n].lists.splice(l, 1)
|
||||
|
@ -169,14 +173,20 @@ export const useNamespaceStore = defineStore('namespace', () => {
|
|||
|
||||
function loadNamespacesIfFavoritesDontExist() {
|
||||
// The first or second namespace should be the one holding all favorites
|
||||
if (namespaces.value[0].id === -2 || namespaces.value[1]?.id === -2) {
|
||||
if (
|
||||
namespaces.value[0].id === NAMESPACE_ID.FAVORITES ||
|
||||
namespaces.value[1]?.id === NAMESPACE_ID.FAVORITES
|
||||
) {
|
||||
return
|
||||
}
|
||||
return loadNamespaces()
|
||||
}
|
||||
|
||||
function removeFavoritesNamespaceIfEmpty() {
|
||||
if (namespaces.value[0].id === -2 && namespaces.value[0].lists.length === 0) {
|
||||
if (
|
||||
namespaces.value[0].id === NAMESPACE_ID.FAVORITES &&
|
||||
namespaces.value[0].lists.length === 0
|
||||
) {
|
||||
namespaces.value.splice(0, 1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
<template>
|
||||
<create-edit :title="$t('list.create.header')" @create="createNewList()" :primary-disabled="list.title === ''">
|
||||
<create-edit
|
||||
:title="$t('list.create.header')"
|
||||
@create="createNewList()"
|
||||
:primary-disabled="list.title === ''"
|
||||
>
|
||||
<div class="field">
|
||||
<label class="label" for="listTitle">{{ $t('list.title') }}</label>
|
||||
<div
|
||||
:class="{ 'is-loading': listService.loading }"
|
||||
:class="{ 'is-loading': isLoading }"
|
||||
class="control"
|
||||
>
|
||||
<input
|
||||
:class="{ disabled: listService.loading }"
|
||||
:class="{ disabled: isLoading }"
|
||||
@keyup.enter="createNewList()"
|
||||
@keyup.esc="$router.back()"
|
||||
class="input"
|
||||
|
@ -19,7 +23,7 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="showError && list.title === ''">
|
||||
<p class="help is-danger" v-if="errors.createList && list.title === ''">
|
||||
{{ $t('list.create.addTitleRequired') }}
|
||||
</p>
|
||||
<div class="field">
|
||||
|
@ -32,43 +36,29 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, shallowReactive} from 'vue'
|
||||
import {watchEffect} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRouter, useRoute} from 'vue-router'
|
||||
|
||||
import ListService from '@/services/list'
|
||||
import ListModel from '@/models/list'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import ColorPicker from '@/components/input/ColorPicker.vue'
|
||||
|
||||
import {success} from '@/message'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useList} from '@/stores/lists'
|
||||
|
||||
const {namespaceId} = defineProps<{
|
||||
namespaceId: INamespace['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
useTitle(() => t('list.create.header'))
|
||||
|
||||
const showError = ref(false)
|
||||
const list = reactive(new ListModel())
|
||||
const listService = shallowReactive(new ListService())
|
||||
const listStore = useListStore()
|
||||
const {isLoading, errors, list, createList} = useList()
|
||||
|
||||
async function createNewList() {
|
||||
if (list.title === '') {
|
||||
showError.value = true
|
||||
return
|
||||
}
|
||||
showError.value = false
|
||||
watchEffect(() => {
|
||||
list.value.namespaceId = namespaceId
|
||||
})
|
||||
|
||||
list.namespaceId = Number(route.params.namespaceId as string)
|
||||
const newList = await listStore.createList(list)
|
||||
await router.push({
|
||||
name: 'list.index',
|
||||
params: { listId: newList.id },
|
||||
})
|
||||
success({message: t('list.create.createdSuccess') })
|
||||
}
|
||||
const createNewList = () => createList()
|
||||
</script>
|
|
@ -1,12 +1,22 @@
|
|||
<template>
|
||||
<modal
|
||||
@close="$router.back()"
|
||||
@submit="archiveList()"
|
||||
@submit="listStore.toggleArchiveList(list)"
|
||||
>
|
||||
<template #header><span>{{ list.isArchived ? $t('list.archive.unarchive') : $t('list.archive.archive') }}</span></template>
|
||||
<template #header>
|
||||
<span>{{
|
||||
list.isArchived
|
||||
? $t('list.archive.unarchive')
|
||||
: $t('list.archive.archive')
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<p>{{ list.isArchived ? $t('list.archive.unarchiveText') : $t('list.archive.archiveText') }}</p>
|
||||
<p>{{
|
||||
list.isArchived
|
||||
? $t('list.archive.unarchiveText')
|
||||
: $t('list.archive.archiveText')
|
||||
}}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</template>
|
||||
|
@ -16,34 +26,21 @@ export default {name: 'list-setting-archive'}
|
|||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
import {useRouter, useRoute} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import {success} from '@/message'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useList, useListStore} from '@/stores/lists'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
const {listId} = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const listStore = useListStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const {list} = useList(listId)
|
||||
|
||||
const list = computed(() => listStore.getListById(route.params.listId))
|
||||
useTitle(() => t('list.archive.title', {list: list.value.title}))
|
||||
|
||||
async function archiveList() {
|
||||
try {
|
||||
const newList = await listStore.updateList({
|
||||
...list.value,
|
||||
isArchived: !list.value.isArchived,
|
||||
})
|
||||
useBaseStore().setCurrentList(newList)
|
||||
success({message: t('list.archive.success')})
|
||||
} finally {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
>
|
||||
<div class="mb-4" v-if="uploadBackgroundEnabled">
|
||||
<input
|
||||
@change="uploadBackground"
|
||||
@change="listStore.uploadListBackground(listId, ($event.target as HTMLInputElement)?.files?.[0])"
|
||||
accept="image/*"
|
||||
class="is-hidden"
|
||||
ref="backgroundUploadInput"
|
||||
type="file"
|
||||
/>
|
||||
<x-button
|
||||
:loading="backgroundUploadService.loading"
|
||||
:loading="listStore.isUploadingBackground"
|
||||
@click="backgroundUploadInput?.click()"
|
||||
variant="primary"
|
||||
>
|
||||
|
@ -47,7 +47,7 @@
|
|||
<BaseButton
|
||||
v-if="backgroundThumbs[im.id]"
|
||||
class="image-search__image-button"
|
||||
@click="setBackground(im.id)"
|
||||
@click="listStore.setUnsplashBackground(listId, im.id)"
|
||||
>
|
||||
<img class="image-search__image" :src="backgroundThumbs[im.id]" alt="" />
|
||||
</BaseButton>
|
||||
|
@ -79,7 +79,7 @@
|
|||
:shadow="false"
|
||||
variant="tertiary"
|
||||
class="is-danger"
|
||||
@click.prevent.stop="removeBackground"
|
||||
@click.prevent.stop="listStore.removeListBackground(listId)"
|
||||
>
|
||||
{{ $t('list.background.remove') }}
|
||||
</x-button>
|
||||
|
@ -100,42 +100,40 @@ export default { name: 'list-setting-background' }
|
|||
<script setup lang="ts">
|
||||
import {ref, computed, shallowReactive} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
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 CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import {success} from '@/message'
|
||||
import type {IBackgroundImage} from '@/modelTypes/IBackgroundImage'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import {getBlurHash} from '@/helpers/blurhash'
|
||||
|
||||
const SEARCH_DEBOUNCE = 300
|
||||
|
||||
const {listId} = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const baseStore = useBaseStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
useTitle(() => t('list.background.title'))
|
||||
|
||||
const backgroundService = shallowReactive(new BackgroundUnsplashService())
|
||||
const backgroundSearchTerm = ref('')
|
||||
const backgroundSearchResult = ref([])
|
||||
const backgroundSearchResult = ref<IBackgroundImage[]>([])
|
||||
const backgroundThumbs = ref<Record<string, string>>({})
|
||||
const backgroundBlurHashes = ref<Record<string, string>>({})
|
||||
const backgroundBlurHashes = ref<Record<string, string | undefined>>({})
|
||||
const currentPage = ref(1)
|
||||
|
||||
// We're using debounce to not search on every keypress but with a delay.
|
||||
|
@ -143,25 +141,23 @@ const debounceNewBackgroundSearch = debounce(newBackgroundSearch, SEARCH_DEBOUNC
|
|||
trailing: true,
|
||||
})
|
||||
|
||||
const backgroundUploadService = ref(new BackgroundUploadService())
|
||||
const listService = ref(new ListService())
|
||||
const listStore = useListStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const unsplashBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('unsplash'))
|
||||
const uploadBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('upload'))
|
||||
const currentList = computed(() => baseStore.currentList)
|
||||
const hasBackground = computed(() => baseStore.background !== null)
|
||||
const hasBackground = computed(() => baseStore.currentList?.backgroundInformation !== undefined)
|
||||
|
||||
// Show the default collection of backgrounds
|
||||
newBackgroundSearch()
|
||||
|
||||
/**
|
||||
* Reset a few things before searching to not break loading more photos.
|
||||
*/
|
||||
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()
|
||||
|
@ -171,11 +167,10 @@ 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)
|
||||
})
|
||||
result.forEach((background: IBackgroundImage) => {
|
||||
getBlurHash(background.blurHash).then((b) => {
|
||||
backgroundBlurHashes.value[background.id] = b
|
||||
})
|
||||
|
||||
backgroundService.thumb(background).then(b => {
|
||||
backgroundThumbs.value[background.id] = b
|
||||
|
@ -183,47 +178,7 @@ async function searchBackgrounds(page = 1) {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
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 baseStore.handleSetCurrentList({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 baseStore.handleSetCurrentList({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 baseStore.handleSetCurrentList({list, forceUpdate: true})
|
||||
namespaceStore.setListInNamespaceById(list)
|
||||
listStore.setList(list)
|
||||
success({message: t('list.background.removeSuccess')})
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -27,46 +27,37 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watchEffect} from 'vue'
|
||||
import {ref, watch} from 'vue'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {success} from '@/message'
|
||||
import TaskCollectionService from '@/services/taskCollection'
|
||||
import Loading from '@/components/misc/loading.vue'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
|
||||
import {useList} from '@/stores/lists'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
const props = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const listStore = useListStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const totalTasks = ref<number | null>(null)
|
||||
|
||||
const list = computed(() => listStore.getListById(route.params.listId))
|
||||
|
||||
watchEffect(
|
||||
() => {
|
||||
if (!route.params.listId) {
|
||||
watch(
|
||||
() => props.listId,
|
||||
async (currentListId) => {
|
||||
if (!currentListId) {
|
||||
return
|
||||
}
|
||||
|
||||
const taskCollectionService = new TaskCollectionService()
|
||||
taskCollectionService.getAll({listId: route.params.listId}).then(() => {
|
||||
totalTasks.value = taskCollectionService.totalPages * taskCollectionService.resultCount
|
||||
})
|
||||
await taskCollectionService.getAll({listId: currentListId})
|
||||
totalTasks.value = taskCollectionService.totalPages * taskCollectionService.resultCount
|
||||
},
|
||||
{immediate: true},
|
||||
|
||||
)
|
||||
|
||||
const {list, deleteList} = useList(listId)
|
||||
useTitle(() => t('list.delete.title', {list: list?.value?.title}))
|
||||
|
||||
async function deleteList() {
|
||||
if (!list.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await listStore.deleteList(list.value)
|
||||
success({message: t('list.delete.success')})
|
||||
router.push({name: 'home'})
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
:title="$t('list.duplicate.title')"
|
||||
primary-icon="paste"
|
||||
:primary-label="$t('list.duplicate.label')"
|
||||
@primary="duplicateList"
|
||||
:loading="listDuplicateService.loading"
|
||||
:primary-disabled="selectedNamespace === undefined"
|
||||
@primary="duplicateList(listId, selectedNamespace?.id)"
|
||||
:loading="isDuplicatingList"
|
||||
>
|
||||
<p>{{ $t('list.duplicate.text') }}</p>
|
||||
|
||||
|
@ -12,7 +13,7 @@
|
|||
:placeholder="$t('namespace.search')"
|
||||
@search="findNamespaces"
|
||||
:search-results="namespaces"
|
||||
@select="selectNamespace"
|
||||
@select="(namespace) => selectNamespace(namespace as INamespace)"
|
||||
label="title"
|
||||
:search-delay="10"
|
||||
/>
|
||||
|
@ -20,22 +21,23 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, shallowReactive} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {ref, toRefs} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import ListDuplicateService from '@/services/listDuplicateService'
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import Multiselect from '@/components/input/multiselect.vue'
|
||||
|
||||
import ListDuplicateModel from '@/models/listDuplicateModel'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
import {success} from '@/message'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useNamespaceSearch} from '@/composables/useNamespaceSearch'
|
||||
import {useListStore} from '@/stores/lists'
|
||||
import {useNamespaceStore} from '@/stores/namespaces'
|
||||
|
||||
const props = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
const {listId} = toRefs(props)
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
useTitle(() => t('list.duplicate.title'))
|
||||
|
@ -46,30 +48,12 @@ const {
|
|||
} = useNamespaceSearch()
|
||||
|
||||
const selectedNamespace = ref<INamespace>()
|
||||
|
||||
function selectNamespace(namespace: INamespace) {
|
||||
selectedNamespace.value = namespace
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const listStore = useListStore()
|
||||
const namespaceStore = useNamespaceStore()
|
||||
|
||||
const listDuplicateService = shallowReactive(new ListDuplicateService())
|
||||
|
||||
async function duplicateList() {
|
||||
const listDuplicate = new ListDuplicateModel({
|
||||
// FIXME: should be parameter
|
||||
listId: route.params.listId,
|
||||
namespaceId: selectedNamespace.value?.id,
|
||||
})
|
||||
|
||||
const duplicate = await listDuplicateService.create(listDuplicate)
|
||||
|
||||
namespaceStore.addListToNamespace(duplicate.list)
|
||||
listStore.setList(duplicate.list)
|
||||
success({message: t('list.duplicate.success')})
|
||||
router.push({name: 'list.index', params: {listId: duplicate.list.id}})
|
||||
}
|
||||
const {
|
||||
duplicateList,
|
||||
isDuplicatingList,
|
||||
} = useListStore()
|
||||
</script>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
:title="$t('list.edit.header')"
|
||||
primary-icon=""
|
||||
:primary-label="$t('misc.save')"
|
||||
@primary="save"
|
||||
@primary="saveList"
|
||||
:tertiary="$t('misc.delete')"
|
||||
@tertiary="$router.push({ name: 'list.settings.delete', params: { id: listId } })"
|
||||
>
|
||||
|
@ -13,7 +13,7 @@
|
|||
<input
|
||||
:class="{ 'disabled': isLoading}"
|
||||
:disabled="isLoading || undefined"
|
||||
@keyup.enter="save"
|
||||
@keyup.enter="saveList"
|
||||
class="input"
|
||||
id="title"
|
||||
:placeholder="$t('list.edit.titlePlaceholder')"
|
||||
|
@ -33,7 +33,7 @@
|
|||
<input
|
||||
:class="{ 'disabled': isLoading}"
|
||||
:disabled="isLoading || undefined"
|
||||
@keyup.enter="save"
|
||||
@keyup.enter="saveList"
|
||||
class="input"
|
||||
id="identifier"
|
||||
:placeholder="$t('list.edit.identifierPlaceholder')"
|
||||
|
@ -70,8 +70,6 @@ export default { name: 'list-setting-edit' }
|
|||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {PropType} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import Editor from '@/components/input/AsyncEditor'
|
||||
|
@ -80,29 +78,16 @@ import CreateEdit from '@/components/misc/create-edit.vue'
|
|||
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useList} from '@/stores/lists'
|
||||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
const props = defineProps({
|
||||
listId: {
|
||||
type: Number as PropType<IList['id']>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const {listId} = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const {list, save: saveList, isLoading} = useList(props.listId)
|
||||
const {list, saveList, isLoading} = useList(listId)
|
||||
|
||||
useTitle(() => list?.title ? t('list.edit.title', {list: list.title}) : '')
|
||||
|
||||
async function save() {
|
||||
await saveList()
|
||||
await useBaseStore().handleSetCurrentList({list})
|
||||
router.back()
|
||||
}
|
||||
useTitle(() => list.value.title ? t('list.edit.title', {list: list.value.title}) : '')
|
||||
</script>
|
||||
|
|
|
@ -18,7 +18,11 @@
|
|||
/>
|
||||
</template>
|
||||
|
||||
<link-sharing :list-id="listId" v-if="linkSharingEnabled" class="mt-4"/>
|
||||
<link-sharing
|
||||
v-if="linkSharingEnabled"
|
||||
class="mt-4"
|
||||
:list-id="listId"
|
||||
/>
|
||||
</create-edit>
|
||||
</template>
|
||||
|
||||
|
@ -27,26 +31,28 @@ export default {name: 'list-setting-share'}
|
|||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, computed, watchEffect} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import {computed} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useTitle} from '@vueuse/core'
|
||||
|
||||
import ListService from '@/services/list'
|
||||
import ListModel from '@/models/list'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import LinkSharing from '@/components/sharing/linkSharing.vue'
|
||||
import userTeam from '@/components/sharing/userTeam.vue'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
import {useList} from '@/stores/lists'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
const {listId} = defineProps<{
|
||||
listId: IList['id']
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const list = ref<IList>()
|
||||
const {list} = useList(listId)
|
||||
|
||||
const title = computed(() => list.value?.title
|
||||
? t('list.share.title', {list: list.value.title})
|
||||
: '',
|
||||
|
@ -54,21 +60,8 @@ const title = computed(() => list.value?.title
|
|||
useTitle(title)
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const linkSharingEnabled = computed(() => configStore.linkSharingEnabled)
|
||||
const userIsAdmin = computed(() => list?.value?.maxRight === RIGHTS.ADMIN)
|
||||
|
||||
async function loadList(listId: number) {
|
||||
const listService = new ListService()
|
||||
const newList = await listService.get(new ListModel({id: listId}))
|
||||
await useBaseStore().handleSetCurrentList({list: newList})
|
||||
list.value = newList
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const listId = computed(() => route.params.listId !== undefined
|
||||
? parseInt(route.params.listId as string)
|
||||
: undefined,
|
||||
)
|
||||
watchEffect(() => listId.value !== undefined && loadList(listId.value))
|
||||
const userIsAdmin = computed(() => list.value.owner.id === authStore.info?.id)
|
||||
</script>
|
||||
|
|
Reference in New Issue