feat(ui): Select upload destination with funkwhale-ui Card component

This commit is contained in:
upsiflu 2024-10-23 12:29:08 +02:00
parent 0592a68a2f
commit be6df0fc3e
3 changed files with 321 additions and 0 deletions

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import type { Channel } from '~/types'
import { momentFormat } from '~/utils/filters'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import { computed } from 'vue'
import moment from 'moment'
import TagsList from '~/components/tags/List.vue'
interface Props {
channel: Channel
callback: (object:Channel)=>void
}
const props = defineProps<Props>()
const store = useStore()
const fallbackImageUrl = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzODQgNTEyIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDYuNi4wIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjQgRm9udGljb25zLCBJbmMuLS0+PHBhdGggZD0iTTM4MS45IDM4OC4yYy02LjQgMjcuNC0yNy4yIDQyLjgtNTUuMSA0OC0yNC41IDQuNS00NC45IDUuNi02NC41LTEwLjItMjMuOS0yMC4xLTI0LjItNTMuNC0yLjctNzQuNCAxNy0xNi4yIDQwLjktMTkuNSA3Ni44LTI1LjggNi0xLjEgMTEuMi0yLjUgMTUuNi03LjQgNi40LTcuMiA0LjQtNC4xIDQuNC0xNjMuMiAwLTExLjItNS41LTE0LjMtMTctMTIuMy04LjIgMS40LTE4NS43IDM0LjYtMTg1LjcgMzQuNi0xMC4yIDIuMi0xMy40IDUuMi0xMy40IDE2LjcgMCAyMzQuNyAxLjEgMjIzLjktMi41IDIzOS41LTQuMiAxOC4yLTE1LjQgMzEuOS0zMC4yIDM5LjUtMTYuOCA5LjMtNDcuMiAxMy40LTYzLjQgMTAuNC00My4yLTguMS01OC40LTU4LTI5LjEtODYuNiAxNy0xNi4yIDQwLjktMTkuNSA3Ni44LTI1LjggNi0xLjEgMTEuMi0yLjUgMTUuNi03LjQgMTAuMS0xMS41IDEuOC0yNTYuNiA1LjItMjcwLjIgLjgtNS4yIDMtOS42IDcuMS0xMi45IDQuMi0zLjUgMTEuOC01LjUgMTMuNC01LjUgMjA0LTM4LjIgMjI4LjktNDMuMSAyMzIuNC00My4xIDExLjUtLjggMTguMSA2IDE4LjEgMTcuNiAuMiAzNDQuNSAxLjEgMzI2LTEuOCAzMzguNXoiLz48L3N2Zz4='
/* TODO: Replace with actual target: */
const imageUrl = computed(() => props.channel.artist?.cover
? store.getters['instance/absoluteUrl'](props.channel.artist?.cover.urls.medium_square_crop)
: fallbackImageUrl
)
const urlId = computed(() => props.channel.actor?.is_local
? props.channel.actor.preferred_username
: props.channel.actor
? props.channel.actor.full_username
: props.channel.uuid
)
const { t } = useI18n()
const updatedTitle = computed(() => {
const date = momentFormat(new Date(props.channel.artist?.modification_date ?? '1970-01-01'))
return t('components.audio.ChannelCard.title', { date })
})
const updatedAgo = computed(() => moment(props.channel.artist?.modification_date).fromNow())
</script>
<template>
<fw-button @click="callback(channel)" style="border:transparent; background:transparent;">
<fw-card
v-bind:title="channel.artist?.name"
v-bind:image="imageUrl"
>
<div class="description" style="display:flex; justify-content:space-between; min-height:32px; align-items: baseline;" :title="updatedTitle">
<!-- <p>
{{ channel.artist?.description.text }}
</p> -->
<span
class="meta ellipsis"
>
{{ $t('components.audio.ChannelCard.meta.tracks', channel.artist?.tracks_count ?? 0) }}
</span>
<tags-list
style="pointer-events:none;"
label-classes="tiny"
:truncate-size="20"
:limit="2"
:show-more="false"
:tags="channel.artist?.tags ?? []"
/>
</div>
<!-- <div class="extra content">
<time
class="meta ellipsis"
:datetime="channel.artist?.modification_date"
:title="updatedTitle"
>
{{ updatedAgo }}
</time>
</div> -->
</fw-card>
</fw-button>
</template>

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import type { Library, PrivacyLevel } from '~/types'
import { humanSize } from '~/utils/filters'
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
import useSharedLabels from '~/composables/locale/useSharedLabels'
interface Props {
library: Library
}
interface Props {
new : boolean
}
const props = defineProps<Props>()
const title = computed(() => props.library?.name || props.new)
const imageUrl = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDYuNi4wIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjQgRm9udGljb25zLCBJbmMuLS0+PHBhdGggZD0iTTI1NiA4MEMxNDkuOSA4MCA2Mi40IDE1OS40IDQ5LjYgMjYyYzkuNC0zLjggMTkuNi02IDMwLjQtNmMyNi41IDAgNDggMjEuNSA0OCA0OGwwIDEyOGMwIDI2LjUtMjEuNSA0OC00OCA0OGMtNDQuMiAwLTgwLTM1LjgtODAtODBsMC0xNiAwLTQ4IDAtNDhDMCAxNDYuNiAxMTQuNiAzMiAyNTYgMzJzMjU2IDExNC42IDI1NiAyNTZsMCA0OCAwIDQ4IDAgMTZjMCA0NC4yLTM1LjggODAtODAgODBjLTI2LjUgMC00OC0yMS41LTQ4LTQ4bDAtMTI4YzAtMjYuNSAyMS41LTQ4IDQ4LTQ4YzEwLjggMCAyMSAyLjEgMzAuNCA2QzQ0OS42IDE1OS40IDM2Mi4xIDgwIDI1NiA4MHoiLz48L3N2Zz4='
const { t } = useI18n()
const sharedLabels = useSharedLabels()
const sizeLabel = computed(() => t('views.content.libraries.Card.label.size'))
const privacyTooltips = (level: PrivacyLevel) => `Visibility: ${sharedLabels.fields.privacy_level.choices[level].toLowerCase()}`
</script>
<template>
<fw-card
v-bind:title="title"
v-bind:image="imageUrl"
>
<div class="content">
<h4 class="header">
<span
v-if="library.privacy_level === 'me'"
class="right floated"
:data-tooltip="privacyTooltips('me')"
>
<i class="small lock icon" />
</span>
<span
v-else-if="library.privacy_level === 'instance'"
class="right floated"
:data-tooltip="privacyTooltips('instance')"
>
<i class="small circle outline icon" />
</span>
<span
v-else-if="library.privacy_level === 'everyone'"
class="right floated"
:data-tooltip="privacyTooltips('everyone')"
>
<i class="small globe icon" />
</span>
</h4>
<div class="description">
{{ library.description }}
<div class="ui hidden divider" />
</div>
<div class="content">
<i class="music icon" />
{{ $t('views.content.libraries.Card.meta.tracks', library.uploads_count) }}
</div>
</div>
<!-- <div class="ui bottom basic attached buttons">
<router-link
:to="{name: 'library.detail.upload', params: {id: library.uuid}}"
class="ui button"
>
{{ $t('views.content.libraries.Card.button.upload') }}
</router-link>
</div> -->
</fw-card>
</template>

View File

@ -0,0 +1,161 @@
<script setup lang="ts">
import { humanSize } from '~/utils/filters'
import { useI18n } from 'vue-i18n'
import { computed, ref, type Ref, defineAsyncComponent } from 'vue'
import { useStore } from '~/store'
// LIBRARIES BEGIN
import type { Library, Channel } from '~/types'
import { useRouter } from 'vue-router'
import axios from 'axios'
import LibraryForm from '../libraries/Form.vue'
import LibraryCard from '../libraries/CardUpload.vue'
import ChannelCard from '../channels/CardUpload.vue'
import Quota from '../libraries/Quota.vue'
import Upload from '~/ui/pages/upload.vue'
// import UploadModal from '~/ui/pages/upload.vue'
// const ChannelUploadModal = defineAsyncComponent(() => import('~/components/channels/UploadModal.vue'))
import useErrorHandler from '~/composables/useErrorHandler'
const router = useRouter()
const libraries = ref([] as Library[])
const channels = ref([] as Channel[])
const musicChannels = computed(() => channels.value.filter((channel=>channel.artist?.content_category !== 'podcast')))
const podcastChannels = computed(() => channels.value.filter((channel=>channel.artist?.content_category === 'podcast')))
const isLoading = ref(false)
const hiddenForm = ref(true)
const fetchLibraries = async () => {
isLoading.value = true
try {
const response = await axios.get('libraries/', { params: { scope: 'me' } })
libraries.value = response.data.results
if (libraries.value.length === 0) {
hiddenForm.value = false
}
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
const fetchChannels = async () => {
isLoading.value = true
try {
const response = await axios.get('channels/', { params: { scope: 'me' } })
channels.value = response.data.results
if (channels.value.length === 0) {
hiddenForm.value = false
}
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
fetchLibraries()
fetchChannels()
const libraryCreated = (library: Library) => {
router.SpEush({ name: 'library.detail', params: { id: library.uuid } })
}
// LIBRARIES END
const { t } = useI18n()
const labels = computed(() => ({
title: t('views.content.Home.title')
}))
const store = useStore()
console.log(store.state)
const quota = computed(() => store.state.instance.settings.users.upload_quota.value)
const defaultQuota = computed(() => humanSize(quota.value * 1e6))
// Plus Icon
const plusIcon = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBVcGxvYWRlZCB0bzogU1ZHIFJlcG8sIHd3dy5zdmdyZXBvLmNvbSwgR2VuZXJhdG9yOiBTVkcgUmVwbyBNaXhlciBUb29scyAtLT4NCjxzdmcgZmlsbD0iIzAwMDAwMCIgaGVpZ2h0PSI4MDBweCIgd2lkdGg9IjgwMHB4IiB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIA0KCSB2aWV3Qm94PSIwIDAgNDkwIDQ5MCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8Zz4NCgkJPGc+DQoJCQk8cGF0aCBkPSJNMjI3LjgsMTc0LjF2NTMuN2gtNTMuN2MtOS41LDAtMTcuMiw3LjctMTcuMiwxNy4yczcuNywxNy4yLDE3LjIsMTcuMmg1My43djUzLjdjMCw5LjUsNy43LDE3LjIsMTcuMiwxNy4yDQoJCQkJczE3LjEtNy43LDE3LjEtMTcuMnYtNTMuN2g1My43YzkuNSwwLDE3LjItNy43LDE3LjItMTcuMnMtNy43LTE3LjItMTcuMi0xNy4yaC01My43di01My43YzAtOS41LTcuNy0xNy4yLTE3LjEtMTcuMg0KCQkJCVMyMjcuOCwxNjQuNiwyMjcuOCwxNzQuMXoiLz4NCgkJCTxwYXRoIGQ9Ik03MS43LDcxLjdDMjUuNSwxMTgsMCwxNzkuNSwwLDI0NXMyNS41LDEyNyw3MS44LDE3My4zQzExOCw0NjQuNSwxNzkuNiw0OTAsMjQ1LDQ5MHMxMjctMjUuNSwxNzMuMy03MS44DQoJCQkJQzQ2NC41LDM3Miw0OTAsMzEwLjQsNDkwLDI0NXMtMjUuNS0xMjctNzEuOC0xNzMuM0MzNzIsMjUuNSwzMTAuNSwwLDI0NSwwQzE3OS42LDAsMTE4LDI1LjUsNzEuNyw3MS43eiBNNDU1LjcsMjQ1DQoJCQkJYzAsNTYuMy0yMS45LDEwOS4yLTYxLjcsMTQ5cy05Mi43LDYxLjctMTQ5LDYxLjdTMTM1LjgsNDMzLjgsOTYsMzk0cy02MS43LTkyLjctNjEuNy0xNDlTNTYuMiwxMzUuOCw5Niw5NnM5Mi43LTYxLjcsMTQ5LTYxLjcNCgkJCQlTMzU0LjIsNTYuMiwzOTQsOTZTNDU1LjcsMTg4LjcsNDU1LjcsMjQ1eiIvPg0KCQk8L2c+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KPC9nPg0KPC9zdmc+"
// Open modal
const upload = ref(false)
const object:Ref<Library | Channel | undefined> = ref()
const openModal = (object_: Library | Channel) => {
// upload.value = true;
object.value = object_;
//open old modal model
store.state.channels.showUploadModal = true;
store.state.channels.uploadModalConfig = { channel: "artist" in object ? (object as Ref<Channel>).value : null }
}
</script>
<template>
<section
v-title="labels.title"
class="ui vertical aligned stripe segment"
>
<div class="ui text container">
<h1>{{ labels.title }}</h1>
<p>
<strong>{{ $t('views.content.Home.help.uploadQuota', { quota: defaultQuota }) }}</strong>
</p>
<hr/>
<h3>Choose a library:</h3>
<fw-card
v-bind:image="plusIcon"
:title="'New Library'"
></fw-card>
<library-card v-for="library in libraries"
:key="library.uuid" :library="library" />
<h3>Choose a music channel:</h3>
<section style="display:flex;margin:-16px;flex-wrap:wrap;">
<channel-card v-for="channel in musicChannels"
:key="channel.uuid" :channel="channel" :callback="openModal" />
</section>
<h3>Choose a podcast channel:</h3>
<section style="display:flex;gap:16px;flex-wrap:wrap;">
<channel-card
v-for="channel in podcastChannels"
:key="channel.uuid"
class="column"
:channel="channel" :callback="openModal" />
</section>
</div>
</section>
<fw-modal v-model="upload" :title="`Upload to ${
object ?
'artist' in object ?
object.artist?.name
: 'I am a library'
: 'I am undefined'}`">
<upload></upload>
</fw-modal>
<!-- <channel-upload-modal v-if="store.state.auth.authenticated" /> -->
</template>