fix(front): make (`lint:tsc`) completely happy

This commit is contained in:
upsiflu 2025-04-04 19:39:08 +02:00
parent d8a7c033eb
commit 1972f6c8a0
69 changed files with 264 additions and 166 deletions

View File

@ -18,7 +18,7 @@
"test:generate-mock-server": "msw-auto-mock ../docs/schema.yml -o test/msw-server.ts --node", "test:generate-mock-server": "msw-auto-mock ../docs/schema.yml -o test/msw-server.ts --node",
"lint": "yarn lint:es && yarn lint:tsc", "lint": "yarn lint:es && yarn lint:tsc",
"lint:es": "eslint --max-warnings 0 --cache --cache-strategy content --ext .ts,.js,.vue,.json,.html,.cjs . cypress public/embed.html src test ui-docs", "lint:es": "eslint --max-warnings 0 --cache --cache-strategy content --ext .ts,.js,.vue,.json,.html,.cjs . cypress public/embed.html src test ui-docs",
"lint:tsc": "vue-tsc --noEmit --incremental && tsc --noEmit --incremental -p cypress src test ui-docs", "lint:tsc": "vue-tsc --noEmit --incremental && tsc --noEmit --incremental --project tsconfig.json",
"generate-types-from-local-schema": "yarn run openapi-typescript ../api/funkwhale_api/common/schema.yml -o src/generated/types.ts", "generate-types-from-local-schema": "yarn run openapi-typescript ../api/funkwhale_api/common/schema.yml -o src/generated/types.ts",
"generate-types-from-remote-schema": "yarn run openapi-typescript https://docs.funkwhale.audio/develop/swagger/schema.yml -o src/generated/types.ts", "generate-types-from-remote-schema": "yarn run openapi-typescript https://docs.funkwhale.audio/develop/swagger/schema.yml -o src/generated/types.ts",
"fmt:es": "yarn lint:es --fix", "fmt:es": "yarn lint:es --fix",

View File

@ -108,7 +108,7 @@ watch(
/> />
</slot> </slot>
<Pagination <Pagination
v-if="albums && count > props.limit" v-if="page && albums && count > props.limit"
v-model:page="page" v-model:page="page"
:pages="Math.ceil((count || 0) / props.limit)" :pages="Math.ceil((count || 0) / props.limit)"
style="grid-column: 1 / -1;" style="grid-column: 1 / -1;"

View File

@ -108,7 +108,7 @@ watch(
:artist="artist" :artist="artist"
/> />
<Pagination <Pagination
v-if="artists && count > limit" v-if="page && artists && count > limit"
v-model:page="page" v-model:page="page"
style="grid-column: 1 / -1;" style="grid-column: 1 / -1;"
:pages="Math.ceil((count || 0) / limit)" :pages="Math.ceil((count || 0) / limit)"

View File

@ -90,7 +90,7 @@ watch([() => props.filters, page],
/> />
</template> </template>
<Pagination <Pagination
v-if="result && count > limit && limit > 16" v-if="page && result && count > limit && limit > 16"
v-model:page="page" v-model:page="page"
:pages="Math.ceil((count || 0) / limit)" :pages="Math.ceil((count || 0) / limit)"
style="grid-column: 1 / -1;" style="grid-column: 1 / -1;"
@ -101,7 +101,7 @@ watch([() => props.filters, page],
:object="channel" :object="channel"
/> />
<Pagination <Pagination
v-if="result && count > limit" v-if="page && result && count > limit"
v-model:page="page" v-model:page="page"
:pages="Math.ceil((count || 0) / limit)" :pages="Math.ceil((count || 0) / limit)"
style="grid-column: 1 / -1;" style="grid-column: 1 / -1;"

View File

@ -216,7 +216,7 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
/> />
</div> </div>
<Pagination <Pagination
v-if="count > props.limit" v-if="page && count > props.limit"
v-model:page="page" v-model:page="page"
:pages="Math.ceil((count || 0) / props.limit)" :pages="Math.ceil((count || 0) / props.limit)"
style="grid-column: 1 / -1;" style="grid-column: 1 / -1;"

View File

@ -215,7 +215,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
</Layout> </Layout>
</Layout> </Layout>
<Pagination <Pagination
v-if="results && count > paginateBy" v-if="page && results && count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil((count || 0) / paginateBy)" :pages="Math.ceil((count || 0) / paginateBy)"
style="grid-column: 1 / -1;" style="grid-column: 1 / -1;"

View File

@ -229,7 +229,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</Layout> </Layout>
<Loader v-if="isLoading" /> <Loader v-if="isLoading" />
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil((result.count || 0)/paginateBy)" :pages="Math.ceil((result.count || 0)/paginateBy)"
/> />
@ -271,7 +271,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</Layout> </Layout>
<Spacer grow /> <Spacer grow />
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil((result.count || 0)/paginateBy)" :pages="Math.ceil((result.count || 0)/paginateBy)"
/> />

View File

@ -237,7 +237,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</Layout> </Layout>
<Loader v-if="isLoading" /> <Loader v-if="isLoading" />
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />
@ -279,7 +279,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</Layout> </Layout>
<Spacer grow /> <Spacer grow />
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />

View File

@ -309,7 +309,7 @@ const { to: upload } = useModal('upload')
</Layout> </Layout>
<Spacer grow /> <Spacer grow />
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
:page="page" :page="page"
:pages="Math.ceil((result?.results.length || 0)/paginateBy)" :pages="Math.ceil((result?.results.length || 0)/paginateBy)"
/> />

View File

@ -277,7 +277,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
flex flex
> >
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />
@ -288,7 +288,7 @@ const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value]
:custom-radio="radio" :custom-radio="radio"
/> />
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />

View File

@ -250,13 +250,13 @@ const labels = computed(() => ({
</action-table> </action-table>
<div> <div>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.ChannelsTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} {{ t('components.manage.ChannelsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }}
</span> </span>
</div> </div>
</template> </template>

View File

@ -253,12 +253,12 @@ const labels = computed(() => ({
</template> </template>
</action-table> </action-table>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && page !== undefined && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.library.AlbumsTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} {{ t('components.manage.library.AlbumsTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }}
</span> </span>
</template> </template>

View File

@ -249,12 +249,12 @@ const getUrl = (artist: { channel?: number; id: number }) => {
</template> </template>
</action-table> </action-table>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.library.ArtistsTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} {{ t('components.manage.library.ArtistsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }}
</span> </span>
</template> </template>

View File

@ -266,12 +266,12 @@ const getCurrentState = (target?: StateTarget): ReviewState => {
@refresh="fetchData()" @refresh="fetchData()"
/> />
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.library.EditsCardList.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} {{ t('components.manage.library.EditsCardList.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }}
</span> </span>
</template> </template>

View File

@ -273,12 +273,12 @@ const getPrivacyLevelChoice = (privacyLevel: PrivacyLevel) => {
</template> </template>
</action-table> </action-table>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.library.LibrariesTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} {{ t('components.manage.library.LibrariesTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }}
</span> </span>
</template> </template>

View File

@ -215,12 +215,12 @@ const showUploadDetailModal = ref(false)
</template> </template>
</action-table> </action-table>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.library.TagsTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} {{ t('components.manage.library.TagsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }}
</span> </span>
</template> </template>

View File

@ -254,13 +254,13 @@ const labels = computed(() => ({
</action-table> </action-table>
<div> <div>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.library.TracksTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} {{ t('components.manage.library.TracksTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }}
</span> </span>
</div> </div>
</template> </template>

View File

@ -365,12 +365,12 @@ const getPrivacyLevelChoice = (privacyLevel: PrivacyLevel) => {
</template> </template>
</action-table> </action-table>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.library.UploadsTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} {{ t('components.manage.library.UploadsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }}
</span> </span>
</template> </template>

View File

@ -231,14 +231,14 @@ const labels = computed(() => ({
</div> </div>
<div> <div>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:paginate-by="paginateBy" :paginate-by="paginateBy"
:pages="result.count" :pages="result.count"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.moderation.AccountsTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} {{ t('components.manage.moderation.AccountsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }}
</span> </span>
</div> </div>
</template> </template>

View File

@ -262,16 +262,16 @@ const labels = computed(() => ({
</div> </div>
<div> <div>
<pagination <pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
v-model:pages="result.count" :pages="result.count"
:compact="true" :compact="true"
:paginate-by="paginateBy" :paginate-by="paginateBy"
:total="result.count" :total="result.count"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.moderation.DomainsTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} {{ t('components.manage.moderation.DomainsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }}
</span> </span>
</div> </div>
</template> </template>

View File

@ -230,13 +230,13 @@ const labels = computed(() => ({
</div> </div>
<div> <div>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
v-model:pages="result.count" v-model:pages="result.count"
:paginate-by="paginateBy" :paginate-by="paginateBy"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.users.InvitationsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }, result.results.length) }} {{ t('components.manage.users.InvitationsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }, result.results.length) }}
</span> </span>
</div> </div>

View File

@ -275,7 +275,7 @@ const labels = computed(() => ({
/> />
<span v-if="result && result.results.length > 0"> <span v-if="result && result.results.length > 0">
{{ t('components.manage.users.UsersTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}, result.results.length) }} {{ t('components.manage.users.UsersTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }, result.results.length) }}
</span> </span>
</div> </div>
</Layout> </Layout>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Notification, LibraryFollow } from '~/types' import type { Notification, LibraryFollow } from '~/types'
import type { components } from '~/types/generated' import type { components } from '~/generated/types'
import type { RouteLocationRaw } from 'vue-router'
import { computed, ref, watchEffect, watch } from 'vue' import { computed, ref, watchEffect, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@ -72,7 +73,10 @@ const notificationData = computed(() => {
if (activity.related_object?.approved === null) { if (activity.related_object?.approved === null) {
return { return {
detailUrl, detailUrl,
message: t('components.notifications.NotificationRow.message.userPendingFollow', { username: username.value, user: activity.object.target?.full_username }), message: t('components.notifications.NotificationRow.message.userPendingFollow', { username: username.value,
// TODO: This is just wrong. Start with fixing the types upstream.
// @ts-expect-error `activity.object needs to have a type. Where is it declared?
user: activity.object.target?.full_username }),
acceptFollow: { acceptFollow: {
buttonClass: 'success', buttonClass: 'success',
icon: 'check', icon: 'check',
@ -134,24 +138,37 @@ const handleAction = (handler?: () => void) => {
const approveLibraryFollow = async (follow: LibraryFollow) => { const approveLibraryFollow = async (follow: LibraryFollow) => {
await axios.post(`federation/follows/library/${follow.uuid}/accept/`) await axios.post(`federation/follows/library/${follow.uuid}/accept/`)
// TODO: This is not how Axios works. You have to send a request with
// the correct type as a parameter.
// @ts-expect-error Post this with the axios payload: { ...follow, approved: true}
follow.approved = true follow.approved = true
item.value.is_read = true item.value.is_read = true
} }
const rejectLibraryFollow = async (follow: LibraryFollow) => { const rejectLibraryFollow = async (follow: LibraryFollow) => {
await axios.post(`federation/follows/library/${follow.uuid}/reject/`) await axios.post(`federation/follows/library/${follow.uuid}/reject/`)
// TODO: This is not how Axios works. You have to send a request with
// the correct type as a parameter.
// @ts-expect-error Post this with the axios payload: { ...follow, approved: false}
follow.approved = false follow.approved = false
item.value.is_read = true item.value.is_read = true
} }
const approveUserFollow = async (follow: UserFollow) => { const approveUserFollow = async (follow: components["schemas"]["Follow"]) => {
await axios.post(`federation/follows/user/${follow.uuid}/accept/`) await axios.post(`federation/follows/user/${follow.uuid}/accept/`)
// TODO: This is not how Axios works. You have to send a request with
// the correct type as a parameter.
// @ts-expect-error Post this with the axios payload: { ...follow, approved: true}
follow.approved = true follow.approved = true
item.value.is_read = true item.value.is_read = true
} }
const rejectUserFollow = async (follow: UserFollow) => { const rejectUserFollow = async (follow: components["schemas"]["Follow"]) => {
await axios.post(`federation/follows/user/${follow.uuid}/reject/`) await axios.post(`federation/follows/user/${follow.uuid}/reject/`)
// TODO: This is not how Axios works. You have to send a request with
// the correct type as a parameter.
// @ts-expect-error Post this with the axios payload: { ...follow, approved: false}
follow.approved = false follow.approved = false
item.value.is_read = true item.value.is_read = true
} }
@ -166,11 +183,13 @@ const rejectUserFollow = async (follow: UserFollow) => {
/> />
</td> </td>
<td> <td>
<!-- TODO: Make sure `notificationData.detailUrl` has a type that satisfies `RouteLocationRaw` -->
<!-- @vue-ignore -->
<router-link <router-link
v-if="notificationData.detailUrl" v-if="notificationData.detailUrl"
v-slot="{ navigate }" v-slot="{ navigate }"
custom custom
:to="notificationData.detailUrl" :to="notificationData.detailUrl as RouteLocationRaw"
> >
<sanitized-html <sanitized-html
tag="span" tag="span"

View File

@ -6,7 +6,6 @@ import defaultCover from '~/assets/audio/default-cover.png'
import { momentFormat } from '~/utils/filters' import { momentFormat } from '~/utils/filters'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import moment from 'moment' import moment from 'moment'
@ -21,7 +20,6 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const store = useStore() const store = useStore()
const router = useRouter()
const { t } = useI18n() const { t } = useI18n()
const images = computed(() => { const images = computed(() => {

View File

@ -46,7 +46,7 @@ interface ModifiedPlaylistTrack extends PlaylistTrack {
} }
const tracks = computed({ const tracks = computed({
get: () => playlistTracks.value.map((playlistTrack, index) => ({ ...playlistTrack, _id: `${index}-${playlistTrack.track.id}` } as ModifiedPlaylistTrack)), get: () => playlistTracks.value.map((playlistTrack, index) => ({ ...playlistTrack, _id: `${ index }-${ playlistTrack.track }` } as ModifiedPlaylistTrack)),
set: (playlist) => { set: (playlist) => {
playlistTracks.value = playlist.map((modifiedPlaylistTrack, index) => { playlistTracks.value = playlist.map((modifiedPlaylistTrack, index) => {
const res = { ...modifiedPlaylistTrack, index } as ModifiedPlaylistTrack const res = { ...modifiedPlaylistTrack, index } as ModifiedPlaylistTrack

View File

@ -105,7 +105,7 @@ watch(
/> />
</Section> </Section>
<Pagination <Pagination
v-if="objects && count > (props.filters.limit as number)" v-if="page && objects && count > (props.filters.limit as number)"
v-model:page="page" v-model:page="page"
:pages="Math.ceil((count || 0) / (props.filters.limit as number))" :pages="Math.ceil((count || 0) / (props.filters.limit as number))"
/> />

View File

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ComponentProps } from 'vue-component-type-helpers' import type { ComponentProps } from 'vue-component-type-helpers'
import { useAttrs } from 'vue'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/Spacer.vue' import Spacer from '~/components/ui/Spacer.vue'
@ -60,6 +59,9 @@ const props = defineProps<{
/> />
</div> </div>
<slot name="topleft" /> <slot name="topleft" />
<!-- The inferred type of props occasionally overloads the typescript compiler. -->
<!-- TODO: Remove @vue-ignore once tsc is re-implemented in Go (and 10x faster) -->
<!-- @vue-ignore -->
<Heading <Heading
v-bind="props" v-bind="props"
style=" style="

View File

@ -3,7 +3,6 @@ import { useElementSize } from '@vueuse/core'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { isMobileView } from '~/composables/screen' import { isMobileView } from '~/composables/screen'
import { preventNonNumeric } from '~/utils/event-validators'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Input from '~/components/ui/Input.vue' import Input from '~/components/ui/Input.vue'

View File

@ -109,13 +109,23 @@ onMounted(() => {
:key="key" :key="key"
:class="$style.description" :class="$style.description"
:style="`margin-right: -20%; --current-step: 0; color: magenta;`" :style="`margin-right: -20%; --current-step: 0; color: magenta;`"
><Markdown :md="options[key]" /></span> >
<!-- For some reason, the linter complains that (Record<T, string>)[T] is not string... -->
<!-- TODO: https://dev.funkwhale.audio/funkwhale/funkwhale/-/issues/2437 -->
<!-- @vue-ignore -->
<Markdown :md="options[model]" />
</span>
</span> </span>
<span <span
v-if="model !== undefined" v-if="model !== undefined"
style="position: absolute;" style="position: absolute;"
:class="$style.description" :class="$style.description"
><Markdown :md="options[model]" /></span> >
<!-- For some reason, the linter complains that (Record<T, string>)[T] is not string... -->
<!-- TODO: https://dev.funkwhale.audio/funkwhale/funkwhale/-/issues/2437 -->
<!-- @vue-ignore -->
<Markdown :md="options[model]" />
</span>
</span> </span>
</Layout> </Layout>
</template> </template>

View File

@ -1,16 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { type RouterLinkProps } from 'vue-router' import { type TabProps, TABS_INJECTION_KEY } from '~/injection-keys'
import { TABS_INJECTION_KEY } from '~/injection-keys'
import { whenever } from '@vueuse/core' import { whenever } from '@vueuse/core'
import { inject, ref } from 'vue' import { inject, ref } from 'vue'
export type Props = { const props = defineProps<TabProps>()
title: string,
to?: RouterLinkProps['to']
icon?: string
}
const props = defineProps<Props>()
const { currentTitle, tabs } = inject(TABS_INJECTION_KEY, { const { currentTitle, tabs } = inject(TABS_INJECTION_KEY, {
currentTitle: ref(props.title), currentTitle: ref(props.title),

View File

@ -1,9 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { TABS_INJECTION_KEY } from '~/injection-keys' import { type TabProps, TABS_INJECTION_KEY } from '~/injection-keys'
import { computed, provide, reactive, ref, watch } from 'vue' import { computed, provide, reactive, ref, watch } from 'vue'
import { type Props as TabProps } from '~/components/ui/Tab.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Link from '~/components/ui/Link.vue' import Link from '~/components/ui/Link.vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'

View File

@ -39,16 +39,12 @@ const styles = {
}[a!]) }[a!])
} as const } as const
const getStyle = (props : Partial<AlignmentProps>) => ([key, value]: Entry<AlignmentProps>) => const getStyle = (props : Partial<AlignmentProps>) => ([key, value]: Entry<AlignmentProps>):string =>
( (
typeof styles[key] === 'function' typeof styles[key] === 'string'
? (styles[key]( ? styles[key]
// @ts-expect-error We know that props[key] is a value accepted by styles[key]. The ts compiler is not so smart. // @ts-expect-error We know that props[key] is a value accepted by styles[key]. The ts compiler is not so smart.
(key in props && props[key]) : (styles[key]((key in props && props[key]) ? props[((props[key]), (key))] : value))
? props[((props[key]), (key))]
: value
))
: styles[key]
) )
const merge = (rules: string[]) => (attributes: HTMLAttributes = {}) => const merge = (rules: string[]) => (attributes: HTMLAttributes = {}) =>

View File

@ -111,6 +111,9 @@ export const useQueue = createGlobalState(() => {
const { uploads } = await axios.get(`tracks/${track.id}/`) const { uploads } = await axios.get(`tracks/${track.id}/`)
.then(response => response.data as Track, () => ({ uploads: [] as Upload[] })) .then(response => response.data as Track, () => ({ uploads: [] as Upload[] }))
// TODO: Either make `track` a writable ref or implement the client/cache model
// See Issue: https://dev.funkwhale.audio/funkwhale/funkwhale/-/issues/2437
// @ts-expect-error `track` is read-only
track.uploads = uploads track.uploads = uploads
} }

View File

@ -2,7 +2,6 @@ import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/
import type { components } from '~/generated/types' import type { components } from '~/generated/types'
import type { ContentFilter } from '~/store/moderation' import type { ContentFilter } from '~/store/moderation'
import { useCurrentElement } from '@vueuse/core'
import { computed, markRaw, ref } from 'vue' import { computed, markRaw, ref } from 'vue'
import { i18n } from '~/init/locale' import { i18n } from '~/init/locale'
import { useStore } from '~/store' import { useStore } from '~/store'
@ -154,7 +153,7 @@ export default (props: PlayOptionsProps) => {
return tracks.filter(track => track.uploads?.length).map(markRaw) return tracks.filter(track => track.uploads?.length).map(markRaw)
} }
const el = useCurrentElement() // const el = useCurrentElement()
const enqueue = async () => { const enqueue = async () => {
const tracks = await getPlayableTracks() const tracks = await getPlayableTracks()

View File

@ -4,10 +4,12 @@ import { ref } from 'vue'
export default () => { export default () => {
const pageQuery = useRouteQuery<string>('page', '1') const pageQuery = useRouteQuery<string>('page', '1')
const page = ref() const page = ref<number>()
syncRef(pageQuery, page, { syncRef(pageQuery, page, {
transform: { transform: {
ltr: (left) => +left, ltr: (left) => +left,
// TODO: Why toString?
// @ts-expect-error string vs. number
rtl: (right) => right.toString() rtl: (right) => right.toString()
} }
}) })

View File

@ -42,13 +42,13 @@ const styles = {
...widths, ...sizes ...widths, ...sizes
} as const satisfies Record<Key, string | ((w: string) => string)> } as const satisfies Record<Key, string | ((w: string) => string)>
// The `lint:tsc` script more errors here than the language server is happy.
// TODO: Fix this Issue: https://dev.funkwhale.audio/funkwhale/funkwhale/-/issues/2437
const getStyle = (props: Partial<WidthProps>) => (key: Key):string => const getStyle = (props: Partial<WidthProps>) => (key: Key):string =>
key in props key in props
? typeof styles[key] === 'function' ? typeof styles[key] === 'function'
? styles[key]( // @ts-expect-error Typescript is hard. Make the typescript compiler understand `key in props`
// @ts-expect-error Typescript is hard. Make the typescript compiler understand `key in props` ? styles[key](props[key])
props[key]
)
: styles[key] as string : styles[key] as string
: '' : ''

View File

@ -70,6 +70,8 @@ export const install: InitModule = ({ store }) => {
const { current } = store.state.radios const { current } = store.state.radios
if (current.clientOnly) { if (current.clientOnly) {
// TODO: Type this event
// @ts-expect-error untyped event
await CLIENT_RADIOS[current.type].handleListen(current, event) await CLIENT_RADIOS[current.type].handleListen(current, event)
} }
} }

View File

@ -1,5 +1,11 @@
import { type InjectionKey, type Ref } from 'vue' import { type InjectionKey, type Ref } from 'vue'
import { type Props as TabProps } from '~/components/ui/Tab.vue' import { type RouterLinkProps } from 'vue-router'
export type TabProps = {
title: string,
to?: RouterLinkProps['to']
icon?: string
}
export const TABS_INJECTION_KEY = Symbol('tabs') as InjectionKey<{ export const TABS_INJECTION_KEY = Symbol('tabs') as InjectionKey<{
tabs: TabProps[] tabs: TabProps[]

View File

@ -16,10 +16,6 @@ export interface State {
settings: Settings settings: Settings
} }
type TotalCount = {
total: number
}
// export interface NodeInfo { // export interface NodeInfo {
// version: string; // version: string;
// software: { // software: {

View File

@ -10,7 +10,8 @@ import Alert from '~/components/ui/Alert.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Modal from '~/components/ui/Modal.vue' import Modal from '~/components/ui/Modal.vue'
import Input from '~/components/ui/Input.vue' import Input from '~/components/ui/Input.vue'
import FileUploadWidget from '~/components/library/FileUploadWidget.vue'
// TODO: Delete this file once all upload functionality is moved to the new UI.
const { t } = useI18n() const { t } = useI18n()
@ -38,13 +39,15 @@ const combinedFileSize = computed(() => bytesToHumanSize(
)) ))
// Actions // Actions
const processFiles = (fileList: FileList) => {
if (!uploads.currentUploadGroup) return
for (const file of fileList) { // TODO: Is this needed?
uploads.currentUploadGroup.queueUpload(file) // const processFiles = (fileList: FileList) => {
} // if (!uploads.currentUploadGroup) return
}
// for (const file of fileList) {
// uploads.currentUploadGroup.queueUpload(file)
// }
// }
const router = useRouter() const router = useRouter()
const cancel = () => { const cancel = () => {
@ -53,14 +56,14 @@ const cancel = () => {
uploads.currentUploadGroup = undefined uploads.currentUploadGroup = undefined
if (uploads.queue.length > 0) { if (uploads.queue.length > 0) {
return router.push('/upload/running') router.push('/upload/running')
} }
} }
const continueInBackground = () => { const continueInBackground = () => {
libraryOpen.value = false libraryOpen.value = false
uploads.currentUploadGroup = undefined uploads.currentUploadGroup = undefined
return router.push('/upload/running') router.push('/upload/running')
} }
// TODO (whole file): Delete this file, please. // TODO (whole file): Delete this file, please.
@ -106,12 +109,16 @@ const isOpen = computed({
</Alert> </Alert>
</template> </template>
<!-- TODO: Use a file input. We haven't implemented this yet.
We could say v-model can be of type `string | number | File | File[]`
and then implement this functionality. -->
<!-- v-model="processFiles" -->
<!-- @vue-ignore -->
<Input <Input
type="file" type="file"
:accept="['.flac', '.ogg', '.opus', '.mp3', '.aac', '.aif', '.aiff', '.m4a'].join(', ')" :accept="['.flac', '.ogg', '.opus', '.mp3', '.aac', '.aif', '.aiff', '.m4a'].join(', ')"
multiple multiple
auto-reset auto-reset
@files="processFiles"
/> />
<!-- Upload path --> <!-- Upload path -->

View File

@ -14,7 +14,6 @@ import Popover from '~/components/ui/Popover.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.vue' import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
import PopoverSubmenu from '~/components/ui/popover/PopoverSubmenu.vue' import PopoverSubmenu from '~/components/ui/popover/PopoverSubmenu.vue'
import Spacer from '~/components/ui/Spacer.vue' import Spacer from '~/components/ui/Spacer.vue'
import Pill from '~/components/ui/Pill.vue'
const route = useRoute() const route = useRoute()
const store = useStore() const store = useStore()

View File

@ -23,7 +23,9 @@
// return Metadata.parseBlob(file).then(metadata => metadata.common) // return Metadata.parseBlob(file).then(metadata => metadata.common)
// } // }
// @ts-expect-error This is not installed...?
import * as jsmediaTags from 'jsmediatags/dist/jsmediatags.min.js' import * as jsmediaTags from 'jsmediatags/dist/jsmediatags.min.js'
// @ts-expect-error This is not installed...?
import type { ShortcutTags } from 'jsmediatags' import type { ShortcutTags } from 'jsmediatags'
const REQUIRED_TAGS = ['title', 'artist', 'album'] const REQUIRED_TAGS = ['title', 'artist', 'album']
@ -47,6 +49,7 @@ export const getCoverUrl = async (tags: Tags): Promise<string | undefined> => {
export const getTags = async (file: File) => { export const getTags = async (file: File) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
jsmediaTags.read(file, { jsmediaTags.read(file, {
// @ts-expect-error Please type `tags`
onSuccess: ({ tags }) => { onSuccess: ({ tags }) => {
if (tags.picture?.data) { if (tags.picture?.data) {
tags.picture.data = new Uint8Array(tags.picture.data) tags.picture.data = new Uint8Array(tags.picture.data)
@ -59,6 +62,7 @@ export const getTags = async (file: File) => {
resolve(tags) resolve(tags)
}, },
// @ts-expect-error Please type `error`
onError: (error) => reject(error) onError: (error) => reject(error)
}) })
}) })

View File

@ -4,8 +4,8 @@ import { computed, ref, type Ref } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import axios from 'axios' import axios from 'axios'
type Item = { type: 'custom' | 'preset', label: string } export type Item = { type: 'custom' | 'preset', label: string }
type Model = { currents: Item[], others?: Item[] } export type Model = { currents: Item[], others?: Item[] }
/** /**
* Load and cache all tags. * Load and cache all tags.

View File

@ -190,9 +190,13 @@ export const useUploadsStore = defineStore('uploads', () => {
const upload = group.queue.find(entry => entry.guid === event.upload.uuid) const upload = group.queue.find(entry => entry.guid === event.upload.uuid)
if (!upload) continue if (!upload) continue
if (event.new_status !== 'failed') { if (event.new_status !== 'errored') {
// TODO: Find out what other field to use here
// @ts-expect-error wrong field
upload.importedAt = event.upload.import_date upload.importedAt = event.upload.import_date
} else { } else {
// TODO: Add second parameter `error`
// @ts-expect-error missing parameter
upload.fail('import-failed') upload.fail('import-failed')
} }
break break

View File

@ -16,7 +16,6 @@ import useMarkdown from '~/composables/useMarkdown'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import Loader from '~/components/ui/Loader.vue' import Loader from '~/components/ui/Loader.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Header from '~/components/ui/Header.vue' import Header from '~/components/ui/Header.vue'
import Toggle from '~/components/ui/Toggle.vue' import Toggle from '~/components/ui/Toggle.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'

View File

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
import EditsCardList from '~/components/manage/library/EditsCardList.vue' import EditsCardList from '~/components/manage/library/EditsCardList.vue'
@ -15,9 +14,11 @@ withDefaults(defineProps<Props>(), {
}) })
const { t } = useI18n() const { t } = useI18n()
const labels = computed(() => ({
title: t('views.admin.library.EditsList.title') // TODO: Do we want to use this title?
})) // const labels = computed(() => ({
// title: t('views.admin.library.EditsList.title')
// }))
</script> </script>
<template> <template>

View File

@ -8,7 +8,6 @@ import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
import axios from 'axios' import axios from 'axios'
import $ from 'jquery'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import Loader from '~/components/ui/Loader.vue' import Loader from '~/components/ui/Loader.vue'
@ -108,6 +107,7 @@ fetchData()
const el = useCurrentElement() const el = useCurrentElement()
watch(object, async () => { watch(object, async () => {
await nextTick() await nextTick()
// @ts-expect-error JQuery owhere to be found...
$(el.value).find('select.dropdown').dropdown() $(el.value).find('select.dropdown').dropdown()
}) })

View File

@ -47,12 +47,14 @@ const logger = useLogger()
const search = ref() const search = ref()
const page = usePage()
const result = ref<BackendResponse<Report>>() const result = ref<BackendResponse<Report>>()
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props) const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props)
const { onSearch, query, addSearchToken, getTokenValue } = useSmartSearch(props) const { onSearch, query, addSearchToken, getTokenValue } = useSmartSearch(props)
const page = usePage()
const pages = computed(() => result.value?.count ? Math.ceil(result.value.count / paginateBy.value) : 0)
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [ const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
['creation_date', 'creation_date'], ['creation_date', 'creation_date'],
['applied_date', 'applied_date'] ['applied_date', 'applied_date']
@ -214,8 +216,9 @@ const labels = computed(() => ({
</div> </div>
<div class="ui center aligned basic segment"> <div class="ui center aligned basic segment">
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:current="page" v-model:page="page"
:pages="pages"
:paginate-by="paginateBy" :paginate-by="paginateBy"
/> />
</div> </div>

View File

@ -110,10 +110,10 @@ const labels = computed(() => ({
<Input <Input
id="requests-search" id="requests-search"
ref="search" ref="search"
v-model="query"
name="search" name="search"
search search
:label="t('views.admin.moderation.RequestsList.label.search')" :label="t('views.admin.moderation.RequestsList.label.search')"
:value="query"
:placeholder="labels.searchPlaceholder" :placeholder="labels.searchPlaceholder"
/> />
</form> </form>
@ -194,8 +194,9 @@ const labels = computed(() => ({
@handled="fetchData" @handled="fetchData"
/> />
<Pagination <Pagination
v-if="result.count > paginateBy" v-if="page && result.count > paginateBy"
v-model:current="page" v-model:page="page"
v-model:pages="result.count"
:paginate-by="paginateBy" :paginate-by="paginateBy"
/> />
</template> </template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import Nav from '~/components/ui/Nav.vue' import Nav from '~/components/ui/Nav.vue'
@ -33,6 +34,6 @@ const tabs = ref([
> >
<Nav v-model="tabs" /> <Nav v-model="tabs" />
<router-view :key="$route.fullPath" /> <router-view :key="useRoute().fullPath" />
</Layout> </Layout>
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ImportStatus, PrivacyLevel, Upload, BackendResponse } from '~/types' import type { ImportStatus, PrivacyLevel, Upload } from '~/types'
import type { SmartSearchProps } from '~/composables/navigation/useSmartSearch' import type { SmartSearchProps } from '~/composables/navigation/useSmartSearch'
import type { OrderingProps } from '~/composables/navigation/useOrdering' import type { OrderingProps } from '~/composables/navigation/useOrdering'
import type { RouteRecordName } from 'vue-router' import type { RouteRecordName } from 'vue-router'
@ -595,12 +595,12 @@ const getPrivacyLevelChoice = (privacyLevel: PrivacyLevel) => {
</template> </template>
</action-table> </action-table>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />
<span v-if="result && result.results.length > 0"> <span v-if="page && result && result.results.length > 0">
{{ t('components.manage.library.UploadsTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }} {{ t('components.manage.library.UploadsTable.pagination.results', { start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count }) }}
</span> </span>
</template> </template>

View File

@ -1,13 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { BackendError } from '~/types' import type { BackendError } from '~/types'
import { computed, ref, onMounted, nextTick } from 'vue' import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import Input from '~/components/ui/Input.vue' import Input from '~/components/ui/Input.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import Link from '~/components/ui/Link.vue' import Link from '~/components/ui/Link.vue'

View File

@ -12,7 +12,6 @@ import UserFollowButton from '~/components/federation/UserFollowButton.vue'
import axios from 'axios' import axios from 'axios'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
import useReport from '~/composables/moderation/useReport'
import RenderedDescription from '~/components/common/RenderedDescription.vue' import RenderedDescription from '~/components/common/RenderedDescription.vue'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
@ -35,7 +34,6 @@ const props = withDefaults(defineProps<Props>(), {
domain: null domain: null
}) })
const { report, getReportableObjects } = useReport()
const store = useStore() const store = useStore()
const object = ref<components['schemas']['FullActor'] | null>(null) const object = ref<components['schemas']['FullActor'] | null>(null)
@ -43,7 +41,9 @@ const object = ref<components['schemas']['FullActor'] | null>(null)
const actorColor = computed(() => intToRGB(hashCode(object.value?.full_username))) const actorColor = computed(() => intToRGB(hashCode(object.value?.full_username)))
const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${actorColor.value}` })) const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${actorColor.value}` }))
const displayName = computed(() => object.value?.name ?? object.value?.preferred_username) // TODO: Check if still needed
//const displayName = computed(() => object.value?.name ?? object.value?.preferred_username)
const fullUsername = computed(() => props.domain const fullUsername = computed(() => props.domain
? `${props.username}@${props.domain}` ? `${props.username}@${props.domain}`
: `${props.username}@${store.getters['instance/domain']}` : `${props.username}@${store.getters['instance/domain']}`
@ -79,7 +79,6 @@ const fetchData = async () => {
} }
watch(props, fetchData, { immediate: true }) watch(props, fetchData, { immediate: true })
const recentActivity = ref(0)
const { copy, copied, isSupported } = useClipboard() const { copy, copied, isSupported } = useClipboard()
@ -106,13 +105,19 @@ const tabs = ref([{
main main
> >
<!-- TODO: Translate Edit Link --> <!-- TODO: Translate Edit Link -->
<!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! -->
<!-- @vue-ignore -->
<Header <Header
:h1="props.username" :h1="props.username"
:action="{ :action="{
text:'Edit profile', text:'Edit profile',
// @ts-ignore
to:'/settings', to:'/settings',
// @ts-ignore
primary: true, primary: true,
// @ts-ignore
solid: true, solid: true,
// @ts-ignore
icon: 'bi-pencil-fill' icon: 'bi-pencil-fill'
}" }"
style="margin-top: 58px;" style="margin-top: 58px;"
@ -169,6 +174,8 @@ const tabs = ref([{
flex flex
no-gap no-gap
> >
<!-- TODO: Fix error with `$event` not being the right type -->
<!-- @vue-ignore -->
<RenderedDescription <RenderedDescription
:content="{ html: object?.summary?.html || '' }" :content="{ html: object?.summary?.html || '' }"
:field-name="'summary'" :field-name="'summary'"

View File

@ -142,6 +142,9 @@ const remove = async () => {
const updateSubscriptionCount = (delta: number) => { const updateSubscriptionCount = (delta: number) => {
if (object.value) { if (object.value) {
// TODO: Store a modified copy in the cache or on the db instead of mutating the object in-memory.
// #2438
// @ts-expect-error Property 'subscriptions_count' is readonly on type 'Channel'
object.value.subscriptions_count -= delta object.value.subscriptions_count -= delta
} }
} }

View File

@ -139,12 +139,17 @@ const showCreateModal = ref(false)
stack stack
main main
> >
<!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! -->
<!-- @vue-ignore -->
<Header <Header
:h1="t('views.channels.SubscriptionsList.title')" :h1="t('views.channels.SubscriptionsList.title')"
:action="{ :action="{
text: t('views.channels.SubscriptionsList.link.addNew'), text: t('views.channels.SubscriptionsList.link.addNew'),
// @ts-ignore
onClick: () => { showSubscribeModal = true }, onClick: () => { showSubscribeModal = true },
// @ts-ignore
primary: true, primary: true,
// @ts-ignore
icon: 'bi-plus' icon: 'bi-plus'
}" }"
large-section-heading large-section-heading
@ -194,13 +199,17 @@ const showCreateModal = ref(false)
:show-modification-date="true" :show-modification-date="true"
:filters="{q: subscribedQuery, subscribed: 'true'}" :filters="{q: subscribedQuery, subscribed: 'true'}"
/> />
<!-- TODO: Translations --> <!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! -->
<!-- @vue-ignore -->
<Header <Header
:h1="t('views.auth.ProfileOverview.header.channels')" :h1="t('views.auth.ProfileOverview.header.channels')"
:action="{ :action="{
text: t('views.channels.SubscriptionsList.link.addNew'), text: t('views.channels.SubscriptionsList.link.addNew'),
// @ts-ignore
onClick: () => { showCreateModal = true }, onClick: () => { showCreateModal = true },
// @ts-ignore
icon: 'bi-plus', icon: 'bi-plus',
// @ts-ignore
primary: true primary: true
}" }"
large-section-heading large-section-heading

View File

@ -64,12 +64,17 @@ const showSubscribeModal = ref(false)
stack stack
main main
> >
<!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! -->
<!-- @vue-ignore -->
<Header <Header
:h1="labels.title" :h1="labels.title"
:action="{ :action="{
text: t('views.channels.SubscriptionsList.link.addNew'), text: t('views.channels.SubscriptionsList.link.addNew'),
// @ts-ignore
onClick: () => { showSubscribeModal = true }, onClick: () => { showSubscribeModal = true },
// @ts-ignore
primary: true, primary: true,
// @ts-ignore
icon: 'bi-plus' icon: 'bi-plus'
}" }"
page-heading page-heading

View File

@ -6,8 +6,6 @@ import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
import { computed } from 'vue' import { computed } from 'vue'
import moment from 'moment'
import TagsList from '~/components/tags/List.vue' import TagsList from '~/components/tags/List.vue'
interface Props { interface Props {
@ -25,20 +23,20 @@ const imageUrl = computed(() => props.channel.artist?.cover
? store.getters['instance/absoluteUrl'](props.channel.artist?.cover.urls.medium_square_crop) ? store.getters['instance/absoluteUrl'](props.channel.artist?.cover.urls.medium_square_crop)
: fallbackImageUrl : fallbackImageUrl
) )
const urlId = computed(() => props.channel.actor?.is_local
? props.channel.actor.preferred_username // TODO: Find out if still useful:
: props.channel.actor // const urlId = computed(() => props.channel.actor?.is_local
? props.channel.actor.full_username // ? props.channel.actor.preferred_username
: props.channel.uuid // : props.channel.actor
) // ? props.channel.actor.full_username
// : props.channel.uuid
// )
const { t } = useI18n() const { t } = useI18n()
const updatedTitle = computed(() => { const updatedTitle = computed(() => {
const date = momentFormat(new Date(props.channel.artist?.modification_date ?? '1970-01-01')) const date = momentFormat(new Date(props.channel.artist?.modification_date ?? '1970-01-01'))
return t('components.audio.ChannelCard.title', { date }) return t('components.audio.ChannelCard.title', { date })
}) })
const updatedAgo = computed(() => moment(props.channel.artist?.modification_date).fromNow())
</script> </script>
<template> <template>

View File

@ -55,18 +55,30 @@ const privacyTooltips = (level: PrivacyLevel) => `Visibility: ${sharedLabels.fie
<human-date :date="library.creation_date" /> <human-date :date="library.creation_date" />
</span> </span>
</div> </div>
<!-- TODO: Add `description` field to `Library` -->
<!-- @vue-ignore -->
<div class="description"> <div class="description">
{{ library.description }} {{
// @ts-expect-error Property 'description' does not exist on type 'Library'
library.description
}}
<div class="ui hidden divider" /> <div class="ui hidden divider" />
</div> </div>
<div class="content"> <div class="content">
<!-- TODO: Add `size` field to `Library` (or find out how else to load size) -->
<!-- @vue-ignore -->
<span <span
v-if="library.size" v-if="library.size"
class="right floated" class="right floated"
:data-tooltip="sizeLabel" :data-tooltip="sizeLabel"
> >
<i class="database icon" /> <i class="database icon" />
{{ humanSize(library.size) }} {{
// @ts-expect-error Property 'size' does not exist on type 'Library'
humanSize(library.size)
}}
</span> </span>
<i class="music icon" /> <i class="music icon" />
{{ t('views.content.libraries.Card.meta.tracks', library.uploads_count) }} {{ t('views.content.libraries.Card.meta.tracks', library.uploads_count) }}

View File

@ -56,8 +56,6 @@ const props = withDefaults(defineProps<Props>(), {
orderingConfigName: undefined orderingConfigName: undefined
}) })
const search = ref()
const page = usePage() const page = usePage()
const result = ref<BackendResponse<Upload>>() const result = ref<BackendResponse<Upload>>()
@ -347,7 +345,7 @@ const getImportStatusChoice = (importStatus: ImportStatus) => {
</action-table> </action-table>
<div> <div>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
:pages="Math.ceil(result.count / paginateBy)" :pages="Math.ceil(result.count / paginateBy)"
/> />

View File

@ -37,6 +37,9 @@ const labels = computed(() => ({
})) }))
const currentVisibilityLevel = ref(props.library?.privacy_level ?? 'me') const currentVisibilityLevel = ref(props.library?.privacy_level ?? 'me')
// TODO: Add 'description' to the Library type
// @ts-expect-error Property 'description' does not exist on type 'Library'
const currentDescription = ref(props.library?.description ?? '') const currentDescription = ref(props.library?.description ?? '')
const currentName = ref(props.library?.name ?? '') const currentName = ref(props.library?.name ?? '')

View File

@ -48,7 +48,9 @@ const isLoadingFollow = ref(false)
const showScan = ref(false) const showScan = ref(false)
const latestScan = ref(props.initialLibrary.latest_scan) const latestScan = ref(props.initialLibrary.latest_scan)
const scanProgress = computed(() => Math.min(latestScan.value.processed_files * 100 / latestScan.value.total_files, 100)) const scanProgress = computed(() => latestScan.value && latestScan.value.processed_files && latestScan.value.total_files
? Math.min(latestScan.value.processed_files * 100 / latestScan.value.total_files, 100)
: 0)
const scanStatus = computed(() => latestScan.value?.status ?? 'unknown') const scanStatus = computed(() => latestScan.value?.status ?? 'unknown')
const canLaunchScan = computed(() => scanStatus.value !== 'pending' && scanStatus.value !== 'scanning') const canLaunchScan = computed(() => scanStatus.value !== 'pending' && scanStatus.value !== 'scanning')
const radioPlayable = computed(() => ( const radioPlayable = computed(() => (
@ -191,8 +193,13 @@ const isOpen = ref(false)
</template> </template>
<div class="content"> <div class="content">
<!-- TODO: Add `description` field to `Library` -->
<!-- @vue-ignore -->
<div class="description"> <div class="description">
{{ library.description }} {{
// @ts-ignore
library.description
}}
</div> </div>
<Spacer :size="8" /> <Spacer :size="8" />
<div <div
@ -215,7 +222,7 @@ const isOpen = ref(false)
<i class="bi bi-check-circle" /> <i class="bi bi-check-circle" />
{{ t('views.content.remote.Card.label.scanSuccess') }} {{ t('views.content.remote.Card.label.scanSuccess') }}
</template> </template>
<template v-else-if="latestScan.status === 'finished' && latestScan.errored_files > 0"> <template v-else-if="latestScan.status === 'finished' && latestScan.errored_files && latestScan.errored_files > 0">
<i class="bi bi-exclamation-circle" /> <i class="bi bi-exclamation-circle" />
{{ t('views.content.remote.Card.label.scanPartialSuccess') }} {{ t('views.content.remote.Card.label.scanPartialSuccess') }}
</template> </template>

View File

@ -37,8 +37,9 @@ fetchData()
const getLibraryFromFollow = (follow: LibraryFollow) => { const getLibraryFromFollow = (follow: LibraryFollow) => {
const { target } = follow const { target } = follow
target.follow = follow // TODO: Actually load the target from the database or the cache. Use `client` with cache.
return target as Library // @ts-expect-error target is a string, not a library!
return ({ ...target, follow: follow } as Library)
} }
const scanResult = ref() const scanResult = ref()

View File

@ -18,6 +18,7 @@ defineProps<Props>()
<template> <template>
<section> <section>
<album-widget <album-widget
v-if="object.uuid"
:key="String(object.uploads_count)" :key="String(object.uploads_count)"
:header="false" :header="false"
:search="true" :search="true"

View File

@ -4,10 +4,8 @@ import type { Library } from '~/types'
import ArtistWidget from '~/components/artist/Widget.vue' import ArtistWidget from '~/components/artist/Widget.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
const { t } = useI18n() const { t } = useI18n()
const store = useStore()
interface Props { interface Props {
object: Library object: Library
@ -20,12 +18,13 @@ defineProps<Props>()
<template> <template>
<section> <section>
<artist-widget <artist-widget
v-if="object.uuid"
:key="object.uploads_count" :key="object.uploads_count"
ref="artists" ref="artists"
:header="false" :header="false"
:search="true" :search="true"
:controls="false" :controls="false"
:filters="{playable: true, ordering: '-creation_date', library: object.uuid}" :filters="{ playable: true, ordering: '-creation_date', library: object.uuid }"
> >
<template #empty-state> <template #empty-state>
<empty-state> <empty-state>

View File

@ -2,6 +2,9 @@
import type { Library } from '~/types' import type { Library } from '~/types'
import TrackTable from '~/components/audio/track/Table.vue' import TrackTable from '~/components/audio/track/Table.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface Props { interface Props {
object: Library object: Library

View File

@ -51,6 +51,10 @@ fetchData()
const updateApproved = async (follow: LibraryFollow, approved: boolean) => { const updateApproved = async (follow: LibraryFollow, approved: boolean) => {
try { try {
await axios.post(`federation/follows/library/${follow.uuid}/${approved ? 'accept' : 'reject'}/`) await axios.post(`federation/follows/library/${follow.uuid}/${approved ? 'accept' : 'reject'}/`)
// TODO: This is not how Axios works. You have to send a request with
// the correct type as a parameter.
// @ts-expect-error Post this with the axios payload: { ...follow, approved }
follow.approved = approved follow.approved = approved
} catch (error) { } catch (error) {
useErrorHandler(error as Error) useErrorHandler(error as Error)

View File

@ -194,9 +194,9 @@ const tabs = ref([{
<i class="bi bi-music-note-list" /> <i class="bi bi-music-note-list" />
{{ t('views.library.LibraryBase.meta.tracks', object.uploads_count) }} {{ t('views.library.LibraryBase.meta.tracks', object.uploads_count) }}
</span> </span>
<span v-if="object && 'size' in object && object.size"> <span v-if="object && 'size' in object && object.size && typeof object.size === 'number'">
<i class="bi bi-database-fill" /> <i class="bi bi-database-fill" />
{{ humanSize(object.size) }} {{ humanSize(object.size as number) }}
</span> </span>
</Layout> </Layout>
@ -213,15 +213,18 @@ const tabs = ref([{
v-if="!isOwner" v-if="!isOwner"
> >
<library-follow-button <library-follow-button
v-if="store.state.auth.authenticated" v-if="store.state.auth.authenticated && object"
:library="object" :library="object"
/> />
</div> </div>
</Layout> </Layout>
<!-- TODO: Add `description` field to library -->
<!-- @vue-ignore -->
<rendered-description <rendered-description
:content="object?.description ? {html: object?.description} : null" v-if="object && 'description' in object"
:update-url="`channels/${object?.uuid}/`" :content="{ html: object.description }"
:update-url="`channels/${object.uuid}/`"
:can-update="false" :can-update="false"
/> />
<Layout form> <Layout form>

View File

@ -117,16 +117,20 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
stack stack
main main
> >
<!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! -->
<!-- @vue-ignore -->
<Header <Header
v-if="store.state.auth.authenticated" v-if="store.state.auth.authenticated"
:h1="t('views.playlists.List.header.browse')" :h1="t('views.playlists.List.header.browse')"
page-heading page-heading
:action="{ :action="{
onClick: () => { store.commit('playlists/showModal', true) }, text: t('views.playlists.List.button.create'),
text: t('views.playlists.List.button.manage'), // @ts-ignore
icon: 'bi-plus',
// @ts-ignore
primary: true, primary: true,
icon: 'bi-music-note-list', // @ts-ignore
ariaPressed: store.state.playlists.showModal || undefined onClick: () => { store.commit('playlists/showModal', true) }
}" }"
/> />
<Header <Header
@ -243,7 +247,7 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</Button> </Button>
</Alert> </Alert>
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
style="grid-column: 1 / -1;" style="grid-column: 1 / -1;"
:pages="Math.ceil(result.count/paginateBy)" :pages="Math.ceil(result.count/paginateBy)"
@ -255,10 +259,10 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
/> />
<Spacer grow /> <Spacer grow />
<Pagination <Pagination
v-if="result && result.count > paginateBy" v-if="page && result && result.count > paginateBy"
v-model:page="page" v-model:page="page"
style="grid-column: 1 / -1;"
:pages="Math.ceil(result.count/paginateBy)" :pages="Math.ceil(result.count/paginateBy)"
style="grid-column: 1 / -1;"
/> />
</Section> </Section>
</Layout> </Layout>