forked from Mirrors/elk
feat: finish list CRUD (#1532)
Co-authored-by: userquin <userquin@gmail.com> closes https://github.com/elk-zone/elk/issues/372
This commit is contained in:
parent
809d4a6eb3
commit
e58d09b6cc
10 changed files with 383 additions and 150 deletions
|
@ -91,56 +91,58 @@ const isNotifiedOnPost = $computed(() => !!relationship?.notifying)
|
||||||
</component>
|
</component>
|
||||||
<div p4 mt--18 flex flex-col gap-4>
|
<div p4 mt--18 flex flex-col gap-4>
|
||||||
<div relative>
|
<div relative>
|
||||||
<div flex="~ col gap-2 1">
|
<div flex justify-between>
|
||||||
<button :class="{ 'rounded-full': !isSelf, 'squircle': isSelf }" w-30 h-30 p1 bg-base border-bg-base z-2 @click="previewAvatar">
|
<button shrink-0 :class="{ 'rounded-full': !isSelf, 'squircle': isSelf }" w-30 h-30 p1 bg-base border-bg-base z-2 @click="previewAvatar">
|
||||||
<AccountAvatar :square="isSelf" :account="account" hover:opacity-90 transition-opacity />
|
<AccountAvatar :square="isSelf" :account="account" hover:opacity-90 transition-opacity />
|
||||||
</button>
|
</button>
|
||||||
<div flex="~ col gap1">
|
<div inset-ie-0 flex="~ wrap row-reverse" gap-2 items-center pt18 justify-start>
|
||||||
<div flex justify-between>
|
<!-- Edit profile -->
|
||||||
<AccountDisplayName :account="account" font-bold sm:text-2xl text-xl />
|
<NuxtLink
|
||||||
<AccountBotIndicator v-if="account.bot" show-label />
|
v-if="isSelf"
|
||||||
</div>
|
to="/settings/profile/appearance"
|
||||||
<AccountHandle :account="account" />
|
gap-1 items-center border="1" rounded-full flex="~ gap2 center" font-500 min-w-30 h-fit px3 py1
|
||||||
|
hover="border-primary text-primary bg-active"
|
||||||
|
>
|
||||||
|
{{ $t('settings.profile.appearance.title') }}
|
||||||
|
</NuxtLink>
|
||||||
|
<AccountFollowButton :account="account" :command="command" />
|
||||||
|
<span inset-ie-0 flex gap-2 items-center>
|
||||||
|
<AccountMoreButton :account="account" :command="command" />
|
||||||
|
<CommonTooltip v-if="!isSelf && relationship?.following" :content="getNotificationIconTitle()">
|
||||||
|
<button
|
||||||
|
:aria-pressed="isNotifiedOnPost"
|
||||||
|
:aria-label="t('account.notifications_on_post_enable', { username: `@${account.username}` })"
|
||||||
|
rounded-full text-sm p2 border-1 transition-colors
|
||||||
|
:class="isNotifiedOnPost ? 'text-primary border-primary hover:bg-red/20 hover:text-red hover:border-red' : 'border-base hover:text-primary'"
|
||||||
|
@click="toggleNotifications"
|
||||||
|
>
|
||||||
|
<span v-if="isNotifiedOnPost" i-ri:notification-4-fill block text-current />
|
||||||
|
<span v-else i-ri-notification-4-line block text-current />
|
||||||
|
</button>
|
||||||
|
</CommonTooltip>
|
||||||
|
<CommonTooltip :content="$t('list.modify_account')">
|
||||||
|
<VDropdown v-if="!isSelf && relationship?.following">
|
||||||
|
<button
|
||||||
|
:aria-label="$t('list.modify_account')"
|
||||||
|
rounded-full text-sm p2 border-1 transition-colors
|
||||||
|
border-base hover:text-primary
|
||||||
|
>
|
||||||
|
<span i-ri:play-list-add-fill block text-current />
|
||||||
|
</button>
|
||||||
|
<template #popper>
|
||||||
|
<ListLists :user-id="account.id" />
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</CommonTooltip>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div absolute top-18 inset-ie-0 flex gap-2 items-center>
|
<div flex="~ col gap1" pt2>
|
||||||
<AccountMoreButton :account="account" :command="command" />
|
<div flex justify-between>
|
||||||
<CommonTooltip v-if="!isSelf && relationship?.following" :content="getNotificationIconTitle()">
|
<AccountDisplayName :account="account" font-bold sm:text-2xl text-xl />
|
||||||
<button
|
<AccountBotIndicator v-if="account.bot" show-label />
|
||||||
:aria-pressed="isNotifiedOnPost"
|
</div>
|
||||||
:aria-label="t('account.notifications_on_post_enable', { username: `@${account.username}` })"
|
<AccountHandle :account="account" />
|
||||||
rounded-full text-sm p2 border-1 transition-colors
|
|
||||||
:class="isNotifiedOnPost ? 'text-primary border-primary hover:bg-red/20 hover:text-red hover:border-red' : 'border-base hover:text-primary'"
|
|
||||||
@click="toggleNotifications"
|
|
||||||
>
|
|
||||||
<span v-if="isNotifiedOnPost" i-ri:notification-4-fill block text-current />
|
|
||||||
<span v-else i-ri-notification-4-line block text-current />
|
|
||||||
</button>
|
|
||||||
</CommonTooltip>
|
|
||||||
<CommonTooltip :content="$t('list.modify_account')">
|
|
||||||
<VDropdown v-if="!isSelf && relationship?.following">
|
|
||||||
<button
|
|
||||||
:aria-label="$t('list.modify_account')"
|
|
||||||
rounded-full text-sm p2 border-1 transition-colors
|
|
||||||
border-base hover:text-primary
|
|
||||||
>
|
|
||||||
<span i-ri:play-list-add-fill block text-current />
|
|
||||||
</button>
|
|
||||||
<template #popper>
|
|
||||||
<ListLists :user-id="account.id" />
|
|
||||||
</template>
|
|
||||||
</VDropdown>
|
|
||||||
</CommonTooltip>
|
|
||||||
<AccountFollowButton :account="account" :command="command" />
|
|
||||||
<!-- Edit profile -->
|
|
||||||
<NuxtLink
|
|
||||||
v-if="isSelf"
|
|
||||||
to="/settings/profile/appearance"
|
|
||||||
gap-1 items-center border="1" rounded-full flex="~ gap2 center" font-500 min-w-30 h-fit px3 py1
|
|
||||||
hover="border-primary text-primary bg-active"
|
|
||||||
>
|
|
||||||
{{ $t('settings.profile.appearance.title') }}
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="account.note" max-h-100 overflow-y-auto>
|
<div v-if="account.note" max-h-100 overflow-y-auto>
|
||||||
|
|
|
@ -51,6 +51,23 @@ nuxtApp.hook('elk-logo:click', () => {
|
||||||
update()
|
update()
|
||||||
nuxtApp.$scrollToTop()
|
nuxtApp.$scrollToTop()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function createEntry(item: any) {
|
||||||
|
items.value = [...items.value, preprocess?.([item]) ?? item]
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEntry(item: any) {
|
||||||
|
const id = item[keyProp]
|
||||||
|
const index = items.value.findIndex(i => (i as any)[keyProp] === id)
|
||||||
|
if (index > -1)
|
||||||
|
items.value = [...items.value.slice(0, index), preprocess?.([item]) ?? item, ...items.value.slice(index + 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeEntry(entryId: any) {
|
||||||
|
items.value = items.value.filter(i => (i as any)[keyProp] !== entryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ createEntry, removeEntry, updateEntry })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
169
components/list/ListEntry.vue
Normal file
169
components/list/ListEntry.vue
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { mastodon } from 'masto'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'listUpdated', list: mastodon.v1.List): void
|
||||||
|
(e: 'listRemoved', id: string): void
|
||||||
|
}>()
|
||||||
|
const { list } = $defineProps<{
|
||||||
|
list: mastodon.v1.List
|
||||||
|
}>()
|
||||||
|
const { modelValue } = defineModel<{
|
||||||
|
modelValue: string
|
||||||
|
}>()
|
||||||
|
modelValue.value = list.title
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const client = useMastoClient()
|
||||||
|
|
||||||
|
let isEditing = $ref<boolean>(false)
|
||||||
|
let busy = $ref<boolean>(false)
|
||||||
|
let deleteBusy = $ref<boolean>(false)
|
||||||
|
|
||||||
|
const enableSaveButton = computed(() => list.title !== modelValue.value)
|
||||||
|
|
||||||
|
const edit = ref()
|
||||||
|
const input = ref()
|
||||||
|
|
||||||
|
const prepareEdit = () => {
|
||||||
|
isEditing = true
|
||||||
|
nextTick(() => {
|
||||||
|
input.value.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const cancelEdit = (updateTitle = true) => {
|
||||||
|
isEditing = false
|
||||||
|
if (updateTitle)
|
||||||
|
modelValue.value = list.title
|
||||||
|
nextTick(() => {
|
||||||
|
edit.value.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async function finishEditing() {
|
||||||
|
if (busy || !isEditing || !enableSaveButton.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
busy = true
|
||||||
|
await nextTick()
|
||||||
|
try {
|
||||||
|
const updateList = await client.v1.lists.update(list.id, {
|
||||||
|
title: modelValue.value,
|
||||||
|
})
|
||||||
|
cancelEdit(false)
|
||||||
|
emit('listUpdated', updateList)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
busy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function removeList() {
|
||||||
|
if (deleteBusy)
|
||||||
|
return
|
||||||
|
|
||||||
|
deleteBusy = true
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const confirmDelete = await openConfirmDialog({
|
||||||
|
title: t('confirm.delete_list.title'),
|
||||||
|
confirm: t('confirm.delete_list.confirm'),
|
||||||
|
cancel: t('confirm.delete_list.cancel'),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (confirmDelete === 'confirm') {
|
||||||
|
await nextTick()
|
||||||
|
try {
|
||||||
|
await client.v1.lists.remove(list.id)
|
||||||
|
emit('listRemoved', list.id)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
deleteBusy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
deleteBusy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onBeforeUnmount(() => cancelEdit(false))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form hover:bg-active flex justify-between items-center @submit.prevent="finishEditing">
|
||||||
|
<div
|
||||||
|
v-if="isEditing"
|
||||||
|
bg-base border="~ base" h10 m2 ps-1 pe-4 rounded-3 w-full flex="~ row"
|
||||||
|
items-center relative focus-within:box-shadow-outline gap-3
|
||||||
|
>
|
||||||
|
<CommonTooltip v-if="isEditing" :content="$t('list.cancel_edit')">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
rounded-full text-sm p2 transition-colors
|
||||||
|
hover:text-primary
|
||||||
|
@click="cancelEdit(true)"
|
||||||
|
>
|
||||||
|
<span block text-current i-ri:close-fill />
|
||||||
|
</button>
|
||||||
|
</CommonTooltip>
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="modelValue"
|
||||||
|
rounded-3
|
||||||
|
w-full
|
||||||
|
bg-transparent
|
||||||
|
outline="focus:none"
|
||||||
|
pe-4
|
||||||
|
pb="1px"
|
||||||
|
flex-1
|
||||||
|
placeholder-text-secondary
|
||||||
|
@keydown.esc="cancelEdit(true)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<NuxtLink v-else :to="`list/${list.id}`" block grow p4>
|
||||||
|
{{ list.title }}
|
||||||
|
</NuxtLink>
|
||||||
|
<div mr4 flex gap2>
|
||||||
|
<CommonTooltip v-if="isEditing" :content="$t('list.save')">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
text-sm p2 border-1 transition-colors
|
||||||
|
border-dark hover:text-primary
|
||||||
|
btn-action-icon
|
||||||
|
:disabled="deleteBusy || !enableSaveButton || busy"
|
||||||
|
>
|
||||||
|
<template v-if="isEditing">
|
||||||
|
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
|
||||||
|
<span block i-ri:loader-2-fill aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
<span v-else block text-current i-ri:save-2-fill class="rtl-flip" />
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</CommonTooltip>
|
||||||
|
<CommonTooltip v-else :content="$t('list.edit')">
|
||||||
|
<button
|
||||||
|
ref="edit"
|
||||||
|
type="button"
|
||||||
|
text-sm p2 border-1 transition-colors
|
||||||
|
border-dark hover:text-primary
|
||||||
|
btn-action-icon
|
||||||
|
@click.prevent="prepareEdit"
|
||||||
|
>
|
||||||
|
<span block text-current i-ri:edit-2-line class="rtl-flip" />
|
||||||
|
</button>
|
||||||
|
</CommonTooltip>
|
||||||
|
<CommonTooltip :content="$t('list.delete')">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
text-sm p2 border-1 transition-colors
|
||||||
|
border-dark hover:text-primary
|
||||||
|
btn-action-icon
|
||||||
|
:disabled="isEditing"
|
||||||
|
@click.prevent="removeList"
|
||||||
|
>
|
||||||
|
<span v-if="deleteBusy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
|
||||||
|
<span block i-ri:loader-2-fill aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
<span v-else block text-current i-ri:delete-bin-2-line class="rtl-flip" />
|
||||||
|
</button>
|
||||||
|
</CommonTooltip>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
|
@ -116,6 +116,11 @@
|
||||||
"cancel": "No",
|
"cancel": "No",
|
||||||
"confirm": "Yes"
|
"confirm": "Yes"
|
||||||
},
|
},
|
||||||
|
"delete_list": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Delete",
|
||||||
|
"title": "Are you sure you want to delete this list?"
|
||||||
|
},
|
||||||
"delete_posts": {
|
"delete_posts": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"confirm": "Delete",
|
"confirm": "Delete",
|
||||||
|
@ -177,8 +182,14 @@
|
||||||
},
|
},
|
||||||
"list": {
|
"list": {
|
||||||
"add_account": "Add account to list",
|
"add_account": "Add account to list",
|
||||||
|
"cancel_edit": "Cancel editing",
|
||||||
|
"create": "Create",
|
||||||
|
"delete": "Delete this list",
|
||||||
|
"edit": "Edit this list",
|
||||||
|
"list_title_placeholder": "List title",
|
||||||
"modify_account": "Modify lists with account",
|
"modify_account": "Modify lists with account",
|
||||||
"remove_account": "Remove account from list"
|
"remove_account": "Remove account from list",
|
||||||
|
"save": "Save changes"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"block_account": "Block {0}",
|
"block_account": "Block {0}",
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { CommonRouteTabOption } from '~/components/common/CommonRouteTabs.vue'
|
|
||||||
|
|
||||||
const list = $computed(() => useRoute().params.list as string)
|
|
||||||
const server = $computed(() => useRoute().params.server as string)
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const tabs = $computed<CommonRouteTabOption[]>(() => [
|
|
||||||
{
|
|
||||||
to: {
|
|
||||||
name: 'list',
|
|
||||||
params: { server, list },
|
|
||||||
},
|
|
||||||
display: t('tab.list'),
|
|
||||||
icon: 'i-ri:list-unordered',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: {
|
|
||||||
name: 'list-accounts',
|
|
||||||
params: { server, list },
|
|
||||||
},
|
|
||||||
display: t('tab.accounts'),
|
|
||||||
icon: 'i-ri:user-line',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
const { client } = $(useMasto())
|
|
||||||
const { data: listInfo, refresh } = $(await useAsyncData(() => client.v1.lists.fetch(list), { default: () => shallowRef() }))
|
|
||||||
|
|
||||||
if (listInfo) {
|
|
||||||
useHeadFixed({
|
|
||||||
title: () => `${listInfo.title}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onReactivated(() => {
|
|
||||||
// Silently update data when reentering the page
|
|
||||||
// The user will see the previous content first, and any changes will be updated to the UI when the request is completed
|
|
||||||
refresh()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<MainContent back>
|
|
||||||
<template #title>
|
|
||||||
<span text-lg font-bold>{{ listInfo ? listInfo.title : t('nav.list') }}</span>
|
|
||||||
</template>
|
|
||||||
<template #header>
|
|
||||||
<CommonRouteTabs replace :options="tabs" />
|
|
||||||
</template>
|
|
||||||
<NuxtPage v-if="isHydrated" />
|
|
||||||
</MainContent>
|
|
||||||
</template>
|
|
|
@ -1,35 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
definePageMeta({
|
|
||||||
name: 'list',
|
|
||||||
})
|
|
||||||
|
|
||||||
const params = useRoute().params
|
|
||||||
const listId = $(computedEager(() => params.list as string))
|
|
||||||
|
|
||||||
const { client } = $(useMasto())
|
|
||||||
const { data: listInfo, refresh } = $(await useAsyncData(() => client.v1.lists.fetch(listId), { default: () => shallowRef() }))
|
|
||||||
|
|
||||||
const paginator = client.v1.timelines.listList(listId)
|
|
||||||
const stream = useStreaming(client => client.v1.stream.streamListTimeline(listId))
|
|
||||||
|
|
||||||
if (listInfo) {
|
|
||||||
useHeadFixed({
|
|
||||||
title: () => `${listInfo.title}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onReactivated(() => {
|
|
||||||
// Silently update data when reentering the page
|
|
||||||
// The user will see the previous content first, and any changes will be updated to the UI when the request is completed
|
|
||||||
refresh()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<MainContent back>
|
|
||||||
<template #title>
|
|
||||||
<span text-lg font-bold>{{ listInfo ? listInfo.title : $t('nav.list') }}</span>
|
|
||||||
</template>
|
|
||||||
<TimelinePaginator v-bind="{ paginator, stream }" :preprocess="reorderedTimeline" context="home" />
|
|
||||||
</MainContent>
|
|
||||||
</template>
|
|
|
@ -1,17 +1,56 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
import type { CommonRouteTabOption } from '~/components/common/CommonRouteTabs.vue'
|
||||||
name: 'list',
|
|
||||||
})
|
|
||||||
|
|
||||||
const params = useRoute().params
|
const list = $computed(() => useRoute().params.list as string)
|
||||||
const listId = $(computedEager(() => params.list as string))
|
const server = $computed(() => useRoute().params.server as string)
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const tabs = $computed<CommonRouteTabOption[]>(() => [
|
||||||
|
{
|
||||||
|
to: {
|
||||||
|
name: 'list',
|
||||||
|
params: { server, list },
|
||||||
|
},
|
||||||
|
display: t('tab.posts'),
|
||||||
|
icon: 'i-ri:list-unordered',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: {
|
||||||
|
name: 'list-accounts',
|
||||||
|
params: { server, list },
|
||||||
|
},
|
||||||
|
display: t('tab.accounts'),
|
||||||
|
icon: 'i-ri:user-line',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
const { client } = $(useMasto())
|
const { client } = $(useMasto())
|
||||||
|
const { data: listInfo, refresh } = $(await useAsyncData(() => client.v1.lists.fetch(list), { default: () => shallowRef() }))
|
||||||
|
|
||||||
const paginator = client.v1.timelines.listList(listId)
|
if (listInfo) {
|
||||||
const stream = useStreaming(client => client.v1.stream.streamListTimeline(listId))
|
useHeadFixed({
|
||||||
|
title: () => `${listInfo.title} | ${route.fullPath.endsWith('/accounts') ? t('tab.accounts') : t('tab.posts')} | ${t('nav.lists')}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onReactivated(() => {
|
||||||
|
// Silently update data when reentering the page
|
||||||
|
// The user will see the previous content first, and any changes will be updated to the UI when the request is completed
|
||||||
|
refresh()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TimelinePaginator v-bind="{ paginator, stream }" :preprocess="reorderedTimeline" context="home" />
|
<MainContent back>
|
||||||
|
<template #title>
|
||||||
|
<span text-lg font-bold>{{ listInfo ? listInfo.title : t('nav.list') }}</span>
|
||||||
|
</template>
|
||||||
|
<template #header>
|
||||||
|
<CommonRouteTabs replace :options="tabs" />
|
||||||
|
</template>
|
||||||
|
<NuxtPage v-if="isHydrated" />
|
||||||
|
</MainContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
17
pages/[[server]]/list/[list]/index/index.vue
Normal file
17
pages/[[server]]/list/[list]/index/index.vue
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
name: 'list',
|
||||||
|
})
|
||||||
|
|
||||||
|
const params = useRoute().params
|
||||||
|
const listId = $(computedEager(() => params.list as string))
|
||||||
|
|
||||||
|
const client = useMastoClient()
|
||||||
|
|
||||||
|
const paginator = client.v1.timelines.listList(listId)
|
||||||
|
const stream = useStreaming(client => client.v1.stream.streamListTimeline(listId))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TimelinePaginator v-bind="{ paginator, stream }" :preprocess="reorderedTimeline" context="home" />
|
||||||
|
</template>
|
|
@ -1,12 +1,44 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { mastodon } from 'masto'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const { client } = $(useMasto())
|
const client = useMastoClient()
|
||||||
|
|
||||||
const paginator = client.v1.lists.list()
|
const paginator = client.v1.lists.list()
|
||||||
|
|
||||||
useHeadFixed({
|
useHeadFixed({
|
||||||
title: () => t('nav.lists'),
|
title: () => t('nav.lists'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const paginatorRef = ref()
|
||||||
|
let busy = $ref<boolean>(false)
|
||||||
|
const createText = ref('')
|
||||||
|
const enableSubmit = computed(() => createText.value.length > 0)
|
||||||
|
|
||||||
|
async function createList() {
|
||||||
|
if (busy || !enableSubmit.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
busy = true
|
||||||
|
await nextTick()
|
||||||
|
try {
|
||||||
|
const newEntry = await client.v1.lists.create({
|
||||||
|
title: createText.value,
|
||||||
|
})
|
||||||
|
paginatorRef.value.createEntry(newEntry)
|
||||||
|
createText.value = ''
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
busy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateEntry(list: mastodon.v1.List) {
|
||||||
|
paginatorRef.value.updateEntry(list)
|
||||||
|
}
|
||||||
|
function removeEntry(id: string) {
|
||||||
|
paginatorRef.value.removeEntry(id)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -18,11 +50,47 @@ useHeadFixed({
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
<slot>
|
<slot>
|
||||||
<CommonPaginator :paginator="paginator">
|
<CommonPaginator ref="paginatorRef" :paginator="paginator">
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<NuxtLink :to="`list/${item.id}`" block p4 hover:bg-active flex justify-between>
|
<ListEntry
|
||||||
{{ item.title }}
|
:list="item"
|
||||||
</NuxtLink>
|
@list-updated="updateEntry"
|
||||||
|
@list-removed="removeEntry"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #done>
|
||||||
|
<form
|
||||||
|
border="t base"
|
||||||
|
p-4 w-full
|
||||||
|
flex="~ wrap" relative gap-3
|
||||||
|
@submit.prevent="createList"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
bg-base border="~ base" flex-1 h10 ps-1 pe-4 rounded-2 w-full flex="~ row"
|
||||||
|
items-center relative focus-within:box-shadow-outline gap-3
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="createText"
|
||||||
|
bg-transparent
|
||||||
|
outline="focus:none"
|
||||||
|
px-4
|
||||||
|
pb="1px"
|
||||||
|
flex-1
|
||||||
|
placeholder-text-secondary
|
||||||
|
:placeholder="$t('list.list_title_placeholder')"
|
||||||
|
@keypress.enter="createList"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div flex="~ col" gap-y-4 gap-x-2 sm="~ justify-between flex-row">
|
||||||
|
<button flex="~ row" gap-x-2 items-center btn-solid :disabled="!enableSubmit || busy">
|
||||||
|
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
|
||||||
|
<span block i-ri:loader-2-fill aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
<span v-else aria-hidden="true" block i-material-symbols:playlist-add-rounded class="rtl-flip" />
|
||||||
|
{{ $t('list.create') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</template>
|
</template>
|
||||||
</CommonPaginator>
|
</CommonPaginator>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
Loading…
Reference in a new issue