WIP: List composable
This commit is contained in:
parent
c3c9960f1c
commit
6722601467
|
@ -0,0 +1,195 @@
|
||||||
|
<template>
|
||||||
|
<router-link
|
||||||
|
:class="{
|
||||||
|
'has-light-text': !colorIsDark(list.hexColor) || background !== null,
|
||||||
|
'has-background': blurHashUrl !== '' || background !== null,
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
'background-color': list.hexColor,
|
||||||
|
'background-image': blurHashUrl !== null ? `url(${blurHashUrl})` : false,
|
||||||
|
}"
|
||||||
|
:to="{ name: 'list.index', params: { listId: list.id} }"
|
||||||
|
class="list-card"
|
||||||
|
v-if="list !== null && (showArchived ? true : !list.isArchived)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="list-background background-fade-in"
|
||||||
|
:class="{'is-visible': background}"
|
||||||
|
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
|
||||||
|
/>
|
||||||
|
<div class="list-content">
|
||||||
|
<span class="is-archived" v-if="list.isArchived">
|
||||||
|
{{ $t('namespace.archived') }}
|
||||||
|
</span>
|
||||||
|
<BaseButton
|
||||||
|
v-else
|
||||||
|
:class="{'is-favorite': list.isFavorite}"
|
||||||
|
@click.stop="listStore.toggleListFavorite(list)"
|
||||||
|
class="favorite"
|
||||||
|
>
|
||||||
|
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']"/>
|
||||||
|
</BaseButton>
|
||||||
|
|
||||||
|
<div class="title">{{ list.title }}</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type {PropType} from 'vue'
|
||||||
|
|
||||||
|
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||||
|
|
||||||
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
import type {IList} from '@/modelTypes/IList'
|
||||||
|
import {useListBackground, useListStore} from '@/stores/lists'
|
||||||
|
|
||||||
|
const {list} = defineProps({
|
||||||
|
list: {
|
||||||
|
type: Object as PropType<IList>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
showArchived: {
|
||||||
|
default: false,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const listStore = useListStore()
|
||||||
|
|
||||||
|
const {background, blurHash: blurHashUrl} = useListBackground(list.id)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.list-card {
|
||||||
|
cursor: pointer;
|
||||||
|
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||||
|
height: $list-height;
|
||||||
|
background: var(--white);
|
||||||
|
margin: 0 $list-spacing $list-spacing 0;
|
||||||
|
border-radius: $radius;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: box-shadow $transition;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.has-light-text .title {
|
||||||
|
color: var(--grey-100) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-background,
|
||||||
|
.list-background {
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-background .title {
|
||||||
|
text-shadow: 0 0 10px var(--black), 1px 1px 5px var(--grey-700), -1px -1px 5px var(--grey-700);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:focus:not(:active) {
|
||||||
|
box-shadow: var(--shadow-xs) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: $widescreen) {
|
||||||
|
&:nth-child(#{$lists-per-row}n) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $widescreen) and (min-width: $tablet) {
|
||||||
|
$lists-per-row: 3;
|
||||||
|
& {
|
||||||
|
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(#{$lists-per-row}n) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $tablet) {
|
||||||
|
$lists-per-row: 2;
|
||||||
|
& {
|
||||||
|
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(#{$lists-per-row}n) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $mobile) {
|
||||||
|
$lists-per-row: 1;
|
||||||
|
& {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-content {
|
||||||
|
display: flex;
|
||||||
|
align-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 1rem;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
|
||||||
|
.is-archived {
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite {
|
||||||
|
margin-left: auto;
|
||||||
|
transition: opacity $transition, color $transition;
|
||||||
|
opacity: 0;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.is-favorite {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite.is-favorite,
|
||||||
|
&:hover .favorite {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
align-self: flex-end;
|
||||||
|
font-family: $vikunja-font;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--text);
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
max-height: calc(100% - 2rem); // 1rem padding, 1rem height of the "is archived" badge
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -69,7 +69,7 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useListStore} from '@/stores/lists'
|
import {useList, useListStore} from '@/stores/lists'
|
||||||
import {useNamespaceStore} from '@/stores/namespaces'
|
import {useNamespaceStore} from '@/stores/namespaces'
|
||||||
import {useLabelStore} from '@/stores/labels'
|
import {useLabelStore} from '@/stores/labels'
|
||||||
import {useTaskStore} from '@/stores/tasks'
|
import {useTaskStore} from '@/stores/tasks'
|
||||||
|
@ -493,18 +493,16 @@ async function newTask() {
|
||||||
await router.push({ name: 'task.detail', params: { id: task.id } })
|
await router.push({ name: 'task.detail', params: { id: task.id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {createList} = useList()
|
||||||
|
|
||||||
async function newList() {
|
async function newList() {
|
||||||
if (currentList.value === null) {
|
if (currentList.value === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const newList = await listStore.createList(new ListModel({
|
|
||||||
|
await createList({
|
||||||
title: query.value,
|
title: query.value,
|
||||||
namespaceId: currentList.value.namespaceId,
|
namespaceId: currentList.value.namespaceId,
|
||||||
}))
|
|
||||||
success({ message: t('list.create.createdSuccess')})
|
|
||||||
await router.push({
|
|
||||||
name: 'list.index',
|
|
||||||
params: { listId: newList.id },
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
export const LIST_ID = {
|
export const LIST_ID = {
|
||||||
FAVORITES: -1
|
FAVORITES: -1,
|
||||||
} as const
|
} as const
|
|
@ -2,4 +2,4 @@ export const NAMESPACE_ID = {
|
||||||
SHARED_LIST: -1,
|
SHARED_LIST: -1,
|
||||||
FAVORITES: -2,
|
FAVORITES: -2,
|
||||||
FILTERS: -3,
|
FILTERS: -3,
|
||||||
} as const;
|
} as const
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {i18n} from '@/i18n'
|
import {i18n} from '@/i18n'
|
||||||
import type {INamespace} from '@/modelTypes/INamespace'
|
import type {INamespace} from '@/modelTypes/INamespace'
|
||||||
|
|
||||||
import {NAMESPACE_ID} from "@/constants/namespaces"
|
import {NAMESPACE_ID} from '@/constants/namespaces'
|
||||||
|
|
||||||
export const getNamespaceTitle = (n: INamespace) => {
|
export const getNamespaceTitle = (n: INamespace) => {
|
||||||
if (n.id === NAMESPACE_ID.SHARED_LIST) {
|
if (n.id === NAMESPACE_ID.SHARED_LIST) {
|
||||||
|
|
|
@ -300,6 +300,7 @@ const router = createRouter({
|
||||||
meta: {
|
meta: {
|
||||||
showAsModal: true,
|
showAsModal: true,
|
||||||
},
|
},
|
||||||
|
props: route => ({ listId: Number(route.params.listId as string) }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lists/:listId/settings/share',
|
path: '/lists/:listId/settings/share',
|
||||||
|
|
|
@ -14,6 +14,11 @@ interface Paths {
|
||||||
reset?: string
|
reset?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The replacement pattern for url paths, can be overwritten by implementations.
|
||||||
|
*/
|
||||||
|
const ROUTE_PARAMETER_PATTERN = /{([^}]+)}/
|
||||||
|
|
||||||
function convertObject(o: Record<string, unknown>) {
|
function convertObject(o: Record<string, unknown>) {
|
||||||
if (o instanceof Date) {
|
if (o instanceof Date) {
|
||||||
return o.toISOString()
|
return o.toISOString()
|
||||||
|
@ -74,10 +79,8 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
this.http.interceptors.request.use((config) => {
|
this.http.interceptors.request.use((config) => {
|
||||||
switch (config.method) {
|
switch (config.method) {
|
||||||
case 'post':
|
case 'post':
|
||||||
if (this.useUpdateInterceptor()) {
|
config.data = this.beforeUpdate(config.data)
|
||||||
config.data = this.beforeUpdate(config.data)
|
config.data = objectToSnakeCase(config.data)
|
||||||
config.data = objectToSnakeCase(config.data)
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
case 'put':
|
case 'put':
|
||||||
if (this.useCreateInterceptor()) {
|
if (this.useCreateInterceptor()) {
|
||||||
|
@ -86,10 +89,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'delete':
|
case 'delete':
|
||||||
if (this.useDeleteInterceptor()) {
|
config.data = objectToSnakeCase(config.data)
|
||||||
config.data = this.beforeDelete(config.data)
|
|
||||||
config.data = objectToSnakeCase(config.data)
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
|
@ -105,20 +105,6 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
return true
|
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
|
// Helper functions
|
||||||
///////////////
|
///////////////
|
||||||
|
@ -126,10 +112,9 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
/**
|
/**
|
||||||
* Returns an object with all route parameters and their values.
|
* 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> = {}
|
const replace$$1: Record<string, unknown> = {}
|
||||||
let pattern = this.getRouteParameterPattern()
|
const pattern = new RegExp(ROUTE_PARAMETER_PATTERN, 'g')
|
||||||
pattern = new RegExp(pattern instanceof RegExp ? pattern.source : pattern, 'g')
|
|
||||||
|
|
||||||
for (let parameter; (parameter = pattern.exec(route)) !== null;) {
|
for (let parameter; (parameter = pattern.exec(route)) !== null;) {
|
||||||
replace$$1[parameter[0]] = parameters[parameter[1]]
|
replace$$1[parameter[0]] = parameters[parameter[1]]
|
||||||
|
@ -138,17 +123,10 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
return replace$$1
|
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.
|
* 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)
|
const replacements = this.getRouteReplacements(path, pathparams)
|
||||||
return Object.entries(replacements).reduce(
|
return Object.entries(replacements).reduce(
|
||||||
(result, [parameter, value]) => result.replace(parameter, value as string),
|
(result, [parameter, value]) => result.replace(parameter, value as string),
|
||||||
|
@ -157,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.
|
* 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
|
* 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.
|
* 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
|
* 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.
|
* already finished, so we return a method to call in that case.
|
||||||
|
@ -184,36 +162,36 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
* The modelFactory returns a model from an object.
|
* 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.
|
* This one here is the default one, usually the service definitions for a model will override this.
|
||||||
*/
|
*/
|
||||||
modelFactory(data : Partial<Model> = {}) {
|
modelFactory<DefaultModel extends {[key: string]: unknown} = Partial<Model>>(data: DefaultModel = {}) {
|
||||||
return {...data} as Model
|
return {...data} as DefaultModel
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the model factory for get requests.
|
* This is the model factory for get requests.
|
||||||
*/
|
*/
|
||||||
modelGetFactory(data : Partial<Model> = {}) {
|
modelGetFactory<GetModel extends {[key: string]: unknown} = Partial<Model>>(data: GetModel = {}) {
|
||||||
return this.modelFactory(data)
|
return this.modelFactory<GetModel>(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the model factory for get all requests.
|
* This is the model factory for get all requests.
|
||||||
*/
|
*/
|
||||||
modelGetAllFactory(data : Partial<Model>) {
|
modelGetAllFactory<GetAllModel extends {[key: string]: unknown} = Partial<Model>>(data: GetAllModel = {}) {
|
||||||
return this.modelFactory(data)
|
return this.modelFactory<GetAllModel>(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the model factory for create requests.
|
* This is the model factory for create requests.
|
||||||
*/
|
*/
|
||||||
modelCreateFactory(data : Partial<Model>) {
|
modelCreateFactory<CreateModel extends {[key: string]: unknown} = Partial<Model>>(data: CreateModel = {}) {
|
||||||
return this.modelFactory(data)
|
return this.modelFactory<CreateModel>(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the model factory for update requests.
|
* This is the model factory for update requests.
|
||||||
*/
|
*/
|
||||||
modelUpdateFactory(data : Partial<Model>) {
|
modelUpdateFactory<UpdateModel extends {[key: string]: unknown} = Partial<Model>>(data: UpdateModel = {}) {
|
||||||
return this.modelFactory(data)
|
return this.modelFactory<UpdateModel>(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////
|
//////////////
|
||||||
|
@ -223,28 +201,21 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
/**
|
/**
|
||||||
* Default preprocessor for get requests
|
* Default preprocessor for get requests
|
||||||
*/
|
*/
|
||||||
beforeGet(model : Model) {
|
beforeGet(model: Model) {
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default preprocessor for create requests
|
* Default preprocessor for create requests
|
||||||
*/
|
*/
|
||||||
beforeCreate(model : Model) {
|
beforeCreate(model: Model) {
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default preprocessor for update requests
|
* Default preprocessor for update requests
|
||||||
*/
|
*/
|
||||||
beforeUpdate(model : Model) {
|
beforeUpdate(model: Model) {
|
||||||
return model
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default preprocessor for delete requests
|
|
||||||
*/
|
|
||||||
beforeDelete(model : Model) {
|
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,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 model The model to use. The request path is built using the values from the model.
|
||||||
* @param params Optional query parameters
|
* @param params Optional query parameters
|
||||||
*/
|
*/
|
||||||
get(model : Model, params = {}) {
|
get(model: Model, params = {}) {
|
||||||
if (this.paths.get === '') {
|
if (this.paths.get === '') {
|
||||||
throw new Error('This model is not able to get data.')
|
throw new Error('This model is not able to get data.')
|
||||||
}
|
}
|
||||||
|
@ -269,14 +240,14 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
* This is a more abstract implementation which only does a get request.
|
* This is a more abstract implementation which only does a get request.
|
||||||
* Services which need more flexibility can use this.
|
* Services which need more flexibility can use this.
|
||||||
*/
|
*/
|
||||||
async getM(url : string, model : Model = this.modelGetFactory(), params: Record<string, unknown> = {}) {
|
async getM(url : string, model: Model = this.modelGetFactory(), params: Record<string, unknown> = {}) {
|
||||||
const cancel = this.setLoading()
|
const cancel = this.setLoading()
|
||||||
|
|
||||||
model = this.beforeGet(model)
|
model = this.beforeGet(model)
|
||||||
const finalUrl = this.getReplacedRoute(url, model)
|
const finalUrl = this.getReplacedRoute(url, model)
|
||||||
|
|
||||||
try {
|
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)
|
const result = this.modelGetFactory(response.data)
|
||||||
result.maxRight = Number(response.headers['x-max-right']) as Right
|
result.maxRight = Number(response.headers['x-max-right']) as Right
|
||||||
return result
|
return result
|
||||||
|
@ -314,7 +285,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
const finalUrl = this.getReplacedRoute(this.paths.getAll, model)
|
const finalUrl = this.getReplacedRoute(this.paths.getAll, model)
|
||||||
|
|
||||||
try {
|
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.resultCount = Number(response.headers['x-pagination-result-count'])
|
||||||
this.totalPages = Number(response.headers['x-pagination-total-pages'])
|
this.totalPages = Number(response.headers['x-pagination-total-pages'])
|
||||||
|
|
||||||
|
@ -325,7 +296,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
if (Array.isArray(response.data)) {
|
if (Array.isArray(response.data)) {
|
||||||
return response.data.map(entry => this.modelGetAllFactory(entry))
|
return response.data.map(entry => this.modelGetAllFactory(entry))
|
||||||
}
|
}
|
||||||
return this.modelGetAllFactory(response.data)
|
return [this.modelGetAllFactory(response.data)]
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
@ -333,9 +304,8 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a put request to the url specified before
|
* 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 === '') {
|
if (this.paths.create === '') {
|
||||||
throw new Error('This model is not able to create data.')
|
throw new Error('This model is not able to create data.')
|
||||||
}
|
}
|
||||||
|
@ -344,7 +314,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
const finalUrl = this.getReplacedRoute(this.paths.create, model)
|
const finalUrl = this.getReplacedRoute(this.paths.create, model)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.http.put(finalUrl, model)
|
const response = await this.http.put<M>(finalUrl, model)
|
||||||
const result = this.modelCreateFactory(response.data)
|
const result = this.modelCreateFactory(response.data)
|
||||||
if (typeof model.maxRight !== 'undefined') {
|
if (typeof model.maxRight !== 'undefined') {
|
||||||
result.maxRight = model.maxRight
|
result.maxRight = model.maxRight
|
||||||
|
@ -359,11 +329,11 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
* An abstract implementation to send post requests.
|
* An abstract implementation to send post requests.
|
||||||
* Services can use this to implement functions to do post requests other than using the update method.
|
* 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()
|
const cancel = this.setLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.http.post(url, model)
|
const response = await this.http.post<PostModel>(url, model)
|
||||||
const result = this.modelUpdateFactory(response.data)
|
const result = this.modelUpdateFactory(response.data)
|
||||||
if (typeof model.maxRight !== 'undefined') {
|
if (typeof model.maxRight !== 'undefined') {
|
||||||
result.maxRight = model.maxRight
|
result.maxRight = model.maxRight
|
||||||
|
@ -377,7 +347,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
|
||||||
/**
|
/**
|
||||||
* Performs a post request to the update url
|
* Performs a post request to the update url
|
||||||
*/
|
*/
|
||||||
update(model : Model) {
|
update<M = Model>(model: M) {
|
||||||
if (this.paths.update === '') {
|
if (this.paths.update === '') {
|
||||||
throw new Error('This model is not able to update data.')
|
throw new Error('This model is not able to update data.')
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import AbstractService from './abstractService'
|
import type {IBackgroundImage} from '@/modelTypes/IBackgroundImage'
|
||||||
import BackgroundImageModel from '../models/backgroundImage'
|
|
||||||
|
import AbstractService from '@/services/abstractService'
|
||||||
|
|
||||||
|
import BackgroundImageModel from '@/models/backgroundImage'
|
||||||
import ListModel from '@/models/list'
|
import ListModel from '@/models/list'
|
||||||
import type { IBackgroundImage } from '@/modelTypes/IBackgroundImage'
|
|
||||||
|
|
||||||
export default class BackgroundUnsplashService extends AbstractService<IBackgroundImage> {
|
export default class BackgroundUnsplashService extends AbstractService<IBackgroundImage> {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -11,7 +13,7 @@ export default class BackgroundUnsplashService extends AbstractService<IBackgrou
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
modelFactory(data: Partial<IBackgroundImage>) {
|
modelFactory(data) {
|
||||||
return new BackgroundImageModel(data)
|
return new BackgroundImageModel(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import AbstractService from './abstractService'
|
import AbstractService from './abstractService'
|
||||||
import ListModel from '@/models/list'
|
import ListModel from '@/models/list'
|
||||||
|
|
||||||
import type { IList } from '@/modelTypes/IList'
|
import type {IList} from '@/modelTypes/IList'
|
||||||
import type { IFile } from '@/modelTypes/IFile'
|
|
||||||
|
|
||||||
export default class BackgroundUploadService extends AbstractService {
|
export default class BackgroundUploadService extends AbstractService<IList> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
create: '/lists/{listId}/backgrounds/upload',
|
create: '/lists/{listId}/backgrounds/upload',
|
||||||
|
@ -20,9 +19,9 @@ export default class BackgroundUploadService extends AbstractService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a file to the server
|
* Uploads a file to the server
|
||||||
*/
|
*/
|
||||||
create(listId: IList['id'], file: IFile) {
|
create(listId: IList['id'], file: File) {
|
||||||
return this.uploadFile(
|
return this.uploadFile(
|
||||||
this.getReplacedRoute(this.paths.create, {listId}),
|
this.getReplacedRoute(this.paths.create, {listId}),
|
||||||
file,
|
file,
|
||||||
|
|
|
@ -1,23 +1,12 @@
|
||||||
import {computed, ref, shallowReactive, unref, watch} from 'vue'
|
|
||||||
import type {MaybeRef} from '@vueuse/core'
|
|
||||||
import {useI18n} from 'vue-i18n'
|
|
||||||
import {useRouter} from 'vue-router'
|
|
||||||
|
|
||||||
import type {IList} from '@/modelTypes/IList'
|
import type {IList} from '@/modelTypes/IList'
|
||||||
import type {INamespace} from '@/modelTypes/INamespace'
|
|
||||||
|
|
||||||
import {useListStore} from '@/stores/lists'
|
|
||||||
import {useNamespaceStore} from '@/stores/namespaces'
|
|
||||||
|
|
||||||
import AbstractService from '@/services/abstractService'
|
import AbstractService from '@/services/abstractService'
|
||||||
import TaskService from '@/services/task'
|
import TaskService from '@/services/task'
|
||||||
import ListDuplicateService from '@/services/listDuplicateService'
|
|
||||||
|
|
||||||
import ListModel from '@/models/list'
|
import ListModel from '@/models/list'
|
||||||
import ListDuplicateModel from '@/models/listDuplicateModel'
|
|
||||||
|
|
||||||
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
import {colorFromHex} from '@/helpers/color/colorFromHex'
|
||||||
import {success} from '@/message'
|
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||||
|
|
||||||
export default class ListService extends AbstractService<IList> {
|
export default class ListService extends AbstractService<IList> {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -54,17 +43,35 @@ export default class ListService extends AbstractService<IList> {
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
async background(list: Pick<IList, 'id' | 'backgroundInformation'>) {
|
// FIXME: move out of service
|
||||||
if (list.backgroundInformation === null) {
|
async getBlurHash(blurHashString: IList['backgroundBlurHash']) {
|
||||||
return ''
|
if (!blurHashString) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
const blurHashBlob = await getBlobFromBlurHash(blurHashString)
|
||||||
|
if (!blurHashBlob) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return URL.createObjectURL(blurHashBlob)
|
||||||
|
}
|
||||||
|
|
||||||
const response = await this.http({
|
async loadBackground(list: Pick<IList, 'id' | 'backgroundInformation'>) {
|
||||||
url: `/lists/${list.id}/background`,
|
if (list === null || !list.backgroundInformation) {
|
||||||
method: 'GET',
|
return
|
||||||
responseType: 'blob',
|
}
|
||||||
})
|
|
||||||
return window.URL.createObjectURL(new Blob([response.data]))
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeBackground(list: Pick<IList, 'id'>) {
|
async removeBackground(list: Pick<IList, 'id'>) {
|
||||||
|
@ -77,59 +84,4 @@ export default class ListService extends AbstractService<IList> {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export function useList(listId?: MaybeRef<IList['id']>) {
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const listStore = useListStore()
|
|
||||||
const namespaceStore = useNamespaceStore()
|
|
||||||
|
|
||||||
const currentListId = computed(() => unref(listId))
|
|
||||||
const list = ref<IList>(new ListModel())
|
|
||||||
|
|
||||||
const listDuplicateService = shallowReactive(new ListDuplicateService())
|
|
||||||
const isDuplicatingList = computed(() => listDuplicateService.loading)
|
|
||||||
|
|
||||||
// load list
|
|
||||||
watch(() => unref(listId), async (watchedListId) => {
|
|
||||||
if (watchedListId === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
list.value = listStore.getListById(watchedListId) || list.value
|
|
||||||
|
|
||||||
// TODO: load list from server
|
|
||||||
|
|
||||||
}, {immediate: true})
|
|
||||||
|
|
||||||
async function duplicateList(namespaceId: INamespace['id']) {
|
|
||||||
const listDuplicate = new ListDuplicateModel({
|
|
||||||
listId: currentListId.value,
|
|
||||||
namespaceId: namespaceId,
|
|
||||||
})
|
|
||||||
|
|
||||||
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}})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteList() {
|
|
||||||
if (!list.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await listStore.deleteList(list.value)
|
|
||||||
success({message: t('list.delete.success')})
|
|
||||||
router.push({name: 'home'})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
duplicateList,
|
|
||||||
|
|
||||||
isDuplicatingList,
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -101,7 +101,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) {
|
if (model.attachments.length > 0) {
|
||||||
const attachmentService = new AttachmentService()
|
const attachmentService = new AttachmentService()
|
||||||
model.attachments.map(a => {
|
model.attachments.map(a => {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import {readonly, ref} from 'vue'
|
import {readonly, ref} from 'vue'
|
||||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||||
|
|
||||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
|
||||||
|
|
||||||
import ListModel from '@/models/list'
|
import ListModel from '@/models/list'
|
||||||
import ListService from '@/services/list'
|
import ListService from '@/services/list'
|
||||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||||
|
@ -97,20 +95,30 @@ export const useBaseStore = defineStore('base', () => {
|
||||||
|
|
||||||
// The forceUpdate parameter is used only when updating a list background directly because in that case
|
// 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.
|
// the current list stays the same, but we want to show the new background right away.
|
||||||
if (list.id !== currentList.value.id || forceUpdate) {
|
if (
|
||||||
if (list.backgroundInformation) {
|
(list.id !== currentList.value.id || forceUpdate) &&
|
||||||
try {
|
list.backgroundInformation
|
||||||
const blurHash = await getBlobFromBlurHash(list.backgroundBlurHash)
|
) {
|
||||||
|
try {
|
||||||
|
const listService = new ListService()
|
||||||
|
const blurHashPromise = listService.getBlurHash(list.backgroundBlurHash).then(blurHash => {
|
||||||
if (blurHash) {
|
if (blurHash) {
|
||||||
setBlurHash(window.URL.createObjectURL(blurHash))
|
setBlurHash(blurHash)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const backgroundPromise = listService.loadBackground(list).then(background => {
|
||||||
|
if (background === undefined) {
|
||||||
|
throw new Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
const listService = new ListService()
|
|
||||||
const background = await listService.background(list)
|
|
||||||
setBackground(background)
|
setBackground(background)
|
||||||
} catch (e) {
|
})
|
||||||
console.error('Error getting background image for list', list.id, e)
|
|
||||||
}
|
await Promise.all([
|
||||||
|
blurHashPromise,
|
||||||
|
backgroundPromise,
|
||||||
|
])
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error getting background image for list', list.id, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,32 @@
|
||||||
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 type {MaybeRef} from '@vueuse/core'
|
||||||
import {acceptHMRUpdate, defineStore} from 'pinia'
|
import {acceptHMRUpdate, defineStore} from 'pinia'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
|
||||||
import type {IList} from '@/modelTypes/IList'
|
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 {LIST_ID} from '@/constants/lists'
|
||||||
import {NAMESPACE_ID} from "@/constants/namespaces"
|
import {NAMESPACE_ID} from '@/constants/namespaces'
|
||||||
|
|
||||||
import {useNamespaceStore} from '@/stores/namespaces'
|
import ListService from '@/services/list'
|
||||||
import {useBaseStore} from '@/stores/base'
|
import ListDuplicateService from '@/services/listDuplicateService'
|
||||||
|
import BackgroundUnsplashService from '@/services/backgroundUnsplash'
|
||||||
|
|
||||||
import ListModel from '@/models/list'
|
import ListModel from '@/models/list'
|
||||||
|
import ListDuplicateModel from '@/models/listDuplicateModel'
|
||||||
import {removeListFromHistory} from '@/modules/listHistory'
|
import {removeListFromHistory} from '@/modules/listHistory'
|
||||||
import ListService from '@/services/list'
|
|
||||||
import {setModuleLoading} from '@/stores/helper'
|
import {setModuleLoading} from '@/stores/helper'
|
||||||
|
import {useBaseStore} from './base'
|
||||||
|
import {useNamespaceStore} from './namespaces'
|
||||||
|
|
||||||
import {createNewIndexer} from '@/indexes'
|
import {createNewIndexer} from '@/indexes'
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
|
import { i18n } from '@/i18n'
|
||||||
|
import BackgroundUploadService from '@/services/backgroundUpload'
|
||||||
|
|
||||||
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
|
const {add, remove, search, update} = createNewIndexer('lists', ['title', 'description'])
|
||||||
|
|
||||||
|
@ -25,6 +35,8 @@ export interface ListState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useListStore = defineStore('list', () => {
|
export const useListStore = defineStore('list', () => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const namespaceStore = useNamespaceStore()
|
const namespaceStore = useNamespaceStore()
|
||||||
|
|
||||||
|
@ -55,7 +67,7 @@ export const useListStore = defineStore('list', () => {
|
||||||
?.filter(value => value > 0)
|
?.filter(value => value > 0)
|
||||||
.map(id => lists.value[id])
|
.map(id => lists.value[id])
|
||||||
.filter(list => list.isArchived === includeArchived)
|
.filter(list => list.isArchived === includeArchived)
|
||||||
|| []
|
|| [] as IList[]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -79,11 +91,6 @@ export const useListStore = defineStore('list', () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeListById(list: IList) {
|
|
||||||
remove(list)
|
|
||||||
delete lists.value[list.id]
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleListFavorite(list: IList) {
|
function toggleListFavorite(list: IList) {
|
||||||
// The favorites pseudo list is always favorite
|
// The favorites pseudo list is always favorite
|
||||||
// Archived lists cannot be marked favorite
|
// Archived lists cannot be marked favorite
|
||||||
|
@ -96,6 +103,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) {
|
async function createList(list: IList) {
|
||||||
const cancel = setModuleLoading(setIsLoading)
|
const cancel = setModuleLoading(setIsLoading)
|
||||||
const listService = new ListService()
|
const listService = new ListService()
|
||||||
|
@ -152,7 +173,8 @@ export const useListStore = defineStore('list', () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await listService.delete(list)
|
const response = await listService.delete(list)
|
||||||
removeListById(list)
|
remove(list)
|
||||||
|
delete lists.value[list.id]
|
||||||
namespaceStore.removeListFromNamespaceById(list)
|
namespaceStore.removeListFromNamespaceById(list)
|
||||||
removeListFromHistory({id: list.id})
|
removeListFromHistory({id: list.id})
|
||||||
return response
|
return response
|
||||||
|
@ -161,6 +183,118 @@ export const useListStore = defineStore('list', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
await baseStore.handleSetCurrentList({list, forceUpdate: true})
|
||||||
|
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)
|
||||||
|
|
||||||
|
await baseStore.handleSetCurrentList({list, forceUpdate: true})
|
||||||
|
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)
|
||||||
|
await baseStore.handleSetCurrentList({list, forceUpdate: true})
|
||||||
|
namespaceStore.setListInNamespaceById(list)
|
||||||
|
setList(list)
|
||||||
|
success({message: i18n.global.t('list.background.removeSuccess')})
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function loadListBackground(list: IList) {
|
||||||
|
const result = {
|
||||||
|
blurHash: '',
|
||||||
|
background: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const listService = new ListService()
|
||||||
|
const blurHashPromise = listService.getBlurHash(list.backgroundBlurHash).then(blurHash => {
|
||||||
|
if (blurHash) {
|
||||||
|
result.blurHash = blurHash
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const backgroundPromise = listService.loadBackground(list).then(background => {
|
||||||
|
if (background === undefined) {
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
result.background = background
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
blurHashPromise,
|
||||||
|
backgroundPromise,
|
||||||
|
])
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error getting background image for list', list.id, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading: readonly(isLoading),
|
isLoading: readonly(isLoading),
|
||||||
lists: readonly(lists),
|
lists: readonly(lists),
|
||||||
|
@ -171,43 +305,168 @@ export const useListStore = defineStore('list', () => {
|
||||||
|
|
||||||
setList,
|
setList,
|
||||||
setLists,
|
setLists,
|
||||||
removeListById,
|
|
||||||
toggleListFavorite,
|
toggleListFavorite,
|
||||||
|
|
||||||
|
// crud
|
||||||
|
loadList,
|
||||||
createList,
|
createList,
|
||||||
updateList,
|
updateList,
|
||||||
deleteList,
|
deleteList,
|
||||||
|
|
||||||
|
archiveList,
|
||||||
|
|
||||||
|
//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
|
// support hot reloading
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
import.meta.hot.accept(acceptHMRUpdate(useListStore, 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')})
|
||||||
|
await 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)
|
||||||
|
await useBaseStore().handleSetCurrentList({list: list.value, forceUpdate: true})
|
||||||
|
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(listId?: MaybeRef<IList['id']>) {
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
list,
|
||||||
|
currentListId,
|
||||||
|
} = useListBase(listId)
|
||||||
|
|
||||||
|
const blurHash = ref<string>()
|
||||||
|
const background = ref<string>()
|
||||||
|
|
||||||
|
const listStore = useListStore()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
currentListId,
|
||||||
|
async (id) => {
|
||||||
|
if (id === undefined || isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = await listStore.loadListBackground(list.value)
|
||||||
|
blurHash.value = result.blurHash
|
||||||
|
background.value = result.background
|
||||||
|
},
|
||||||
|
{immediate: true},
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
blurHash,
|
||||||
|
background,
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,7 +4,7 @@ import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||||
import type {INamespace} from '@/modelTypes/INamespace'
|
import type {INamespace} from '@/modelTypes/INamespace'
|
||||||
import type {IList} from '@/modelTypes/IList'
|
import type {IList} from '@/modelTypes/IList'
|
||||||
|
|
||||||
import {NAMESPACE_ID} from "@/constants/namespaces"
|
import {NAMESPACE_ID} from '@/constants/namespaces'
|
||||||
|
|
||||||
import {setModuleLoading} from '@/stores/helper'
|
import {setModuleLoading} from '@/stores/helper'
|
||||||
import {useListStore} from '@/stores/lists'
|
import {useListStore} from '@/stores/lists'
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
<template>
|
<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">
|
<div class="field">
|
||||||
<label class="label" for="listTitle">{{ $t('list.title') }}</label>
|
<label class="label" for="listTitle">{{ $t('list.title') }}</label>
|
||||||
<div
|
<div
|
||||||
:class="{ 'is-loading': listService.loading }"
|
:class="{ 'is-loading': isLoading }"
|
||||||
class="control"
|
class="control"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:class="{ disabled: listService.loading }"
|
:class="{ disabled: isLoading }"
|
||||||
@keyup.enter="createNewList()"
|
@keyup.enter="createNewList()"
|
||||||
@keyup.esc="$router.back()"
|
@keyup.esc="$router.back()"
|
||||||
class="input"
|
class="input"
|
||||||
|
@ -19,7 +23,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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') }}
|
{{ $t('list.create.addTitleRequired') }}
|
||||||
</p>
|
</p>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
@ -32,43 +36,29 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, reactive, shallowReactive} from 'vue'
|
import {watchEffect} from 'vue'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {useRouter, useRoute} from 'vue-router'
|
|
||||||
|
|
||||||
import ListService from '@/services/list'
|
import type {INamespace} from '@/modelTypes/INamespace'
|
||||||
import ListModel from '@/models/list'
|
|
||||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||||
import ColorPicker from '@/components/input/colorPicker.vue'
|
import ColorPicker from '@/components/input/colorPicker.vue'
|
||||||
|
|
||||||
import {success} from '@/message'
|
|
||||||
import {useTitle} from '@/composables/useTitle'
|
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 {t} = useI18n({useScope: 'global'})
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
useTitle(() => t('list.create.header'))
|
useTitle(() => t('list.create.header'))
|
||||||
|
|
||||||
const showError = ref(false)
|
const {isLoading, errors, list, createList} = useList()
|
||||||
const list = reactive(new ListModel())
|
|
||||||
const listService = shallowReactive(new ListService())
|
|
||||||
const listStore = useListStore()
|
|
||||||
|
|
||||||
async function createNewList() {
|
watchEffect(() => {
|
||||||
if (list.title === '') {
|
list.value.namespaceId = namespaceId
|
||||||
showError.value = true
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
showError.value = false
|
|
||||||
|
|
||||||
list.namespaceId = Number(route.params.namespaceId as string)
|
const createNewList = () => createList()
|
||||||
const newList = await listStore.createList(list)
|
|
||||||
await router.push({
|
|
||||||
name: 'list.index',
|
|
||||||
params: { listId: newList.id },
|
|
||||||
})
|
|
||||||
success({message: t('list.create.createdSuccess') })
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<modal
|
<modal
|
||||||
@close="$router.back()"
|
@close="$router.back()"
|
||||||
@submit="archiveList()"
|
@submit="listStore.archiveList(list)"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<span>{{
|
<span>{{
|
||||||
|
@ -26,34 +26,21 @@ export default {name: 'list-setting-archive'}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed} from 'vue'
|
|
||||||
import {useRouter, useRoute} from 'vue-router'
|
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
import {success} from '@/message'
|
|
||||||
import {useTitle} from '@/composables/useTitle'
|
import {useTitle} from '@/composables/useTitle'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useList, useListStore} from '@/stores/lists'
|
||||||
import {useListStore} from '@/stores/lists'
|
import type {IList} from '@/modelTypes/IList'
|
||||||
|
|
||||||
|
const {listId} = defineProps<{
|
||||||
|
listId: IList['id']
|
||||||
|
}>()
|
||||||
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
const listStore = useListStore()
|
const listStore = useListStore()
|
||||||
const router = useRouter()
|
const {list} = useList(listId)
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const list = computed(() => listStore.getListById(route.params.listId))
|
|
||||||
useTitle(() => t('list.archive.title', {list: list.value.title}))
|
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>
|
</script>
|
||||||
|
|
|
@ -8,14 +8,14 @@
|
||||||
>
|
>
|
||||||
<div class="mb-4" v-if="uploadBackgroundEnabled">
|
<div class="mb-4" v-if="uploadBackgroundEnabled">
|
||||||
<input
|
<input
|
||||||
@change="uploadBackground"
|
@change="listStore.uploadListBackground(listId, ($event.target as HTMLInputElement)?.files?.[0])"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
class="is-hidden"
|
class="is-hidden"
|
||||||
ref="backgroundUploadInput"
|
ref="backgroundUploadInput"
|
||||||
type="file"
|
type="file"
|
||||||
/>
|
/>
|
||||||
<x-button
|
<x-button
|
||||||
:loading="backgroundUploadService.loading"
|
:loading="listStore.isUploadingBackground"
|
||||||
@click="backgroundUploadInput?.click()"
|
@click="backgroundUploadInput?.click()"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
>
|
>
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="backgroundThumbs[im.id]"
|
v-if="backgroundThumbs[im.id]"
|
||||||
class="image-search__image-button"
|
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="" />
|
<img class="image-search__image" :src="backgroundThumbs[im.id]" alt="" />
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
:shadow="false"
|
:shadow="false"
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
class="is-danger"
|
class="is-danger"
|
||||||
@click.prevent.stop="removeBackground"
|
@click.prevent.stop="listStore.removeListBackground(listId)"
|
||||||
>
|
>
|
||||||
{{ $t('list.background.remove') }}
|
{{ $t('list.background.remove') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
|
@ -100,7 +100,6 @@ export default { name: 'list-setting-background' }
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed, shallowReactive} from 'vue'
|
import {ref, computed, shallowReactive} from 'vue'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {useRoute, useRouter} from 'vue-router'
|
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
|
@ -108,34 +107,33 @@ import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useListStore} from '@/stores/lists'
|
import {useListStore} from '@/stores/lists'
|
||||||
import {useNamespaceStore} from '@/stores/namespaces'
|
|
||||||
import {useConfigStore} from '@/stores/config'
|
import {useConfigStore} from '@/stores/config'
|
||||||
|
|
||||||
import BackgroundUnsplashService from '@/services/backgroundUnsplash'
|
import BackgroundUnsplashService from '@/services/backgroundUnsplash'
|
||||||
import BackgroundUploadService from '@/services/backgroundUpload'
|
|
||||||
import ListService from '@/services/list'
|
import ListService from '@/services/list'
|
||||||
import type BackgroundImageModel from '@/models/backgroundImage'
|
|
||||||
|
|
||||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
|
||||||
import {useTitle} from '@/composables/useTitle'
|
import {useTitle} from '@/composables/useTitle'
|
||||||
|
|
||||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||||
import {success} from '@/message'
|
import type {IBackgroundImage} from '@/modelTypes/IBackgroundImage'
|
||||||
|
import type {IList} from '@/modelTypes/IList'
|
||||||
|
|
||||||
const SEARCH_DEBOUNCE = 300
|
const SEARCH_DEBOUNCE = 300
|
||||||
|
|
||||||
|
const {listId} = defineProps<{
|
||||||
|
listId: IList['id']
|
||||||
|
}>()
|
||||||
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
useTitle(() => t('list.background.title'))
|
useTitle(() => t('list.background.title'))
|
||||||
|
|
||||||
const backgroundService = shallowReactive(new BackgroundUnsplashService())
|
const backgroundService = shallowReactive(new BackgroundUnsplashService())
|
||||||
const backgroundSearchTerm = ref('')
|
const backgroundSearchTerm = ref('')
|
||||||
const backgroundSearchResult = ref([])
|
const backgroundSearchResult = ref<IBackgroundImage[]>([])
|
||||||
const backgroundThumbs = ref<Record<string, string>>({})
|
const backgroundThumbs = ref<Record<string, string>>({})
|
||||||
const backgroundBlurHashes = ref<Record<string, string>>({})
|
const backgroundBlurHashes = ref<Record<string, string | undefined>>({})
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
|
|
||||||
// We're using debounce to not search on every keypress but with a delay.
|
// We're using debounce to not search on every keypress but with a delay.
|
||||||
|
@ -143,25 +141,24 @@ const debounceNewBackgroundSearch = debounce(newBackgroundSearch, SEARCH_DEBOUNC
|
||||||
trailing: true,
|
trailing: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const backgroundUploadService = ref(new BackgroundUploadService())
|
const listService = shallowReactive(new ListService())
|
||||||
const listService = ref(new ListService())
|
|
||||||
const listStore = useListStore()
|
const listStore = useListStore()
|
||||||
const namespaceStore = useNamespaceStore()
|
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
const unsplashBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('unsplash'))
|
const unsplashBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('unsplash'))
|
||||||
const uploadBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('upload'))
|
const uploadBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('upload'))
|
||||||
const currentList = computed(() => baseStore.currentList)
|
|
||||||
const hasBackground = computed(() => baseStore.background !== null)
|
const hasBackground = computed(() => baseStore.background !== null)
|
||||||
|
|
||||||
// Show the default collection of backgrounds
|
// Show the default collection of backgrounds
|
||||||
newBackgroundSearch()
|
newBackgroundSearch()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset a few things before searching to not break loading more photos.
|
||||||
|
*/
|
||||||
function newBackgroundSearch() {
|
function newBackgroundSearch() {
|
||||||
if (!unsplashBackgroundEnabled.value) {
|
if (!unsplashBackgroundEnabled.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// This is an extra method to reset a few things when searching to not break loading more photos.
|
|
||||||
backgroundSearchResult.value = []
|
backgroundSearchResult.value = []
|
||||||
backgroundThumbs.value = {}
|
backgroundThumbs.value = {}
|
||||||
searchBackgrounds()
|
searchBackgrounds()
|
||||||
|
@ -171,11 +168,10 @@ async function searchBackgrounds(page = 1) {
|
||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
const result = await backgroundService.getAll({}, {s: backgroundSearchTerm.value, p: page})
|
const result = await backgroundService.getAll({}, {s: backgroundSearchTerm.value, p: page})
|
||||||
backgroundSearchResult.value = backgroundSearchResult.value.concat(result)
|
backgroundSearchResult.value = backgroundSearchResult.value.concat(result)
|
||||||
result.forEach((background: BackgroundImageModel) => {
|
result.forEach((background: IBackgroundImage) => {
|
||||||
getBlobFromBlurHash(background.blurHash)
|
listService.getBlurHash(background.blurHash).then((b) => {
|
||||||
.then((b) => {
|
backgroundBlurHashes.value[background.id] = b
|
||||||
backgroundBlurHashes.value[background.id] = window.URL.createObjectURL(b)
|
})
|
||||||
})
|
|
||||||
|
|
||||||
backgroundService.thumb(background).then(b => {
|
backgroundService.thumb(background).then(b => {
|
||||||
backgroundThumbs.value[background.id] = b
|
backgroundThumbs.value[background.id] = b
|
||||||
|
@ -183,46 +179,7 @@ async function searchBackgrounds(page = 1) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setBackground(backgroundId: IBackgroundImage['id']) {
|
|
||||||
// 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)
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -27,15 +27,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, ref, watch} from 'vue'
|
import {ref, watch} from 'vue'
|
||||||
import {useTitle} from '@/composables/useTitle'
|
import {useTitle} from '@/composables/useTitle'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {useRoute, useRouter} from 'vue-router'
|
|
||||||
import {success} from '@/message'
|
|
||||||
import TaskCollectionService from '@/services/taskCollection'
|
import TaskCollectionService from '@/services/taskCollection'
|
||||||
import Loading from '@/components/misc/loading.vue'
|
import Loading from '@/components/misc/loading.vue'
|
||||||
|
|
||||||
import {useListStore} from '@/stores/lists'
|
import {useList} from '@/stores/lists'
|
||||||
import type {IList} from '@/modelTypes/IList'
|
import type {IList} from '@/modelTypes/IList'
|
||||||
|
|
||||||
const {listId} = defineProps<{
|
const {listId} = defineProps<{
|
||||||
|
@ -43,8 +41,6 @@ const {listId} = defineProps<{
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
const listStore = useListStore()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const totalTasks = ref<number | null>(null)
|
const totalTasks = ref<number | null>(null)
|
||||||
watch(
|
watch(
|
||||||
|
@ -62,13 +58,5 @@ watch(
|
||||||
|
|
||||||
useTitle(() => t('list.delete.title', {list: list?.value?.title}))
|
useTitle(() => t('list.delete.title', {list: list?.value?.title}))
|
||||||
|
|
||||||
async function deleteList() {
|
const {list, deleteList} = useList(listId)
|
||||||
if (!list.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await listStore.deleteList(list.value)
|
|
||||||
success({message: t('list.delete.success')})
|
|
||||||
router.push({name: 'home'})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
:title="$t('list.duplicate.title')"
|
:title="$t('list.duplicate.title')"
|
||||||
primary-icon="paste"
|
primary-icon="paste"
|
||||||
:primary-label="$t('list.duplicate.label')"
|
:primary-label="$t('list.duplicate.label')"
|
||||||
@primary="duplicateList"
|
:primary-disabled="selectedNamespace === undefined"
|
||||||
|
@primary="duplicateList(listId, selectedNamespace?.id)"
|
||||||
:loading="isDuplicatingList"
|
:loading="isDuplicatingList"
|
||||||
>
|
>
|
||||||
<p>{{ $t('list.duplicate.text') }}</p>
|
<p>{{ $t('list.duplicate.text') }}</p>
|
||||||
|
@ -12,7 +13,7 @@
|
||||||
:placeholder="$t('namespace.search')"
|
:placeholder="$t('namespace.search')"
|
||||||
@search="findNamespaces"
|
@search="findNamespaces"
|
||||||
:search-results="namespaces"
|
:search-results="namespaces"
|
||||||
@select="selectNamespace"
|
@select="(namespace) => selectNamespace(namespace as INamespace)"
|
||||||
label="title"
|
label="title"
|
||||||
:search-delay="10"
|
:search-delay="10"
|
||||||
/>
|
/>
|
||||||
|
@ -31,7 +32,7 @@ import type {IList} from '@/modelTypes/IList'
|
||||||
|
|
||||||
import {useTitle} from '@/composables/useTitle'
|
import {useTitle} from '@/composables/useTitle'
|
||||||
import {useNamespaceSearch} from '@/composables/useNamespaceSearch'
|
import {useNamespaceSearch} from '@/composables/useNamespaceSearch'
|
||||||
import {useList} from '@/services/list'
|
import {useListStore} from '@/stores/lists'
|
||||||
|
|
||||||
const {listId} = defineProps<{
|
const {listId} = defineProps<{
|
||||||
listId: IList['id']
|
listId: IList['id']
|
||||||
|
@ -53,5 +54,5 @@ function selectNamespace(namespace: INamespace) {
|
||||||
const {
|
const {
|
||||||
duplicateList,
|
duplicateList,
|
||||||
isDuplicatingList,
|
isDuplicatingList,
|
||||||
} = useList(listId)
|
} = useListStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
:title="$t('list.edit.header')"
|
:title="$t('list.edit.header')"
|
||||||
primary-icon=""
|
primary-icon=""
|
||||||
:primary-label="$t('misc.save')"
|
:primary-label="$t('misc.save')"
|
||||||
@primary="save"
|
@primary="saveList"
|
||||||
:tertiary="$t('misc.delete')"
|
:tertiary="$t('misc.delete')"
|
||||||
@tertiary="$router.push({ name: 'list.settings.delete', params: { id: listId } })"
|
@tertiary="$router.push({ name: 'list.settings.delete', params: { id: listId } })"
|
||||||
>
|
>
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
<input
|
<input
|
||||||
:class="{ 'disabled': isLoading}"
|
:class="{ 'disabled': isLoading}"
|
||||||
:disabled="isLoading || undefined"
|
:disabled="isLoading || undefined"
|
||||||
@keyup.enter="save"
|
@keyup.enter="saveList"
|
||||||
class="input"
|
class="input"
|
||||||
id="title"
|
id="title"
|
||||||
:placeholder="$t('list.edit.titlePlaceholder')"
|
:placeholder="$t('list.edit.titlePlaceholder')"
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
<input
|
<input
|
||||||
:class="{ 'disabled': isLoading}"
|
:class="{ 'disabled': isLoading}"
|
||||||
:disabled="isLoading || undefined"
|
:disabled="isLoading || undefined"
|
||||||
@keyup.enter="save"
|
@keyup.enter="saveList"
|
||||||
class="input"
|
class="input"
|
||||||
id="identifier"
|
id="identifier"
|
||||||
:placeholder="$t('list.edit.identifierPlaceholder')"
|
:placeholder="$t('list.edit.identifierPlaceholder')"
|
||||||
|
@ -70,8 +70,6 @@ export default { name: 'list-setting-edit' }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {PropType} from 'vue'
|
|
||||||
import {useRouter} from 'vue-router'
|
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
import Editor from '@/components/input/AsyncEditor'
|
import Editor from '@/components/input/AsyncEditor'
|
||||||
|
@ -80,23 +78,16 @@ import CreateEdit from '@/components/misc/create-edit.vue'
|
||||||
|
|
||||||
import type {IList} from '@/modelTypes/IList'
|
import type {IList} from '@/modelTypes/IList'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
|
||||||
import {useList} from '@/stores/lists'
|
import {useList} from '@/stores/lists'
|
||||||
|
|
||||||
import {useTitle} from '@/composables/useTitle'
|
import {useTitle} from '@/composables/useTitle'
|
||||||
|
|
||||||
|
const {listId} = defineProps<{
|
||||||
const router = useRouter()
|
listId: IList['id']
|
||||||
const baseStore = useBaseStore()
|
}>()
|
||||||
|
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
|
const {list, saveList, isLoading} = useList(listId)
|
||||||
|
|
||||||
useTitle(() => list?.title ? t('list.edit.title', {list: list.title}) : '')
|
useTitle(() => list.value.title ? t('list.edit.title', {list: list.value.title}) : '')
|
||||||
|
|
||||||
async function save() {
|
|
||||||
await saveList()
|
|
||||||
await baseStore.handleSetCurrentList({list})
|
|
||||||
router.back()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -18,7 +18,11 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</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>
|
</create-edit>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -27,26 +31,28 @@ export default {name: 'list-setting-share'}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {ref, computed, watchEffect} from 'vue'
|
import {computed} from 'vue'
|
||||||
import {useRoute} from 'vue-router'
|
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {useTitle} from '@vueuse/core'
|
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 CreateEdit from '@/components/misc/create-edit.vue'
|
||||||
import LinkSharing from '@/components/sharing/linkSharing.vue'
|
import LinkSharing from '@/components/sharing/linkSharing.vue'
|
||||||
import userTeam from '@/components/sharing/userTeam.vue'
|
import userTeam from '@/components/sharing/userTeam.vue'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
import {useConfigStore} from '@/stores/config'
|
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 {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
const list = ref<IList>()
|
const {list} = useList(listId)
|
||||||
|
|
||||||
const title = computed(() => list.value?.title
|
const title = computed(() => list.value?.title
|
||||||
? t('list.share.title', {list: list.value.title})
|
? t('list.share.title', {list: list.value.title})
|
||||||
: '',
|
: '',
|
||||||
|
@ -54,21 +60,8 @@ const title = computed(() => list.value?.title
|
||||||
useTitle(title)
|
useTitle(title)
|
||||||
|
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const linkSharingEnabled = computed(() => configStore.linkSharingEnabled)
|
const linkSharingEnabled = computed(() => configStore.linkSharingEnabled)
|
||||||
const userIsAdmin = computed(() => list?.value?.maxRight === RIGHTS.ADMIN)
|
const userIsAdmin = computed(() => list.value.owner.id === authStore.info?.id)
|
||||||
|
|
||||||
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))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
Reference in New Issue