fix(ui):[WIP] replace popover menus, buttons, messages

This commit is contained in:
ArneBo 2024-12-07 11:46:42 +01:00 committed by upsiflu
parent 481fee8f5f
commit 4d78c2143c
11 changed files with 311 additions and 302 deletions

View File

@ -1,6 +1,9 @@
<script setup lang="ts">
import SemanticModal from '~/components/semantic/Modal.vue'
import ChannelUploadForm from '~/components/channels/UploadForm.vue'
import Popover from '~/components/ui/Popover.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
import Button from '~/components/ui/Button.vue'
import { humanSize } from '~/utils/filters'
import { useRouter } from 'vue-router'
import { useStore } from '~/store'
@ -45,6 +48,8 @@ const statusInfo = computed(() => {
const step = ref(1)
const isLoading = ref(false)
const dropdownOpen = ref(false)
</script>
<template>
@ -86,70 +91,70 @@ const isLoading = ref(false)
</template>
</div>
<div class="ui hidden clearing divider mobile-only" />
<button
<Button
v-if="step === 1"
class="ui basic cancel button"
color="secondary"
variant="outline"
@click="update(false)"
>
{{ $t('components.channels.UploadModal.button.cancel') }}
</button>
<button
</Button>
<Button
v-else-if="step < 3"
class="ui basic button"
color="secondary"
variant="outline"
@click.stop.prevent="uploadForm.step -= 1"
>
{{ $t('components.channels.UploadModal.button.previous') }}
</button>
<button
</Button>
<Button
v-else-if="step === 3"
class="ui basic button"
color="secondary"
@click.stop.prevent="uploadForm.step -= 1"
>
{{ $t('components.channels.UploadModal.button.update') }}
</button>
<button
</Button>
<Button
v-if="step === 1"
class="ui primary button"
color="secondary"
@click.stop.prevent="uploadForm.step += 1"
>
{{ $t('components.channels.UploadModal.button.next') }}
</button>
<div
v-if="step === 2"
class="ui primary buttons"
>
<button
:class="['ui', 'primary button', {loading: isLoading}]"
</Button>
<div class="ui primary buttons">
<Button
:is-loading="isLoading"
type="submit"
:disabled="!statusData?.canSubmit || undefined"
:disabled="!statusData?.canSubmit"
@click.prevent.stop="uploadForm.publish"
>
{{ $t('components.channels.UploadModal.button.publish') }}
</button>
<button
ref="dropdown"
v-dropdown
class="ui floating dropdown icon button"
:disabled="!statusData?.canSubmit || undefined"
>
<i class="dropdown icon" />
<div class="menu">
<div
role="button"
class="basic item"
@click="update(false)"
>
</Button>
<Popover v-model:open="dropdownOpen">
<template #default="{ toggleOpen }">
<Button
color="primary"
icon="bi-chevron-down"
:disabled="!statusData?.canSubmit"
@click="toggleOpen"
/>
</template>
<template #items>
<PopoverItem @click="update(false)">
{{ $t('components.channels.UploadModal.button.finishLater') }}
</div>
</div>
</button>
</PopoverItem>
</template>
</Popover>
</div>
<button
<Button
v-if="step === 4"
class="ui basic cancel button"
color="secondary"
@click="update(false)"
>
{{ $t('components.channels.UploadModal.button.close') }}
</button>
</Button>
</div>
</semantic-modal>
</template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import SemanticModal from '~/components/semantic/Modal.vue'
import Button from '~/components/ui/Button.vue'
import { ref } from 'vue'
interface Events {
@ -9,7 +10,7 @@ interface Events {
interface Props {
action?: () => void
disabled?: boolean
confirmColor?: 'danger' | 'success'
confirmColor?: 'destructive' | 'primary'
}
const emit = defineEmits<Events>()
@ -30,7 +31,8 @@ const confirm = () => {
<template>
<button
:class="[{disabled: disabled}]"
class="funkwhale dangerous-button"
:class="{ 'is-disabled': disabled }"
:disabled="disabled"
@click.prevent.stop="showModal = true"
>
@ -51,18 +53,22 @@ const confirm = () => {
</div>
</div>
<div class="actions">
<button class="ui basic cancel button">
<Button
color="secondary"
variant="outline"
@click="showModal = false"
>
{{ $t('components.common.DangerousButton.button.cancel') }}
</button>
<button
:class="['ui', 'confirm', confirmColor, 'button']"
</Button>
<Button
:color="confirmColor"
@click="confirm"
>
<slot name="modal-confirm">
{{ $t('components.common.DangerousButton.button.confirm') }}
</slot>
</button>
</Button>
</div>
</semantic-modal>
</button>
</template>
</template>

View File

@ -1,35 +1,49 @@
<script setup lang="ts">
import $ from 'jquery'
import { onMounted } from 'vue'
import { useStore } from '~/store'
import Alert from '~/components/ui/Alert.vue'
interface Message {
content: string
key: string
color?: 'blue' | 'red' | 'purple' | 'green' | 'yellow'
error?: boolean | string
date?: Date
}
const props = defineProps<{ message: Message }>()
const isVisible = ref(true)
const store = useStore()
onMounted(() => {
const params = {
context: '#app',
message: props.message.content,
showProgress: 'top',
position: 'bottom right',
progressUp: true,
onRemove () {
store.commit('ui/removeMessage', props.message.key)
},
...props.message
const messageColor = computed(() => {
if (props.message.color) {
return props.message.color
}
// @ts-expect-error fomantic ui
$('body').toast(params)
$('.ui.toast.visible').last().attr('role', 'alert')
if (props.message.error || props.message.content?.toLowerCase().includes('error')) {
return 'red'
}
return 'blue'
})
onMounted(() => {
setTimeout(() => {
isVisible.value = false
store.commit('ui/removeMessage', props.message.key)
}, 5000)
})
</script>
<template>
<div />
<Transition name="fade">
<Alert
v-if="isVisible"
role="alert"
:color="messageColor"
class="is-notification"
>
{{ message.content }}
</Alert>
</Transition>
</template>

View File

@ -10,6 +10,10 @@ import useReport from '~/composables/moderation/useReport'
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
import SemanticModal from '~/components/semantic/Modal.vue'
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'
interface Events {
(e: 'remove'): void
@ -43,6 +47,8 @@ const musicbrainzUrl = computed(() => props.object?.mbid ? `https://musicbrainz.
const discogsUrl = computed(() => `https://discogs.com/search/?type=release&title=${encodeURI(props.object?.title)}&artist=${encodeURI(props.object?.artist_credit[0].artist.name)}`)
const remove = () => emit('remove')
const open = ref(false)
</script>
<template>
@ -65,120 +71,116 @@ const remove = () => emit('remove')
</div>
</div>
<div class="actions">
<button class="ui basic deny button">
<Button
color="secondary"
variant="outline"
@click="showEmbedModal = false"
>
{{ $t('components.library.AlbumDropdown.button.cancel') }}
</button>
</Button>
</div>
</semantic-modal>
<button
v-dropdown="{direction: 'downward'}"
class="ui floating dropdown circular icon basic button"
:title="labels.more"
>
<i class="ellipsis vertical icon" />
<div class="menu">
<a
<Popover v-model:open="open">
<template #default="{ toggleOpen }">
<Button
variant="ghost"
color="secondary"
round
icon="bi-three-dots-vertical"
:title="labels.more"
@click="toggleOpen"
/>
</template>
<template #items>
<PopoverItem
v-if="domain != $store.getters['instance/domain']"
:href="object.fid"
target="_blank"
class="basic item"
>
<i class="external icon" />
<i class="bi bi-box-arrow-up-right" />
{{ $t('components.library.AlbumDropdown.link.domain') }}
</a>
</PopoverItem>
<div
<PopoverItem
v-if="isEmbedable"
role="button"
class="basic item"
@click="showEmbedModal = !showEmbedModal"
>
<i class="code icon" />
<i class="bi bi-code" />
{{ $t('components.library.AlbumDropdown.button.embed') }}
</div>
<a
</PopoverItem>
<PopoverItem
v-if="isAlbum && musicbrainzUrl"
:href="musicbrainzUrl"
target="_blank"
rel="noreferrer noopener"
class="basic item"
>
<i class="external icon" />
<i class="bi bi-box-arrow-up-right" />
{{ $t('components.library.AlbumDropdown.link.musicbrainz') }}
</a>
<a
</PopoverItem>
<PopoverItem
v-if="!isChannel && isAlbum"
:href="discogsUrl"
target="_blank"
rel="noreferrer noopener"
class="basic item"
>
<i class="external icon" />
<i class="bi bi-box-arrow-up-right" />
{{ $t('components.library.AlbumDropdown.link.discogs') }}
</a>
<router-link
</PopoverItem>
<PopoverItem
v-if="object.is_local"
:to="{name: 'library.albums.edit', params: {id: object.id }}"
class="basic item"
>
<i class="edit icon" />
<i class="bi bi-pencil" />
{{ $t('components.library.AlbumDropdown.button.edit') }}
</router-link>
<dangerous-button
</PopoverItem>
<PopoverItem
v-if="artistCredit[0] && $store.state.auth.authenticated && artistCredit[0].artist.channel && artistCredit[0].artist.attributed_to?.full_username === $store.state.auth.fullUsername"
:class="['ui', {loading: isLoading}, 'item']"
@confirm="remove()"
>
<i class="ui trash icon" />
{{ $t('components.library.AlbumDropdown.button.delete') }}
<template #modal-header>
<p>
{{ $t('components.library.AlbumDropdown.modal.delete.header') }}
</p>
</template>
<template #modal-content>
<div>
<p>
{{ $t('components.library.AlbumDropdown.modal.delete.content.warning') }}
</p>
</div>
</template>
<template #modal-confirm>
<p>
{{ $t('components.library.AlbumDropdown.button.delete') }}
</p>
</template>
</dangerous-button>
<div class="divider" />
<div
<DangerousButton
:is-loading="isLoading"
@confirm="remove()"
>
<i class="bi bi-trash" />
{{ $t('components.library.AlbumDropdown.button.delete') }}
</DangerousButton>
</PopoverItem>
<hr>
<PopoverItem
v-for="obj in getReportableObjects({album: object, channel: artistCredit[0]?.artist.channel})"
:key="obj.target.type + obj.target.id"
role="button"
class="basic item"
@click.stop.prevent="report(obj)"
@click="report(obj)"
>
<i class="share icon" /> {{ obj.label }}
</div>
<div class="divider" />
<router-link
<i class="bi bi-flag" />
{{ obj.label }}
</PopoverItem>
<hr>
<PopoverItem
v-if="$store.state.auth.availablePermissions['library']"
class="basic item"
:to="{name: 'manage.library.albums.detail', params: {id: object.id}}"
>
<i class="wrench icon" />
<i class="bi bi-wrench" />
{{ $t('components.library.AlbumDropdown.link.moderation') }}
</router-link>
<a
</PopoverItem>
<PopoverItem
v-if="$store.state.auth.profile && $store.state.auth.profile?.is_superuser"
class="basic item"
:href="$store.getters['instance/absoluteUrl'](`/api/admin/music/album/${object.id}`)"
target="_blank"
rel="noopener noreferrer"
>
<i class="wrench icon" />
<i class="bi bi-wrench" />
{{ $t('components.library.AlbumDropdown.link.django') }}
</a>
</div>
</button>
</PopoverItem>
</template>
</Popover>
</span>
</template>

View File

@ -14,6 +14,8 @@ import SemanticModal from '~/components/semantic/Modal.vue'
import PlayButton from '~/components/audio/PlayButton.vue'
import RadioButton from '~/components/radios/Button.vue'
import TagsList from '~/components/tags/List.vue'
import Popover from '~/components/ui/Popover.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
import useReport from '~/composables/moderation/useReport'
import useLogger from '~/composables/useLogger'
@ -36,8 +38,6 @@ const nextTracksUrl = ref(null)
const totalAlbums = ref(0)
const totalTracks = ref(0)
const dropdown = ref()
const logger = useLogger()
const store = useStore()
const router = useRouter()
@ -166,107 +166,103 @@ watch(() => props.id, fetchData, { immediate: true })
</div>
</semantic-modal>
<div class="ui buttons">
<button
class="ui button"
@click="dropdown.click()"
>
{{ $t('components.library.ArtistBase.button.more') }}
</button>
<button
ref="dropdown"
v-dropdown
class="ui floating dropdown icon button"
>
<i class="dropdown icon" />
<div class="menu">
<a
v-if="domain != $store.getters['instance/domain']"
:href="object.fid"
target="_blank"
class="basic item"
>
<i class="external icon" />
{{ $t('components.library.ArtistBase.link.domain', {domain: domain}) }}
</a>
<Popover>
<template #default="{ toggleOpen }">
<button
class="ui button"
@click="toggleOpen"
>
{{ $t('components.library.ArtistBase.button.more') }}
<i class="dropdown icon" />
</button>
</template>
<button
v-if="publicLibraries.length > 0"
role="button"
class="basic item"
@click.prevent="showEmbedModal = !showEmbedModal"
>
<i class="code icon" />
{{ $t('components.library.ArtistBase.button.embed') }}
</button>
<a
:href="wikipediaUrl"
target="_blank"
rel="noreferrer noopener"
class="basic item"
>
<i class="wikipedia w icon" />
{{ $t('components.library.ArtistBase.link.wikipedia') }}
</a>
<a
v-if="musicbrainzUrl"
:href="musicbrainzUrl"
target="_blank"
rel="noreferrer noopener"
class="basic item"
>
<i class="external icon" />
{{ $t('components.library.ArtistBase.link.musicbrainz') }}
</a>
<a
:href="discogsUrl"
target="_blank"
rel="noreferrer noopener"
class="basic item"
>
<i class="external icon" />
{{ $t('components.library.ArtistBase.link.discogs') }}
</a>
<router-link
v-if="object.is_local"
:to="{name: 'library.artists.edit', params: {id: object.id }}"
class="basic item"
>
<i class="edit icon" />
{{ $t('components.library.ArtistBase.button.edit') }}
</router-link>
<div class="divider" />
<div
v-for="obj in getReportableObjects({artist: object})"
:key="obj.target.type + obj.target.id"
role="button"
class="basic item"
@click.stop.prevent="report(obj)"
>
<i class="share icon" /> {{ obj.label }}
</div>
<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>
<div class="divider" />
<router-link
v-if="$store.state.auth.availablePermissions['library']"
class="basic item"
:to="{name: 'manage.library.artists.detail', params: {id: object.id}}"
>
<i class="wrench icon" />
{{ $t('components.library.ArtistBase.link.moderation') }}
</router-link>
<a
v-if="$store.state.auth.profile && $store.state.auth.profile.is_superuser"
class="basic item"
: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') }}
</a>
</div>
</button>
</div>
<PopoverItem
v-if="publicLibraries.length > 0"
@click="showEmbedModal = !showEmbedModal"
>
<i class="code icon" />
{{ $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
v-if="musicbrainzUrl"
:href="musicbrainzUrl"
target="_blank"
rel="noreferrer noopener"
>
<i class="external icon" />
{{ $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
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>
<hr>
<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>
<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.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>
</div>
</section>

View File

@ -74,14 +74,12 @@ const toggleRadio = () => {
</script>
<template>
<button
:class="['ui', 'primary', {'inverted': running}, 'icon', 'labeled', 'button']"
<Button
:is-active="running"
color="primary"
icon="bi-broadcast"
@click="toggleRadio"
>
<i
class="ui feed icon"
role="button"
/>
{{ buttonLabel }}
</button>
</Button>
</template>

View File

@ -1,9 +1,9 @@
<script setup lang="ts">
import $ from 'jquery'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
import { computed, onBeforeUnmount, ref, watchEffect } from 'vue'
import { useVModel } from '@vueuse/core'
import { useStore } from '~/store'
import Modal from '~/components/ui/Modal.vue'
interface Events {
(e: 'update:show', show: boolean): void
@ -26,55 +26,10 @@ const props = withDefaults(defineProps<Props>(), {
scrolling: false,
additionalClasses: () => []
})
const modal = ref()
const { activate, deactivate, pause, unpause } = useFocusTrap(modal, {
allowOutsideClick: true
})
const show = useVModel(props, 'show', emit)
const control = ref<JQuery | undefined>()
const initModal = () => {
control.value = $(modal.value).modal({
duration: 100,
onApprove: () => emit('approved'),
onDeny: () => emit('deny'),
onHidden: () => (show.value = false)
})
}
watchEffect(() => {
if (show.value) {
initModal()
emit('show')
control.value?.modal('show')
activate()
unpause()
document.body.classList.add('scrolling')
return
}
if (control.value) {
emit('hide')
control.value.modal('hide')
control.value.remove()
deactivate()
pause()
document.body.classList.remove('scrolling')
}
})
onBeforeUnmount(() => {
control.value?.modal('hide')
})
const store = useStore()
const classes = computed(() => [
...props.additionalClasses,
'ui', 'modal',
{
active: show.value,
scrolling: props.scrolling,
'overlay fullscreen': props.fullscreen && ['phone', 'tablet'].includes(store.getters['ui/windowSize'])
}
@ -82,14 +37,15 @@ const classes = computed(() => [
</script>
<template>
<div
ref="modal"
<Modal
:model-value="show"
@update:model-value="(value) => emit('update:show', value)"
:class="classes"
@approve="emit('approved')"
@deny="emit('deny')"
@show="emit('show')"
@hide="emit('hide')"
>
<i
tabindex="0"
class="close inside icon"
/>
<slot v-if="show" />
</div>
<slot />
</Modal>
</template>

View File

@ -3,10 +3,21 @@ import { useColorOrPastel, type ColorProps, type PastelProps } from '~/composabl
const props = defineProps<ColorProps | PastelProps>()
const color = useColorOrPastel(() => props.color, 'secondary')
const emit = defineEmits<{
click: [event: MouseEvent]
}>()
const handleClick = (event: MouseEvent) => {
emit('click', event)
}
</script>
<template>
<button class="funkwhale is-colored pill" :class="[color]">
<button
type="button"
class="funkwhale is-colored pill"
:class="[color]"
@click.stop="handleClick"
>
<div v-if="!!$slots.image" class="pill-image">
<slot name="image" />
</div>

View File

@ -41,4 +41,25 @@
> .actions {
margin-left: auto;
}
}
// Add styles for when alert is used as a notification
&.is-notification {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 1000;
min-width: 200px;
max-width: 400px;
&.fade-enter-active,
&.fade-leave-active {
transition: all 0.3s ease;
}
&.fade-enter-from,
&.fade-leave-to {
opacity: 0;
transform: translateY(1rem);
}
}
}

View File

@ -81,7 +81,7 @@
}
}
@include docs {
@if $docs {
color: var(--fw-text-color) !important;
}

View File

@ -2,7 +2,7 @@
import { inject, ref } from 'vue'
import { POPOVER_CONTEXT_INJECTION_KEY, type PopoverContext } from '~/injection-keys'
const setId = defineEmit<[value: number]>('internal:id')
const emit = defineEmits<{'internal:id': [value: number]}>()
const { parentPopoverContext } = defineProps<{ parentPopoverContext?: PopoverContext }>()
const { items, hoveredItem } = parentPopoverContext ?? inject(POPOVER_CONTEXT_INJECTION_KEY, {
@ -11,7 +11,7 @@ const { items, hoveredItem } = parentPopoverContext ?? inject(POPOVER_CONTEXT_IN
})
const id = items.value++
setId(id)
emit('internal:id', id)
</script>
<template>