2506-fix-frontend-regressions
Flupsi 2025-08-09 18:27:56 +02:00
rodzic 7c2d414bc0
commit 7aed9c26f8
28 zmienionych plików z 467 dodań i 286 usunięć

Wyświetl plik

@ -76,15 +76,21 @@ useIntervalFn(() => {
// NOTE: We're not checking if we're authenticated in the store,
// because we want to learn if we are authenticated at all
store.dispatch('auth/fetchUser')
</script>
<template>
<div class="funkwhale responsive">
<Sidebar style="grid-area: sidebar;" />
<RouterView v-slot="{ Component }" v-bind="color({}, ['default', 'solid'])()" :class="$style.layout"
style="grid-area: main;">
<Transition v-if="Component" mode="out-in">
<RouterView
v-slot="{ Component }"
v-bind="color({}, ['default', 'solid'])()"
:class="$style.layout"
style="grid-area: main;"
>
<Transition
v-if="Component"
mode="out-in"
>
<KeepAlive :max="10">
<Suspense>
<component :is="Component" />
@ -99,7 +105,10 @@ store.dispatch('auth/fetchUser')
</transition>
</RouterView>
</div>
<AudioPlayer class="funkwhale" v-bind="color({}, ['default', 'solid'])()" />
<AudioPlayer
class="funkwhale"
v-bind="color({}, ['default', 'solid'])()"
/>
<ServiceMessages />
<LanguagesModal />
<ShortcutsModal />

Wyświetl plik

@ -37,7 +37,7 @@ const imageUrl = computed(() => props.album.cover?.urls.original
:title="album.title"
:image="imageUrl"
:tags="album.tags"
:to="{name: 'library.albums.detail', params: {id: album.id}}"
:to="{ name: 'library.albums.detail', params: { id: album.id } }"
small
>
<template #topright>
@ -58,7 +58,7 @@ const imageUrl = computed(() => props.album.cover?.urls.original
>
<Link
align-text="start"
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id }}"
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id } }"
>
{{ ac.credit ?? t('components.Queue.meta.unknownArtist') }}
</Link>

Wyświetl plik

@ -162,7 +162,7 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
<div class="activity-content">
<router-link
class="funkwhale link artist"
:to="{name: 'library.tracks.detail', params: {id: object.track.id}}"
:to="{ name: 'library.tracks.detail', params: { id: object.track.id } }"
>
<Heading
:h3="object.track.title"
@ -202,7 +202,7 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
>
<router-link
class="funkwhale link user"
:to="{name: 'profile.overview', params: {username: object.actor.name}}"
:to="{ name: 'profile.overview', params: { username: object.actor.name } }"
>
<span class="at symbol" />{{ object.actor.name }}
</router-link>
@ -235,9 +235,11 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
@include light-theme {
border-color: var(--fw-gray-300);
}
@include dark-theme {
border-color: var(--fw-gray-800);
}
display: grid;
grid-template-columns: auto 1fr auto;
gap: 12px;
@ -247,25 +249,25 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
border-bottom: 1px solid;
}
> .activity-image {
>.activity-image {
width: 40px;
aspect-ratio: 1;
overflow: hidden;
border-radius: var(--fw-border-radius);
> img {
>img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
}
> i {
>i {
font-size: 40px;
line-height: 36px;
}
> .play-button {
>.play-button {
position: absolute;
top: 0;
left: 0;
@ -284,7 +286,7 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
}
}
> .activity-content {
>.activity-content {
a {
text-decoration: none;
@ -295,9 +297,10 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
}
}
> .track-title {
>.track-title {
font-weight: 700;
line-height: 1.5em;
@include dark-theme {
color: var(--fw-gray-300);
}
@ -307,7 +310,8 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
font-size: 15px;
}
.user, time {
.user,
time {
line-height: 1.5em;
font-size: 0.8125rem;
color: var(--fw-gray-500);

Wyświetl plik

@ -204,7 +204,7 @@ const remove = async () => {
v-if="totalDuration > 0"
:duration="totalDuration"
/>
<!--TODO: License -->
<!--TODO: License -->
</Layout>
</Layout>
<RenderedDescription
@ -295,13 +295,17 @@ const remove = async () => {
</template>
<style scoped lang="scss">
.meta {
font-size: 15px;
@include light-theme {
color: var(--fw-gray-700);
}
@include dark-theme {
color: var(--fw-gray-500);
}
@import '~/style/funkwhale.scss';
.meta {
font-size: 15px;
@include light-theme {
color: var(--fw-gray-700);
}
@include dark-theme {
color: var(--fw-gray-500);
}
}
</style>

Wyświetl plik

@ -128,32 +128,79 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</script>
<template>
<Layout v-title="labels.title" stack main>
<Header :h1="t('components.library.Artists.header.browse')" page-heading />
<Layout form flex :class="['ui', { 'loading': isLoading }, 'form']" @submit.prevent="search">
<Input id="artist-search" v-model="query" search name="search"
:label="t('components.library.Artists.label.search')" autofocus :placeholder="labels.searchPlaceholder" />
<Pills v-if="typeof tags === 'object'" :get="model => { tags = model.currents.map(({ label }) => label) }" :set="model => ({
currents: tags.map(tag => ({ type: 'custom' as const, label: tag })),
others: dataStore.tags().value
.filter(({ name }) => result?.results?.some((object) => object.tags?.includes(name)) && !tags.includes(name))
.map(({ name }) => ({ type: 'preset' as const, label: name })),
})" :label="t('components.library.Artists.label.tags')" style="max-width: 350px;" />
<Layout stack no-gap label for="artist-ordering">
<Layout
v-title="labels.title"
stack
main
>
<Header
:h1="t('components.library.Artists.header.browse')"
page-heading
/>
<Layout
form
flex
:class="['ui', { 'loading': isLoading }, 'form']"
@submit.prevent="search"
>
<Input
id="artist-search"
v-model="query"
search
name="search"
:label="t('components.library.Artists.label.search')"
autofocus
:placeholder="labels.searchPlaceholder"
/>
<Pills
v-if="typeof tags === 'object'"
:get="model => { tags = model.currents.map(({ label }) => label) }"
:set="model => ({
currents: tags.map(tag => ({ type: 'custom' as const, label: tag })),
others: dataStore.tags().value
.filter(({ name }) => result?.results?.some((object) => object.tags?.includes(name)) && !tags.includes(name))
.map(({ name }) => ({ type: 'preset' as const, label: name })),
})"
:label="t('components.library.Artists.label.tags')"
style="max-width: 350px;"
/>
<Layout
stack
no-gap
label
for="artist-ordering"
>
<span class="label">
{{ t('components.library.Artists.ordering.label') }}
</span>
<select id="artist-ordering" v-model="ordering" class="dropdown">
<option v-for="(option, key) in orderingOptions" :key="key" :value="option[0]">
<select
id="artist-ordering"
v-model="ordering"
class="dropdown"
>
<option
v-for="(option, key) in orderingOptions"
:key="key"
:value="option[0]"
>
{{ sharedLabels.filters[option[1]] }}
</option>
</select>
</Layout>
<Layout stack no-gap label for="artist-ordering-direction">
<Layout
stack
no-gap
label
for="artist-ordering-direction"
>
<span class="label">
{{ t('components.library.Artists.ordering.direction.label') }}
</span>
<select id="artist-ordering-direction" v-model="orderingDirection" class="dropdown">
<select
id="artist-ordering-direction"
v-model="orderingDirection"
class="dropdown"
>
<option value="+">
{{ t('components.library.Artists.ordering.direction.ascending') }}
</option>
@ -162,42 +209,86 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</option>
</select>
</Layout>
<Layout stack no-gap label for="artist-results">
<Layout
stack
no-gap
label
for="artist-results"
>
<span class="label">
{{ t('components.library.Artists.pagination.results') }}
</span>
<select id="artist-results" v-model="paginateBy" class="dropdown">
<option v-for="opt in paginateOptions" :key="opt" :value="opt">
<select
id="artist-results"
v-model="paginateBy"
class="dropdown"
>
<option
v-for="opt in paginateOptions"
:key="opt"
:value="opt"
>
{{ opt }}
</option>
</select>
</Layout>
<Toggle id="exclude-compilation" v-model="excludeCompilation"
:label="t('components.library.Artists.label.excludeCompilation')" true-value="true" false-value="null"
type="checkbox" />
<Toggle
id="exclude-compilation"
v-model="excludeCompilation"
:label="t('components.library.Artists.label.excludeCompilation')"
true-value="true"
false-value="null"
type="checkbox"
/>
</Layout>
<Loader v-if="isLoading" />
<Pagination v-if="page && result && result.count > paginateBy" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" />
<Layout v-if="result && result.results.length > 0" grid
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;">
<ArtistCard v-for="artist in result.results" :key="artist.id" :artist="artist" />
<Pagination
v-if="page && result && result.count > paginateBy"
v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)"
/>
<Layout
v-if="result && result.results.length > 0"
grid
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;"
>
<ArtistCard
v-for="artist in result.results"
:key="artist.id"
:artist="artist"
/>
</Layout>
<Layout v-else-if="result && result.results.length === 0" stack>
<Layout
v-else-if="result && result.results.length === 0"
stack
>
<Alert yellow>
<i class="compact disc icon" />
{{ t('components.library.Artists.empty.noResults') }}
</Alert>
<Card v-if="store.state.auth.authenticated" :title="t('components.library.Artists.button.upload')" solid small
primary style="text-align: center;" :to="useModal('upload').to">
<Card
v-if="store.state.auth.authenticated"
:title="t('components.library.Artists.button.upload')"
solid
small
primary
style="text-align: center;"
:to="useModal('upload').to"
>
<template #image>
<i class="bi bi-upload" style="font-size: 100px; position: relative; top: 50px;" />
<i
class="bi bi-upload"
style="font-size: 100px; position: relative; top: 50px;"
/>
</template>
</Card>
</Layout>
<Spacer grow />
<Pagination v-if="page && result && result.count > paginateBy" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" />
<Pagination
v-if="page && result && result.count > paginateBy"
v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)"
/>
</Layout>
</template>

Wyświetl plik

@ -17,7 +17,8 @@ const props = defineProps<Props>()
...$attrs,
...color(props, ['solid'])(
align(props)(
))}"
))
}"
>
<slot />

Wyświetl plik

@ -80,71 +80,118 @@ onUnmounted(() =>
</script>
<template>
<div v-if="split" class="funkwhale split-button">
<button v-if="!dropdownOnly" ref="button" v-bind="{
...$attrs,
...color(props, ['interactive'])(
width(props, isIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
align(props, { alignText: 'center' })(
)))
}" class="funkwhale button split-main" :autofocus="autofocus || undefined"
:disabled="disabled || undefined" :aria-pressed="props.ariaPressed" :class="{
<div
v-if="split"
class="funkwhale split-button"
>
<button
v-if="!dropdownOnly"
ref="button"
v-bind="{
...$attrs,
...color(props, ['interactive'])(
width(props, isIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
align(props, { alignText: 'center' })(
))
)
}"
class="funkwhale button split-main"
:autofocus="autofocus || undefined"
:disabled="disabled || undefined"
:aria-pressed="props.ariaPressed"
:class="{
'is-loading': isLoading,
'is-icon-only': isIconOnly,
'has-icon': !!icon,
'is-round': round,
'is-shadow': shadow,
}" @click="click">
}"
@click="click"
>
<slot name="main">
<i v-if="icon && !icon.startsWith('right ')" :class="['bi', icon]" />
<i
v-if="icon && !icon.startsWith('right ')"
:class="['bi', icon]"
/>
<span v-if="!isIconOnly">
<slot />
</span>
<i v-if="icon && icon.startsWith('right ')" :class="['bi', icon.replace('right ', '')]" />
<i
v-if="icon && icon.startsWith('right ')"
:class="['bi', icon.replace('right ', '')]"
/>
</slot>
<Loader v-if="isLoading" :container="false" />
<Loader
v-if="isLoading"
:container="false"
/>
</button>
<button v-bind="{
...$attrs,
...color(props, ['interactive'])(
width(props, isSplitIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
align(props, { alignSelf: 'start', alignText: 'center' })(
)))
}" :disabled="disabled || undefined" :autofocus="autofocus || undefined" :class="[
'funkwhale',
'button',
{
'split-toggle': true,
'is-loading': isLoading,
'is-icon-only': isSplitIconOnly,
'has-icon': !!splitIcon,
'is-round': round,
'is-shadow': shadow
}
]" @click="onSplitClick">
<button
v-bind="{
...$attrs,
...color(props, ['interactive'])(
width(props, isSplitIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
align(props, { alignSelf: 'start', alignText: 'center' })(
)))
}"
:disabled="disabled || undefined"
:autofocus="autofocus || undefined"
:class="[
'funkwhale',
'button',
{
'split-toggle': true,
'is-loading': isLoading,
'is-icon-only': isSplitIconOnly,
'has-icon': !!splitIcon,
'is-round': round,
'is-shadow': shadow
}
]"
@click="onSplitClick"
>
<span v-if="splitTitle">{{ splitTitle }}</span>
<i :class="['bi', splitIcon]" />
</button>
</div>
<button v-else ref="button" v-bind="color(props, ['interactive'])(
width(props, isIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
align(props, { alignText: 'center' })(
)))" :disabled="disabled || undefined" :autofocus="autofocus || undefined" class="funkwhale button"
:aria-pressed="props.ariaPressed" :class="{
<button
v-else
ref="button"
v-bind="color(props, ['interactive'])(
width(props, isIconOnly ? ['square'] : ['normalHeight', 'buttonWidth'])(
align(props, { alignText: 'center' })(
)))"
:disabled="disabled || undefined"
:autofocus="autofocus || undefined"
class="funkwhale button"
:aria-pressed="props.ariaPressed"
:class="{
'is-loading': isLoading,
'is-icon-only': isIconOnly,
'has-icon': !!icon,
'is-round': round,
'is-shadow': shadow,
}" :type="onClick ? 'button' : 'submit' /* Prevents default `submit` if onCLick is set */" @click="click">
<i v-if="icon && !icon.startsWith('right ')" :class="['bi', icon]" />
}"
:type="onClick ? 'button' : 'submit' /* Prevents default `submit` if onCLick is set */"
@click="click"
>
<i
v-if="icon && !icon.startsWith('right ')"
:class="['bi', icon]"
/>
<span v-if="!isIconOnly">
<slot />
</span>
<i v-if="icon && icon.startsWith('right ')" :class="['bi', icon.replace('right ', '')]" />
<Loader v-if="isLoading" :container="false" />
<i
v-if="icon && icon.startsWith('right ')"
:class="['bi', icon.replace('right ', '')]"
/>
<Loader
v-if="isLoading"
:container="false"
/>
</button>
</template>

Wyświetl plik

@ -9,18 +9,18 @@ import Button from '~/components/ui/Button.vue'
import Layout from '~/components/ui/Layout.vue'
const { icon, placeholder, ...props } = defineProps<{
icon?: string;
placeholder?: string;
password?: true;
search?: true;
numeric?: true;
label?: string;
autofocus?: boolean;
reset?:() => void;
} & (ColorProps | DefaultProps | PastelProps)
& VariantProps
& RaisedProps
& WidthProps
icon?: string;
placeholder?: string;
password?: true;
search?: true;
numeric?: true;
label?: string;
autofocus?: boolean;
reset?: () => void;
} & (ColorProps | DefaultProps | PastelProps)
& VariantProps
& RaisedProps
& WidthProps
>()
// TODO(A11y): Add `inputmode="numeric" pattern="[0-9]*"` to input if model type is number:
@ -56,7 +56,7 @@ onUnmounted(() =>
previouslyFocusedElement.value?.focus()
)
const model = defineModel<string|number>({ required: true })
const model = defineModel<string | number>({ required: true })
</script>
<template>
@ -82,7 +82,7 @@ const model = defineModel<string|number>({ required: true })
</span>
<input
v-bind="{...$attrs, ...attributes, ...color(props, ['solid', 'default', 'secondary'])(width(props)())}"
v-bind="{ ...$attrs, ...attributes, ...color(props, ['solid', 'default', 'secondary'])(width(props)()) }"
ref="input"
v-model="model"
:autofocus="autofocus || undefined"
@ -122,7 +122,7 @@ const model = defineModel<string|number>({ required: true })
<!-- Password -->
<button
v-if="props.password"
v-bind="{...$attrs, ...attributes, ...color(props, ['solid', 'default', 'secondary'])()}"
v-bind="{ ...$attrs, ...attributes, ...color(props, ['solid', 'default', 'secondary'])() }"
style="background:transparent; border:none; appearance:none; height:calc(100% - 16px); color:var(--color); cursor:pointer;"
role="switch"
type="button"

Wyświetl plik

@ -16,10 +16,10 @@ const props = defineProps<{
icon?: string;
round?: true;
autofocus? : boolean
forceUnderline? : true
autofocus?: boolean
forceUnderline?: true
} & RouterLinkProps
&(ColorProps | DefaultProps)
& (ColorProps | DefaultProps)
& VariantProps
& WidthProps
& AlignmentProps>()
@ -40,8 +40,8 @@ const [fontWeight, activeFontWeight] = 'solid' in props || props.thickWhenActive
const isIconOnly = computed(() =>
!!props.icon && (
!useSlots().default
|| 'square' in props && props.square
|| 'squareSmall' in props && props.squareSmall
|| 'square' in props && props.square
|| 'squareSmall' in props && props.squareSmall
)
)
@ -55,13 +55,12 @@ onMounted(() => {
<template>
<component
:is="isExternalLink ? 'a' : RouterLink"
v-bind="
color(props, ['interactive'])(
width(props,
isNoColors(props) ? [] : ['normalHeight', 'solid' in props ? 'buttonWidth' : 'auto']
)(
align(props, 'solid' in props ? {alignText: 'center'} : {})(
)))"
v-bind="color(props, ['interactive'])(
width(props,
isNoColors(props) ? [] : ['normalHeight', 'solid' in props ? 'buttonWidth' : 'auto']
)(
align(props, 'solid' in props ? { alignText: 'center' } : {})(
)))"
ref="button"
:autofocus="autofocus || undefined"
:class="[
@ -88,81 +87,85 @@ onMounted(() => {
</template>
<style module lang="scss">
.link {
.link {
// Layout
// Layout
--padding: 16px;
--shift-by: 0.5px;
--padding: 16px;
--shift-by: 0.5px;
position: relative;
display: inline-flex;
white-space: nowrap;
align-items: center;
padding: calc(var(--padding) / 2 - var(--shift-by)) var(--padding) calc(var(--padding) / 2 + var(--shift-by)) var(--padding);
&.is-icon-only {
padding: var(--padding);
}
&.no-spacing {
padding: 0;
margin: 0;
font-size: 1em;
}
// Font
font-family: var(--font-main);
font-weight: v-bind(fontWeight);
font-size: 14px;
line-height: 1em;
// Content
>span {
position: relative;
display: inline-flex;
white-space: nowrap;
align-items: center;
top: calc(0px - var(--shift-by));
}
padding: calc(var(--padding) / 2 - var(--shift-by)) var(--padding) calc(var(--padding) / 2 + var(--shift-by)) var(--padding);
&.is-icon-only {
padding: var(--padding);
}
&.no-spacing {
padding: 0;
margin: 0;
font-size: 1em;
// Decoration
&:not([disabled]) {
cursor: pointer;
}
transform: translateX(var(--fw-translate-x)) translateY(var(--fw-translate-y)) scale(var(--fw-scale));
transition:background-color .2s,
border-color .3s;
&:not(.force-underline) {
text-decoration: none;
// background-color: transparent;
// border-color: transparent;
}
border-radius: var(--fw-border-radius);
&.is-round {
border-radius: 100vh;
}
// States
&:global(.router-link-exact-active) {
font-weight: v-bind(activeFontWeight);
}
// Icon
>i:global(.bi) {
font-size: 1.2rem;
&.large {
font-size: 2rem;
}
// Font
font-family: $font-main;
font-weight: v-bind(fontWeight);
font-size: 14px;
line-height: 1em;
// Content
> span {
position: relative;
top: calc(0px - var(--shift-by));
}
// Decoration
&:not([disabled]) {
cursor: pointer;
}
transform: translateX(var(--fw-translate-x)) translateY(var(--fw-translate-y)) scale(var(--fw-scale));
transition:background-color .2s, border-color .3s;
&:not(.force-underline) {
text-decoration: none;
// background-color: transparent;
// border-color: transparent;
}
border-radius: var(--fw-border-radius);
&.is-round {
border-radius: 100vh;
}
// States
&:global(.router-link-exact-active) {
font-weight: v-bind(activeFontWeight);
}
// Icon
> i:global(.bi) {
font-size: 1.2rem;
&.large {
font-size:2rem;
}
&+span:not(:empty) {
margin-left: 1ch;
}
&+span:not(:empty) {
margin-left: 1ch;
}
}
}
</style>

Wyświetl plik

@ -19,8 +19,8 @@ const shouldDelayClose = ref(true)
const isOpenDelayed = refDebounced(isOpen, () => isOpen.value ? 0 : (shouldDelayClose.value ? 300 : 0))
const { positioning = 'vertical', ...colorProps } = defineProps<{
positioning?:'horizontal' | 'vertical'
} &(ColorProps | DefaultProps) & RaisedProps>()
positioning?: 'horizontal' | 'vertical'
} & (ColorProps | DefaultProps) & RaisedProps>()
// Template refs
const popover = ref()

Wyświetl plik

@ -16,12 +16,12 @@ const keys = computed(() => Object.keys(props.options) as T[])
const model = defineModel<T | undefined>({ required: true })
const index = computed({
get () {
get() {
return model.value
? keys.value.indexOf(model.value)
: undefined
},
set (newIndex) {
set(newIndex) {
model.value = newIndex
? keys.value[newIndex]
: undefined
@ -43,8 +43,8 @@ onMounted(() => {
:style="`
--step-size: calc(100% / ${keys.length + 2});
--slider-width: calc(var(--step-size) * ${keys.length - 1} + 16px);
--slider-opacity: ${ index === undefined ? .5 : 1 };
--current-step: ${ index === undefined ? keys.length - 1 : index };
--slider-opacity: ${index === undefined ? .5 : 1};
--current-step: ${index === undefined ? keys.length - 1 : index};
`"
>
<!-- Label -->
@ -71,7 +71,7 @@ onMounted(() => {
<button
v-for="key in keys"
:key="key"
:class="[$style.key, { [$style.current]: key === model } ]"
:class="[$style.key, { [$style.current]: key === model }]"
style="flex-basis: var(--step-size); padding-bottom: 8px;"
type="button"
tabindex="-1"

Wyświetl plik

@ -230,7 +230,7 @@ onMounted(() => {
:required="required"
:placeholder="placeholder"
:rows="initialLines"
:style="`min-height:${((typeof(initialLines) === 'string' ? parseInt(initialLines) : (initialLines ?? 3)) + 1.2) * 1.5}rem`"
:style="`min-height:${((typeof (initialLines) === 'string' ? parseInt(initialLines) : (initialLines ?? 3)) + 1.2) * 1.5}rem`"
@click="updateLineNumber"
@mousedown.stop
@mouseup.stop
@ -343,7 +343,8 @@ onMounted(() => {
<span
v-if="charLimit !== Infinity && typeof charLimit === 'number'"
class="letter-count"
>{{ charLimit - model.length }}</span>
>{{ charLimit -
model.length }}</span>
<Spacer />

Wyświetl plik

@ -5,7 +5,7 @@ import { useScroll } from '@vueuse/core'
import Button from '~/components/ui/Button.vue'
const { heading = 'h1' } = defineProps<{heading?:'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'}>()
const { heading = 'h1' } = defineProps<{ heading?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' }>()
const toc = ref()

Wyświetl plik

@ -45,24 +45,30 @@ const diameter = big ? '28px' : '20px'
&[checked] {
--void-color: var(--void-on-background-color);
--pin-color: var(--void-on-pin-color);
&::after{
transform:translateX(var(--diameter));
&::after {
transform: translateX(var(--diameter));
}
}
&:hover, &:has(:focus-visible) {
&:hover,
&:has(:focus-visible) {
--void-color: var(--void-off-hover-background-color);
--pin-color: var(--void-off-hover-pin-color);
&[checked] {
--void-color: var(--void-on-hover-background-color);
--pin-color: var(--void-on-hover-pin-color);
}
}
&::before, &::after {
&::before,
&::after {
content: '';
position: absolute;
border-radius: var(--diameter);
}
&::before {
height: var(--diameter);
aspect-ratio: 2;
@ -70,6 +76,7 @@ const diameter = big ? '28px' : '20px'
left: 0;
top: calc(var(--padding) * 2 - var(--diameter) / 2);
}
&::after {
height: calc(var(--diameter) - var(--lineWidth) * 2);
aspect-ratio: 1;
@ -79,7 +86,7 @@ const diameter = big ? '28px' : '20px'
transition: all .2s;
}
> span {
>span {
padding-left: calc(var(--diameter) * 2 - 12px);
}
}

Wyświetl plik

@ -78,14 +78,14 @@ const labels = computed(() => ({
<template #items>
<PopoverItem
v-if="store.state.auth.authenticated"
:to="{name: 'profile.overview', params: { username: store.state.auth.username },}"
:to="{ name: 'profile.overview', params: { username: store.state.auth.username }, }"
>
<i class="bi bi-person-fill" />
{{ labels.profile }}
</PopoverItem>
<PopoverItem
v-if="store.state.auth.authenticated"
:to="{name: 'notifications'}"
:to="{ name: 'notifications' }"
>
<i class="bi bi-inbox-fill" />
{{ labels.notifications }}
@ -123,7 +123,7 @@ const labels = computed(() => ({
<PopoverItem
v-for="th in themes"
:key="th.key"
@click="theme=th.key"
@click="theme = th.key"
>
<i :class="th.icon" />
{{ th.name }}

Wyświetl plik

@ -88,7 +88,7 @@ const tabs = computed(() => [
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
</template>
<style scoped lang="scss">
<style scoped>
h1 {
font-size: 36px;
font-weight: 900;
@ -109,7 +109,8 @@ h1 {
.filesystem-stats {
color: var(--fw-gray-700);
> .flex {
>.flex {
padding: 1ch;
}
}
@ -166,6 +167,7 @@ h1 {
align-items: center;
justify-content: center;
}
.funkwhale.card {
margin-bottom: 2rem;
transition: margin-bottom 0.2s ease;
@ -211,7 +213,7 @@ label {
align-items: center;
margin: 2rem 0 1rem;
> .file-count {
>.file-count {
margin-right: auto;
color: var(--fw-gray-600);
font-weight: 900;
@ -228,7 +230,7 @@ label {
border-top: 1px solid var(--fw-gray-200);
}
> .track-cover {
>.track-cover {
height: 3rem;
width: 3rem;
border-radius: 0.5rem;
@ -243,7 +245,7 @@ label {
position: relative;
overflow: hidden;
> img {
>img {
position: absolute;
top: 0;
left: 0;
@ -287,6 +289,7 @@ label {
opacity: 0;
}
}
.track-progress {
font-size: 0.875rem;
color: var(--fw-gray-600);

Wyświetl plik

@ -162,7 +162,7 @@ const { state: items } = useAsyncState(
justify-content: center;
}
.upload > .funkwhale.button {
.upload>.funkwhale.button {
margin-left: 0;
}
@ -171,22 +171,22 @@ const { state: items } = useAsyncState(
display: flex;
align-items: center;
> .box {
>.box {
width: 2.75rem;
height: 2.75rem;
flex-shrink: 0;
background: var(--fw-pastel-blue-1);
border-radius: 8px;
margin-right:8px;
margin-right: 8px;
+ div {
+div {
width: 100%;
> :last-child {
display: flex;
width: 100%;
> div {
>div {
margin-left: auto;
}
}
@ -197,5 +197,4 @@ const { state: items } = useAsyncState(
font-size: 1rem;
}
}
</style>

Wyświetl plik

@ -459,6 +459,7 @@ h3.category {
@include light-theme {
border-color: var(--fw-gray-300);
}
@include dark-theme {
border-color: var(--fw-gray-800);
}

Wyświetl plik

@ -502,9 +502,7 @@ const open = ref(false)
flex
class="details"
>
<Link
:to="{name: 'manage.library.tracks', query: {q: getQuery('album_id', object?.id) }}"
>
<Link :to="{ name: 'manage.library.tracks', query: { q: getQuery('album_id', object?.id) } }">
{{ t('views.admin.library.AlbumDetail.link.tracks') }}
</Link>
<Spacer
@ -540,6 +538,7 @@ h3.category {
@include light-theme {
border-color: var(--fw-gray-300);
}
@include dark-theme {
border-color: var(--fw-gray-800);
}
@ -564,5 +563,4 @@ h3.category {
border-bottom: 1px solid;
}
}
</style>

Wyświetl plik

@ -141,7 +141,7 @@ const open = ref(false)
primary
low-height
icon="bi-info-circle"
:to="{name: 'library.artists.detail', params: {id: object.id }}"
:to="{ name: 'library.artists.detail', params: { id: object.id } }"
>
{{ t('views.admin.library.ArtistDetail.link.localProfile') }}
</Link>
@ -160,7 +160,7 @@ const open = ref(false)
primary
low-height
icon="bi-pencil-fill"
:to="{name: 'library.artists.edit', params: {id: object.id }}"
:to="{ name: 'library.artists.edit', params: { id: object.id } }"
>
{{ t('views.admin.library.ArtistDetail.button.edit') }}
</Link>
@ -256,7 +256,7 @@ const open = ref(false)
>
<Link
class="label"
:to="{name: 'manage.library.artists', query: {q: getQuery('category', object?.content_category) }}"
:to="{ name: 'manage.library.artists', query: { q: getQuery('category', object?.content_category) } }"
>
{{ t('views.admin.library.ArtistDetail.link.category') }}
</Link>
@ -273,7 +273,7 @@ const open = ref(false)
>
<Link
class="label"
:to="{name: 'manage.moderation.domains.detail', params: {id: object?.domain }}"
:to="{ name: 'manage.moderation.domains.detail', params: { id: object?.domain } }"
>
{{ t('views.admin.library.ArtistDetail.link.domain') }}
</Link>
@ -374,7 +374,7 @@ const open = ref(false)
>
<Link
class="label"
:to="{name: 'manage.moderation.reports.list', query: {q: getQuery('target', `artist:${object?.id}`) }}"
:to="{ name: 'manage.moderation.reports.list', query: { q: getQuery('target', `artist:${object?.id}`) } }"
>
{{ t('views.admin.library.ArtistDetail.link.reports') }}
</Link>
@ -390,7 +390,7 @@ const open = ref(false)
>
<Link
class="label"
:to="{name: 'manage.library.edits', query: {q: getQuery('target', 'artist ' + object?.id) }}"
:to="{ name: 'manage.library.edits', query: { q: getQuery('target', 'artist ' + object?.id) } }"
>
{{ t('views.admin.library.ArtistDetail.link.edits') }}
</Link>
@ -441,7 +441,7 @@ const open = ref(false)
>
<Link
class="label"
:to="{name: 'manage.library.libraries', query: {q: getQuery('artist_id', object?.id) }}"
:to="{ name: 'manage.library.libraries', query: { q: getQuery('artist_id', object?.id) } }"
>
{{ t('views.admin.library.ArtistDetail.link.libraries') }}
</Link>
@ -457,7 +457,7 @@ const open = ref(false)
>
<Link
class="label"
:to="{name: 'manage.library.uploads', query: {q: getQuery('artist_id', object?.id) }}"
:to="{ name: 'manage.library.uploads', query: { q: getQuery('artist_id', object?.id) } }"
>
{{ t('views.admin.library.ArtistDetail.link.uploads') }}
</Link>
@ -473,7 +473,7 @@ const open = ref(false)
>
<Link
class="label"
:to="{name: 'manage.library.albums', query: {q: getQuery('artist_id', object?.id) }}"
:to="{ name: 'manage.library.albums', query: { q: getQuery('artist_id', object?.id) } }"
>
{{ t('views.admin.library.ArtistDetail.link.albums') }}
</Link>
@ -489,7 +489,7 @@ const open = ref(false)
>
<Link
class="label"
:to="{name: 'manage.library.tracks', query: {q: getQuery('artist_id', object?.id) }}"
:to="{ name: 'manage.library.tracks', query: { q: getQuery('artist_id', object?.id) } }"
>
{{ t('views.admin.library.ArtistDetail.link.tracks') }}
</Link>
@ -524,6 +524,7 @@ h3.category {
@include light-theme {
border-color: var(--fw-gray-300);
}
@include dark-theme {
border-color: var(--fw-gray-800);
}

Wyświetl plik

@ -250,6 +250,7 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
@include light-theme {
border-color: var(--fw-gray-300);
}
@include dark-theme {
border-color: var(--fw-gray-800);
}

Wyświetl plik

@ -499,6 +499,8 @@ const getQuery = (field: string, value: string) => `${field}:"${value}"`
</template>
<style scoped lang="scss">
@import '~/style/funkwhale.scss';
.channel-image {
width: 200px;
height: 200px;
@ -519,6 +521,7 @@ h3.category {
@include light-theme {
border-color: var(--fw-gray-300);
}
@include dark-theme {
border-color: var(--fw-gray-800);
}

Wyświetl plik

@ -392,7 +392,9 @@ const displayName = (object: any) => object?.filename ?? object?.source ?? objec
/>
<span class="value">
<template v-if="object?.bitrate">
{{ t('views.admin.library.UploadDetail.table.audioContent.bitrate.value', { bitrate: humanSize(object?.bitrate) }) }}
{{ t('views.admin.library.UploadDetail.table.audioContent.bitrate.value', {
bitrate:
humanSize(object?.bitrate) }) }}
</template>
<span v-else>
{{ t('views.admin.library.UploadDetail.notApplicable') }}

Wyświetl plik

@ -409,7 +409,9 @@ const updatePolicy = (newPolicy: InstancePolicy) => {
flex
class="details"
>
<router-link :to="{name: 'manage.moderation.reports.list', query: {q: getQuery('target', `account:${object?.full_username}`) }}">
<router-link
:to="{ name: 'manage.moderation.reports.list', query: { q: getQuery('target', `account:${object?.full_username}`) } }"
>
{{ t('views.admin.moderation.AccountsDetail.link.linkedReports') }}
</router-link>
<Spacer
@ -423,7 +425,9 @@ const updatePolicy = (newPolicy: InstancePolicy) => {
flex
class="details"
>
<router-link :to="{name: 'manage.moderation.requests.list', query: {q: getQuery('submitter', `${object?.full_username}`) }}">
<router-link
:to="{ name: 'manage.moderation.requests.list', query: { q: getQuery('submitter', `${object?.full_username}`) } }"
>
{{ t('views.admin.moderation.AccountsDetail.link.requests') }}
</router-link>
<Spacer
@ -525,9 +529,11 @@ const updatePolicy = (newPolicy: InstancePolicy) => {
text-align: center;
align-content: center;
border-radius: 50%;
@include light-theme {
background-color: var(--fw-gray-200);
}
@include dark-theme {
background-color: var(--fw-gray-800);
}
@ -547,6 +553,7 @@ h3.category {
@include light-theme {
border-color: var(--fw-gray-300);
}
@include dark-theme {
border-color: var(--fw-gray-800);
}

Wyświetl plik

@ -183,9 +183,7 @@ const setAllowList = async (value: boolean) => {
</Layout>
</Header>
<Alert
blue
>
<Alert blue>
<template v-if="isLoadingPolicy">
<div class="paragraph">
<div class="line" />
@ -296,7 +294,11 @@ const setAllowList = async (value: boolean) => {
grow
/>
<span class="value">
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.software.value', {name: get(object, 'nodeinfo.payload.software.name', t('views.admin.moderation.DomainsDetail.notApplicable')), version: get(object, 'nodeinfo.payload.software.version', t('views.admin.moderation.DomainsDetail.notApplicable'))}) }}
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.software.value', {
name: get(object,
'nodeinfo.payload.software.name', t('views.admin.moderation.DomainsDetail.notApplicable')), version:
get(object,
'nodeinfo.payload.software.version', t('views.admin.moderation.DomainsDetail.notApplicable'))}) }}
</span>
</Layout>
<Layout
@ -312,7 +314,11 @@ const setAllowList = async (value: boolean) => {
grow
/>
<span class="value">
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.nodeInfoStatus.value', {name: get(object, 'nodeinfo.payload.software.name', t('views.admin.moderation.DomainsDetail.notApplicable')), version: get(object, 'nodeinfo.payload.software.version', t('views.admin.moderation.DomainsDetail.notApplicable'))}) }}
{{ t('views.admin.moderation.DomainsDetail.table.instanceData.nodeInfoStatus.value', {
name: get(object,
'nodeinfo.payload.software.name', t('views.admin.moderation.DomainsDetail.notApplicable')), version:
get(object,
'nodeinfo.payload.software.version', t('views.admin.moderation.DomainsDetail.notApplicable'))}) }}
</span>
<span :data-tooltip="object.nodeinfo.error"><i class="bi bi-question-circle" /></span>
</Layout>
@ -358,7 +364,7 @@ const setAllowList = async (value: boolean) => {
>
<Link
class="label"
:to="{name: 'manage.moderation.accounts.list', query: {q: 'domain:' + object?.name }}"
:to="{ name: 'manage.moderation.accounts.list', query: { q: 'domain:' + object?.name } }"
>
{{ t('views.admin.moderation.DomainsDetail.link.knownAccounts') }}
</Link>
@ -415,7 +421,7 @@ const setAllowList = async (value: boolean) => {
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.channels', query: {q: getQuery('domain', object.name) }}">
<router-link :to="{ name: 'manage.channels', query: { q: getQuery('domain', object.name) } }">
{{ t('views.admin.moderation.DomainsDetail.link.channels') }}
</router-link>
</span>
@ -430,7 +436,7 @@ const setAllowList = async (value: boolean) => {
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.library.libraries', query: {q: getQuery('domain', object.name) }}">
<router-link :to="{ name: 'manage.library.libraries', query: { q: getQuery('domain', object.name) } }">
{{ t('views.admin.moderation.DomainsDetail.link.libraries') }}
</router-link>
</span>
@ -445,7 +451,7 @@ const setAllowList = async (value: boolean) => {
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.library.uploads', query: {q: getQuery('domain', object.name) }}">
<router-link :to="{ name: 'manage.library.uploads', query: { q: getQuery('domain', object.name) } }">
{{ t('views.admin.moderation.DomainsDetail.link.uploads') }}
</router-link>
</span>
@ -460,7 +466,7 @@ const setAllowList = async (value: boolean) => {
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.library.artists', query: {q: getQuery('domain', object.name) }}">
<router-link :to="{ name: 'manage.library.artists', query: { q: getQuery('domain', object.name) } }">
{{ t('views.admin.moderation.DomainsDetail.link.artists') }}
</router-link>
</span>
@ -475,7 +481,7 @@ const setAllowList = async (value: boolean) => {
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.library.albums', query: {q: getQuery('domain', object.name) }}">
<router-link :to="{ name: 'manage.library.albums', query: { q: getQuery('domain', object.name) } }">
{{ t('views.admin.moderation.DomainsDetail.link.albums') }}
</router-link>
</span>
@ -490,7 +496,7 @@ const setAllowList = async (value: boolean) => {
class="details"
>
<span class="label">
<router-link :to="{name: 'manage.library.tracks', query: {q: getQuery('domain', object.name) }}">
<router-link :to="{ name: 'manage.library.tracks', query: { q: getQuery('domain', object.name) } }">
{{ t('views.admin.moderation.DomainsDetail.link.tracks') }}
</router-link>
</span>

Wyświetl plik

@ -83,17 +83,17 @@ watch(props, fetchData, { immediate: true })
const { copy, copied, isSupported } = useClipboard()
const tabs = ref([{
title: t('views.auth.ProfileBase.link.overview') ,
to: { name: 'profile.overview', params: routerParams }
title: t('views.auth.ProfileBase.link.overview'),
to: { name: 'profile.overview', params: routerParams }
}, {
title: t('views.auth.ProfileBase.link.activity') ,
to: { name: 'profile.activity', params: routerParams }
title: t('views.auth.ProfileBase.link.activity'),
to: { name: 'profile.activity', params: routerParams }
}, ...(
store.state.auth.authenticated && fullUsername.value === store.state.auth.fullUsername
? [{
title: t('views.auth.ProfileBase.link.manageUploads') ,
to: { name: 'profile.manageUploads', params: routerParams }
}]
title: t('views.auth.ProfileBase.link.manageUploads'),
to: { name: 'profile.manageUploads', params: routerParams }
}]
: []
)])
@ -115,7 +115,7 @@ const isOpen = useModal('artist-description').isOpen
:action="{
text: t('views.auth.ProfileBase.link.edit'),
// @ts-ignore
to:'/settings',
to: '/settings',
// @ts-ignore
primary: true,
// @ts-ignore

Wyświetl plik

@ -153,12 +153,12 @@ const updateSubscriptionCount = (delta: number) => {
const tabs = ref([
{
title: t('views.channels.DetailBase.link.channelOverview'),
to: {name: 'channels.detail', params: { id: props.id }}
to: { name: 'channels.detail', params: { id: props.id } }
},
{
title: t('views.channels.DetailBase.link.channelEpisodes'),
to: {name: 'channels.detail.episodes', params: { id: props.id }}
to: { name: 'channels.detail.episodes', params: { id: props.id } }
}
])
</script>
@ -200,14 +200,10 @@ const tabs = ref([
no-gap
>
<template v-if="totalTracks > 0">
<span
v-if="object.artist?.content_category === 'podcast'"
>
<span v-if="object.artist?.content_category === 'podcast'">
{{ t('views.channels.DetailBase.meta.episodes', totalTracks) }}
</span>
<span
v-else
>
<span v-else>
{{ t('views.channels.DetailBase.meta.tracks', totalTracks) }}
</span>
<i class="bi bi-dot" />
@ -232,9 +228,7 @@ const tabs = ref([
<span>
{{ t('views.channels.DetailBase.header.podcastChannel') }}
</span>
<span
v-if="!object.actor"
>
<span v-if="!object.actor">
<i class="bi bi-dot" />
<a
:href="object.url || object.rss_url"
@ -242,7 +236,7 @@ const tabs = ref([
target="_blank"
>
<i class="bi bi-box-arrow-up-right" />
{{ t('views.channels.DetailBase.link.mirrored', {domain: externalDomain}) }}
{{ t('views.channels.DetailBase.link.mirrored', { domain: externalDomain }) }}
</a>
</span>
</template>
@ -327,11 +321,11 @@ const tabs = ref([
target="_blank"
icon="bi-box-arrow-up-right"
>
{{ t('views.channels.DetailBase.link.domainView', {domain: object.actor.domain}) }}
{{ t('views.channels.DetailBase.link.domainView', { domain: object.actor.domain }) }}
</PopoverItem>
<hr>
<PopoverItem
v-for="obj in getReportableObjects({account: object.attributed_to, channel: object})"
v-for="obj in getReportableObjects({ account: object.attributed_to, channel: object })"
:key="obj.target.type + obj.target.id"
icon="bi-share"
@click.stop.prevent="report(obj)"
@ -412,10 +406,9 @@ const tabs = ref([
<Modal
v-if="isOwner"
v-model="showEditModal"
:title="
object.artist?.content_category === 'podcast'
? t('views.channels.DetailBase.header.podcastChannel')
: t('views.channels.DetailBase.header.artistChannel')
:title="object.artist?.content_category === 'podcast'
? t('views.channels.DetailBase.header.podcastChannel')
: t('views.channels.DetailBase.header.artistChannel')
"
>
<div class="scrolling content">

Wyświetl plik

@ -53,12 +53,12 @@ const fullPlaylistTracks = ref<FullPlaylistTrack[]>([])
const tracks = computed(() => fullPlaylistTracks.value.map(({ track }, index) => ({ ...track as Track, position: index + 1 })))
const updateTrack = (updatedTrack: Track) => {
fullPlaylistTracks.value = fullPlaylistTracks.value.map((item) =>
item.track.id === updatedTrack.id ? { ...item, track: updatedTrack } : item
);
fullPlaylistTracks.value = fullPlaylistTracks.value.map((item) =>
item.track.id === updatedTrack.id ? { ...item, track: updatedTrack } : item
);
};
useWebSocketHandler('playlist.track_updated', async (event) => {
updateTrack(event.track);
updateTrack(event.track);
});
const { t } = useI18n()
@ -136,7 +136,7 @@ const bgcolors = ref([
'#322f2f'
])
function shuffleArray (array: string[]): string[] {
function shuffleArray(array: string[]): string[] {
return [...array].sort(() => Math.random() - 0.5)
}
@ -149,7 +149,7 @@ const randomizedColors = computed(() => shuffleArray(bgcolors.value))
// })
// TODO: Implement shuffle
const shuffle = () => {}
const shuffle = () => { }
</script>
<template>
@ -194,9 +194,7 @@ const shuffle = () => {}
{{ playlist.actor.full_username }}
<i class="bi bi-dot" />
{{ t('views.playlists.Detail.meta.updated') }}
<HumanDate
:date="playlist.modification_date"
/>
<HumanDate :date="playlist.modification_date" />
</Layout>
</Layout>
<RenderedDescription
@ -337,9 +335,11 @@ const shuffle = () => {}
.meta {
font-size: 15px;
@include light-theme {
color: var(--fw-gray-700);
}
@include dark-theme {
color: var(--fw-gray-500);
}