fix(ui): artist and album detail and edit pages
This commit is contained in:
parent
0bdb026cf1
commit
a84868e1df
|
@ -2,6 +2,8 @@
|
|||
import type { ArtistCredit } from '~/types'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import Link from '~/components/ui/Link.vue'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
interface Props {
|
||||
|
@ -26,7 +28,7 @@ const getRoute = (ac: ArtistCredit) => {
|
|||
v-for="ac in props.artistCredit"
|
||||
:key="ac.artist.id"
|
||||
>
|
||||
<router-link
|
||||
<Link solid secondary round min-content
|
||||
:to="getRoute(ac)"
|
||||
>
|
||||
<img
|
||||
|
@ -37,10 +39,10 @@ const getRoute = (ac: ArtistCredit) => {
|
|||
>
|
||||
<i
|
||||
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 }}
|
||||
</router-link>
|
||||
</Link>
|
||||
<span>{{ ac.joinphrase }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -10,10 +10,11 @@ import { useStore } from '~/store'
|
|||
import axios from 'axios'
|
||||
|
||||
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 Input from '~/components/ui/Input.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
|
||||
|
@ -175,18 +176,13 @@ const updatePage = (page: number) => {
|
|||
<div
|
||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
|
||||
>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<Loader v-if="isLoading" />
|
||||
<div class="track-table row">
|
||||
<div
|
||||
v-if="showPosition"
|
||||
class="actions left floated column"
|
||||
>
|
||||
<i class="hashtag icon" />
|
||||
<i class="bi bi-hash" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
|
@ -220,7 +216,7 @@ const updatePage = (page: number) => {
|
|||
class="meta right floated column"
|
||||
>
|
||||
<i
|
||||
class="clock outline icon"
|
||||
class="bi bi-clock"
|
||||
style="padding: 0.5rem"
|
||||
/>
|
||||
</div>
|
||||
|
@ -263,12 +259,7 @@ const updatePage = (page: number) => {
|
|||
<div
|
||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
|
||||
>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui inverted active dimmer"
|
||||
>
|
||||
<div class="ui loader" />
|
||||
</div>
|
||||
<Loader v-if="isLoading" />
|
||||
|
||||
<!-- For each item, build a row -->
|
||||
|
||||
|
@ -289,7 +280,7 @@ const updatePage = (page: number) => {
|
|||
v-if="tracks && paginateResults && totalTracks > paginateBy"
|
||||
class="ui center aligned basic segment tablet-and-below"
|
||||
>
|
||||
<pagination
|
||||
<Pagination
|
||||
v-if="paginateResults && totalTracks > paginateBy"
|
||||
:paginate-by="paginateBy"
|
||||
:total="totalTracks"
|
||||
|
|
|
@ -27,3 +27,16 @@ const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${actorColor.val
|
|||
class="ui avatar circular label"
|
||||
>{{ actor.preferred_username?.[0] || "" }}</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.ui.circular.avatar {
|
||||
float: left;
|
||||
margin-right: 8px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&.label {
|
||||
align-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -5,6 +5,8 @@ import { toRefs } from '@vueuse/core'
|
|||
import { computed } from 'vue'
|
||||
import { truncate } from '~/utils/filters'
|
||||
|
||||
import Link from '~/components/ui/Link.vue'
|
||||
|
||||
interface Props {
|
||||
actor: Actor
|
||||
avatar?: boolean
|
||||
|
@ -50,15 +52,18 @@ const url = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<router-link
|
||||
<Link
|
||||
:to="url"
|
||||
:title="actor.full_username"
|
||||
class="username"
|
||||
solid
|
||||
secondary
|
||||
round
|
||||
>
|
||||
<actor-avatar
|
||||
v-if="avatar"
|
||||
:actor="actor"
|
||||
/>
|
||||
<span> </span>
|
||||
<slot>{{ repr }}</slot>
|
||||
</router-link>
|
||||
</Link>
|
||||
</template>
|
||||
|
|
|
@ -8,6 +8,9 @@ import { useStore } from '~/store'
|
|||
import { useI18n } from 'vue-i18n'
|
||||
import useFormData from '~/composables/useFormData'
|
||||
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Alert from '~/components/ui/Alert.vue'
|
||||
|
||||
interface Events {
|
||||
(e: 'update:modelValue', value: string | null): void
|
||||
(e: 'delete'): void
|
||||
|
@ -104,10 +107,9 @@ const getAttachmentUrl = (uuid: string) => {
|
|||
|
||||
<template>
|
||||
<div class="ui form">
|
||||
<div
|
||||
<Alert red
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.common.AttachmentInput.header.failure') }}
|
||||
|
@ -120,7 +122,7 @@ const getAttachmentUrl = (uuid: string) => {
|
|||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Alert>
|
||||
<div class="ui field">
|
||||
<span id="avatarLabel">
|
||||
<slot />
|
||||
|
@ -158,19 +160,19 @@ const getAttachmentUrl = (uuid: string) => {
|
|||
type="file"
|
||||
accept="image/png,image/jpeg"
|
||||
@change="submit"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<div class="ui very small hidden divider" />
|
||||
<p>
|
||||
{{ t('components.common.AttachmentInput.help.upload') }}
|
||||
</p>
|
||||
<button
|
||||
<Button
|
||||
v-if="value"
|
||||
class="ui basic tiny button"
|
||||
@click.stop.prevent="remove(value as string)"
|
||||
>
|
||||
{{ t('components.common.AttachmentInput.button.remove') }}
|
||||
</button>
|
||||
</Button>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui active inverted dimmer"
|
||||
|
|
|
@ -86,57 +86,55 @@ onMounted(async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
@click.prevent="isPreviewing = false"
|
||||
:aria-pressed="!isPreviewing"
|
||||
title="write"
|
||||
>
|
||||
{{ t('components.common.ContentForm.button.write') }}
|
||||
</Button>
|
||||
<Button
|
||||
@click.prevent="isPreviewing = true"
|
||||
:aria-pressed="!isPreviewing"
|
||||
title="preview"
|
||||
>
|
||||
{{ t('components.common.ContentForm.button.preview') }}
|
||||
</Button>
|
||||
<template v-if="isPreviewing">
|
||||
<div
|
||||
v-if="isLoadingPreview"
|
||||
class="ui placeholder"
|
||||
>
|
||||
<div class="paragraph">
|
||||
<div class="line" />
|
||||
<div class="line" />
|
||||
<div class="line" />
|
||||
<div class="line" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-else-if="!preview">
|
||||
{{ t('components.common.ContentForm.empty.noContent') }}
|
||||
</p>
|
||||
<sanitized-html
|
||||
v-else
|
||||
:html="preview"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="ui transparent input">
|
||||
<Textarea
|
||||
ref="textarea"
|
||||
v-model="value"
|
||||
:required="required"
|
||||
:placeholder="labels.placeholder"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<span
|
||||
v-if="charLimit"
|
||||
:class="['right', 'floated', {'ui danger text': remainingChars < 0}]"
|
||||
>
|
||||
{{ remainingChars }}
|
||||
</span>
|
||||
<p>
|
||||
{{ t('components.common.ContentForm.help.markdown') }}
|
||||
</p>
|
||||
<Button
|
||||
@click.prevent="isPreviewing = false"
|
||||
:aria-pressed="!isPreviewing"
|
||||
title="write"
|
||||
>
|
||||
{{ t('components.common.ContentForm.button.write') }}
|
||||
</Button>
|
||||
<Button
|
||||
@click.prevent="isPreviewing = true"
|
||||
:aria-pressed="!isPreviewing"
|
||||
title="preview"
|
||||
>
|
||||
{{ t('components.common.ContentForm.button.preview') }}
|
||||
</Button>
|
||||
<template v-if="isPreviewing">
|
||||
<div
|
||||
v-if="isLoadingPreview"
|
||||
class="ui placeholder"
|
||||
>
|
||||
<div class="paragraph">
|
||||
<div class="line" />
|
||||
<div class="line" />
|
||||
<div class="line" />
|
||||
<div class="line" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-else-if="!preview">
|
||||
{{ t('components.common.ContentForm.empty.noContent') }}
|
||||
</p>
|
||||
<sanitized-html
|
||||
v-else
|
||||
:html="preview"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Textarea
|
||||
ref="textarea"
|
||||
v-model="value"
|
||||
:required="required"
|
||||
:placeholder="labels.placeholder"
|
||||
/>
|
||||
</template>
|
||||
<span
|
||||
v-if="charLimit"
|
||||
:class="['right', 'floated', {'ui danger text': remainingChars < 0}]"
|
||||
>
|
||||
{{ remainingChars }}
|
||||
</span>
|
||||
<p>
|
||||
{{ t('components.common.ContentForm.help.markdown') }}
|
||||
</p>
|
||||
</template>
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
import { toRefs, useClipboard } from '@vueuse/core'
|
||||
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 {
|
||||
value: string
|
||||
buttonClasses?: string
|
||||
|
@ -20,27 +24,53 @@ const { copy, isSupported: canCopy, copied } = useClipboard({ source: value, cop
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui fluid action input component-copy-input">
|
||||
<p
|
||||
v-if="copied"
|
||||
class="message"
|
||||
>
|
||||
{{ t('components.common.CopyInput.message.success') }}
|
||||
</p>
|
||||
<input
|
||||
<Input
|
||||
:id="id"
|
||||
:value="value"
|
||||
readonly
|
||||
:name="id"
|
||||
type="text"
|
||||
readonly
|
||||
>
|
||||
<button
|
||||
:class="['ui', buttonClasses, 'right', 'labeled', 'icon', 'button']"
|
||||
:disabled="!canCopy || undefined"
|
||||
@click="copy()"
|
||||
>
|
||||
<i class="copy icon" />
|
||||
{{ t('components.common.CopyInput.button.copy') }}
|
||||
</button>
|
||||
</div>
|
||||
<template #input-right>
|
||||
<Button
|
||||
:class="['ui', buttonClasses, 'input-right']"
|
||||
min-content
|
||||
secondary
|
||||
:disabled="!canCopy || undefined"
|
||||
@click="copy()"
|
||||
>
|
||||
<i class="bi bi-copy" />
|
||||
{{ t('components.common.CopyInput.button.copy') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Input>
|
||||
</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>
|
|
@ -1,9 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
|
||||
interface Events {
|
||||
(e: 'confirm'): void
|
||||
}
|
||||
|
@ -33,16 +34,17 @@ const confirm = () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="funkwhale dangerous-button"
|
||||
<Button
|
||||
destructive
|
||||
:class="{ 'is-disabled': disabled }"
|
||||
:disabled="disabled"
|
||||
@click.prevent.stop="showModal = true"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<semantic-modal
|
||||
<SemanticModal
|
||||
v-model:show="showModal"
|
||||
:title="t('components.common.DangerousButton.header.confirm')"
|
||||
class="small"
|
||||
>
|
||||
<h4 class="header">
|
||||
|
@ -55,10 +57,10 @@ const confirm = () => {
|
|||
<slot name="modal-content" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<template #actions>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outline"
|
||||
secondary
|
||||
outline
|
||||
@click="showModal = false"
|
||||
>
|
||||
{{ t('components.common.DangerousButton.button.cancel') }}
|
||||
|
@ -71,7 +73,7 @@ const confirm = () => {
|
|||
{{ t('components.common.DangerousButton.button.confirm') }}
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</semantic-modal>
|
||||
</button>
|
||||
</template>
|
||||
</SemanticModal>
|
||||
</Button>
|
||||
</template>
|
||||
|
|
|
@ -25,8 +25,15 @@ const realDate = useTimeAgo(date)
|
|||
>
|
||||
<i
|
||||
v-if="props.icon"
|
||||
class="outline clock icon"
|
||||
class="bi bi-clock"
|
||||
/>
|
||||
{{ realDate }}
|
||||
</time>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
i {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
|
@ -14,6 +14,9 @@ import ArtistCreditLabel from '~/components/audio/ArtistCreditLabel.vue'
|
|||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
import TagsList from '~/components/tags/List.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 useLogger from '~/composables/useLogger'
|
||||
|
@ -136,19 +139,15 @@ const remove = async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div
|
||||
<Layout stack main>
|
||||
<Loader
|
||||
v-if="isLoading"
|
||||
v-title="labels.title"
|
||||
class="ui vertical segment"
|
||||
>
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
|
||||
</div>
|
||||
/>
|
||||
<template v-if="object">
|
||||
<section class="ui vertical stripe segment channel-serie">
|
||||
<div class="ui stackable grid container">
|
||||
<div class="ui seven wide column">
|
||||
/front/src/components/library/AlbumBase.vue
|
||||
<section class="segment channel-serie">
|
||||
<Layout flex gap-64 style="align-content: stretch;">
|
||||
<div style="flex: 2;">
|
||||
<div
|
||||
v-if="isSerie"
|
||||
class="padded basic segment"
|
||||
|
@ -204,8 +203,7 @@ const remove = async () => {
|
|||
{{ t('components.library.AlbumBase.meta.tracks', totalTracks) }}
|
||||
</span>
|
||||
</template>
|
||||
<div class="ui small hidden divider" />
|
||||
<play-button
|
||||
<PlayButton
|
||||
class="vibrant"
|
||||
:tracks="object.tracks"
|
||||
:is-playable="object.is_playable"
|
||||
|
@ -223,7 +221,6 @@ const remove = async () => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui small hidden divider" />
|
||||
<header>
|
||||
<h2
|
||||
class="ui header"
|
||||
|
@ -266,6 +263,7 @@ const remove = async () => {
|
|||
:artist-credit="artistCredit"
|
||||
/>
|
||||
</header>
|
||||
<Spacer :size="16"/>
|
||||
<div
|
||||
v-if="object.release_date || (totalTracks > 0)"
|
||||
class="ui small hidden divider"
|
||||
|
@ -342,7 +340,7 @@ const remove = async () => {
|
|||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
<div class="nine wide column">
|
||||
<div style="flex 1;">
|
||||
<router-view
|
||||
v-if="object"
|
||||
:key="route.fullPath"
|
||||
|
@ -356,8 +354,8 @@ const remove = async () => {
|
|||
@libraries-loaded="libraries = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</section>
|
||||
</template>
|
||||
</main>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
|
@ -56,7 +56,6 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
|||
<template>
|
||||
<div
|
||||
v-if="!isLoadingTracks"
|
||||
class="ui vertical segment"
|
||||
>
|
||||
<h2 class="ui header">
|
||||
<span v-if="isSerie">
|
||||
|
@ -82,7 +81,7 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
|||
>
|
||||
<template v-if="tracks.length > 0">
|
||||
<div class="ui hidden divider" />
|
||||
<play-button
|
||||
<PlayButton
|
||||
class="right floated mini inverted vibrant"
|
||||
:tracks="discs[index]"
|
||||
/>
|
||||
|
|
|
@ -15,6 +15,7 @@ import Button from '~/components/ui/Button.vue'
|
|||
import Popover from '~/components/ui/Popover.vue'
|
||||
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
|
||||
import DangerousButton from '~/components/common/DangerousButton.vue'
|
||||
import OptionsButton from "~/components/ui/button/Options.vue"
|
||||
|
||||
interface Events {
|
||||
(e: 'remove'): void
|
||||
|
@ -55,7 +56,6 @@ const open = ref(false)
|
|||
|
||||
<template>
|
||||
<span>
|
||||
|
||||
<semantic-modal
|
||||
v-if="isEmbedable"
|
||||
v-model:show="showEmbedModal"
|
||||
|
@ -84,11 +84,7 @@ const open = ref(false)
|
|||
</semantic-modal>
|
||||
<Popover v-model:open="open">
|
||||
<template #default="{ toggleOpen }">
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
round
|
||||
icon="bi-three-dots-vertical"
|
||||
<OptionsButton
|
||||
:title="labels.more"
|
||||
@click="toggleOpen()"
|
||||
/>
|
||||
|
|
|
@ -21,27 +21,23 @@ const canEdit = store.state.auth.availablePermissions.library
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<section class="ui vertical stripe segment">
|
||||
<div class="ui text container">
|
||||
<h2>
|
||||
<span v-if="canEdit">
|
||||
{{ t('components.library.AlbumEdit.header.edit') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('components.library.AlbumEdit.header.suggest') }}
|
||||
</span>
|
||||
</h2>
|
||||
<div
|
||||
v-if="!object.is_local"
|
||||
class="ui message"
|
||||
>
|
||||
{{ t('components.library.AlbumEdit.message.remote') }}
|
||||
</div>
|
||||
<edit-form
|
||||
v-else
|
||||
:object-type="objectType"
|
||||
:object="object"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<h2>
|
||||
<span v-if="canEdit">
|
||||
{{ t('components.library.AlbumEdit.header.edit') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('components.library.AlbumEdit.header.suggest') }}
|
||||
</span>
|
||||
</h2>
|
||||
<div
|
||||
v-if="!object.is_local"
|
||||
class="ui message"
|
||||
>
|
||||
{{ t('components.library.AlbumEdit.message.remote') }}
|
||||
</div>
|
||||
<edit-form
|
||||
v-else
|
||||
:object-type="objectType"
|
||||
:object="object"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -13,7 +13,9 @@ import useLogger from '~/composables/useLogger'
|
|||
|
||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||
import SemanticModal from '~/components/semantic/Modal.vue'
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import OptionsButton from '~/components/ui/button/Options.vue'
|
||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
import RadioButton from '~/components/radios/Button.vue'
|
||||
import Popover from '~/components/ui/Popover.vue'
|
||||
|
@ -98,16 +100,11 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
|
||||
<template>
|
||||
<main v-title="labels.title">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="ui vertical segment"
|
||||
>
|
||||
<div :class="['ui', 'centered', 'active', 'inline', 'loader']" />
|
||||
</div>
|
||||
<Loader v-if="isLoading" />
|
||||
<template v-if="object && !isLoading">
|
||||
<section
|
||||
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"
|
||||
>
|
||||
<div class="segment-content">
|
||||
|
@ -167,104 +164,99 @@ watch(() => props.id, fetchData, { immediate: true })
|
|||
</Button>
|
||||
</div>
|
||||
</semantic-modal>
|
||||
<div class="ui buttons">
|
||||
<Popover>
|
||||
<template #default="{ toggleOpen }">
|
||||
<Button
|
||||
@click="toggleOpen"
|
||||
icon="caret-down-fill"
|
||||
>
|
||||
{{ t('components.library.ArtistBase.button.more') }}
|
||||
</Button>
|
||||
</template>
|
||||
<Popover>
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton
|
||||
@click="toggleOpen"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #items>
|
||||
<PopoverItem
|
||||
v-if="domain != store.getters['instance/domain']"
|
||||
:href="object.fid"
|
||||
target="_blank"
|
||||
class="funkwhale item"
|
||||
>
|
||||
<i class="external icon" />
|
||||
{{ t('components.library.ArtistBase.link.domain', {domain: domain}) }}
|
||||
</PopoverItem>
|
||||
<template #items>
|
||||
<PopoverItem
|
||||
v-if="domain != store.getters['instance/domain']"
|
||||
:href="object.fid"
|
||||
target="_blank"
|
||||
class="funkwhale item"
|
||||
>
|
||||
<i class="bi bi-box-arrow-up-right" />
|
||||
{{ t('components.library.ArtistBase.link.domain', {domain: domain}) }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="publicLibraries.length > 0"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
>
|
||||
<i class="code icon" />
|
||||
{{ t('components.library.ArtistBase.button.embed') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-if="publicLibraries.length > 0"
|
||||
@click="showEmbedModal = !showEmbedModal"
|
||||
>
|
||||
<i class="bi bi-code-square" />
|
||||
{{ t('components.library.ArtistBase.button.embed') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
:href="wikipediaUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<i class="wikipedia w icon" />
|
||||
{{ t('components.library.ArtistBase.link.wikipedia') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
:href="wikipediaUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<i class="bi bi-wikipedia" />
|
||||
{{ t('components.library.ArtistBase.link.wikipedia') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="musicbrainzUrl"
|
||||
:href="musicbrainzUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<i class="external icon" />
|
||||
{{ t('components.library.ArtistBase.link.musicbrainz') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-if="musicbrainzUrl"
|
||||
:href="musicbrainzUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<i class="bi bi-box-arrow-up-right" />
|
||||
{{ t('components.library.ArtistBase.link.musicbrainz') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
:href="discogsUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<i class="external icon" />
|
||||
{{ t('components.library.ArtistBase.link.discogs') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
:href="discogsUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<i class="bi bi-box-arrow-up-right" />
|
||||
{{ t('components.library.ArtistBase.link.discogs') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="object.is_local"
|
||||
:to="{name: 'library.artists.edit', params: {id: object.id }}"
|
||||
>
|
||||
<i class="edit icon" />
|
||||
{{ t('components.library.ArtistBase.button.edit') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-if="object.is_local"
|
||||
:to="{name: 'library.artists.edit', params: {id: object.id }}"
|
||||
>
|
||||
<i class="bi bi-pencil-fill" />
|
||||
{{ t('components.library.ArtistBase.button.edit') }}
|
||||
</PopoverItem>
|
||||
|
||||
<hr>
|
||||
<hr v-for="obj in getReportableObjects({artist: object})">
|
||||
|
||||
<PopoverItem
|
||||
v-for="obj in getReportableObjects({artist: object})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
@click="report(obj)"
|
||||
>
|
||||
<i class="share icon" /> {{ obj.label }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-for="obj in getReportableObjects({artist: object})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
@click="report(obj)"
|
||||
>
|
||||
<i class="bi bi-share-fill" /> {{ obj.label }}
|
||||
</PopoverItem>
|
||||
|
||||
<hr>
|
||||
<hr>
|
||||
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.availablePermissions['library']"
|
||||
:to="{name: 'manage.library.artists.detail', params: {id: object.id}}"
|
||||
>
|
||||
<i class="wrench icon" />
|
||||
{{ t('components.library.ArtistBase.link.moderation') }}
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.availablePermissions['library']"
|
||||
:to="{name: 'manage.library.artists.detail', params: {id: object.id}}"
|
||||
>
|
||||
<i class="bi bi-wrench" />
|
||||
{{ t('components.library.ArtistBase.link.moderation') }}
|
||||
</PopoverItem>
|
||||
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.profile && store.state.auth.profile.is_superuser"
|
||||
:href="store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i class="wrench icon" />
|
||||
{{ t('components.library.ArtistBase.link.django') }}
|
||||
</PopoverItem>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
<PopoverItem
|
||||
v-if="store.state.auth.profile && store.state.auth.profile.is_superuser"
|
||||
:href="store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i class="bi bi-wrench" />
|
||||
{{ t('components.library.ArtistBase.link.django') }}
|
||||
</PopoverItem>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -3,10 +3,13 @@ import type { EditObjectType } from '~/composables/moderation/useEditConfigs'
|
|||
import type { Artist, Library } from '~/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
import { useStore } from '~/store'
|
||||
|
||||
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 {
|
||||
objectType: EditObjectType
|
||||
|
@ -23,27 +26,23 @@ const canEdit = store.state.auth.availablePermissions.library
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<section class="ui vertical stripe segment">
|
||||
<div class="ui text container">
|
||||
<h2>
|
||||
<span v-if="canEdit">
|
||||
{{ t('components.library.ArtistEdit.header.edit') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('components.library.ArtistEdit.header.suggest') }}
|
||||
</span>
|
||||
</h2>
|
||||
<div
|
||||
v-if="!object.is_local"
|
||||
class="ui message"
|
||||
>
|
||||
{{ t('components.library.ArtistEdit.message.remote') }}
|
||||
</div>
|
||||
<edit-form
|
||||
v-else
|
||||
:object-type="objectType"
|
||||
:object="object"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<Layout stack>
|
||||
<Spacer />
|
||||
<Section no-items alignLeft
|
||||
:h2="canEdit
|
||||
? t('components.library.ArtistEdit.header.edit')
|
||||
: t('components.library.ArtistEdit.header.suggest')
|
||||
"
|
||||
/>
|
||||
<Alert yellow
|
||||
v-if="!object.is_local"
|
||||
>
|
||||
{{ t('components.library.ArtistEdit.message.remote') }}
|
||||
</Alert>
|
||||
<edit-form
|
||||
v-else
|
||||
:object-type="objectType"
|
||||
:object="object"
|
||||
/>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
|
@ -8,13 +8,20 @@ import { useRouter } from 'vue-router'
|
|||
import { computed, ref } from 'vue'
|
||||
import { useStore } from '~/store'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { color, setColors } from '~/composables/color'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import useEditConfigs from '~/composables/moderation/useEditConfigs'
|
||||
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 {
|
||||
(e: 'approved', isApproved: boolean): void
|
||||
(e: 'deleted'): void
|
||||
|
@ -155,7 +162,8 @@ const approve = async (approved: boolean) => {
|
|||
</script>
|
||||
|
||||
<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">
|
||||
<h4 class="header">
|
||||
<router-link :to="detailUrl">
|
||||
|
@ -166,34 +174,16 @@ const approve = async (approved: boolean) => {
|
|||
<router-link
|
||||
v-if="obj.target && obj.target.type === 'track'"
|
||||
: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}) }}
|
||||
</router-link>
|
||||
<br>
|
||||
<human-date
|
||||
:date="obj.creation_date"
|
||||
: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
|
||||
|
@ -202,117 +192,145 @@ const approve = async (approved: boolean) => {
|
|||
>
|
||||
{{ obj.summary }}
|
||||
</div>
|
||||
<div class="content">
|
||||
<table
|
||||
v-if="obj.type === 'update'"
|
||||
class="ui celled very basic fixed stacking table"
|
||||
<template #alert>
|
||||
<!--TODO: Pass Alert colors through to card.vue -->
|
||||
<Alert
|
||||
:green="obj.is_approved && obj.is_applied"
|
||||
:red="obj.is_approved === false"
|
||||
:yellow="obj.is_approved === null"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.field') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.oldValue') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.newValue') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="field in updatedFields"
|
||||
:key="field.id"
|
||||
>
|
||||
<td>{{ field.id }}</td>
|
||||
|
||||
<td v-if="field.diff">
|
||||
<template v-if="field.config?.type === 'attachment' && field.oldRepr">
|
||||
<img
|
||||
class="ui image"
|
||||
alt=""
|
||||
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.oldRepr}/proxy?next=medium_square_crop`)"
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
v-for="(part, key) in field.diff.filter(p => !p.added)"
|
||||
:key="key"
|
||||
:class="['diff', {removed: part.removed}]"
|
||||
>
|
||||
{{ part.value }}
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
<td v-else>
|
||||
{{ t('components.library.EditCard.table.update.notApplicable') }}
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-if="field.diff"
|
||||
:title="field.newRepr"
|
||||
<span class="right floated">
|
||||
<span v-if="obj.is_approved && obj.is_applied">
|
||||
<i class="green bi bi-check"/>
|
||||
{{ t('components.library.EditCard.status.applied') }}
|
||||
</span>
|
||||
<span v-else-if="obj.is_approved">
|
||||
<i class="green bi bi-check" />
|
||||
{{ t('components.library.EditCard.status.approved') }}
|
||||
</span>
|
||||
<span v-else-if="obj.is_approved === null">
|
||||
<i class="yellow bi bi-hourglass" />
|
||||
{{ t('components.library.EditCard.status.pending') }}
|
||||
</span>
|
||||
<span v-else-if="obj.is_approved === false">
|
||||
<i class="destructive bi bi-x" />
|
||||
{{ t('components.library.EditCard.status.rejected') }}
|
||||
</span>
|
||||
</span>
|
||||
<table
|
||||
v-if="obj.type === 'update'"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.field') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.oldValue') }}
|
||||
</th>
|
||||
<th>
|
||||
{{ t('components.library.EditCard.table.update.header.newValue') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="field in updatedFields"
|
||||
:key="field.id"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<td>{{ field.id }}</td>
|
||||
|
||||
<td v-if="field.diff">
|
||||
<template v-if="field.config?.type === 'attachment' && field.oldRepr">
|
||||
<img
|
||||
class="image"
|
||||
alt=""
|
||||
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.oldRepr}/proxy?next=medium_square_crop`)"
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span
|
||||
v-for="(part, key) in field.diff.filter(p => !p.added)"
|
||||
:key="key"
|
||||
:class="['diff', {removed: part.removed}]"
|
||||
>
|
||||
{{ part.value }}
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
<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">
|
||||
<img
|
||||
class="ui image"
|
||||
alt=""
|
||||
:src="store.getters['instance/absoluteUrl'](`api/v1/attachments/${field.newRepr}/proxy?next=medium_square_crop`)"
|
||||
>
|
||||
</template>
|
||||
<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
|
||||
v-if="obj.created_by"
|
||||
class="extra content"
|
||||
>
|
||||
<!--TODO: Center actor name horizontally -->
|
||||
<Spacer :size="8"/>
|
||||
<actor-link :actor="obj.created_by" />
|
||||
</div>
|
||||
<div
|
||||
<template #action
|
||||
v-if="canDelete || canApprove"
|
||||
class="ui bottom attached buttons"
|
||||
>
|
||||
<button
|
||||
<Button
|
||||
v-if="canApprove && obj.is_approved !== true"
|
||||
:class="['ui', {loading: isLoading}, 'success', 'basic', 'button']"
|
||||
primary
|
||||
:isLoading="isLoading"
|
||||
@click="approve(true)"
|
||||
>
|
||||
{{ t('components.library.EditCard.button.approve') }}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
v-if="canApprove && obj.is_approved === null"
|
||||
:class="['ui', {loading: isLoading}, 'warning', 'basic', 'button']"
|
||||
destructive
|
||||
:isLoading="isLoading"
|
||||
@click="approve(false)"
|
||||
>
|
||||
{{ t('components.library.EditCard.button.reject') }}
|
||||
</button>
|
||||
</Button>
|
||||
<!--TODO: Make Dangerous Button hand through isLoading prop -->
|
||||
<dangerous-button
|
||||
v-if="canDelete"
|
||||
:class="['ui', {loading: isLoading}, 'basic danger button']"
|
||||
|
@ -337,6 +355,21 @@ const approve = async (approved: boolean) => {
|
|||
</p>
|
||||
</template>
|
||||
</dangerous-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
table {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
|
||||
th, td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
.image {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -2,19 +2,27 @@
|
|||
import type { EditObject, EditObjectType } from '~/composables/moderation/useEditConfigs'
|
||||
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 { useI18n } from 'vue-i18n'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import axios from 'axios'
|
||||
import $ from 'jquery'
|
||||
|
||||
import AttachmentInput from '~/components/common/AttachmentInput.vue'
|
||||
import useEditConfigs from '~/composables/moderation/useEditConfigs'
|
||||
import TagsSelector from '~/components/library/TagsSelector.vue'
|
||||
import EditList from '~/components/library/EditList.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 {
|
||||
objectType: EditObjectType
|
||||
|
@ -93,20 +101,6 @@ for (const { id, getValue } of config.value.fields) {
|
|||
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 summary = ref('')
|
||||
|
||||
|
@ -145,23 +139,21 @@ const resetField = (fieldId: string) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="submittedMutation">
|
||||
<div class="ui positive message">
|
||||
<Alert green v-if="submittedMutation">
|
||||
<h4 class="header">
|
||||
{{ t('components.library.EditForm.header.success') }}
|
||||
</h4>
|
||||
</div>
|
||||
<edit-card
|
||||
:obj="submittedMutation"
|
||||
:current-state="currentState"
|
||||
/>
|
||||
<button
|
||||
class="ui button"
|
||||
<Link
|
||||
solid primary
|
||||
@click.prevent="submittedMutation = null"
|
||||
>
|
||||
{{ t('components.library.EditForm.button.new') }}
|
||||
</button>
|
||||
</div>
|
||||
</Link>
|
||||
</Alert>
|
||||
<div v-else>
|
||||
<edit-list
|
||||
:filters="editListFilters"
|
||||
|
@ -170,6 +162,7 @@ const resetField = (fieldId: string) => {
|
|||
:current-state="currentState"
|
||||
>
|
||||
<div>
|
||||
<!--TODO: Use Section component with conditional headlines and action buttons-->
|
||||
<template v-if="showPendingReview">
|
||||
{{ t('components.library.EditForm.header.unreviewed') }}
|
||||
<button
|
||||
|
@ -195,15 +188,13 @@ const resetField = (fieldId: string) => {
|
|||
</empty-state>
|
||||
</template>
|
||||
</edit-list>
|
||||
<form
|
||||
class="ui form"
|
||||
<Layout form
|
||||
@submit.prevent="submit()"
|
||||
>
|
||||
<div class="ui hidden divider" />
|
||||
<div
|
||||
<Alert red
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.library.EditForm.header.failure') }}
|
||||
|
@ -216,13 +207,12 @@ const resetField = (fieldId: string) => {
|
|||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
</Alert>
|
||||
<Alert red
|
||||
v-if="!canEdit"
|
||||
class="ui message"
|
||||
>
|
||||
{{ t('components.library.EditForm.message.noPermission') }}
|
||||
</div>
|
||||
</Alert>
|
||||
<template v-if="values">
|
||||
<div
|
||||
v-for="fieldConfig in config.fields"
|
||||
|
@ -230,14 +220,15 @@ const resetField = (fieldId: string) => {
|
|||
class="ui field"
|
||||
>
|
||||
<template v-if="fieldConfig.type === 'text'">
|
||||
<label :for="fieldConfig.id">{{ fieldConfig.label }}</label>
|
||||
<input
|
||||
<Spacer :size="64"/>
|
||||
<Input
|
||||
:id="fieldConfig.id"
|
||||
v-model="values[fieldConfig.id]"
|
||||
:type="fieldConfig.inputType || 'text'"
|
||||
:required="fieldConfig.required"
|
||||
:name="fieldConfig.id"
|
||||
>
|
||||
:label="fieldConfig.label"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="fieldConfig.type === 'license'">
|
||||
<label :for="fieldConfig.id">{{ fieldConfig.label }}</label>
|
||||
|
@ -260,14 +251,14 @@ const resetField = (fieldId: string) => {
|
|||
{{ name }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
class="ui tiny basic left floated button"
|
||||
<Button
|
||||
tiny
|
||||
icon="bi-x"
|
||||
form="noop"
|
||||
@click.prevent="values[fieldConfig.id] = null"
|
||||
>
|
||||
<i class="x icon" />
|
||||
{{ t('components.library.EditForm.button.clear') }}
|
||||
</button>
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else-if="fieldConfig.type === 'content'">
|
||||
<label :for="fieldConfig.id">{{ fieldConfig.label }}</label>
|
||||
|
@ -277,7 +268,9 @@ const resetField = (fieldId: string) => {
|
|||
:rows="3"
|
||||
/>
|
||||
</template>
|
||||
<!-- TODO: Style Attachment Input -->
|
||||
<template v-else-if="fieldConfig.type === 'attachment'">
|
||||
<Spacer />
|
||||
<attachment-input
|
||||
:id="fieldConfig.id"
|
||||
v-model="values[fieldConfig.id]"
|
||||
|
@ -290,53 +283,56 @@ const resetField = (fieldId: string) => {
|
|||
</attachment-input>
|
||||
</template>
|
||||
<template v-else-if="fieldConfig.type === 'tags'">
|
||||
<label :for="fieldConfig.id">{{ fieldConfig.label }}</label>
|
||||
<tags-selector
|
||||
<Spacer/>
|
||||
<Pills
|
||||
:id="fieldConfig.id"
|
||||
:label="fieldConfig.label"
|
||||
ref="tags"
|
||||
v-model="values[fieldConfig.id]"
|
||||
required="fieldConfig.required"
|
||||
/>
|
||||
<button
|
||||
>
|
||||
<Button
|
||||
class="ui tiny basic left floated button"
|
||||
icon="bi-x"
|
||||
form="noop"
|
||||
@click.prevent="values[fieldConfig.id] = []"
|
||||
>
|
||||
<i class="x icon" />
|
||||
{{ t('components.library.EditForm.button.clear') }}
|
||||
</button>
|
||||
</Button>
|
||||
</Pills>
|
||||
</template>
|
||||
<div v-if="fieldValuesChanged(fieldConfig.id)">
|
||||
<button
|
||||
class="ui tiny basic right floated reset button"
|
||||
<Button
|
||||
tiny
|
||||
alignSelf="end"
|
||||
icon="bi-arrow-counterclockwise"
|
||||
form="noop"
|
||||
@click.prevent="resetField(fieldConfig.id)"
|
||||
>
|
||||
<i class="undo icon" />
|
||||
{{ t('components.library.EditForm.button.reset') }}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="field">
|
||||
<label for="summary">{{ t('components.library.EditForm.label.summary') }}</label>
|
||||
<textarea
|
||||
id="change-summary"
|
||||
v-model="summary"
|
||||
name="change-summary"
|
||||
rows="3"
|
||||
:placeholder="labels.summaryPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
<router-link
|
||||
<Spacer/>
|
||||
<Textarea
|
||||
id="change-summary"
|
||||
v-model="summary"
|
||||
name="change-summary"
|
||||
rows="3"
|
||||
:label="t('components.library.EditForm.label.summary')"
|
||||
:placeholder="labels.summaryPlaceholder"
|
||||
/>
|
||||
<Button
|
||||
v-if="objectType === 'track'"
|
||||
class="ui left floated button"
|
||||
:to="{name: 'library.tracks.detail', params: {id: object.id }}"
|
||||
>
|
||||
{{ t('components.library.EditForm.button.cancel') }}
|
||||
</router-link>
|
||||
<button
|
||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']"
|
||||
</Button>
|
||||
<Button
|
||||
:class="['ui', 'right', 'floated', 'success', 'button']"
|
||||
:isLoading="isLoading"
|
||||
primary
|
||||
type="submit"
|
||||
:disabled="isLoading || !mutationPayload"
|
||||
>
|
||||
|
@ -346,7 +342,7 @@ const resetField = (fieldId: string) => {
|
|||
<span v-else>
|
||||
{{ t('components.library.EditForm.button.suggest') }}
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</Button>
|
||||
</Layout>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -5,6 +5,9 @@ import { ref, watchEffect } from 'vue'
|
|||
import axios from 'axios'
|
||||
|
||||
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 {
|
||||
url: string
|
||||
|
@ -45,37 +48,24 @@ watchEffect(() => fetchData())
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<h3 class="ui header">
|
||||
<slot />
|
||||
</h3>
|
||||
<slot
|
||||
v-if="!isLoading && objects.length === 0"
|
||||
name="empty-state"
|
||||
/>
|
||||
<button
|
||||
<h3>
|
||||
<slot />
|
||||
</h3>
|
||||
<slot
|
||||
v-if="!isLoading && objects.length === 0"
|
||||
name="empty-state"
|
||||
/>
|
||||
<Layout grid>
|
||||
<Button
|
||||
v-if="nextPage || previousPage"
|
||||
:disabled="!previousPage"
|
||||
:class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"
|
||||
round
|
||||
alignSelf="center"
|
||||
icon="bi-chevron-left"
|
||||
@click="fetchData(previousPage)"
|
||||
>
|
||||
<i :class="['ui', 'angle left', 'icon']" />
|
||||
</button>
|
||||
<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>
|
||||
</Button>
|
||||
<Loader v-if="isLoading" />
|
||||
<edit-card
|
||||
v-for="obj in objects"
|
||||
:key="obj.uuid"
|
||||
|
@ -84,5 +74,14 @@ watchEffect(() => fetchData())
|
|||
@updated="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>
|
||||
|
|
|
@ -9,6 +9,11 @@ import { useStore } from '~/store'
|
|||
import axios from 'axios'
|
||||
|
||||
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 useReport from '~/composables/moderation/useReport'
|
||||
|
@ -142,58 +147,63 @@ watch(showScan, (shouldShow) => {
|
|||
|
||||
stopFetching()
|
||||
})
|
||||
|
||||
const isOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui card">
|
||||
<Card
|
||||
:title="library.name"
|
||||
>
|
||||
<div class="content">
|
||||
<h4 class="header">
|
||||
<router-link :to="{name: 'library.detail', params: {id: library.uuid}}">
|
||||
{{ library.name }}
|
||||
</router-link>
|
||||
<div
|
||||
v-dropdown
|
||||
class="ui right floated dropdown"
|
||||
>
|
||||
<i class="ellipsis vertical large icon nomargin" />
|
||||
<div class="menu">
|
||||
<button
|
||||
|
||||
<Popover v-model:open="isOpen">
|
||||
<template #default="{ toggleOpen }">
|
||||
<OptionsButton
|
||||
@click="toggleOpen"
|
||||
/>
|
||||
</template>
|
||||
<template #items>
|
||||
<PopoverItem
|
||||
v-for="obj in getReportableObjects({library, account: library.actor})"
|
||||
:key="obj.target.type + obj.target.id"
|
||||
class="item basic"
|
||||
@click.stop.prevent="report(obj)"
|
||||
>
|
||||
<i class="share icon" /> {{ obj.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<i class="bi bi-share" /> {{ obj.label }}
|
||||
</PopoverItem>
|
||||
</template>
|
||||
</Popover>
|
||||
<span
|
||||
v-if="library.privacy_level === 'me'"
|
||||
class="right floated"
|
||||
:data-tooltip="labels.tooltips.me"
|
||||
>
|
||||
<i class="small lock icon" />
|
||||
<i class="bi bi-lock" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="library.privacy_level === 'everyone'"
|
||||
class="right floated"
|
||||
:data-tooltip="labels.tooltips.everyone"
|
||||
>
|
||||
<i class="small globe icon" />
|
||||
<i class="bi bi-globe" />
|
||||
</span>
|
||||
</h4>
|
||||
<div class="meta">
|
||||
<span>
|
||||
<i class="small outline clock icon" />
|
||||
<i class="bi bi-clock" />
|
||||
<human-date :date="library.creation_date" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
{{ library.description }}
|
||||
<div class="ui hidden divider" />
|
||||
</div>
|
||||
<Spacer :size="8" />
|
||||
<div class="meta">
|
||||
<i class="music icon" />
|
||||
<i class="bi bi-music-note" />
|
||||
{{ t('views.content.remote.Card.meta.tracks', library.uploads_count) }}
|
||||
</div>
|
||||
<div
|
||||
|
@ -201,7 +211,7 @@ watch(showScan, (shouldShow) => {
|
|||
class="meta"
|
||||
>
|
||||
<template v-if="latestScan.status === 'pending'">
|
||||
<i class="hourglass icon" />
|
||||
<i class="bi bi-hourglass" />
|
||||
{{ t('views.content.remote.Card.label.scanPending') }}
|
||||
</template>
|
||||
<template v-if="latestScan.status === 'scanning'">
|
||||
|
@ -209,15 +219,15 @@ watch(showScan, (shouldShow) => {
|
|||
{{ t('views.content.remote.Card.label.scanProgress', {progress: scanProgress}) }}
|
||||
</template>
|
||||
<template v-else-if="latestScan.status === 'errored'">
|
||||
<i class="dangerdownload icon" />
|
||||
<i class="bi bi-exclamation-triangle" />
|
||||
{{ t('views.content.remote.Card.label.scanFailure') }}
|
||||
</template>
|
||||
<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') }}
|
||||
</template>
|
||||
<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') }}
|
||||
</template>
|
||||
<a
|
||||
|
@ -228,11 +238,11 @@ watch(showScan, (shouldShow) => {
|
|||
{{ t('views.content.remote.Card.link.scanDetails') }}
|
||||
<i
|
||||
v-if="showScan"
|
||||
class="angle down icon"
|
||||
class="bi bi-chevron-down"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="angle right icon"
|
||||
class="bi bi-chevron-right"
|
||||
/>
|
||||
</a>
|
||||
<div v-if="showScan">
|
||||
|
@ -251,16 +261,19 @@ watch(showScan, (shouldShow) => {
|
|||
class="right floated link"
|
||||
@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>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer :size="8" />
|
||||
<div class="extra content">
|
||||
<actor-link
|
||||
style="color: var(--link-color)"
|
||||
:actor="library.actor"
|
||||
/>
|
||||
</div>
|
||||
<Spacer :size="8" />
|
||||
<div
|
||||
v-if="displayCopyFid"
|
||||
class="extra content"
|
||||
|
@ -334,5 +347,5 @@ watch(showScan, (shouldShow) => {
|
|||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
Loading…
Reference in New Issue