Migrate a bunch of components

This commit is contained in:
wvffle 2022-07-11 00:31:08 +00:00 committed by Georg Krause
parent de4f445e9b
commit c5f7022869
20 changed files with 417 additions and 475 deletions

View File

@ -1,3 +1,20 @@
<script setup lang="ts">
import type { Artist } from '~/types'
import { computed } from 'vue'
interface Props {
artist: Artist
}
const props = defineProps<Props>()
const route = computed(() => props.artist.channel
? { name: 'channels.detail', params: { id: props.artist.channel.uuid } }
: { name: 'library.artists.detail', params: { id: props.artist.id } }
)
</script>
<template>
<router-link
class="artist-label ui image label"
@ -16,20 +33,3 @@
{{ artist.name }}
</router-link>
</template>
<script>
export default {
props: {
artist: { type: Object, required: true }
},
computed: {
route () {
if (this.artist.channel) {
return { name: 'channels.detail', params: { id: this.artist.channel.uuid } }
}
return { name: 'library.artists.detail', params: { id: this.artist.id } }
}
}
}
</script>

View File

@ -1,3 +1,44 @@
<script setup lang="ts">
import type { Channel } from '~/types'
import PlayButton from '~/components/audio/PlayButton.vue'
import TagsList from '~/components/tags/List.vue'
import { momentFormat } from '~/utils/filters'
import { useStore } from '~/store'
import { computed } from 'vue'
import { useGettext } from 'vue3-gettext'
import moment from 'moment'
interface Props {
// TODO (wvffle) : Find type
object: Channel
}
const props = defineProps<Props>()
const store = useStore()
const imageUrl = computed(() => props.object.artist?.cover
? store.getters['instance/absoluteUrl'](props.object.artist.cover.urls.medium_square_crop)
: null
)
const urlId = computed(() => props.object.actor?.is_local
? props.object.actor.preferred_username
: props.object.actor
? props.object.actor.full_username
: props.object.uuid
)
const { $pgettext } = useGettext()
const updatedTitle = computed(() => {
const date = momentFormat(new Date(props.object.artist?.modification_date ?? '1970-01-01'))
return $pgettext('*/*/*', 'Updated on %{ date }', { date })
})
// TODO (wvffle): Use time ago
const updatedAgo = computed(() => moment(props.object.artist?.modification_date).fromNow())
</script>
<template>
<div class="card app-card">
<div
@ -52,7 +93,8 @@
</div>
<div class="extra content">
<time
v-translate
v-translate="{ updatedAgo }"
:translate-params="{ updatedAgo }"
class="meta ellipsis"
:datetime="object.artist.modification_date"
:title="updatedTitle"
@ -71,46 +113,3 @@
</div>
</div>
</template>
<script>
import PlayButton from '~/components/audio/PlayButton.vue'
import TagsList from '~/components/tags/List.vue'
import { momentFormat } from '~/utils/filters'
import moment from 'moment'
export default {
components: {
PlayButton,
TagsList
},
props: {
object: { type: Object, required: true }
},
computed: {
imageUrl () {
if (this.object.artist.cover) {
return this.$store.getters['instance/absoluteUrl'](this.object.artist.cover.urls.medium_square_crop)
}
return null
},
urlId () {
if (this.object.actor && this.object.actor.is_local) {
return this.object.actor.preferred_username
} else if (this.object.actor) {
return this.object.actor.full_username
} else {
return this.object.uuid
}
},
updatedTitle () {
const d = momentFormat(this.object.artist.modification_date)
const message = this.$pgettext('*/*/*', 'Updated on %{ date }')
return this.$gettextInterpolate(message, { date: d })
},
updatedAgo () {
return moment(this.object.artist.modification_date).fromNow()
}
}
}
</script>

View File

@ -1,5 +1,30 @@
<script setup lang="ts">
import type { Cover, Track } from '~/types'
import PlayButton from '~/components/audio/PlayButton.vue'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import useQueue from '~/composables/audio/useQueue'
import usePlayer from '~/composables/audio/usePlayer'
import { computed } from 'vue'
interface Props {
// TODO (wvffle): Is it correct type?
entry: Track
defaultCover: Cover
}
const props = defineProps<Props>()
const { currentTrack } = useQueue()
const { playing } = usePlayer()
const cover = computed(() => props.entry.cover ?? null)
const duration = computed(() => props.entry.uploads.find(upload => upload.duration)?.duration ?? null)
</script>
<template>
<div :class="[{active: currentTrack && isPlaying && entry.id === currentTrack.id}, 'channel-entry-card']">
<div :class="[{active: currentTrack && playing && entry.id === currentTrack.id}, 'channel-entry-card']">
<div class="controls">
<play-button
class="basic circular icon"
@ -75,45 +100,3 @@
</div>
</div>
</template>
<script>
import PlayButton from '~/components/audio/PlayButton.vue'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import { mapGetters } from 'vuex'
export default {
components: {
PlayButton,
TrackFavoriteIcon
},
props: {
entry: { type: Object, required: true },
defaultCover: { type: Object, required: true }
},
computed: {
...mapGetters({
currentTrack: 'queue/currentTrack'
}),
isPlaying () {
return this.$store.state.player.playing
},
cover () {
if (this.entry.cover) {
return this.entry.cover
}
return null
},
duration () {
const uploads = this.entry.uploads.filter((e) => {
return e.duration
})
if (uploads.length > 0) {
return uploads[0].duration
}
return null
}
}
}
</script>

View File

@ -1,3 +1,18 @@
<script setup lang="ts">
import type { Album } from '~/types'
import PlayButton from '~/components/audio/PlayButton.vue'
import { computed } from 'vue'
interface Props {
serie: Album
}
const props = defineProps<Props>()
const cover = computed(() => props.serie?.cover ?? null)
</script>
<template>
<div class="channel-serie-card">
<div class="two-images">
@ -60,28 +75,3 @@
</div>
</div>
</template>
<script>
import PlayButton from '~/components/audio/PlayButton.vue'
export default {
components: {
PlayButton
},
props: { serie: { type: Object, required: true } },
computed: {
cover () {
if (this.serie.cover) {
return this.serie.cover
}
return null
},
duration () {
const uploads = this.serie.uploads.filter((e) => {
return e.duration
})
return uploads[0].duration
}
}
}
</script>

View File

@ -1,3 +1,32 @@
<script setup lang="ts">
import type { Library } from '~/types'
import { computed } from 'vue'
import { useStore } from '~/store'
interface Props {
library: Library
}
const props = defineProps<Props>()
const store = useStore()
const follow = computed(() => store.getters['libraries/follow'](props.library.uuid))
const isPending = computed(() => follow.value && follow.value.approved === null)
const isApproved = computed(() => follow.value && (follow.value?.approved === true || (isPending.value && props.library.privacy_level === 'everyone')))
const emit = defineEmits(['followed', 'unfollowed'])
const toggle = () => {
if (isPending.value || isApproved.value) {
emit('unfollowed')
} else {
emit('followed')
}
return store.dispatch('libraries/toggle', props.library.uuid)
}
</script>
<template>
<button
:class="['ui', 'pink', {'inverted': isApproved || isPending}, {'favorited': isApproved}, 'icon', 'labeled', 'button']"
@ -24,33 +53,3 @@
</translate>
</button>
</template>
<script>
export default {
props: {
library: { type: Object, required: true }
},
computed: {
isPending () {
return this.follow && this.follow.approved === null
},
isApproved () {
return this.follow && (this.follow.approved === true || (this.follow.approved === null && this.library.privacy_level === 'everyone'))
},
follow () {
return this.$store.getters['libraries/follow'](this.library.uuid)
}
},
methods: {
toggle () {
if (this.isApproved || this.isPending) {
this.$emit('unfollowed')
} else {
this.$emit('followed')
}
this.$store.dispatch('libraries/toggle', this.library.uuid)
}
}
}
</script>

View File

@ -1,3 +1,30 @@
<script setup lang="ts">
import type { Artist } from '~/types'
import PlayButton from '~/components/audio/PlayButton.vue'
import TagsList from '~/components/tags/List.vue'
import { computed } from 'vue'
import { useStore } from '~/store'
import { truncate } from '~/utils/filters'
interface Props {
artist: Artist
}
const props = defineProps<Props>()
const cover = computed(() => !props.artist.cover?.urls.original
? props.artist.albums.find(album => !!album.cover?.urls.original)?.cover
: props.artist.cover
)
const store = useStore()
const imageUrl = computed(() => cover.value?.urls.original
? store.getters['instance/absoluteUrl'](cover.value.urls.medium_square_crop)
: null
)
</script>
<template>
<div class="app-card card">
<router-link
@ -63,45 +90,3 @@
</div>
</div>
</template>
<script>
import PlayButton from '~/components/audio/PlayButton.vue'
import TagsList from '~/components/tags/List.vue'
import { truncate } from '~/utils/filters'
export default {
components: {
PlayButton,
TagsList
},
props: { artist: { type: Object, required: true } },
setup () {
return { truncate }
},
data () {
return {
initialAlbums: 30,
showAllAlbums: true
}
},
computed: {
imageUrl () {
const cover = this.cover
if (cover && cover.urls.original) {
return this.$store.getters['instance/absoluteUrl'](cover.urls.medium_square_crop)
}
return null
},
cover () {
if (this.artist.cover && this.artist.cover.urls.original) {
return this.artist.cover
}
return this.artist.albums.map((a) => {
return a.cover
}).filter((c) => {
return c && c.urls.original
})[0]
}
}
}
</script>

View File

@ -1,3 +1,40 @@
<script setup lang="ts">
import type { Track } from '~/types'
import PodcastRow from '~/components/audio/podcast/Row.vue'
import TrackMobileRow from '~/components/audio/track/MobileRow.vue'
import Pagination from '~/components/vui/Pagination.vue'
interface Props {
tracks: Track[]
showPosition?: boolean
showArt?: boolean
showDuration?: boolean
displayActions?: boolean
isArtist?: boolean
isAlbum?: boolean
isPodcast?: boolean
paginateResults?: boolean
paginateBy?: number
page?: number
total?: number
}
withDefaults(defineProps<Props>(), {
showPosition: false,
showArt: true,
showDuration: true,
displayActions: true,
isArtist: false,
isAlbum: false,
isPodcast: true,
paginateResults: true,
paginateBy: 25,
page: 1,
total: 0
})
</script>
<template>
<div>
<div class="ui hidden divider" />
@ -36,12 +73,6 @@
</div>
<div :class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']">
<div
v-if="isLoading"
class="ui inverted active dimmer"
>
<div class="ui loader" />
</div>
<!-- For each item, build a row -->
@ -73,54 +104,3 @@
</div>
</div>
</template>
<script>
import PodcastRow from '~/components/audio/podcast/Row.vue'
import TrackMobileRow from '~/components/audio/track/MobileRow.vue'
import Pagination from '~/components/vui/Pagination.vue'
export default {
components: {
TrackMobileRow,
Pagination,
PodcastRow
},
props: {
tracks: { type: Array, required: true },
showAlbum: { type: Boolean, required: false, default: true },
showArtist: { type: Boolean, required: false, default: true },
showPosition: { type: Boolean, required: false, default: false },
showArt: { type: Boolean, required: false, default: true },
search: { type: Boolean, required: false, default: false },
filters: { type: Object, required: false, default: null },
nextUrl: { type: String, required: false, default: null },
displayActions: { type: Boolean, required: false, default: true },
showDuration: { type: Boolean, required: false, default: true },
isArtist: { type: Boolean, required: false, default: false },
isAlbum: { type: Boolean, required: false, default: false },
paginateResults: { type: Boolean, required: false, default: true },
total: { type: Number, required: false, default: 0 },
page: { type: Number, required: false, default: 1 },
paginateBy: { type: Number, required: false, default: 25 },
isPodcast: { type: Boolean, required: true },
defaultCover: { type: Object, required: false, default: () => { return {} } }
},
data () {
return {
isLoading: false
}
},
computed: {
labels () {
return {
title: this.$pgettext('*/*/*/Noun', 'Title'),
album: this.$pgettext('*/*/*/Noun', 'Album'),
artist: this.$pgettext('*/*/*/Noun', 'Artist')
}
}
}
}
</script>

View File

@ -1,3 +1,32 @@
<script setup lang="ts">
import ApplicationForm from '~/components/auth/ApplicationForm.vue'
import { computed, reactive } from 'vue'
import { useGettext } from 'vue3-gettext'
interface Props {
name?: string
scopes?: string
redirectUris?: string
}
const props = withDefaults(defineProps<Props>(), {
name: '',
scopes: '',
redirectUris: ''
})
const defaults = reactive({
name: props.name,
scopes: props.scopes,
redirectUris: props.redirectUris,
})
const { $pgettext } = useGettext()
const labels = computed(() => ({
title: $pgettext('Content/Settings/Button.Label', 'Create a new application')
}))
</script>
<template>
<main
v-title="labels.title"
@ -23,36 +52,3 @@
</div>
</main>
</template>
<script>
import ApplicationForm from '~/components/auth/ApplicationForm.vue'
export default {
components: {
ApplicationForm
},
props: {
name: { type: String, default: '' },
redirectUris: { type: String, default: '' },
scopes: { type: String, default: '' }
},
data () {
return {
application: null,
isLoading: false,
defaults: {
name: this.name,
redirectUris: this.redirectUris,
scopes: this.scopes
}
}
},
computed: {
labels () {
return {
title: this.$pgettext('Content/Settings/Button.Label', 'Create a new application')
}
}
}
}
</script>

View File

@ -1,3 +1,13 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useGettext } from 'vue3-gettext'
const { $pgettext } = useGettext()
const labels = computed(() => ({
title: $pgettext('Head/Login/Title', 'Log Out')
}))
</script>
<template>
<main
v-title="labels.title"
@ -49,15 +59,3 @@
</section>
</main>
</template>
<script>
export default {
computed: {
labels () {
return {
title: this.$pgettext('Head/Login/Title', 'Log Out')
}
}
}
}
</script>

View File

@ -1,5 +1,28 @@
<script setup lang="ts">
import type { Channel } from '~/types'
import SemanticModal from '~/components/semantic/Modal.vue'
import ChannelAlbumForm from '~/components/channels/AlbumForm.vue'
import { watch, ref } from 'vue'
interface Props {
channel: Channel
}
defineProps<Props>()
const isLoading = ref(false)
const submittable = ref(false)
const show = ref(false)
watch(show, () => {
isLoading.value = false
submittable.value = false
})
</script>
<template>
<modal
<semantic-modal
v-model:show="show"
class="small"
>
@ -34,7 +57,7 @@
</button>
<button
:class="['ui', 'primary', {loading: isLoading}, 'button']"
:disabled="!submittable || null"
:disabled="!submittable"
@click.stop.prevent="$refs.albumForm.submit()"
>
<translate translate-context="*/*/Button.Label">
@ -42,31 +65,5 @@
</translate>
</button>
</div>
</modal>
</semantic-modal>
</template>
<script>
import Modal from '~/components/semantic/Modal.vue'
import ChannelAlbumForm from '~/components/channels/AlbumForm.vue'
export default {
components: {
Modal,
ChannelAlbumForm
},
props: { channel: { type: Object, required: true } },
data () {
return {
isLoading: false,
submittable: false,
show: false
}
},
watch: {
show () {
this.isLoading = false
this.submittable = false
}
}
}
</script>

View File

@ -1,3 +1,19 @@
<script setup lang="ts">
import type { Actor } from '~/types'
import { hashCode, intToRGB } from '~/utils/color'
import { computed } from 'vue'
interface Props {
actor: Actor
}
const props = defineProps<Props>()
const actorColor = computed(() => intToRGB(hashCode(props.actor.full_username)))
const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${actorColor.value}` }))
</script>
<template>
<img
v-if="actor.icon && actor.icon.urls.original"
@ -11,21 +27,3 @@
class="ui avatar circular label"
>{{ actor.preferred_username[0] }}</span>
</template>
<script>
import { hashCode, intToRGB } from '~/utils/color'
export default {
props: { actor: { type: Object, required: true } },
computed: {
actorColor () {
return intToRGB(hashCode(this.actor.full_username))
},
defaultAvatarStyle () {
return {
'background-color': `#${this.actorColor}`
}
}
}
}
</script>

View File

@ -1,3 +1,13 @@
<script setup lang="ts">
interface Props {
refresh?: boolean
}
withDefaults(defineProps<Props>(), {
refresh: false
})
</script>
<template>
<div class="ui small placeholder segment component-placeholder component-empty-state">
<h4 class="ui header">
@ -23,11 +33,4 @@
</button>
</div>
</div>
</template>
<script>
export default {
props: {
refresh: { type: Boolean, default: false }
}
}
</script>
</template>

View File

@ -1,15 +1,15 @@
<script setup lang="ts">
interface Props {
content: string
}
defineProps<Props>()
</script>
<template>
<span
class="tooltip"
:data-tooltip="content"
><i class="question circle icon" /></span>
</template>
<script>
export default {
props: {
content: { type: String, required: true }
}
}
</script>

View File

@ -1,3 +1,22 @@
<script setup lang="ts">
import type { User } from '~/types'
import { hashCode, intToRGB } from '~/utils/color'
import { computed } from 'vue'
interface Props {
user: User
avatar?: boolean
}
const props = withDefaults(defineProps<Props>(), {
avatar: true
})
const userColor = computed(() => intToRGB(hashCode(props.user.username + props.user.id)))
const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${userColor.value}` }))
</script>
<template>
<span class="component-user-link">
<template v-if="avatar">
@ -17,24 +36,3 @@
@{{ user.username }}
</span>
</template>
<script>
import { hashCode, intToRGB } from '~/utils/color'
export default {
props: {
user: { type: Object, required: true },
avatar: { type: Boolean, default: true }
},
computed: {
userColor () {
return intToRGB(hashCode(this.user.username + String(this.user.id)))
},
defaultAvatarStyle () {
return {
'background-color': `#${this.userColor}`
}
}
}
}
</script>

View File

@ -1,8 +1,11 @@
<script setup lang="ts">
interface Props {
username: string
}
defineProps<Props>()
</script>
<template>
<span>{{ username }}</span>
</template>
<script>
export default {
props: { username: { type: String, required: true } }
}
</script>
</template>

View File

@ -1,3 +1,34 @@
<script setup lang="ts">
import type { Note } from '~/types'
import axios from 'axios'
import showdown from 'showdown'
import { ref } from 'vue'
interface Props {
notes: Note[]
}
defineProps<Props>()
const markdown = new showdown.Converter()
const emit = defineEmits(['deleted'])
const isLoading = ref(false)
const remove = async (note: Note) => {
isLoading.value = true
try {
await axios.delete(`manage/moderation/notes/${note.uuid}/`)
emit('deleted', note.uuid)
} catch (error) {
// TODO (wvffle): Handle error
}
isLoading.value = false
}
</script>
<template>
<div class="ui feed">
<div
@ -61,32 +92,3 @@
</div>
</div>
</template>
<script>
import axios from 'axios'
import showdown from 'showdown'
export default {
props: {
notes: { type: Array, required: true }
},
data () {
return {
markdown: new showdown.Converter(),
isLoading: false
}
},
methods: {
remove (obj) {
const self = this
this.isLoading = true
axios.delete(`manage/moderation/notes/${obj.uuid}/`).then((response) => {
self.$emit('deleted', obj.uuid)
self.isLoading = false
}, () => {
self.isLoading = false
})
}
}
}
</script>

View File

@ -1,3 +1,30 @@
<script setup lang="ts">
import type { Playlist } from '~/types'
import PlayButton from '~/components/audio/PlayButton.vue'
import defaultCover from '~/assets/audio/default-cover.png'
import { computed } from 'vue'
import { useStore } from '~/store'
interface Props {
playlist: Playlist
}
const props = defineProps<Props>()
const store = useStore()
const images = computed(() => {
// TODO (wvffle): What is slicing for? is it 'http'?
const urls = props.playlist.album_covers.map(url => store.getters['instance/absoluteUrl'](url).slice(0, 4))
while (urls.length < 4) {
urls.push(defaultCover)
}
return urls
})
</script>
<template>
<div class="ui app-card card">
<div
@ -53,27 +80,3 @@
</div>
</div>
</template>
<script>
import PlayButton from '~/components/audio/PlayButton.vue'
import defaultCover from '~/assets/audio/default-cover.png'
export default {
components: {
PlayButton
},
props: { playlist: { type: Object, required: true } },
computed: {
images () {
const self = this
const urls = this.playlist.album_covers.map((url) => {
return self.$store.getters['instance/absoluteUrl'](url)
}).slice(0, 4)
while (urls.length < 4) {
urls.push(defaultCover)
}
return urls
}
}
}
</script>

View File

@ -1,3 +1,15 @@
<script setup lang="ts">
import type { Playlist } from '~/types'
import PlaylistCard from '~/components/playlists/Card.vue'
interface Props {
playlists: Playlist[]
}
defineProps<Props>()
</script>
<template>
<div v-if="playlists.length > 0">
<div class="ui app-cards cards">
@ -9,15 +21,3 @@
</div>
</div>
</template>
<script>
import PlaylistCard from '~/components/playlists/Card.vue'
export default {
components: {
PlaylistCard
},
props: { playlists: { type: Array, required: true } }
}
</script>

View File

@ -51,6 +51,8 @@ export interface Artist {
tracks_count: number
attributed_to: Actor
is_local: boolean
is_playable: boolean
modification_date?: string
}
export interface Album {
@ -109,6 +111,7 @@ export interface Channel {
rss_url: string
subscriptions_count: number
downloads_count: number
content_category: ContentCategory
}
export type PrivacyLevel = 'everyone' | 'instance' | 'me'
@ -164,6 +167,7 @@ export interface Playlist {
privacy_level: PrivacyLevel
tracks_count: number
duration: number
album_covers: string[]
is_playable: boolean
}
@ -283,7 +287,7 @@ export interface Actor {
export interface User {
id: string
avatar?: string
avatar?: Cover
username: string
full_username: string
instance_support_message_display_date: string
@ -326,4 +330,12 @@ export interface SettingsDataEntry {
additional_data: {
choices: [string, string]
}
}
// Note stuff
export interface Note {
uuid: string
author: Actor // TODO (wvffle): Check if is valid
summary: string
creation_date: string
}

View File

@ -1,3 +1,14 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useGettext } from 'vue3-gettext'
const { $pgettext } = useGettext()
const labels = computed(() => ({
title: $pgettext('Head/Admin/Title', 'Manage library'),
secondaryMenu: $pgettext('Menu/*/Hidden text', 'Secondary menu')
}))
</script>
<template>
<div
v-title="labels.title"
@ -76,18 +87,3 @@
<router-view :key="$route.fullPath" />
</div>
</template>
<script>
export default {
computed: {
labels () {
const title = this.$pgettext('Head/Admin/Title', 'Manage library')
const secondaryMenu = this.$pgettext('Menu/*/Hidden text', 'Secondary menu')
return {
title,
secondaryMenu
}
}
}
}
</script>