fix(ui): artist and album detail and edit pages

This commit is contained in:
ArneBo 2025-01-15 16:18:31 +01:00
parent 0bdb026cf1
commit a84868e1df
19 changed files with 577 additions and 506 deletions

View File

@ -2,6 +2,8 @@
import type { ArtistCredit } from '~/types' import type { ArtistCredit } from '~/types'
import { useStore } from '~/store' import { useStore } from '~/store'
import Link from '~/components/ui/Link.vue'
const store = useStore() const store = useStore()
interface Props { interface Props {
@ -26,7 +28,7 @@ const getRoute = (ac: ArtistCredit) => {
v-for="ac in props.artistCredit" v-for="ac in props.artistCredit"
:key="ac.artist.id" :key="ac.artist.id"
> >
<router-link <Link solid secondary round min-content
:to="getRoute(ac)" :to="getRoute(ac)"
> >
<img <img
@ -37,10 +39,10 @@ const getRoute = (ac: ArtistCredit) => {
> >
<i <i
v-else-if="ac.index === 0" v-else-if="ac.index === 0"
:class="[ac.artist.content_category != 'podcast' ? 'circular' : 'bordered', 'inverted violet users icon']" :class="[ac.artist.content_category != 'podcast' ? 'circular' : 'bordered', 'inverted violet bi bi-users icon']"
/> />
{{ ac.credit }} {{ ac.credit }}
</router-link> </Link>
<span>{{ ac.joinphrase }}</span> <span>{{ ac.joinphrase }}</span>
</template> </template>
</div> </div>

View File

@ -10,10 +10,11 @@ import { useStore } from '~/store'
import axios from 'axios' import axios from 'axios'
import TrackMobileRow from '~/components/audio/track/MobileRow.vue' import TrackMobileRow from '~/components/audio/track/MobileRow.vue'
import Pagination from '~/components/vui/Pagination.vue' import Pagination from '~/components/ui/Pagination.vue'
import TrackRow from '~/components/audio/track/Row.vue' import TrackRow from '~/components/audio/track/Row.vue'
import Input from '~/components/ui/Input.vue' import Input from '~/components/ui/Input.vue'
import Spacer from '~/components/ui/Spacer.vue' import Spacer from '~/components/ui/Spacer.vue'
import Loader from '~/components/ui/Loader.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
@ -175,18 +176,13 @@ const updatePage = (page: number) => {
<div <div
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']" :class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
> >
<div <Loader v-if="isLoading" />
v-if="isLoading"
class="ui inverted active dimmer"
>
<div class="ui loader" />
</div>
<div class="track-table row"> <div class="track-table row">
<div <div
v-if="showPosition" v-if="showPosition"
class="actions left floated column" class="actions left floated column"
> >
<i class="hashtag icon" /> <i class="bi bi-hash" />
</div> </div>
<div <div
v-else v-else
@ -220,7 +216,7 @@ const updatePage = (page: number) => {
class="meta right floated column" class="meta right floated column"
> >
<i <i
class="clock outline icon" class="bi bi-clock"
style="padding: 0.5rem" style="padding: 0.5rem"
/> />
</div> </div>
@ -263,12 +259,7 @@ const updatePage = (page: number) => {
<div <div
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']" :class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
> >
<div <Loader v-if="isLoading" />
v-if="isLoading"
class="ui inverted active dimmer"
>
<div class="ui loader" />
</div>
<!-- For each item, build a row --> <!-- For each item, build a row -->
@ -289,7 +280,7 @@ const updatePage = (page: number) => {
v-if="tracks && paginateResults && totalTracks > paginateBy" v-if="tracks && paginateResults && totalTracks > paginateBy"
class="ui center aligned basic segment tablet-and-below" class="ui center aligned basic segment tablet-and-below"
> >
<pagination <Pagination
v-if="paginateResults && totalTracks > paginateBy" v-if="paginateResults && totalTracks > paginateBy"
:paginate-by="paginateBy" :paginate-by="paginateBy"
:total="totalTracks" :total="totalTracks"

View File

@ -27,3 +27,16 @@ const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${actorColor.val
class="ui avatar circular label" class="ui avatar circular label"
>{{ actor.preferred_username?.[0] || "" }}</span> >{{ actor.preferred_username?.[0] || "" }}</span>
</template> </template>
<style lang="scss">
.ui.circular.avatar {
float: left;
margin-right: 8px;
text-align: center;
border-radius: 50%;
&.label {
align-content: center;
}
}
</style>

View File

@ -5,6 +5,8 @@ import { toRefs } from '@vueuse/core'
import { computed } from 'vue' import { computed } from 'vue'
import { truncate } from '~/utils/filters' import { truncate } from '~/utils/filters'
import Link from '~/components/ui/Link.vue'
interface Props { interface Props {
actor: Actor actor: Actor
avatar?: boolean avatar?: boolean
@ -50,15 +52,18 @@ const url = computed(() => {
</script> </script>
<template> <template>
<router-link <Link
:to="url" :to="url"
:title="actor.full_username" :title="actor.full_username"
class="username"
solid
secondary
round
> >
<actor-avatar <actor-avatar
v-if="avatar" v-if="avatar"
:actor="actor" :actor="actor"
/> />
<span>&nbsp;</span>
<slot>{{ repr }}</slot> <slot>{{ repr }}</slot>
</router-link> </Link>
</template> </template>

View File

@ -8,6 +8,9 @@ import { useStore } from '~/store'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import useFormData from '~/composables/useFormData' import useFormData from '~/composables/useFormData'
import Button from '~/components/ui/Button.vue'
import Alert from '~/components/ui/Alert.vue'
interface Events { interface Events {
(e: 'update:modelValue', value: string | null): void (e: 'update:modelValue', value: string | null): void
(e: 'delete'): void (e: 'delete'): void
@ -104,10 +107,9 @@ const getAttachmentUrl = (uuid: string) => {
<template> <template>
<div class="ui form"> <div class="ui form">
<div <Alert red
v-if="errors.length > 0" v-if="errors.length > 0"
role="alert" role="alert"
class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ t('components.common.AttachmentInput.header.failure') }} {{ t('components.common.AttachmentInput.header.failure') }}
@ -120,7 +122,7 @@ const getAttachmentUrl = (uuid: string) => {
{{ error }} {{ error }}
</li> </li>
</ul> </ul>
</div> </Alert>
<div class="ui field"> <div class="ui field">
<span id="avatarLabel"> <span id="avatarLabel">
<slot /> <slot />
@ -158,19 +160,19 @@ const getAttachmentUrl = (uuid: string) => {
type="file" type="file"
accept="image/png,image/jpeg" accept="image/png,image/jpeg"
@change="submit" @change="submit"
> />
</div> </div>
<div class="ui very small hidden divider" /> <div class="ui very small hidden divider" />
<p> <p>
{{ t('components.common.AttachmentInput.help.upload') }} {{ t('components.common.AttachmentInput.help.upload') }}
</p> </p>
<button <Button
v-if="value" v-if="value"
class="ui basic tiny button" class="ui basic tiny button"
@click.stop.prevent="remove(value as string)" @click.stop.prevent="remove(value as string)"
> >
{{ t('components.common.AttachmentInput.button.remove') }} {{ t('components.common.AttachmentInput.button.remove') }}
</button> </Button>
<div <div
v-if="isLoading" v-if="isLoading"
class="ui active inverted dimmer" class="ui active inverted dimmer"

View File

@ -86,57 +86,55 @@ onMounted(async () => {
</script> </script>
<template> <template>
<Button <Button
@click.prevent="isPreviewing = false" @click.prevent="isPreviewing = false"
:aria-pressed="!isPreviewing" :aria-pressed="!isPreviewing"
title="write" title="write"
> >
{{ t('components.common.ContentForm.button.write') }} {{ t('components.common.ContentForm.button.write') }}
</Button> </Button>
<Button <Button
@click.prevent="isPreviewing = true" @click.prevent="isPreviewing = true"
:aria-pressed="!isPreviewing" :aria-pressed="!isPreviewing"
title="preview" title="preview"
> >
{{ t('components.common.ContentForm.button.preview') }} {{ t('components.common.ContentForm.button.preview') }}
</Button> </Button>
<template v-if="isPreviewing"> <template v-if="isPreviewing">
<div <div
v-if="isLoadingPreview" v-if="isLoadingPreview"
class="ui placeholder" class="ui placeholder"
> >
<div class="paragraph"> <div class="paragraph">
<div class="line" /> <div class="line" />
<div class="line" /> <div class="line" />
<div class="line" /> <div class="line" />
<div class="line" /> <div class="line" />
</div> </div>
</div> </div>
<p v-else-if="!preview"> <p v-else-if="!preview">
{{ t('components.common.ContentForm.empty.noContent') }} {{ t('components.common.ContentForm.empty.noContent') }}
</p> </p>
<sanitized-html <sanitized-html
v-else v-else
:html="preview" :html="preview"
/> />
</template> </template>
<template v-else> <template v-else>
<div class="ui transparent input"> <Textarea
<Textarea ref="textarea"
ref="textarea" v-model="value"
v-model="value" :required="required"
:required="required" :placeholder="labels.placeholder"
:placeholder="labels.placeholder" />
/> </template>
</div> <span
</template> v-if="charLimit"
<span :class="['right', 'floated', {'ui danger text': remainingChars < 0}]"
v-if="charLimit" >
:class="['right', 'floated', {'ui danger text': remainingChars < 0}]" {{ remainingChars }}
> </span>
{{ remainingChars }} <p>
</span> {{ t('components.common.ContentForm.help.markdown') }}
<p> </p>
{{ t('components.common.ContentForm.help.markdown') }}
</p>
</template> </template>

View File

@ -2,6 +2,10 @@
import { toRefs, useClipboard } from '@vueuse/core' import { toRefs, useClipboard } from '@vueuse/core'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Button from '~/components/ui/Button.vue'
import Input from '~/components/ui/Input.vue'
import Alert from '~/components/ui/Alert.vue'
interface Props { interface Props {
value: string value: string
buttonClasses?: string buttonClasses?: string
@ -20,27 +24,53 @@ const { copy, isSupported: canCopy, copied } = useClipboard({ source: value, cop
</script> </script>
<template> <template>
<div class="ui fluid action input component-copy-input">
<p <p
v-if="copied" v-if="copied"
class="message" class="message"
> >
{{ t('components.common.CopyInput.message.success') }} {{ t('components.common.CopyInput.message.success') }}
</p> </p>
<input <Input
:id="id" :id="id"
:value="value" :value="value"
readonly
:name="id" :name="id"
type="text" type="text"
readonly
> >
<button <template #input-right>
:class="['ui', buttonClasses, 'right', 'labeled', 'icon', 'button']" <Button
:disabled="!canCopy || undefined" :class="['ui', buttonClasses, 'input-right']"
@click="copy()" min-content
> secondary
<i class="copy icon" /> :disabled="!canCopy || undefined"
{{ t('components.common.CopyInput.button.copy') }} @click="copy()"
</button> >
</div> <i class="bi bi-copy" />
{{ t('components.common.CopyInput.button.copy') }}
</Button>
</template>
</Input>
</template> </template>
<style scoped>
.input-right {
position: absolute;
right: 0px;
bottom: 0px;
height: 48px;
min-width: 48px;
display: flex;
.button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
p.message {
background-color: var(--hover-background-color);
padding: 8px;
position: absolute;
bottom: -32px;
right: 0px;
}
</style>

View File

@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import SemanticModal from '~/components/semantic/Modal.vue'
import Button from '~/components/ui/Button.vue'
import { ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import SemanticModal from '~/components/semantic/Modal.vue'
import Button from '~/components/ui/Button.vue'
interface Events { interface Events {
(e: 'confirm'): void (e: 'confirm'): void
} }
@ -33,16 +34,17 @@ const confirm = () => {
</script> </script>
<template> <template>
<button <Button
class="funkwhale dangerous-button" destructive
:class="{ 'is-disabled': disabled }" :class="{ 'is-disabled': disabled }"
:disabled="disabled" :disabled="disabled"
@click.prevent.stop="showModal = true" @click.prevent.stop="showModal = true"
> >
<slot /> <slot />
<semantic-modal <SemanticModal
v-model:show="showModal" v-model:show="showModal"
:title="t('components.common.DangerousButton.header.confirm')"
class="small" class="small"
> >
<h4 class="header"> <h4 class="header">
@ -55,10 +57,10 @@ const confirm = () => {
<slot name="modal-content" /> <slot name="modal-content" />
</div> </div>
</div> </div>
<div class="actions"> <template #actions>
<Button <Button
color="secondary" secondary
variant="outline" outline
@click="showModal = false" @click="showModal = false"
> >
{{ t('components.common.DangerousButton.button.cancel') }} {{ t('components.common.DangerousButton.button.cancel') }}
@ -71,7 +73,7 @@ const confirm = () => {
{{ t('components.common.DangerousButton.button.confirm') }} {{ t('components.common.DangerousButton.button.confirm') }}
</slot> </slot>
</Button> </Button>
</div> </template>
</semantic-modal> </SemanticModal>
</button> </Button>
</template> </template>

View File

@ -25,8 +25,15 @@ const realDate = useTimeAgo(date)
> >
<i <i
v-if="props.icon" v-if="props.icon"
class="outline clock icon" class="bi bi-clock"
/> />
{{ realDate }} {{ realDate }}
</time> </time>
</template> </template>
<style scoped>
i {
margin-right: 8px;
font-size: 14px;
}
</style>

View File

@ -14,6 +14,9 @@ import ArtistCreditLabel from '~/components/audio/ArtistCreditLabel.vue'
import PlayButton from '~/components/audio/PlayButton.vue' import PlayButton from '~/components/audio/PlayButton.vue'
import TagsList from '~/components/tags/List.vue' import TagsList from '~/components/tags/List.vue'
import AlbumDropdown from './AlbumDropdown.vue' import AlbumDropdown from './AlbumDropdown.vue'
import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Loader from '~/components/ui/Loader.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
@ -136,19 +139,15 @@ const remove = async () => {
</script> </script>
<template> <template>
<main> <Layout stack main>
<div <Loader
v-if="isLoading" v-if="isLoading"
v-title="labels.title" v-title="labels.title"
class="ui vertical segment" />
>
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
</div>
<template v-if="object"> <template v-if="object">
<section class="ui vertical stripe segment channel-serie"> <section class="segment channel-serie">
<div class="ui stackable grid container"> <Layout flex gap-64 style="align-content: stretch;">
<div class="ui seven wide column"> <div style="flex: 2;">
/front/src/components/library/AlbumBase.vue
<div <div
v-if="isSerie" v-if="isSerie"
class="padded basic segment" class="padded basic segment"
@ -204,8 +203,7 @@ const remove = async () => {
{{ t('components.library.AlbumBase.meta.tracks', totalTracks) }} {{ t('components.library.AlbumBase.meta.tracks', totalTracks) }}
</span> </span>
</template> </template>
<div class="ui small hidden divider" /> <PlayButton
<play-button
class="vibrant" class="vibrant"
:tracks="object.tracks" :tracks="object.tracks"
:is-playable="object.is_playable" :is-playable="object.is_playable"
@ -223,7 +221,6 @@ const remove = async () => {
/> />
</div> </div>
</div> </div>
<div class="ui small hidden divider" />
<header> <header>
<h2 <h2
class="ui header" class="ui header"
@ -266,6 +263,7 @@ const remove = async () => {
:artist-credit="artistCredit" :artist-credit="artistCredit"
/> />
</header> </header>
<Spacer :size="16"/>
<div <div
v-if="object.release_date || (totalTracks > 0)" v-if="object.release_date || (totalTracks > 0)"
class="ui small hidden divider" class="ui small hidden divider"
@ -342,7 +340,7 @@ const remove = async () => {
</router-link> </router-link>
</template> </template>
</div> </div>
<div class="nine wide column"> <div style="flex 1;">
<router-view <router-view
v-if="object" v-if="object"
:key="route.fullPath" :key="route.fullPath"
@ -356,8 +354,8 @@ const remove = async () => {
@libraries-loaded="libraries = $event" @libraries-loaded="libraries = $event"
/> />
</div> </div>
</div> </Layout>
</section> </section>
</template> </template>
</main> </Layout>
</template> </template>

View File

@ -56,7 +56,6 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
<template> <template>
<div <div
v-if="!isLoadingTracks" v-if="!isLoadingTracks"
class="ui vertical segment"
> >
<h2 class="ui header"> <h2 class="ui header">
<span v-if="isSerie"> <span v-if="isSerie">
@ -82,7 +81,7 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
> >
<template v-if="tracks.length > 0"> <template v-if="tracks.length > 0">
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<play-button <PlayButton
class="right floated mini inverted vibrant" class="right floated mini inverted vibrant"
:tracks="discs[index]" :tracks="discs[index]"
/> />

View File

@ -15,6 +15,7 @@ import Button from '~/components/ui/Button.vue'
import Popover from '~/components/ui/Popover.vue' import Popover from '~/components/ui/Popover.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.vue' import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
import DangerousButton from '~/components/common/DangerousButton.vue' import DangerousButton from '~/components/common/DangerousButton.vue'
import OptionsButton from "~/components/ui/button/Options.vue"
interface Events { interface Events {
(e: 'remove'): void (e: 'remove'): void
@ -55,7 +56,6 @@ const open = ref(false)
<template> <template>
<span> <span>
<semantic-modal <semantic-modal
v-if="isEmbedable" v-if="isEmbedable"
v-model:show="showEmbedModal" v-model:show="showEmbedModal"
@ -84,11 +84,7 @@ const open = ref(false)
</semantic-modal> </semantic-modal>
<Popover v-model:open="open"> <Popover v-model:open="open">
<template #default="{ toggleOpen }"> <template #default="{ toggleOpen }">
<Button <OptionsButton
variant="ghost"
color="secondary"
round
icon="bi-three-dots-vertical"
:title="labels.more" :title="labels.more"
@click="toggleOpen()" @click="toggleOpen()"
/> />

View File

@ -21,27 +21,23 @@ const canEdit = store.state.auth.availablePermissions.library
</script> </script>
<template> <template>
<section class="ui vertical stripe segment"> <h2>
<div class="ui text container"> <span v-if="canEdit">
<h2> {{ t('components.library.AlbumEdit.header.edit') }}
<span v-if="canEdit"> </span>
{{ t('components.library.AlbumEdit.header.edit') }} <span v-else>
</span> {{ t('components.library.AlbumEdit.header.suggest') }}
<span v-else> </span>
{{ t('components.library.AlbumEdit.header.suggest') }} </h2>
</span> <div
</h2> v-if="!object.is_local"
<div class="ui message"
v-if="!object.is_local" >
class="ui message" {{ t('components.library.AlbumEdit.message.remote') }}
> </div>
{{ t('components.library.AlbumEdit.message.remote') }} <edit-form
</div> v-else
<edit-form :object-type="objectType"
v-else :object="object"
:object-type="objectType" />
:object="object"
/>
</div>
</section>
</template> </template>

View File

@ -13,7 +13,9 @@ import useLogger from '~/composables/useLogger'
import EmbedWizard from '~/components/audio/EmbedWizard.vue' import EmbedWizard from '~/components/audio/EmbedWizard.vue'
import SemanticModal from '~/components/semantic/Modal.vue' import SemanticModal from '~/components/semantic/Modal.vue'
import Loader from '~/components/ui/Loader.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import OptionsButton from '~/components/ui/button/Options.vue'
import PlayButton from '~/components/audio/PlayButton.vue' import PlayButton from '~/components/audio/PlayButton.vue'
import RadioButton from '~/components/radios/Button.vue' import RadioButton from '~/components/radios/Button.vue'
import Popover from '~/components/ui/Popover.vue' import Popover from '~/components/ui/Popover.vue'
@ -98,16 +100,11 @@ watch(() => props.id, fetchData, { immediate: true })
<template> <template>
<main v-title="labels.title"> <main v-title="labels.title">
<div <Loader v-if="isLoading" />
v-if="isLoading"
class="ui vertical segment"
>
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
</div>
<template v-if="object && !isLoading"> <template v-if="object && !isLoading">
<section <section
v-title="object.name" v-title="object.name"
:class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned', 'stripe', 'segment']" :class="['ui', 'head', {'with-background': cover}, 'vertical', 'center', 'aligned']"
:style="headerStyle" :style="headerStyle"
> >
<div class="segment-content"> <div class="segment-content">
@ -167,104 +164,99 @@ watch(() => props.id, fetchData, { immediate: true })
</Button> </Button>
</div> </div>
</semantic-modal> </semantic-modal>
<div class="ui buttons"> <Popover>
<Popover> <template #default="{ toggleOpen }">
<template #default="{ toggleOpen }"> <OptionsButton
<Button @click="toggleOpen"
@click="toggleOpen" />
icon="caret-down-fill" </template>
>
{{ t('components.library.ArtistBase.button.more') }}
</Button>
</template>
<template #items> <template #items>
<PopoverItem <PopoverItem
v-if="domain != store.getters['instance/domain']" v-if="domain != store.getters['instance/domain']"
:href="object.fid" :href="object.fid"
target="_blank" target="_blank"
class="funkwhale item" class="funkwhale item"
> >
<i class="external icon" /> <i class="bi bi-box-arrow-up-right" />
{{ t('components.library.ArtistBase.link.domain', {domain: domain}) }} {{ t('components.library.ArtistBase.link.domain', {domain: domain}) }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
v-if="publicLibraries.length > 0" v-if="publicLibraries.length > 0"
@click="showEmbedModal = !showEmbedModal" @click="showEmbedModal = !showEmbedModal"
> >
<i class="code icon" /> <i class="bi bi-code-square" />
{{ t('components.library.ArtistBase.button.embed') }} {{ t('components.library.ArtistBase.button.embed') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
:href="wikipediaUrl" :href="wikipediaUrl"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
<i class="wikipedia w icon" /> <i class="bi bi-wikipedia" />
{{ t('components.library.ArtistBase.link.wikipedia') }} {{ t('components.library.ArtistBase.link.wikipedia') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
v-if="musicbrainzUrl" v-if="musicbrainzUrl"
:href="musicbrainzUrl" :href="musicbrainzUrl"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
<i class="external icon" /> <i class="bi bi-box-arrow-up-right" />
{{ t('components.library.ArtistBase.link.musicbrainz') }} {{ t('components.library.ArtistBase.link.musicbrainz') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
:href="discogsUrl" :href="discogsUrl"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
<i class="external icon" /> <i class="bi bi-box-arrow-up-right" />
{{ t('components.library.ArtistBase.link.discogs') }} {{ t('components.library.ArtistBase.link.discogs') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
v-if="object.is_local" v-if="object.is_local"
:to="{name: 'library.artists.edit', params: {id: object.id }}" :to="{name: 'library.artists.edit', params: {id: object.id }}"
> >
<i class="edit icon" /> <i class="bi bi-pencil-fill" />
{{ t('components.library.ArtistBase.button.edit') }} {{ t('components.library.ArtistBase.button.edit') }}
</PopoverItem> </PopoverItem>
<hr> <hr v-for="obj in getReportableObjects({artist: object})">
<PopoverItem <PopoverItem
v-for="obj in getReportableObjects({artist: object})" v-for="obj in getReportableObjects({artist: object})"
:key="obj.target.type + obj.target.id" :key="obj.target.type + obj.target.id"
@click="report(obj)" @click="report(obj)"
> >
<i class="share icon" /> {{ obj.label }} <i class="bi bi-share-fill" /> {{ obj.label }}
</PopoverItem> </PopoverItem>
<hr> <hr>
<PopoverItem <PopoverItem
v-if="store.state.auth.availablePermissions['library']" v-if="store.state.auth.availablePermissions['library']"
:to="{name: 'manage.library.artists.detail', params: {id: object.id}}" :to="{name: 'manage.library.artists.detail', params: {id: object.id}}"
> >
<i class="wrench icon" /> <i class="bi bi-wrench" />
{{ t('components.library.ArtistBase.link.moderation') }} {{ t('components.library.ArtistBase.link.moderation') }}
</PopoverItem> </PopoverItem>
<PopoverItem <PopoverItem
v-if="store.state.auth.profile && store.state.auth.profile.is_superuser" v-if="store.state.auth.profile && store.state.auth.profile.is_superuser"
:href="store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)" :href="store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<i class="wrench icon" /> <i class="bi bi-wrench" />
{{ t('components.library.ArtistBase.link.django') }} {{ t('components.library.ArtistBase.link.django') }}
</PopoverItem> </PopoverItem>
</template> </template>
</Popover> </Popover>
</div>
</div> </div>
</div> </div>
</section> </section>

View File

@ -3,10 +3,13 @@ import type { EditObjectType } from '~/composables/moderation/useEditConfigs'
import type { Artist, Library } from '~/types' import type { Artist, Library } from '~/types'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
import EditForm from '~/components/library/EditForm.vue' import EditForm from '~/components/library/EditForm.vue'
import Section from '~/components/ui/Section.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Layout from '~/components/ui/Layout.vue'
import Alert from '~/components/ui/Alert.vue'
interface Props { interface Props {
objectType: EditObjectType objectType: EditObjectType
@ -23,27 +26,23 @@ const canEdit = store.state.auth.availablePermissions.library
</script> </script>
<template> <template>
<section class="ui vertical stripe segment"> <Layout stack>
<div class="ui text container"> <Spacer />
<h2> <Section no-items alignLeft
<span v-if="canEdit"> :h2="canEdit
{{ t('components.library.ArtistEdit.header.edit') }} ? t('components.library.ArtistEdit.header.edit')
</span> : t('components.library.ArtistEdit.header.suggest')
<span v-else> "
{{ t('components.library.ArtistEdit.header.suggest') }} />
</span> <Alert yellow
</h2> v-if="!object.is_local"
<div >
v-if="!object.is_local" {{ t('components.library.ArtistEdit.message.remote') }}
class="ui message" </Alert>
> <edit-form
{{ t('components.library.ArtistEdit.message.remote') }} v-else
</div> :object-type="objectType"
<edit-form :object="object"
v-else />
:object-type="objectType" </Layout>
:object="object"
/>
</div>
</section>
</template> </template>

View File

@ -8,13 +8,20 @@ import { useRouter } from 'vue-router'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { color, setColors } from '~/composables/color'
import axios from 'axios' import axios from 'axios'
import useEditConfigs from '~/composables/moderation/useEditConfigs' import useEditConfigs from '~/composables/moderation/useEditConfigs'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
import Button from '~/components/ui/Button.vue'
import DangerousButton from '~/components/common/DangerousButton.vue'
import Card from '~/components/ui/Card.vue'
import Link from '~/components/ui/Link.vue'
import Alert from '~/components/ui/Alert.vue'
import Spacer from '~/components/ui/Spacer.vue'
interface Events { interface Events {
(e: 'approved', isApproved: boolean): void (e: 'approved', isApproved: boolean): void
(e: 'deleted'): void (e: 'deleted'): void
@ -155,7 +162,8 @@ const approve = async (approved: boolean) => {
</script> </script>
<template> <template>
<div class="ui fluid card"> <!--TODO: Make "to:" prop on card link the whole card to detailUrl, but leave track link, actor link and action buttons clickable -->
<Card>
<div class="content"> <div class="content">
<h4 class="header"> <h4 class="header">
<router-link :to="detailUrl"> <router-link :to="detailUrl">
@ -166,34 +174,16 @@ const approve = async (approved: boolean) => {
<router-link <router-link
v-if="obj.target && obj.target.type === 'track'" v-if="obj.target && obj.target.type === 'track'"
:to="{name: 'library.tracks.detail', params: {id: obj.target.id }}" :to="{name: 'library.tracks.detail', params: {id: obj.target.id }}"
:class="isInteractive"
> >
<i class="music icon" /> <i class="bi bi-file-music-fill" />
{{ t('components.library.EditCard.link.track', {id: obj.target.id, name: obj.target.repr}) }} {{ t('components.library.EditCard.link.track', {id: obj.target.id, name: obj.target.repr}) }}
</router-link> </router-link>
<br>
<human-date <human-date
:date="obj.creation_date" :date="obj.creation_date"
:icon="true" :icon="true"
/> />
<span class="right floated">
<span v-if="obj.is_approved && obj.is_applied">
<i class="success check icon" />
{{ t('components.library.EditCard.status.applied') }}
</span>
<span v-else-if="obj.is_approved">
<i class="success check icon" />
{{ t('components.library.EditCard.status.approved') }}
</span>
<span v-else-if="obj.is_approved === null">
<i class="warning hourglass icon" />
{{ t('components.library.EditCard.status.pending') }}
</span>
<span v-else-if="obj.is_approved === false">
<i class="danger x icon" />
{{ t('components.library.EditCard.status.rejected') }}
</span>
</span>
</div> </div>
</div> </div>
<div <div
@ -202,117 +192,145 @@ const approve = async (approved: boolean) => {
> >
{{ obj.summary }} {{ obj.summary }}
</div> </div>
<div class="content"> <template #alert>
<table <!--TODO: Pass Alert colors through to card.vue -->
v-if="obj.type === 'update'" <Alert
class="ui celled very basic fixed stacking table" :green="obj.is_approved && obj.is_applied"
:red="obj.is_approved === false"
:yellow="obj.is_approved === null"
> >
<thead> <span class="right floated">
<tr> <span v-if="obj.is_approved && obj.is_applied">
<th> <i class="green bi bi-check"/>
{{ t('components.library.EditCard.table.update.header.field') }} {{ t('components.library.EditCard.status.applied') }}
</th> </span>
<th> <span v-else-if="obj.is_approved">
{{ t('components.library.EditCard.table.update.header.oldValue') }} <i class="green bi bi-check" />
</th> {{ t('components.library.EditCard.status.approved') }}
<th> </span>
{{ t('components.library.EditCard.table.update.header.newValue') }} <span v-else-if="obj.is_approved === null">
</th> <i class="yellow bi bi-hourglass" />
</tr> {{ t('components.library.EditCard.status.pending') }}
</thead> </span>
<tbody> <span v-else-if="obj.is_approved === false">
<tr <i class="destructive bi bi-x" />
v-for="field in updatedFields" {{ t('components.library.EditCard.status.rejected') }}
:key="field.id" </span>
> </span>
<td>{{ field.id }}</td> <table
v-if="obj.type === 'update'"
<td v-if="field.diff"> >
<template v-if="field.config?.type === 'attachment' && field.oldRepr"> <thead>
<img <tr>
class="ui image" <th>
alt="" {{ t('components.library.EditCard.table.update.header.field') }}
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.oldRepr}/proxy?next=medium_square_crop`)" </th>
> <th>
</template> {{ t('components.library.EditCard.table.update.header.oldValue') }}
<template v-else> </th>
<span <th>
v-for="(part, key) in field.diff.filter(p => !p.added)" {{ t('components.library.EditCard.table.update.header.newValue') }}
:key="key" </th>
:class="['diff', {removed: part.removed}]" </tr>
> </thead>
{{ part.value }} <tbody>
</span> <tr
</template> v-for="field in updatedFields"
</td> :key="field.id"
<td v-else>
{{ t('components.library.EditCard.table.update.notApplicable') }}
</td>
<td
v-if="field.diff"
:title="field.newRepr"
> >
<template v-if="field.config?.type === 'attachment' && field.newRepr"> <td>{{ field.id }}</td>
<img
class="ui image" <td v-if="field.diff">
alt="" <template v-if="field.config?.type === 'attachment' && field.oldRepr">
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)" <img
> class="image"
</template> alt=""
<template v-else> :src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.oldRepr}/proxy?next=medium_square_crop`)"
<span >
v-for="(part, key) in field.diff.filter(p => !p.removed)" </template>
:key="key" <template v-else>
:class="['diff', {added: part.added}]" <span
> v-for="(part, key) in field.diff.filter(p => !p.added)"
{{ part.value }} :key="key"
</span> :class="['diff', {removed: part.removed}]"
</template> >
</td> {{ part.value }}
<td </span>
v-else </template>
:title="field.newRepr" </td>
> <td v-else>
<template v-if="field.config?.type === 'attachment' && field.newRepr"> {{ t('components.library.EditCard.table.update.notApplicable') }}
<img </td>
class="ui image"
alt="" <td
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)" v-if="field.diff"
> :title="field.newRepr"
</template> >
<template v-else> <template v-if="field.config?.type === 'attachment' && field.newRepr">
{{ field.newRepr }} <img
</template> class="ui image"
</td> alt=""
</tr> :src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)"
</tbody> >
</table> </template>
</div> <template v-else>
<span
v-for="(part, key) in field.diff.filter(p => !p.removed)"
:key="key"
:class="['diff', {added: part.added}]"
>
{{ part.value }}
</span>
</template>
</td>
<td
v-else
:title="field.newRepr"
>
<template v-if="field.config?.type === 'attachment' && field.newRepr">
<img
class="ui image"
alt=""
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)"
>
</template>
<template v-else>
{{ field.newRepr }}
</template>
</td>
</tr>
</tbody>
</table>
</Alert>
</template>
<div <div
v-if="obj.created_by" v-if="obj.created_by"
class="extra content" class="extra content"
> >
<!--TODO: Center actor name horizontally -->
<Spacer :size="8"/>
<actor-link :actor="obj.created_by" /> <actor-link :actor="obj.created_by" />
</div> </div>
<div <template #action
v-if="canDelete || canApprove" v-if="canDelete || canApprove"
class="ui bottom attached buttons"
> >
<button <Button
v-if="canApprove && obj.is_approved !== true" v-if="canApprove && obj.is_approved !== true"
:class="['ui', {loading: isLoading}, 'success', 'basic', 'button']" primary
:isLoading="isLoading"
@click="approve(true)" @click="approve(true)"
> >
{{ t('components.library.EditCard.button.approve') }} {{ t('components.library.EditCard.button.approve') }}
</button> </Button>
<button <Button
v-if="canApprove && obj.is_approved === null" v-if="canApprove && obj.is_approved === null"
:class="['ui', {loading: isLoading}, 'warning', 'basic', 'button']" destructive
:isLoading="isLoading"
@click="approve(false)" @click="approve(false)"
> >
{{ t('components.library.EditCard.button.reject') }} {{ t('components.library.EditCard.button.reject') }}
</button> </Button>
<!--TODO: Make Dangerous Button hand through isLoading prop -->
<dangerous-button <dangerous-button
v-if="canDelete" v-if="canDelete"
:class="['ui', {loading: isLoading}, 'basic danger button']" :class="['ui', {loading: isLoading}, 'basic danger button']"
@ -337,6 +355,21 @@ const approve = async (approved: boolean) => {
</p> </p>
</template> </template>
</dangerous-button> </dangerous-button>
</div> </template>
</div> </Card>
</template> </template>
<style scoped>
table {
width: 100%;
font-size: 12px;
th, td {
padding: 8px;
text-align: left;
}
.image {
width: 100%;
}
}
</style>

View File

@ -2,19 +2,27 @@
import type { EditObject, EditObjectType } from '~/composables/moderation/useEditConfigs' import type { EditObject, EditObjectType } from '~/composables/moderation/useEditConfigs'
import type { BackendError, License, ReviewState } from '~/types' import type { BackendError, License, ReviewState } from '~/types'
import { computed, onMounted, reactive, ref, watchEffect } from 'vue' import { computed, reactive, ref } from 'vue'
import { isEqual, clone } from 'lodash-es' import { isEqual, clone } from 'lodash-es'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
import axios from 'axios' import axios from 'axios'
import $ from 'jquery'
import AttachmentInput from '~/components/common/AttachmentInput.vue' import AttachmentInput from '~/components/common/AttachmentInput.vue'
import useEditConfigs from '~/composables/moderation/useEditConfigs' import useEditConfigs from '~/composables/moderation/useEditConfigs'
import TagsSelector from '~/components/library/TagsSelector.vue' import TagsSelector from '~/components/library/TagsSelector.vue'
import EditList from '~/components/library/EditList.vue' import EditList from '~/components/library/EditList.vue'
import EditCard from '~/components/library/EditCard.vue' import EditCard from '~/components/library/EditCard.vue'
import Layout from '~/components/ui/Layout.vue'
import Button from '~/components/ui/Button.vue'
import Link from '~/components/ui/Link.vue'
import Section from '~/components/ui/Section.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Input from '~/components/ui/Input.vue'
import Textarea from "~/components/ui/Textarea.vue"
import Pills from "~/components/ui/Pills.vue"
import Alert from "~/components/ui/Alert.vue"
interface Props { interface Props {
objectType: EditObjectType objectType: EditObjectType
@ -93,20 +101,6 @@ for (const { id, getValue } of config.value.fields) {
initialValues[id] = clone(values[id]) initialValues[id] = clone(values[id])
} }
const license = ref()
watchEffect(() => {
if (values.license === null) {
$(license.value).dropdown('clear')
return
}
$(license.value).dropdown('set selected', values.license)
})
onMounted(() => {
$('.ui.dropdown').dropdown({ fullTextSearch: true })
})
const submittedMutation = ref() const submittedMutation = ref()
const summary = ref('') const summary = ref('')
@ -145,23 +139,21 @@ const resetField = (fieldId: string) => {
</script> </script>
<template> <template>
<div v-if="submittedMutation"> <Alert green v-if="submittedMutation">
<div class="ui positive message">
<h4 class="header"> <h4 class="header">
{{ t('components.library.EditForm.header.success') }} {{ t('components.library.EditForm.header.success') }}
</h4> </h4>
</div>
<edit-card <edit-card
:obj="submittedMutation" :obj="submittedMutation"
:current-state="currentState" :current-state="currentState"
/> />
<button <Link
class="ui button" solid primary
@click.prevent="submittedMutation = null" @click.prevent="submittedMutation = null"
> >
{{ t('components.library.EditForm.button.new') }} {{ t('components.library.EditForm.button.new') }}
</button> </Link>
</div> </Alert>
<div v-else> <div v-else>
<edit-list <edit-list
:filters="editListFilters" :filters="editListFilters"
@ -170,6 +162,7 @@ const resetField = (fieldId: string) => {
:current-state="currentState" :current-state="currentState"
> >
<div> <div>
<!--TODO: Use Section component with conditional headlines and action buttons-->
<template v-if="showPendingReview"> <template v-if="showPendingReview">
{{ t('components.library.EditForm.header.unreviewed') }} {{ t('components.library.EditForm.header.unreviewed') }}
<button <button
@ -195,15 +188,13 @@ const resetField = (fieldId: string) => {
</empty-state> </empty-state>
</template> </template>
</edit-list> </edit-list>
<form <Layout form
class="ui form"
@submit.prevent="submit()" @submit.prevent="submit()"
> >
<div class="ui hidden divider" /> <div class="ui hidden divider" />
<div <Alert red
v-if="errors.length > 0" v-if="errors.length > 0"
role="alert" role="alert"
class="ui negative message"
> >
<h4 class="header"> <h4 class="header">
{{ t('components.library.EditForm.header.failure') }} {{ t('components.library.EditForm.header.failure') }}
@ -216,13 +207,12 @@ const resetField = (fieldId: string) => {
{{ error }} {{ error }}
</li> </li>
</ul> </ul>
</div> </Alert>
<div <Alert red
v-if="!canEdit" v-if="!canEdit"
class="ui message"
> >
{{ t('components.library.EditForm.message.noPermission') }} {{ t('components.library.EditForm.message.noPermission') }}
</div> </Alert>
<template v-if="values"> <template v-if="values">
<div <div
v-for="fieldConfig in config.fields" v-for="fieldConfig in config.fields"
@ -230,14 +220,15 @@ const resetField = (fieldId: string) => {
class="ui field" class="ui field"
> >
<template v-if="fieldConfig.type === 'text'"> <template v-if="fieldConfig.type === 'text'">
<label :for="fieldConfig.id">{{ fieldConfig.label }}</label> <Spacer :size="64"/>
<input <Input
:id="fieldConfig.id" :id="fieldConfig.id"
v-model="values[fieldConfig.id]" v-model="values[fieldConfig.id]"
:type="fieldConfig.inputType || 'text'" :type="fieldConfig.inputType || 'text'"
:required="fieldConfig.required" :required="fieldConfig.required"
:name="fieldConfig.id" :name="fieldConfig.id"
> :label="fieldConfig.label"
/>
</template> </template>
<template v-else-if="fieldConfig.type === 'license'"> <template v-else-if="fieldConfig.type === 'license'">
<label :for="fieldConfig.id">{{ fieldConfig.label }}</label> <label :for="fieldConfig.id">{{ fieldConfig.label }}</label>
@ -260,14 +251,14 @@ const resetField = (fieldId: string) => {
{{ name }} {{ name }}
</option> </option>
</select> </select>
<button <Button
class="ui tiny basic left floated button" tiny
icon="bi-x"
form="noop" form="noop"
@click.prevent="values[fieldConfig.id] = null" @click.prevent="values[fieldConfig.id] = null"
> >
<i class="x icon" />
{{ t('components.library.EditForm.button.clear') }} {{ t('components.library.EditForm.button.clear') }}
</button> </Button>
</template> </template>
<template v-else-if="fieldConfig.type === 'content'"> <template v-else-if="fieldConfig.type === 'content'">
<label :for="fieldConfig.id">{{ fieldConfig.label }}</label> <label :for="fieldConfig.id">{{ fieldConfig.label }}</label>
@ -277,7 +268,9 @@ const resetField = (fieldId: string) => {
:rows="3" :rows="3"
/> />
</template> </template>
<!-- TODO: Style Attachment Input -->
<template v-else-if="fieldConfig.type === 'attachment'"> <template v-else-if="fieldConfig.type === 'attachment'">
<Spacer />
<attachment-input <attachment-input
:id="fieldConfig.id" :id="fieldConfig.id"
v-model="values[fieldConfig.id]" v-model="values[fieldConfig.id]"
@ -290,53 +283,56 @@ const resetField = (fieldId: string) => {
</attachment-input> </attachment-input>
</template> </template>
<template v-else-if="fieldConfig.type === 'tags'"> <template v-else-if="fieldConfig.type === 'tags'">
<label :for="fieldConfig.id">{{ fieldConfig.label }}</label> <Spacer/>
<tags-selector <Pills
:id="fieldConfig.id" :id="fieldConfig.id"
:label="fieldConfig.label"
ref="tags" ref="tags"
v-model="values[fieldConfig.id]" v-model="values[fieldConfig.id]"
required="fieldConfig.required" required="fieldConfig.required"
/> >
<button <Button
class="ui tiny basic left floated button" class="ui tiny basic left floated button"
icon="bi-x"
form="noop" form="noop"
@click.prevent="values[fieldConfig.id] = []" @click.prevent="values[fieldConfig.id] = []"
> >
<i class="x icon" />
{{ t('components.library.EditForm.button.clear') }} {{ t('components.library.EditForm.button.clear') }}
</button> </Button>
</Pills>
</template> </template>
<div v-if="fieldValuesChanged(fieldConfig.id)"> <div v-if="fieldValuesChanged(fieldConfig.id)">
<button <Button
class="ui tiny basic right floated reset button" tiny
alignSelf="end"
icon="bi-arrow-counterclockwise"
form="noop" form="noop"
@click.prevent="resetField(fieldConfig.id)" @click.prevent="resetField(fieldConfig.id)"
> >
<i class="undo icon" />
{{ t('components.library.EditForm.button.reset') }} {{ t('components.library.EditForm.button.reset') }}
</button> </Button>
</div> </div>
</div> </div>
</template> </template>
<div class="field"> <Spacer/>
<label for="summary">{{ t('components.library.EditForm.label.summary') }}</label> <Textarea
<textarea id="change-summary"
id="change-summary" v-model="summary"
v-model="summary" name="change-summary"
name="change-summary" rows="3"
rows="3" :label="t('components.library.EditForm.label.summary')"
:placeholder="labels.summaryPlaceholder" :placeholder="labels.summaryPlaceholder"
/> />
</div> <Button
<router-link
v-if="objectType === 'track'" v-if="objectType === 'track'"
class="ui left floated button"
:to="{name: 'library.tracks.detail', params: {id: object.id }}" :to="{name: 'library.tracks.detail', params: {id: object.id }}"
> >
{{ t('components.library.EditForm.button.cancel') }} {{ t('components.library.EditForm.button.cancel') }}
</router-link> </Button>
<button <Button
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']" :class="['ui', 'right', 'floated', 'success', 'button']"
:isLoading="isLoading"
primary
type="submit" type="submit"
:disabled="isLoading || !mutationPayload" :disabled="isLoading || !mutationPayload"
> >
@ -346,7 +342,7 @@ const resetField = (fieldId: string) => {
<span v-else> <span v-else>
{{ t('components.library.EditForm.button.suggest') }} {{ t('components.library.EditForm.button.suggest') }}
</span> </span>
</button> </Button>
</form> </Layout>
</div> </div>
</template> </template>

View File

@ -5,6 +5,9 @@ import { ref, watchEffect } from 'vue'
import axios from 'axios' import axios from 'axios'
import EditCard from '~/components/library/EditCard.vue' import EditCard from '~/components/library/EditCard.vue'
import Layout from '~/components/ui/Layout.vue'
import Button from '~/components/ui/Button.vue'
import Loader from '~/components/ui/Loader.vue'
interface Props { interface Props {
url: string url: string
@ -45,37 +48,24 @@ watchEffect(() => fetchData())
</script> </script>
<template> <template>
<div class="wrapper"> <h3>
<h3 class="ui header"> <slot />
<slot /> </h3>
</h3> <slot
<slot v-if="!isLoading && objects.length === 0"
v-if="!isLoading && objects.length === 0" name="empty-state"
name="empty-state" />
/> <Layout grid>
<button <Button
v-if="nextPage || previousPage" v-if="nextPage || previousPage"
:disabled="!previousPage" :disabled="!previousPage"
:class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']" round
alignSelf="center"
icon="bi-chevron-left"
@click="fetchData(previousPage)" @click="fetchData(previousPage)"
> >
<i :class="['ui', 'angle left', 'icon']" /> </Button>
</button> <Loader v-if="isLoading" />
<button
v-if="nextPage || previousPage"
:disabled="!nextPage"
:class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"
@click="fetchData(nextPage)"
>
<i :class="['ui', 'angle right', 'icon']" />
</button>
<div class="ui hidden divider" />
<div
v-if="isLoading"
class="ui inverted active dimmer"
>
<div class="ui loader" />
</div>
<edit-card <edit-card
v-for="obj in objects" v-for="obj in objects"
:key="obj.uuid" :key="obj.uuid"
@ -84,5 +74,14 @@ watchEffect(() => fetchData())
@updated="fetchData(url)" @updated="fetchData(url)"
@deleted="fetchData(url)" @deleted="fetchData(url)"
/> />
</div> <Button
v-if="nextPage || previousPage"
:disabled="!nextPage"
alignSelf="center"
round
icon="bi-chevron-right"
@click="fetchData(nextPage)"
>
</Button>
</Layout>
</template> </template>

View File

@ -9,6 +9,11 @@ import { useStore } from '~/store'
import axios from 'axios' import axios from 'axios'
import RadioButton from '~/components/radios/Button.vue' import RadioButton from '~/components/radios/Button.vue'
import Card from '~/components/ui/Card.vue'
import OptionsButton from '~/components/ui/button/Options.vue'
import Popover from '~/components/ui/Popover.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
import Spacer from '~/components/ui/Spacer.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
import useReport from '~/composables/moderation/useReport' import useReport from '~/composables/moderation/useReport'
@ -142,58 +147,63 @@ watch(showScan, (shouldShow) => {
stopFetching() stopFetching()
}) })
const isOpen = ref(false)
</script> </script>
<template> <template>
<div class="ui card"> <Card
:title="library.name"
>
<div class="content"> <div class="content">
<h4 class="header"> <h4 class="header">
<router-link :to="{name: 'library.detail', params: {id: library.uuid}}"> <router-link :to="{name: 'library.detail', params: {id: library.uuid}}">
{{ library.name }} {{ library.name }}
</router-link> </router-link>
<div
v-dropdown <Popover v-model:open="isOpen">
class="ui right floated dropdown" <template #default="{ toggleOpen }">
> <OptionsButton
<i class="ellipsis vertical large icon nomargin" /> @click="toggleOpen"
<div class="menu"> />
<button </template>
<template #items>
<PopoverItem
v-for="obj in getReportableObjects({library, account: library.actor})" v-for="obj in getReportableObjects({library, account: library.actor})"
:key="obj.target.type + obj.target.id" :key="obj.target.type + obj.target.id"
class="item basic"
@click.stop.prevent="report(obj)" @click.stop.prevent="report(obj)"
> >
<i class="share icon" /> {{ obj.label }} <i class="bi bi-share" /> {{ obj.label }}
</button> </PopoverItem>
</div> </template>
</div> </Popover>
<span <span
v-if="library.privacy_level === 'me'" v-if="library.privacy_level === 'me'"
class="right floated" class="right floated"
:data-tooltip="labels.tooltips.me" :data-tooltip="labels.tooltips.me"
> >
<i class="small lock icon" /> <i class="bi bi-lock" />
</span> </span>
<span <span
v-else-if="library.privacy_level === 'everyone'" v-else-if="library.privacy_level === 'everyone'"
class="right floated" class="right floated"
:data-tooltip="labels.tooltips.everyone" :data-tooltip="labels.tooltips.everyone"
> >
<i class="small globe icon" /> <i class="bi bi-globe" />
</span> </span>
</h4> </h4>
<div class="meta"> <div class="meta">
<span> <span>
<i class="small outline clock icon" /> <i class="bi bi-clock" />
<human-date :date="library.creation_date" /> <human-date :date="library.creation_date" />
</span> </span>
</div> </div>
<div class="description"> <div class="description">
{{ library.description }} {{ library.description }}
<div class="ui hidden divider" />
</div> </div>
<Spacer :size="8" />
<div class="meta"> <div class="meta">
<i class="music icon" /> <i class="bi bi-music-note" />
{{ t('views.content.remote.Card.meta.tracks', library.uploads_count) }} {{ t('views.content.remote.Card.meta.tracks', library.uploads_count) }}
</div> </div>
<div <div
@ -201,7 +211,7 @@ watch(showScan, (shouldShow) => {
class="meta" class="meta"
> >
<template v-if="latestScan.status === 'pending'"> <template v-if="latestScan.status === 'pending'">
<i class="hourglass icon" /> <i class="bi bi-hourglass" />
{{ t('views.content.remote.Card.label.scanPending') }} {{ t('views.content.remote.Card.label.scanPending') }}
</template> </template>
<template v-if="latestScan.status === 'scanning'"> <template v-if="latestScan.status === 'scanning'">
@ -209,15 +219,15 @@ watch(showScan, (shouldShow) => {
{{ t('views.content.remote.Card.label.scanProgress', {progress: scanProgress}) }} {{ t('views.content.remote.Card.label.scanProgress', {progress: scanProgress}) }}
</template> </template>
<template v-else-if="latestScan.status === 'errored'"> <template v-else-if="latestScan.status === 'errored'">
<i class="dangerdownload icon" /> <i class="bi bi-exclamation-triangle" />
{{ t('views.content.remote.Card.label.scanFailure') }} {{ t('views.content.remote.Card.label.scanFailure') }}
</template> </template>
<template v-else-if="latestScan.status === 'finished' && latestScan.errored_files === 0"> <template v-else-if="latestScan.status === 'finished' && latestScan.errored_files === 0">
<i class="success download icon" /> <i class="bi bi-check-circle" />
{{ t('views.content.remote.Card.label.scanSuccess') }} {{ t('views.content.remote.Card.label.scanSuccess') }}
</template> </template>
<template v-else-if="latestScan.status === 'finished' && latestScan.errored_files > 0"> <template v-else-if="latestScan.status === 'finished' && latestScan.errored_files > 0">
<i class="warning download icon" /> <i class="bi bi-exclamation-circle" />
{{ t('views.content.remote.Card.label.scanPartialSuccess') }} {{ t('views.content.remote.Card.label.scanPartialSuccess') }}
</template> </template>
<a <a
@ -228,11 +238,11 @@ watch(showScan, (shouldShow) => {
{{ t('views.content.remote.Card.link.scanDetails') }} {{ t('views.content.remote.Card.link.scanDetails') }}
<i <i
v-if="showScan" v-if="showScan"
class="angle down icon" class="bi bi-chevron-down"
/> />
<i <i
v-else v-else
class="angle right icon" class="bi bi-chevron-right"
/> />
</a> </a>
<div v-if="showScan"> <div v-if="showScan">
@ -251,16 +261,19 @@ watch(showScan, (shouldShow) => {
class="right floated link" class="right floated link"
@click.prevent="launchScan" @click.prevent="launchScan"
> >
{{ t('views.content.remote.Card.link.scan') }}<i class="paper plane icon" /> {{ t('views.content.remote.Card.link.scan') }}
<i class="bi bi-send" />
</a> </a>
</div> </div>
</div> </div>
<Spacer :size="8" />
<div class="extra content"> <div class="extra content">
<actor-link <actor-link
style="color: var(--link-color)" style="color: var(--link-color)"
:actor="library.actor" :actor="library.actor"
/> />
</div> </div>
<Spacer :size="8" />
<div <div
v-if="displayCopyFid" v-if="displayCopyFid"
class="extra content" class="extra content"
@ -334,5 +347,5 @@ watch(showScan, (shouldShow) => {
</template> </template>
</template> </template>
</div> </div>
</div> </Card>
</template> </template>