@meeovi/layer-lists 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/features/archived.vue +64 -0
- package/app/components/features/bookmarks.vue +64 -0
- package/app/components/features/lists.vue +61 -0
- package/app/components/features/starred.vue +64 -0
- package/app/components/lists/ListItemCard.vue +190 -0
- package/app/components/lists/add-bookmark.vue +52 -0
- package/app/components/lists/add-list-item.vue +88 -0
- package/app/components/lists/add-list.vue +57 -0
- package/app/components/lists/lists.vue +6 -0
- package/app/components/lists/listsettings.vue +145 -0
- package/app/components/lists/update-bookmark.vue +267 -0
- package/app/components/lists/update-list.vue +192 -0
- package/app/components/media/MediaPlayer.vue +302 -0
- package/app/components/partials/addtolist.vue +233 -0
- package/app/components/partials/createListBtn.vue +95 -0
- package/app/components/partials/listBtn.vue +35 -0
- package/app/components/related/list.vue +33 -0
- package/app/components/related/relatedlists.vue +43 -0
- package/app/components/tasks/TaskItem.vue +204 -0
- package/app/composables/bookmarks/createBookmark.js +30 -0
- package/app/composables/bookmarks/deleteBookmark.js +15 -0
- package/app/composables/bookmarks/updateBookmark.js +15 -0
- package/app/composables/config.ts +17 -0
- package/app/composables/content/uploadFiles.js +41 -0
- package/app/composables/globals/useDirectusForm.ts +1 -0
- package/app/composables/lists/createList.js +25 -0
- package/app/composables/lists/deleteList.js +14 -0
- package/app/composables/lists/updateList.js +20 -0
- package/app/composables/lists/useBookmarks.js +69 -0
- package/app/composables/lists/useLists.js +120 -0
- package/app/composables/lists/usePlaylist.js +64 -0
- package/app/composables/lists/useSaved.js +29 -0
- package/app/composables/lists/useTasks.js +86 -0
- package/app/composables/lists/useWishlist.js +51 -0
- package/app/composables/providers/atproto.ts +156 -0
- package/app/composables/providers/directus.ts +49 -0
- package/app/composables/providers/memory.ts +88 -0
- package/app/composables/registry.ts +13 -0
- package/app/composables/types.ts +35 -0
- package/app/composables/useLists.ts +20 -0
- package/app/composables/utils/transforms.ts +42 -0
- package/app/composables/utils/validation.ts +21 -0
- package/app/pages/lists/bookmark/[id].vue +76 -0
- package/app/pages/lists/index.vue +152 -0
- package/app/pages/lists/list/[...slug].vue +233 -0
- package/nuxt.config.ts +11 -0
- package/package.json +26 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-sheet class="mx-auto sliderLists row align-items-stretch items-row justify-content-center">
|
|
4
|
+
<v-toolbar color="transparent">
|
|
5
|
+
<v-toolbar-title>Public Lists</v-toolbar-title>
|
|
6
|
+
</v-toolbar>
|
|
7
|
+
<v-slide-group v-model="model" class="pa-4" selected-class="bg-success" show-arrows>
|
|
8
|
+
<v-slide-group-item v-slot="{ isSelected, toggle, selectedClass }" v-for="(list, index) in lists" :key="index">
|
|
9
|
+
<listCard :class="['ma-4', selectedClass]" :list="list" v-if="isSelected" @click="toggle" />
|
|
10
|
+
</v-slide-group-item>
|
|
11
|
+
</v-slide-group>
|
|
12
|
+
</v-sheet>
|
|
13
|
+
</div>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
import {
|
|
18
|
+
ref
|
|
19
|
+
} from 'vue'
|
|
20
|
+
import listCard from '~/components/related/list.vue'
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
$directus,
|
|
24
|
+
$readItems
|
|
25
|
+
} = useNuxtApp()
|
|
26
|
+
|
|
27
|
+
const model = ref(null)
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
data: lists
|
|
31
|
+
} = await useAsyncData('lists', () => {
|
|
32
|
+
return $directus.request($readItems('lists', {
|
|
33
|
+
fields: ['*', {
|
|
34
|
+
'*': ['*']
|
|
35
|
+
}],
|
|
36
|
+
filter: {
|
|
37
|
+
status: {
|
|
38
|
+
_eq: 'Public'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
}))
|
|
42
|
+
})
|
|
43
|
+
</script>
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card
|
|
3
|
+
:class="{ 'task-completed': task.completed }"
|
|
4
|
+
class="task-item mb-2"
|
|
5
|
+
variant="outlined"
|
|
6
|
+
>
|
|
7
|
+
<v-card-text class="py-2">
|
|
8
|
+
<div class="d-flex align-center">
|
|
9
|
+
<v-checkbox
|
|
10
|
+
:model-value="task.completed"
|
|
11
|
+
@update:model-value="toggleComplete"
|
|
12
|
+
hide-details
|
|
13
|
+
class="me-3"
|
|
14
|
+
/>
|
|
15
|
+
|
|
16
|
+
<div class="grow">
|
|
17
|
+
<div
|
|
18
|
+
:class="{ 'text-decoration-line-through text-medium-emphasis': task.completed }"
|
|
19
|
+
class="task-title"
|
|
20
|
+
>
|
|
21
|
+
{{ task.title }}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div v-if="task.description" class="text-body-2 text-medium-emphasis mt-1">
|
|
25
|
+
{{ task.description }}
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="d-flex align-center mt-2 flex-wrap ga-2">
|
|
29
|
+
<v-chip
|
|
30
|
+
v-if="task.priority !== 'medium'"
|
|
31
|
+
:color="getPriorityColor(task.priority)"
|
|
32
|
+
size="x-small"
|
|
33
|
+
variant="tonal"
|
|
34
|
+
>
|
|
35
|
+
{{ task.priority }}
|
|
36
|
+
</v-chip>
|
|
37
|
+
|
|
38
|
+
<v-chip
|
|
39
|
+
v-if="task.due_date"
|
|
40
|
+
:color="getDueDateColor(task.due_date)"
|
|
41
|
+
size="x-small"
|
|
42
|
+
variant="outlined"
|
|
43
|
+
prepend-icon="mdi-calendar"
|
|
44
|
+
>
|
|
45
|
+
{{ formatDueDate(task.due_date) }}
|
|
46
|
+
</v-chip>
|
|
47
|
+
|
|
48
|
+
<v-chip
|
|
49
|
+
v-for="label in task.labels"
|
|
50
|
+
:key="label"
|
|
51
|
+
size="x-small"
|
|
52
|
+
variant="outlined"
|
|
53
|
+
>
|
|
54
|
+
{{ label }}
|
|
55
|
+
</v-chip>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Subtasks -->
|
|
59
|
+
<div v-if="task.subtasks?.length" class="mt-3">
|
|
60
|
+
<div class="text-caption text-medium-emphasis mb-1">
|
|
61
|
+
Subtasks ({{ completedSubtasks }}/{{ task.subtasks.length }})
|
|
62
|
+
</div>
|
|
63
|
+
<div class="subtasks">
|
|
64
|
+
<div
|
|
65
|
+
v-for="subtask in task.subtasks"
|
|
66
|
+
:key="subtask.id"
|
|
67
|
+
class="d-flex align-center py-1"
|
|
68
|
+
>
|
|
69
|
+
<v-checkbox
|
|
70
|
+
:model-value="subtask.completed"
|
|
71
|
+
@update:model-value="toggleSubtask(subtask.id)"
|
|
72
|
+
hide-details
|
|
73
|
+
density="compact"
|
|
74
|
+
class="me-2"
|
|
75
|
+
/>
|
|
76
|
+
<span
|
|
77
|
+
:class="{ 'text-decoration-line-through text-medium-emphasis': subtask.completed }"
|
|
78
|
+
class="text-body-2"
|
|
79
|
+
>
|
|
80
|
+
{{ subtask.title }}
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="task-actions">
|
|
88
|
+
<v-menu>
|
|
89
|
+
<template v-slot:activator="{ props }">
|
|
90
|
+
<v-btn
|
|
91
|
+
icon="mdi-dots-vertical"
|
|
92
|
+
variant="text"
|
|
93
|
+
size="small"
|
|
94
|
+
v-bind="props"
|
|
95
|
+
/>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<v-list>
|
|
99
|
+
<v-list-item @click="$emit('edit', task)">
|
|
100
|
+
<template v-slot:prepend>
|
|
101
|
+
<v-icon icon="mdi-pencil" />
|
|
102
|
+
</template>
|
|
103
|
+
<v-list-item-title>Edit</v-list-item-title>
|
|
104
|
+
</v-list-item>
|
|
105
|
+
|
|
106
|
+
<v-list-item @click="$emit('duplicate', task)">
|
|
107
|
+
<template v-slot:prepend>
|
|
108
|
+
<v-icon icon="mdi-content-copy" />
|
|
109
|
+
</template>
|
|
110
|
+
<v-list-item-title>Duplicate</v-list-item-title>
|
|
111
|
+
</v-list-item>
|
|
112
|
+
|
|
113
|
+
<v-divider />
|
|
114
|
+
|
|
115
|
+
<v-list-item @click="$emit('delete', task)" class="text-error">
|
|
116
|
+
<template v-slot:prepend>
|
|
117
|
+
<v-icon icon="mdi-delete" color="error" />
|
|
118
|
+
</template>
|
|
119
|
+
<v-list-item-title>Delete</v-list-item-title>
|
|
120
|
+
</v-list-item>
|
|
121
|
+
</v-list>
|
|
122
|
+
</v-menu>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</v-card-text>
|
|
126
|
+
</v-card>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<script setup>
|
|
130
|
+
import { format, isToday, isTomorrow, isPast } from 'date-fns'
|
|
131
|
+
|
|
132
|
+
const props = defineProps({
|
|
133
|
+
task: {
|
|
134
|
+
type: Object,
|
|
135
|
+
required: true
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const emit = defineEmits(['update', 'edit', 'duplicate', 'delete'])
|
|
140
|
+
|
|
141
|
+
const completedSubtasks = computed(() => {
|
|
142
|
+
return props.task.subtasks?.filter(st => st.completed).length || 0
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const toggleComplete = (completed) => {
|
|
146
|
+
emit('update', { ...props.task, completed })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const toggleSubtask = (subtaskId) => {
|
|
150
|
+
const subtasks = props.task.subtasks.map(st =>
|
|
151
|
+
st.id === subtaskId ? { ...st, completed: !st.completed } : st
|
|
152
|
+
)
|
|
153
|
+
emit('update', { ...props.task, subtasks })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const getPriorityColor = (priority) => {
|
|
157
|
+
const colors = {
|
|
158
|
+
low: 'blue',
|
|
159
|
+
medium: 'orange',
|
|
160
|
+
high: 'red'
|
|
161
|
+
}
|
|
162
|
+
return colors[priority] || 'grey'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const getDueDateColor = (dueDate) => {
|
|
166
|
+
if (!dueDate) return 'grey'
|
|
167
|
+
|
|
168
|
+
const date = new Date(dueDate)
|
|
169
|
+
if (isPast(date) && !isToday(date)) return 'error'
|
|
170
|
+
if (isToday(date)) return 'warning'
|
|
171
|
+
if (isTomorrow(date)) return 'info'
|
|
172
|
+
return 'success'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const formatDueDate = (dueDate) => {
|
|
176
|
+
if (!dueDate) return ''
|
|
177
|
+
|
|
178
|
+
const date = new Date(dueDate)
|
|
179
|
+
if (isToday(date)) return 'Today'
|
|
180
|
+
if (isTomorrow(date)) return 'Tomorrow'
|
|
181
|
+
if (isPast(date)) return `Overdue (${format(date, 'MMM d')})`
|
|
182
|
+
return format(date, 'MMM d')
|
|
183
|
+
}
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<style scoped>
|
|
187
|
+
.task-completed {
|
|
188
|
+
opacity: 0.7;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.task-item {
|
|
192
|
+
transition: all 0.2s ease;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.task-item:hover {
|
|
196
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.subtasks {
|
|
200
|
+
border-left: 2px solid rgba(var(--v-theme-primary), 0.2);
|
|
201
|
+
padding-left: 12px;
|
|
202
|
+
margin-left: 8px;
|
|
203
|
+
}
|
|
204
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// composables/createWebsite.js
|
|
2
|
+
import { createItem } from '@directus/sdk';
|
|
3
|
+
|
|
4
|
+
export default async function createWebsite(websiteData) {
|
|
5
|
+
const route = useRoute();
|
|
6
|
+
|
|
7
|
+
const id = route.params.id;
|
|
8
|
+
const { $directus } = useNuxtApp();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const website = await $directus.request(createItem('websites', [
|
|
12
|
+
{
|
|
13
|
+
name: websiteData.name,
|
|
14
|
+
note: websiteData.note,
|
|
15
|
+
status: websiteData.status,
|
|
16
|
+
type: websiteData.type,
|
|
17
|
+
image: websiteData.image,
|
|
18
|
+
icon: websiteData.icon,
|
|
19
|
+
slug: websiteData.slug,
|
|
20
|
+
coverFile: null,
|
|
21
|
+
username: websiteData.username,
|
|
22
|
+
}
|
|
23
|
+
]));
|
|
24
|
+
return website;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('Error creating website:', error);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// composables/deleteWebsite.js
|
|
2
|
+
import { deleteItem } from '@directus/sdk';
|
|
3
|
+
|
|
4
|
+
export default async function deleteWebsite(websiteId) {
|
|
5
|
+
const { $directus } = useNuxtApp();
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
$directus.request(deleteItem('websites', websiteId));
|
|
9
|
+
console.log('Bookmark deleted successfully');
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error('Error deleting bookmark:', error);
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// composables/updatePost.js
|
|
2
|
+
import { updateItem } from '@directus/sdk';
|
|
3
|
+
|
|
4
|
+
export default async function updatePost(websiteId, websiteData) {
|
|
5
|
+
const { $directus } = useNuxtApp();
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const website = await $directus.request(updateItem('websites', websiteId));
|
|
9
|
+
return website;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error('Error updating bookmark:', error);
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ListsConfig {
|
|
2
|
+
provider: string
|
|
3
|
+
baseUrl?: string
|
|
4
|
+
apiKey?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let config: ListsConfig = {
|
|
8
|
+
provider: 'memory'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function setListsConfig(newConfig: Partial<ListsConfig>) {
|
|
12
|
+
config = { ...config, ...newConfig }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getListsConfig(): ListsConfig {
|
|
16
|
+
return config
|
|
17
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { uploadFiles } from '@meeovi/directus-client';
|
|
2
|
+
|
|
3
|
+
export default async function uploadFile({ imageFile, documentFile, videoFile, audioFile }) {
|
|
4
|
+
const { $directus } = useNuxtApp();
|
|
5
|
+
const uploadedFiles = {};
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
if (imageFile) {
|
|
9
|
+
const formDataImage = new FormData();
|
|
10
|
+
formDataImage.append('file', imageFile);
|
|
11
|
+
const uploadedImage = await $directus.request(uploadFiles(formDataImage));
|
|
12
|
+
uploadedFiles.imageId = uploadedImage.id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (documentFile) {
|
|
16
|
+
const formDataDocument = new FormData();
|
|
17
|
+
formDataDocument.append('file', documentFile);
|
|
18
|
+
const uploadedDocument = await $directus.request(uploadFiles(formDataDocument));
|
|
19
|
+
uploadedFiles.documentId = uploadedDocument.id;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (videoFile) {
|
|
23
|
+
const formDataVideo = new FormData();
|
|
24
|
+
formDataVideo.append('file', videoFile);
|
|
25
|
+
const uploadedVideo = await $directus.request(uploadFiles(formDataVideo));
|
|
26
|
+
uploadedFiles.videoId = uploadedVideo.id;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (audioFile) {
|
|
30
|
+
const formDataAudio = new FormData();
|
|
31
|
+
formDataAudio.append('file', audioFile);
|
|
32
|
+
const uploadedAudio = await $directus.request(uploadFiles(formDataAudio));
|
|
33
|
+
uploadedFiles.audioId = uploadedAudio.id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return uploadedFiles;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error uploading files:', error);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '#shared/app/composables/globals/useDirectusForm'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// composables/createList.js
|
|
2
|
+
import { createItem } from '@directus/sdk';
|
|
3
|
+
|
|
4
|
+
export default async function createList(listData) {
|
|
5
|
+
const { $directus } = useNuxtApp();
|
|
6
|
+
const user = useSupabaseUser();
|
|
7
|
+
|
|
8
|
+
const data = {
|
|
9
|
+
name: listData.name,
|
|
10
|
+
type: listData.type,
|
|
11
|
+
status: listData.status,
|
|
12
|
+
description: listData.description,
|
|
13
|
+
image: listData.image,
|
|
14
|
+
user: user.value.id,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const list = await $directus.request(createItem('lists', data));
|
|
19
|
+
return list;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error('Error creating list:', error);
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// composables/cms/lists/deleteList.js
|
|
2
|
+
import { deleteItem } from '@directus/sdk';
|
|
3
|
+
|
|
4
|
+
export default async function deleteList(listId) {
|
|
5
|
+
const { $directus } = useNuxtApp();
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
await $directus.request(deleteItem('lists', listId));
|
|
9
|
+
return true;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error('Error deleting list:', error);
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// composables/updateList.js
|
|
2
|
+
import { updateItem } from '@directus/sdk';
|
|
3
|
+
|
|
4
|
+
export default async function updateList(listId, listData) {
|
|
5
|
+
const { $directus } = useNuxtApp();
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const list = await $directus.request(updateItem('lists', listId, listData));
|
|
9
|
+
return list;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error('Error updating list:', {
|
|
12
|
+
error: error.message,
|
|
13
|
+
statusCode: error.status,
|
|
14
|
+
data: error.data,
|
|
15
|
+
listId,
|
|
16
|
+
listData
|
|
17
|
+
});
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const useBookmarks = () => {
|
|
2
|
+
const { createList, addToList, getUserLists, removeFromList } = useLists()
|
|
3
|
+
|
|
4
|
+
const createBookmarkList = async (name = 'Bookmarks', description = '') => {
|
|
5
|
+
return await createList({
|
|
6
|
+
name,
|
|
7
|
+
description,
|
|
8
|
+
type: 'bookmarks',
|
|
9
|
+
visibility: 'private'
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const addBookmark = async (listId, bookmarkData) => {
|
|
14
|
+
const { url, title, description, tags, favicon } = bookmarkData
|
|
15
|
+
|
|
16
|
+
return await addToList(listId, {
|
|
17
|
+
type: 'bookmark',
|
|
18
|
+
title,
|
|
19
|
+
url,
|
|
20
|
+
description,
|
|
21
|
+
favicon,
|
|
22
|
+
tags: tags || [],
|
|
23
|
+
date_added: new Date().toISOString(),
|
|
24
|
+
read: false,
|
|
25
|
+
archived: false
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const getUserBookmarkLists = async () => {
|
|
30
|
+
return await getUserLists('bookmarks')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const removeBookmark = async (itemId) => {
|
|
34
|
+
return await removeFromList(itemId)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const markAsRead = async (itemId) => {
|
|
38
|
+
const { updateListItem } = useLists()
|
|
39
|
+
return await updateListItem(itemId, {
|
|
40
|
+
'content.read': true,
|
|
41
|
+
'content.date_read': new Date().toISOString()
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const archiveBookmark = async (itemId) => {
|
|
46
|
+
const { updateListItem } = useLists()
|
|
47
|
+
return await updateListItem(itemId, {
|
|
48
|
+
'content.archived': true,
|
|
49
|
+
'content.date_archived': new Date().toISOString()
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const addTagsToBookmark = async (itemId, newTags) => {
|
|
54
|
+
const { updateListItem } = useLists()
|
|
55
|
+
return await updateListItem(itemId, {
|
|
56
|
+
'content.tags': newTags
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
createBookmarkList,
|
|
62
|
+
addBookmark,
|
|
63
|
+
getUserBookmarkLists,
|
|
64
|
+
removeBookmark,
|
|
65
|
+
markAsRead,
|
|
66
|
+
archiveBookmark,
|
|
67
|
+
addTagsToBookmark
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createItem, readItems, readItem, updateItem, deleteItem } from '@directus/sdk'
|
|
2
|
+
|
|
3
|
+
export const useLists = () => {
|
|
4
|
+
const { $directus, $readItem } = useNuxtApp()
|
|
5
|
+
|
|
6
|
+
// Cache current user to avoid repeated requests
|
|
7
|
+
let _currentUser = null
|
|
8
|
+
|
|
9
|
+
async function ensureCurrentUser() {
|
|
10
|
+
if (_currentUser) return _currentUser
|
|
11
|
+
try {
|
|
12
|
+
const resp = await $directus.request($readItem('users', 'me'))
|
|
13
|
+
// Directus responses sometimes wrap in { data } or return raw object
|
|
14
|
+
_currentUser = resp?.data || resp || null
|
|
15
|
+
return _currentUser
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const createList = async (listData) => {
|
|
22
|
+
const user = await ensureCurrentUser()
|
|
23
|
+
if (!user) throw new Error('Not logged in')
|
|
24
|
+
|
|
25
|
+
const list = await $directus.request(
|
|
26
|
+
createItem('lists', {
|
|
27
|
+
...listData,
|
|
28
|
+
user_created: user.id,
|
|
29
|
+
date_created: new Date().toISOString()
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
return list
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const updateList = async (listId, updates) => {
|
|
36
|
+
return await $directus.request(
|
|
37
|
+
updateItem('lists', listId, {
|
|
38
|
+
...updates,
|
|
39
|
+
date_updated: new Date().toISOString()
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const deleteList = async (listId) => {
|
|
45
|
+
return await $directus.request(deleteItem('lists', listId))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const getUserLists = async (type = null) => {
|
|
49
|
+
const user = await ensureCurrentUser()
|
|
50
|
+
if (!user) return []
|
|
51
|
+
|
|
52
|
+
const filter = { user_created: { _eq: user.id } }
|
|
53
|
+
if (type) filter.type = { _eq: type }
|
|
54
|
+
|
|
55
|
+
return await $directus.request(
|
|
56
|
+
readItems('lists', {
|
|
57
|
+
filter,
|
|
58
|
+
sort: ['-date_updated', 'name'],
|
|
59
|
+
fields: ['*', 'items.*', 'items.content.*']
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const getPublicLists = async (type = null) => {
|
|
65
|
+
const filter = { visibility: { _eq: 'public' } }
|
|
66
|
+
if (type) filter.type = { _eq: type }
|
|
67
|
+
|
|
68
|
+
return await $directus.request(
|
|
69
|
+
readItems('lists', {
|
|
70
|
+
filter,
|
|
71
|
+
sort: ['-date_updated'],
|
|
72
|
+
fields: ['*', 'user_created.first_name', 'user_created.last_name']
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const getListById = async (listId) => {
|
|
78
|
+
return await $directus.request(
|
|
79
|
+
readItem('lists', listId, {
|
|
80
|
+
fields: ['*', 'items.*', 'items.content.*', 'user_created.first_name', 'user_created.last_name']
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const addToList = async (listId, contentData) => {
|
|
86
|
+
return await $directus.request(
|
|
87
|
+
createItem('list_items', {
|
|
88
|
+
list: listId,
|
|
89
|
+
content: contentData,
|
|
90
|
+
date_created: new Date().toISOString()
|
|
91
|
+
})
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const removeFromList = async (itemId) => {
|
|
96
|
+
return await $directus.request(deleteItem('list_items', itemId))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const updateListItem = async (itemId, updates) => {
|
|
100
|
+
return await $directus.request(
|
|
101
|
+
updateItem('list_items', itemId, {
|
|
102
|
+
...updates,
|
|
103
|
+
date_updated: new Date().toISOString()
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
createList,
|
|
110
|
+
updateList,
|
|
111
|
+
deleteList,
|
|
112
|
+
getUserLists,
|
|
113
|
+
getPublicLists,
|
|
114
|
+
getListById,
|
|
115
|
+
addToList,
|
|
116
|
+
removeFromList,
|
|
117
|
+
updateListItem
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const usePlaylist = () => {
|
|
2
|
+
const { createList, addToList, getUserLists, getListById } = useLists()
|
|
3
|
+
|
|
4
|
+
const createPlaylist = async (name, description = '', visibility = 'private') => {
|
|
5
|
+
return await createList({
|
|
6
|
+
name,
|
|
7
|
+
description,
|
|
8
|
+
type: 'playlist',
|
|
9
|
+
visibility,
|
|
10
|
+
settings: {
|
|
11
|
+
autoplay: false,
|
|
12
|
+
shuffle: false,
|
|
13
|
+
repeat: 'none'
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const addMediaToPlaylist = async (playlistId, mediaData) => {
|
|
19
|
+
const { url, title, duration, type, thumbnail } = mediaData
|
|
20
|
+
|
|
21
|
+
if (!['audio', 'video'].includes(type)) {
|
|
22
|
+
throw new Error('Only audio and video files are supported in playlists')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return await addToList(playlistId, {
|
|
26
|
+
type: 'media',
|
|
27
|
+
title,
|
|
28
|
+
url,
|
|
29
|
+
media_type: type,
|
|
30
|
+
duration,
|
|
31
|
+
thumbnail,
|
|
32
|
+
metadata: {
|
|
33
|
+
artist: mediaData.artist || '',
|
|
34
|
+
album: mediaData.album || '',
|
|
35
|
+
genre: mediaData.genre || ''
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getUserPlaylists = async () => {
|
|
41
|
+
return await getUserLists('playlist')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const getPlaylistWithMedia = async (playlistId) => {
|
|
45
|
+
const playlist = await getListById(playlistId)
|
|
46
|
+
if (playlist.type !== 'playlist') {
|
|
47
|
+
throw new Error('Not a playlist')
|
|
48
|
+
}
|
|
49
|
+
return playlist
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const updatePlaylistSettings = async (playlistId, settings) => {
|
|
53
|
+
const { updateList } = useLists()
|
|
54
|
+
return await updateList(playlistId, { settings })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
createPlaylist,
|
|
59
|
+
addMediaToPlaylist,
|
|
60
|
+
getUserPlaylists,
|
|
61
|
+
getPlaylistWithMedia,
|
|
62
|
+
updatePlaylistSettings
|
|
63
|
+
}
|
|
64
|
+
}
|