Use navigation guards and migrate a couple of components
This commit is contained in:
parent
1d4a3468ee
commit
5ea5ad3c2a
|
@ -1,14 +1,257 @@
|
|||
<script setup lang="ts">
|
||||
import type { Artist, Track, Album, Tag } from '~/types'
|
||||
import type { RouteRecordName, RouteLocationNamedRaw } from 'vue-router'
|
||||
|
||||
import jQuery from 'jquery'
|
||||
import { trim } from 'lodash-es'
|
||||
import { useFocus, useCurrentElement } from '@vueuse/core'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||
|
||||
interface Emits {
|
||||
(e: 'search'): void
|
||||
}
|
||||
|
||||
type CategoryCode = 'federation' | 'podcasts' | 'artists' | 'albums' | 'tracks' | 'tags' | 'more'
|
||||
interface Category {
|
||||
code: CategoryCode,
|
||||
name: string,
|
||||
route: RouteRecordName
|
||||
getId: (obj: unknown) => string
|
||||
getTitle: (obj: unknown) => string
|
||||
getDescription: (obj: unknown) => string
|
||||
}
|
||||
|
||||
type SimpleCategory = Partial<Category> & Pick<Category, 'code' | 'name'>
|
||||
const isCategoryGuard = (object: Category | SimpleCategory): object is Category => typeof object.route === 'string'
|
||||
|
||||
interface Results {
|
||||
name: string,
|
||||
results: Result[]
|
||||
}
|
||||
|
||||
interface Result {
|
||||
title: string
|
||||
id?: string
|
||||
description?: string
|
||||
routerUrl: RouteLocationNamedRaw
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const search = ref()
|
||||
const { focused } = useFocus(search)
|
||||
onKeyboardShortcut(['shift', 'f'], () => (focused.value = true), true)
|
||||
onKeyboardShortcut(['ctrl', 'k'], () => (focused.value = true), true)
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
const labels = computed(() => ({
|
||||
placeholder: $pgettext('Sidebar/Search/Input.Placeholder', 'Search for artists, albums, tracks…'),
|
||||
searchContent: $pgettext('Sidebar/Search/Input.Label', 'Search for content'),
|
||||
artist: $pgettext('*/*/*/Noun', 'Artist'),
|
||||
album: $pgettext('*/*/*', 'Album'),
|
||||
track: $pgettext('*/*/*/Noun', 'Track'),
|
||||
tag: $pgettext('*/*/*/Noun', 'Tag')
|
||||
}))
|
||||
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
const el = useCurrentElement()
|
||||
const query = ref()
|
||||
|
||||
const enter = () => {
|
||||
jQuery(el.value).search('cancel query')
|
||||
|
||||
// Cancel any API search request to backend…
|
||||
return router.push(`/search?q=${query.value}&type=artists`)
|
||||
}
|
||||
|
||||
const blur = () => {
|
||||
search.value.blur()
|
||||
}
|
||||
|
||||
const categories = computed(() => [
|
||||
{
|
||||
code: 'federation',
|
||||
name: $pgettext('*/*/*', 'Federation')
|
||||
},
|
||||
{
|
||||
code: 'podcasts',
|
||||
name: $pgettext('*/*/*', 'Podcasts')
|
||||
},
|
||||
{
|
||||
code: 'artists',
|
||||
route: 'library.artists.detail',
|
||||
name: labels.value.artist,
|
||||
getId: (obj: Artist) => obj.id,
|
||||
getTitle: (obj: Artist) => obj.name,
|
||||
getDescription: () => ''
|
||||
},
|
||||
{
|
||||
code: 'albums',
|
||||
route: 'library.albums.detail',
|
||||
name: labels.value.album,
|
||||
getId: (obj: Album) => obj.id,
|
||||
getTitle: (obj: Album) => obj.title,
|
||||
getDescription: (obj: Album) => obj.artist.name
|
||||
},
|
||||
{
|
||||
code: 'tracks',
|
||||
route: 'library.tracks.detail',
|
||||
name: labels.value.track,
|
||||
getId: (obj: Track) => obj.id,
|
||||
getTitle: (obj: Track) => obj.title,
|
||||
getDescription: (obj: Track) => obj.album?.artist.name ?? obj.artist?.name ?? ''
|
||||
},
|
||||
{
|
||||
code: 'tags',
|
||||
route: 'library.tags.detail',
|
||||
name: labels.value.tag,
|
||||
getId: (obj: Tag) => obj.name,
|
||||
getTitle: (obj: Tag) => `#${obj.name}`,
|
||||
getDescription: (obj: Tag) => ''
|
||||
},
|
||||
{
|
||||
code: 'more',
|
||||
name: ''
|
||||
}
|
||||
] as (Category | SimpleCategory)[])
|
||||
|
||||
const objectId = computed(() => {
|
||||
const trimmedQuery = trim(trim(query.value), '@')
|
||||
|
||||
if (trimmedQuery.startsWith('http://') || trimmedQuery.startsWith('https://') || trimmedQuery.includes('@')) {
|
||||
return query.value
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
jQuery(el.value).search({
|
||||
type: 'category',
|
||||
minCharacters: 3,
|
||||
showNoResults: true,
|
||||
error: {
|
||||
// @ts-expect-error Semantic is broken
|
||||
noResultsHeader: $pgettext('Sidebar/Search/Error', 'No matches found'),
|
||||
noResults: $pgettext('Sidebar/Search/Error.Label', 'Sorry, there are no results for this search')
|
||||
},
|
||||
|
||||
onSelect (result, response) {
|
||||
jQuery(el.value).search('set value', query.value)
|
||||
router.push(result.routerUrl)
|
||||
jQuery(el.value).search('hide results')
|
||||
return false
|
||||
},
|
||||
onSearchQuery (value) {
|
||||
// query.value = value
|
||||
emit('search')
|
||||
},
|
||||
apiSettings: {
|
||||
url: store.getters['instance/absoluteUrl']('api/v1/search?query={query}'),
|
||||
beforeXHR: function (xhrObject) {
|
||||
if (!store.state.auth.authenticated) {
|
||||
return xhrObject
|
||||
}
|
||||
|
||||
if (store.state.auth.oauth.accessToken) {
|
||||
xhrObject.setRequestHeader('Authorization', store.getters['auth/header'])
|
||||
}
|
||||
|
||||
return xhrObject
|
||||
},
|
||||
onResponse: function (initialResponse) {
|
||||
const id = objectId.value
|
||||
const results: Partial<Record<CategoryCode, Results>> = {}
|
||||
|
||||
let resultsEmpty = true
|
||||
for (const category of categories.value) {
|
||||
results[category.code] = {
|
||||
name: category.name,
|
||||
results: []
|
||||
}
|
||||
|
||||
if (category.code === 'federation' && id) {
|
||||
resultsEmpty = false
|
||||
results[category.code]?.results.push({
|
||||
title: $pgettext('Search/*/*', 'Search on the fediverse'),
|
||||
routerUrl: {
|
||||
name: 'search',
|
||||
query: { id }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (category.code === 'podcasts' && id) {
|
||||
resultsEmpty = false
|
||||
results[category.code]?.results.push({
|
||||
title: $pgettext('Search/*/*', 'Subscribe to podcast via RSS'),
|
||||
routerUrl: {
|
||||
name: 'search',
|
||||
query: { id, type: 'rss' }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (category.code === 'more') {
|
||||
results[category.code]?.results.push({
|
||||
title: $pgettext('Search/*/*', 'More results 🡒'),
|
||||
routerUrl: {
|
||||
name: 'search',
|
||||
query: { type: 'artists', q: query.value }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isCategoryGuard(category)) {
|
||||
for (const result of initialResponse[category.code]) {
|
||||
resultsEmpty = false
|
||||
const id = category.getId(result)
|
||||
results[category.code]?.results.push({
|
||||
title: category.getTitle(result),
|
||||
id,
|
||||
routerUrl: {
|
||||
name: category.route,
|
||||
params: { id }
|
||||
},
|
||||
description: category.getDescription(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
results: resultsEmpty
|
||||
? {}
|
||||
: results
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui fluid category search">
|
||||
<slot /><div class="ui icon input">
|
||||
<div
|
||||
class="ui fluid category search"
|
||||
@keypress.enter="enter"
|
||||
>
|
||||
<slot />
|
||||
<div class="ui icon input">
|
||||
<input
|
||||
ref="search"
|
||||
v-model="query"
|
||||
:aria-label="labels.searchContent"
|
||||
type="search"
|
||||
class="prompt"
|
||||
name="search"
|
||||
:placeholder="labels.placeholder"
|
||||
@keydown.esc="$event.target.blur()"
|
||||
@keydown.esc="blur"
|
||||
>
|
||||
<i class="search icon" />
|
||||
</div>
|
||||
|
@ -16,252 +259,3 @@
|
|||
<slot name="after" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import jQuery from 'jquery'
|
||||
import router from '~/router'
|
||||
import { trim } from 'lodash-es'
|
||||
import { useFocus } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||
|
||||
export default {
|
||||
setup () {
|
||||
const search = ref()
|
||||
const { focused } = useFocus(search)
|
||||
onKeyboardShortcut(['shift', 'f'], () => (focused.value = true), true)
|
||||
|
||||
return {
|
||||
search
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
placeholder: this.$pgettext('Sidebar/Search/Input.Placeholder', 'Search for artists, albums, tracks…'),
|
||||
searchContent: this.$pgettext('Sidebar/Search/Input.Label', 'Search for content')
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
const artistLabel = this.$pgettext('*/*/*/Noun', 'Artist')
|
||||
const albumLabel = this.$pgettext('*/*/*', 'Album')
|
||||
const trackLabel = this.$pgettext('*/*/*/Noun', 'Track')
|
||||
const tagLabel = this.$pgettext('*/*/*/Noun', 'Tag')
|
||||
const self = this
|
||||
let searchQuery
|
||||
|
||||
jQuery(this.$el).keypress(function (e) {
|
||||
if (e.which === 13) {
|
||||
// Cancel any API search request to backend…
|
||||
jQuery(this.$el).search('cancel query')
|
||||
// Go direct to the artist page…
|
||||
router.push(`/search?q=${searchQuery}&type=artists`)
|
||||
}
|
||||
})
|
||||
|
||||
jQuery(this.$el).search({
|
||||
type: 'category',
|
||||
minCharacters: 3,
|
||||
showNoResults: true,
|
||||
error: {
|
||||
noResultsHeader: this.$pgettext('Sidebar/Search/Error', 'No matches found'),
|
||||
noResults: this.$pgettext('Sidebar/Search/Error.Label', 'Sorry, there are no results for this search')
|
||||
},
|
||||
onSelect (result, response) {
|
||||
jQuery(self.$el).search('set value', searchQuery)
|
||||
router.push(result.routerUrl)
|
||||
jQuery(self.$el).search('hide results')
|
||||
return false
|
||||
},
|
||||
onSearchQuery (query) {
|
||||
self.$emit('search')
|
||||
searchQuery = query
|
||||
},
|
||||
apiSettings: {
|
||||
beforeXHR: function (xhrObject) {
|
||||
if (!self.$store.state.auth.authenticated) {
|
||||
return xhrObject
|
||||
}
|
||||
|
||||
if (self.$store.state.auth.oauth.accessToken) {
|
||||
xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
|
||||
}
|
||||
return xhrObject
|
||||
},
|
||||
onResponse: function (initialResponse) {
|
||||
const objId = self.extractObjId(searchQuery)
|
||||
const results = {}
|
||||
let isEmptyResults = true
|
||||
const categories = [
|
||||
{
|
||||
code: 'federation',
|
||||
name: self.$pgettext('*/*/*', 'Federation')
|
||||
},
|
||||
{
|
||||
code: 'podcasts',
|
||||
name: self.$pgettext('*/*/*', 'Podcasts')
|
||||
},
|
||||
{
|
||||
code: 'artists',
|
||||
route: 'library.artists.detail',
|
||||
name: artistLabel,
|
||||
getTitle (r) {
|
||||
return r.name
|
||||
},
|
||||
getDescription (r) {
|
||||
return ''
|
||||
},
|
||||
getId (t) {
|
||||
return t.id
|
||||
}
|
||||
},
|
||||
{
|
||||
code: 'albums',
|
||||
route: 'library.albums.detail',
|
||||
name: albumLabel,
|
||||
getTitle (r) {
|
||||
return r.title
|
||||
},
|
||||
getDescription (r) {
|
||||
return r.artist.name
|
||||
},
|
||||
getId (t) {
|
||||
return t.id
|
||||
}
|
||||
},
|
||||
{
|
||||
code: 'tracks',
|
||||
route: 'library.tracks.detail',
|
||||
name: trackLabel,
|
||||
getTitle (r) {
|
||||
return r.title
|
||||
},
|
||||
getDescription (r) {
|
||||
if (r.album) {
|
||||
return `${r.album.artist.name} - ${r.album.title}`
|
||||
} else {
|
||||
return r.artist.name
|
||||
}
|
||||
},
|
||||
getId (t) {
|
||||
return t.id
|
||||
}
|
||||
},
|
||||
{
|
||||
code: 'tags',
|
||||
route: 'library.tags.detail',
|
||||
name: tagLabel,
|
||||
getTitle (r) {
|
||||
return `#${r.name}`
|
||||
},
|
||||
getDescription (r) {
|
||||
return ''
|
||||
},
|
||||
getId (t) {
|
||||
return t.name
|
||||
}
|
||||
},
|
||||
{
|
||||
code: 'more',
|
||||
name: ''
|
||||
}
|
||||
]
|
||||
categories.forEach(category => {
|
||||
results[category.code] = {
|
||||
name: category.name,
|
||||
results: []
|
||||
}
|
||||
if (category.code === 'federation') {
|
||||
if (objId) {
|
||||
isEmptyResults = false
|
||||
const searchMessage = self.$pgettext('Search/*/*', 'Search on the fediverse')
|
||||
results.federation = {
|
||||
name: self.$pgettext('*/*/*', 'Federation'),
|
||||
results: [{
|
||||
title: searchMessage,
|
||||
routerUrl: {
|
||||
name: 'search',
|
||||
query: {
|
||||
id: objId
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
} else if (category.code === 'podcasts') {
|
||||
if (objId) {
|
||||
isEmptyResults = false
|
||||
const searchMessage = self.$pgettext('Search/*/*', 'Subscribe to podcast via RSS')
|
||||
results.podcasts = {
|
||||
name: self.$pgettext('*/*/*', 'Podcasts'),
|
||||
results: [{
|
||||
title: searchMessage,
|
||||
routerUrl: {
|
||||
name: 'search',
|
||||
query: {
|
||||
id: objId,
|
||||
type: 'rss'
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
} else if (category.code === 'more') {
|
||||
const searchMessage = self.$pgettext('Search/*/*', 'More results 🡒')
|
||||
results.more = {
|
||||
name: '',
|
||||
results: [{
|
||||
title: searchMessage,
|
||||
routerUrl: {
|
||||
name: 'search',
|
||||
query: {
|
||||
type: 'artists',
|
||||
q: searchQuery
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
} else {
|
||||
initialResponse[category.code].forEach(result => {
|
||||
isEmptyResults = false
|
||||
const id = category.getId(result)
|
||||
results[category.code].results.push({
|
||||
title: category.getTitle(result),
|
||||
id,
|
||||
routerUrl: {
|
||||
name: category.route,
|
||||
params: {
|
||||
id
|
||||
}
|
||||
},
|
||||
description: category.getDescription(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
return {
|
||||
results: isEmptyResults ? {} : results
|
||||
}
|
||||
},
|
||||
url: this.$store.getters['instance/absoluteUrl']('api/v1/search?query={query}')
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
extractObjId (query) {
|
||||
query = trim(query)
|
||||
query = trim(query, '@')
|
||||
if (query.indexOf(' ') > -1) {
|
||||
return
|
||||
}
|
||||
if (query.startsWith('http://') || query.startsWith('https://')) {
|
||||
return query
|
||||
}
|
||||
if (query.split('@').length > 1) {
|
||||
return query
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,66 @@
|
|||
<script setup lang="ts">
|
||||
import type { Album } from '~/types'
|
||||
|
||||
import axios from 'axios'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import AlbumCard from '~/components/audio/album/Card.vue'
|
||||
|
||||
interface Props {
|
||||
filters: Record<string, string>
|
||||
showCount?: boolean
|
||||
search?: boolean
|
||||
limit?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showCount: false,
|
||||
search: false,
|
||||
limit: 12
|
||||
})
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const query = ref('')
|
||||
const albums = reactive([] as Album[])
|
||||
const count = ref(0)
|
||||
const nextPage = ref()
|
||||
|
||||
const isLoading = ref(false)
|
||||
const fetchData = async (url = 'albums/') => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const params = {
|
||||
q: query.value,
|
||||
...props.filters,
|
||||
page_size: props.limit
|
||||
}
|
||||
|
||||
const response = await axios.get(url, { params })
|
||||
nextPage.value = response.data.next
|
||||
count.value = response.data.count
|
||||
albums.push(...response.data.results)
|
||||
} catch (error) {
|
||||
// TODO (wvffle): Handle error
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const performSearch = () => {
|
||||
albums.length = 0
|
||||
fetchData()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => store.state.moderation.lastUpdate,
|
||||
() => fetchData(),
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<h3
|
||||
|
@ -53,78 +116,3 @@
|
|||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import AlbumCard from '~/components/audio/album/Card.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AlbumCard
|
||||
},
|
||||
props: {
|
||||
filters: { type: Object, required: true },
|
||||
controls: { type: Boolean, default: true },
|
||||
showCount: { type: Boolean, default: false },
|
||||
search: { type: Boolean, default: false },
|
||||
limit: { type: Number, default: 12 }
|
||||
},
|
||||
setup () {
|
||||
const performSearch = () => {
|
||||
this.albums.length = 0
|
||||
this.fetchData()
|
||||
}
|
||||
|
||||
return { performSearch }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
albums: [],
|
||||
count: 0,
|
||||
isLoading: false,
|
||||
errors: null,
|
||||
previousPage: null,
|
||||
nextPage: null,
|
||||
query: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
offset () {
|
||||
this.fetchData()
|
||||
},
|
||||
'$store.state.moderation.lastUpdate': function () {
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
fetchData (url) {
|
||||
url = url || 'albums/'
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const params = { q: this.query, ...this.filters }
|
||||
params.page_size = this.limit
|
||||
params.offset = this.offset
|
||||
axios.get(url, { params }).then((response) => {
|
||||
self.previousPage = response.data.previous
|
||||
self.nextPage = response.data.next
|
||||
self.isLoading = false
|
||||
self.albums = [...self.albums, ...response.data.results]
|
||||
self.count = response.data.count
|
||||
}, error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
},
|
||||
updateOffset (increment) {
|
||||
if (increment) {
|
||||
this.offset += this.limit
|
||||
} else {
|
||||
this.offset = Math.max(this.offset - this.limit, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,66 @@
|
|||
<script setup lang="ts">
|
||||
import type { Artist } from '~/types'
|
||||
|
||||
import axios from 'axios'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
import ArtistCard from '~/components/audio/artist/Card.vue'
|
||||
|
||||
interface Props {
|
||||
filters: Record<string, string>
|
||||
search?: boolean
|
||||
header?: boolean
|
||||
limit?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
search: false,
|
||||
header: true,
|
||||
limit: 12
|
||||
})
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const query = ref('')
|
||||
const artists = reactive([] as Artist[])
|
||||
const count = ref(0)
|
||||
const nextPage = ref()
|
||||
|
||||
const isLoading = ref(false)
|
||||
const fetchData = async (url = 'artists/') => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const params = {
|
||||
q: query.value,
|
||||
...props.filters,
|
||||
page_size: props.limit
|
||||
}
|
||||
|
||||
const response = await axios.get(url, { params })
|
||||
nextPage.value = response.data.next
|
||||
count.value = response.data.count
|
||||
artists.push(...response.data.results)
|
||||
} catch (error) {
|
||||
// TODO (wvffle): Handle error
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const performSearch = () => {
|
||||
artists.length = 0
|
||||
fetchData()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => store.state.moderation.lastUpdate,
|
||||
() => fetchData(),
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<h3
|
||||
|
@ -21,13 +84,13 @@
|
|||
<div class="ui loader" />
|
||||
</div>
|
||||
<artist-card
|
||||
v-for="artist in objects"
|
||||
v-for="artist in artists"
|
||||
:key="artist.id"
|
||||
:artist="artist"
|
||||
/>
|
||||
</div>
|
||||
<slot
|
||||
v-if="!isLoading && objects.length === 0"
|
||||
v-if="!isLoading && artists.length === 0"
|
||||
name="empty-state"
|
||||
>
|
||||
<empty-state
|
||||
|
@ -49,78 +112,3 @@
|
|||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import ArtistCard from '~/components/audio/artist/Card.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ArtistCard
|
||||
},
|
||||
props: {
|
||||
filters: { type: Object, required: true },
|
||||
controls: { type: Boolean, default: true },
|
||||
header: { type: Boolean, default: true },
|
||||
search: { type: Boolean, default: false }
|
||||
},
|
||||
setup () {
|
||||
const performSearch = () => {
|
||||
this.objects.length = 0
|
||||
this.fetchData()
|
||||
}
|
||||
|
||||
return { performSearch }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
objects: [],
|
||||
limit: 12,
|
||||
count: 0,
|
||||
isLoading: false,
|
||||
errors: null,
|
||||
previousPage: null,
|
||||
nextPage: null,
|
||||
query: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
offset () {
|
||||
this.fetchData()
|
||||
},
|
||||
'$store.state.moderation.lastUpdate': function () {
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
fetchData (url) {
|
||||
url = url || 'artists/'
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const params = { q: this.query, ...this.filters }
|
||||
params.page_size = this.limit
|
||||
params.offset = this.offset
|
||||
axios.get(url, { params }).then((response) => {
|
||||
self.previousPage = response.data.previous
|
||||
self.nextPage = response.data.next
|
||||
self.isLoading = false
|
||||
self.objects = [...self.objects, ...response.data.results]
|
||||
self.count = response.data.count
|
||||
}, error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
},
|
||||
updateOffset (increment) {
|
||||
if (increment) {
|
||||
this.offset += this.limit
|
||||
} else {
|
||||
this.offset = Math.max(this.offset - this.limit, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
import type { Track, Listening } from '~/types'
|
||||
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
||||
|
||||
// TODO (wvffle): Fix websocket update (#1534)
|
||||
import { clone } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { useStore } from '~/store'
|
||||
import { clone } from 'lodash-es'
|
||||
|
||||
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
import TagsList from '~/components/tags/List.vue'
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
|
||||
interface Emits {
|
||||
(e: 'count', count: number): void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
filters: Record<string, string>
|
||||
|
@ -19,6 +25,7 @@ interface Props {
|
|||
websocketHandlers?: string[]
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isActivity: true,
|
||||
showCount: false,
|
||||
|
@ -27,6 +34,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
websocketHandlers: () => []
|
||||
})
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const objects = reactive([] as Listening[])
|
||||
const count = ref(0)
|
||||
const nextPage = ref<string | null>(null)
|
||||
|
@ -57,9 +66,12 @@ const fetchData = async (url = props.url) => {
|
|||
isLoading.value = false
|
||||
}
|
||||
|
||||
fetchData()
|
||||
watch(
|
||||
() => store.state.moderation.lastUpdate,
|
||||
() => fetchData(),
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const emit = defineEmits(['count'])
|
||||
watch(count, (to) => emit('count', to))
|
||||
|
||||
watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||
|
|
|
@ -1,3 +1,54 @@
|
|||
<script setup lang="ts">
|
||||
import axios from 'axios'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ApplicationForm from '~/components/auth/ApplicationForm.vue'
|
||||
|
||||
interface Props {
|
||||
id: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
|
||||
const application = ref()
|
||||
|
||||
const labels = computed(() => ({
|
||||
title: $pgettext('Content/Applications/Title', 'Edit application')
|
||||
}))
|
||||
|
||||
const isLoading = ref(false)
|
||||
const fetchApplication = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await axios.get(`oauth/apps/${props.id}/`)
|
||||
application.value = response.data
|
||||
} catch (error) {
|
||||
// TODO (wvffle): Handle error
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const refreshToken = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await axios.post(`oauth/apps/${props.id}/refresh-token`)
|
||||
application.value = response.data
|
||||
} catch (error) {
|
||||
// TODO (wvffle): Handle error
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
fetchApplication()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main
|
||||
v-title="labels.title"
|
||||
|
@ -74,51 +125,3 @@
|
|||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
import ApplicationForm from '~/components/auth/ApplicationForm.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ApplicationForm
|
||||
},
|
||||
props: { id: { type: Number, required: true } },
|
||||
data () {
|
||||
return {
|
||||
application: null,
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('Content/Applications/Title', 'Edit application')
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchApplication()
|
||||
},
|
||||
methods: {
|
||||
fetchApplication () {
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
axios.get(`oauth/apps/${this.id}/`).then((response) => {
|
||||
self.isLoading = false
|
||||
self.application = response.data
|
||||
}, error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
},
|
||||
async refreshToken () {
|
||||
self.isLoading = true
|
||||
const response = await axios.post(`oauth/apps/${this.id}/refresh-token`)
|
||||
this.application = response.data
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,95 @@
|
|||
<script setup lang="ts">
|
||||
import type { BackendError, Application } from '~/types'
|
||||
|
||||
import axios from 'axios'
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { computedEager } from '@vueuse/core'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { uniq } from 'lodash-es'
|
||||
|
||||
import useScopes from '~/composables/auth/useScopes'
|
||||
|
||||
interface Emits {
|
||||
(e: 'updated', application: Application): void
|
||||
(e: 'created', application: Application): void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
app?: Application | null
|
||||
defaults?: Partial<Application>
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
app: () => null,
|
||||
defaults: () => ({})
|
||||
})
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
const scopes = useScopes()
|
||||
.filter(scope => !['reports', 'security'].includes(scope.id))
|
||||
|
||||
const fields = reactive({
|
||||
name: props.app?.name ?? props.defaults.name ?? '',
|
||||
redirect_uris: props.app?.redirect_uris ?? props.defaults.redirect_uris ?? 'urn:ietf:wg:oauth:2.0:oob',
|
||||
scopes: props.app?.scopes ?? props.defaults.scopes ?? 'read'
|
||||
})
|
||||
|
||||
const errors = ref([] as string[])
|
||||
const isLoading = ref(false)
|
||||
const submit = async () => {
|
||||
errors.value = []
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const event = props.app !== null
|
||||
? 'updated'
|
||||
: 'created'
|
||||
|
||||
const request = props.app !== null
|
||||
? () => axios.patch(`oauth/apps/${props.app?.client_id}/`, fields)
|
||||
: () => axios.post('oauth/apps/', fields)
|
||||
|
||||
const response = await request()
|
||||
emit(event, response.data as Application)
|
||||
} catch (error) {
|
||||
errors.value = (error as BackendError).backendErrors
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const scopeArray = computed({
|
||||
get: () => fields.scopes.split(' '),
|
||||
set: (scopes: string[]) => uniq(scopes).join(' ')
|
||||
})
|
||||
|
||||
const scopeParents = computedEager(() => [
|
||||
{
|
||||
id: 'read',
|
||||
label: $pgettext('Content/OAuth Scopes/Label/Verb', 'Read'),
|
||||
description: $pgettext('Content/OAuth Scopes/Help Text', 'Read-only access to user data'),
|
||||
value: scopeArray.value.includes('read')
|
||||
},
|
||||
{
|
||||
id: 'write',
|
||||
label: $pgettext('Content/OAuth Scopes/Label/Verb', 'Write'),
|
||||
description: $pgettext('Content/OAuth Scopes/Help Text', 'Write-only access to user data'),
|
||||
value: scopeArray.value.includes('write')
|
||||
}
|
||||
])
|
||||
|
||||
const allScopes = computed(() => {
|
||||
return scopeParents.value.map(parent => ({
|
||||
...parent,
|
||||
children: scopes.map(scope => {
|
||||
const id = `${parent.id}:${scope.id}`
|
||||
return { id, value: scopeArray.value.includes(id) }
|
||||
})
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
class="ui form component-form"
|
||||
|
@ -75,8 +167,8 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-for="(child, index) in parent.children"
|
||||
:key="index"
|
||||
v-for="child in parent.children"
|
||||
:key="child.id"
|
||||
>
|
||||
<div class="ui child checkbox">
|
||||
<input
|
||||
|
@ -87,9 +179,6 @@
|
|||
>
|
||||
<label :for="child.id">
|
||||
{{ child.id }}
|
||||
<p class="help">
|
||||
{{ child.description }}
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -101,7 +190,7 @@
|
|||
type="submit"
|
||||
>
|
||||
<translate
|
||||
v-if="updating"
|
||||
v-if="app !== null"
|
||||
translate-context="Content/Applications/Button.Label/Verb"
|
||||
>
|
||||
Update application
|
||||
|
@ -115,111 +204,3 @@
|
|||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { uniq } from 'lodash-es'
|
||||
import axios from 'axios'
|
||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
app: { type: Object, required: false, default: () => { return null } },
|
||||
defaults: { type: Object, required: false, default: () => { return {} } }
|
||||
},
|
||||
setup () {
|
||||
const sharedLabels = useSharedLabels()
|
||||
return { sharedLabels }
|
||||
},
|
||||
data () {
|
||||
const defaults = this.defaults || {}
|
||||
const app = this.app || {}
|
||||
return {
|
||||
isLoading: false,
|
||||
errors: [],
|
||||
fields: {
|
||||
name: app.name || defaults.name || '',
|
||||
redirect_uris: app.redirect_uris || defaults.redirect_uris || 'urn:ietf:wg:oauth:2.0:oob',
|
||||
scopes: app.scopes || defaults.scopes || 'read'
|
||||
},
|
||||
scopes: [
|
||||
{ id: 'profile', icon: 'user' },
|
||||
{ id: 'libraries', icon: 'book' },
|
||||
{ id: 'favorites', icon: 'heart' },
|
||||
{ id: 'listenings', icon: 'music' },
|
||||
{ id: 'follows', icon: 'users' },
|
||||
{ id: 'playlists', icon: 'list' },
|
||||
{ id: 'radios', icon: 'rss' },
|
||||
{ id: 'filters', icon: 'eye slash' },
|
||||
{ id: 'notifications', icon: 'bell' },
|
||||
{ id: 'edits', icon: 'pencil alternate' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
updating () {
|
||||
return this.app
|
||||
},
|
||||
scopeArray: {
|
||||
get () {
|
||||
return this.fields.scopes.split(' ')
|
||||
},
|
||||
set (v) {
|
||||
this.fields.scopes = uniq(v).join(' ')
|
||||
}
|
||||
},
|
||||
allScopes () {
|
||||
const self = this
|
||||
const parents = [
|
||||
{
|
||||
id: 'read',
|
||||
label: this.$pgettext('Content/OAuth Scopes/Label/Verb', 'Read'),
|
||||
description: this.$pgettext('Content/OAuth Scopes/Help Text', 'Read-only access to user data'),
|
||||
value: this.scopeArray.indexOf('read') > -1
|
||||
},
|
||||
{
|
||||
id: 'write',
|
||||
label: this.$pgettext('Content/OAuth Scopes/Label/Verb', 'Write'),
|
||||
description: this.$pgettext('Content/OAuth Scopes/Help Text', 'Write-only access to user data'),
|
||||
value: this.scopeArray.indexOf('write') > -1
|
||||
}
|
||||
]
|
||||
parents.forEach((p) => {
|
||||
p.children = self.scopes.map(s => {
|
||||
const id = `${p.id}:${s.id}`
|
||||
return {
|
||||
id,
|
||||
value: this.scopeArray.indexOf(id) > -1
|
||||
}
|
||||
})
|
||||
})
|
||||
return parents
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit () {
|
||||
this.errors = []
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
const payload = this.fields
|
||||
let event, promise
|
||||
if (this.updating) {
|
||||
event = 'updated'
|
||||
promise = axios.patch(`oauth/apps/${this.app.client_id}/`, payload)
|
||||
} else {
|
||||
event = 'created'
|
||||
promise = axios.post('oauth/apps/', payload)
|
||||
}
|
||||
return promise.then(
|
||||
response => {
|
||||
self.isLoading = false
|
||||
self.$emit(event, response.data)
|
||||
},
|
||||
error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,116 @@
|
|||
<script setup lang="ts">
|
||||
import type { BackendError, Application } from '~/types'
|
||||
|
||||
import axios from 'axios'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||
import useScopes from '~/composables/auth/useScopes'
|
||||
import useFormData from '~/composables/useFormData'
|
||||
|
||||
interface Props {
|
||||
clientId: string
|
||||
redirectUri: string
|
||||
scope: string
|
||||
responseType: string
|
||||
nonce: string
|
||||
state: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
const sharedLabels = useSharedLabels()
|
||||
const knownScopes = useScopes()
|
||||
|
||||
const supportedScopes = ['read', 'write']
|
||||
for (const scope of knownScopes) {
|
||||
supportedScopes.push(`read:${scope.id}`)
|
||||
supportedScopes.push(`write:${scope.id}`)
|
||||
}
|
||||
|
||||
const application = ref()
|
||||
|
||||
const errors = ref([] as string[])
|
||||
const isLoading = ref(false)
|
||||
const fetchApplication = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await axios.get(`oauth/apps/${props.clientId}/`)
|
||||
application.value = response.data as Application
|
||||
} catch (error) {
|
||||
errors.value = (error as BackendError).backendErrors
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const code = ref()
|
||||
const submit = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const data = useFormData({
|
||||
redirect_uri: props.redirectUri,
|
||||
scope: props.scope,
|
||||
allow: 'true',
|
||||
client_id: props.clientId,
|
||||
response_type: props.responseType,
|
||||
state: props.state,
|
||||
nonce: props.nonce
|
||||
})
|
||||
|
||||
const response = await axios.post('oauth/authorize/', data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
|
||||
if (props.redirectUri !== 'urn:ietf:wg:oauth:2.0:oob') {
|
||||
window.location.href = response.data.redirect_uri
|
||||
return
|
||||
}
|
||||
|
||||
code.value = response.data.code
|
||||
} catch (error) {
|
||||
errors.value = (error as BackendError).backendErrors
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const labels = computed(() => ({
|
||||
title: $pgettext('Head/Authorize/Title', 'Allow application')
|
||||
}))
|
||||
|
||||
const requestedScopes = computed(() => props.scope.split(' '))
|
||||
const unknownRequestedScopes = computed(() => requestedScopes.value.filter(scope => !supportedScopes.includes(scope)))
|
||||
const topicScopes = computed(() => {
|
||||
const requested = requestedScopes.value
|
||||
|
||||
const write = requested.includes('write')
|
||||
const read = requested.includes('read')
|
||||
|
||||
return knownScopes.map(scope => {
|
||||
const { id } = scope
|
||||
return {
|
||||
id,
|
||||
icon: scope.icon,
|
||||
label: sharedLabels.scopes[id].label,
|
||||
description: sharedLabels.scopes[id].description,
|
||||
read: read || requested.includes(`read:${id}`),
|
||||
write: write || requested.includes(`write:${id}`)
|
||||
}
|
||||
}).filter(scope => scope.read || scope.write)
|
||||
})
|
||||
|
||||
whenever(() => props.clientId, fetchApplication)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main
|
||||
v-title="labels.title"
|
||||
|
@ -138,142 +251,3 @@
|
|||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
import { checkRedirectToLogin } from '~/utils'
|
||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||
import useFormData from '~/composables/useFormData'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
clientId: { type: String, required: true },
|
||||
redirectUri: { type: String, required: true },
|
||||
scope: { type: String, required: true },
|
||||
responseType: { type: String, required: true },
|
||||
nonce: { type: String, required: true },
|
||||
state: { type: String, required: true }
|
||||
},
|
||||
setup () {
|
||||
const sharedLabels = useSharedLabels()
|
||||
return { sharedLabels }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
application: null,
|
||||
isLoading: false,
|
||||
errors: [],
|
||||
code: null,
|
||||
knownScopes: [
|
||||
{ id: 'profile', icon: 'user' },
|
||||
{ id: 'libraries', icon: 'book' },
|
||||
{ id: 'favorites', icon: 'heart' },
|
||||
{ id: 'listenings', icon: 'music' },
|
||||
{ id: 'follows', icon: 'users' },
|
||||
{ id: 'playlists', icon: 'list' },
|
||||
{ id: 'radios', icon: 'rss' },
|
||||
{ id: 'filters', icon: 'eye slash' },
|
||||
{ id: 'notifications', icon: 'bell' },
|
||||
{ id: 'edits', icon: 'pencil alternate' },
|
||||
{ id: 'security', icon: 'lock' },
|
||||
{ id: 'reports', icon: 'warning sign' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('Head/Authorize/Title', 'Allow application')
|
||||
}
|
||||
},
|
||||
requestedScopes () {
|
||||
return (this.scope || '').split(' ')
|
||||
},
|
||||
supportedScopes () {
|
||||
const supported = ['read', 'write']
|
||||
this.knownScopes.forEach(s => {
|
||||
supported.push(`read:${s.id}`)
|
||||
supported.push(`write:${s.id}`)
|
||||
})
|
||||
return supported
|
||||
},
|
||||
unknownRequestedScopes () {
|
||||
const self = this
|
||||
return this.requestedScopes.filter(s => {
|
||||
return self.supportedScopes.indexOf(s) < 0
|
||||
})
|
||||
},
|
||||
topicScopes () {
|
||||
const self = this
|
||||
const requested = this.requestedScopes
|
||||
let write = false
|
||||
let read = false
|
||||
if (requested.indexOf('read') > -1) {
|
||||
read = true
|
||||
}
|
||||
if (requested.indexOf('write') > -1) {
|
||||
write = true
|
||||
}
|
||||
|
||||
return this.knownScopes.map(s => {
|
||||
const id = s.id
|
||||
return {
|
||||
id,
|
||||
icon: s.icon,
|
||||
label: self.sharedLabels.scopes[s.id].label,
|
||||
description: self.sharedLabels.scopes[s.id].description,
|
||||
read: read || requested.indexOf(`read:${id}`) > -1,
|
||||
write: write || requested.indexOf(`write:${id}`) > -1
|
||||
}
|
||||
}).filter(c => {
|
||||
return c.read || c.write
|
||||
})
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
await checkRedirectToLogin(this.$store, this.$router)
|
||||
if (this.clientId) {
|
||||
this.fetchApplication()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchApplication () {
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
axios.get(`oauth/apps/${this.clientId}/`).then((response) => {
|
||||
self.isLoading = false
|
||||
self.application = response.data
|
||||
}, error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
},
|
||||
submit () {
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const data = useFormData({
|
||||
redirect_uri: this.redirectUri,
|
||||
scope: this.scope,
|
||||
allow: true,
|
||||
client_id: this.clientId,
|
||||
response_type: this.responseType,
|
||||
state: this.state,
|
||||
nonce: this.nonce
|
||||
})
|
||||
|
||||
axios.post('oauth/authorize/', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' } }).then((response) => {
|
||||
if (self.redirectUri === 'urn:ietf:wg:oauth:2.0:oob') {
|
||||
self.isLoading = false
|
||||
self.code = response.data.code
|
||||
} else {
|
||||
window.location.href = response.data.redirect_uri
|
||||
}
|
||||
}, error => {
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -7,7 +7,6 @@ import axios from 'axios'
|
|||
import $ from 'jquery'
|
||||
import RadioButton from '~/components/radios/Button.vue'
|
||||
import Pagination from '~/components/vui/Pagination.vue'
|
||||
import { checkRedirectToLogin } from '~/utils'
|
||||
import TrackTable from '~/components/audio/track/Table.vue'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||
|
@ -29,7 +28,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
})
|
||||
|
||||
const store = useStore()
|
||||
await checkRedirectToLogin(store, useRouter())
|
||||
|
||||
// TODO (wvffle): Make sure everything is it's own type
|
||||
const page = ref(+props.defaultPage)
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
export type ScopeId = 'profile' | 'libraries' | 'favorites' | 'listenings' | 'follows'
|
||||
| 'playlists' | 'radios' | 'filters' | 'notifications' | 'edits' | 'security' | 'reports'
|
||||
|
||||
export default () => [
|
||||
{ id: 'profile', icon: 'user' },
|
||||
{ id: 'libraries', icon: 'book' },
|
||||
{ id: 'favorites', icon: 'heart' },
|
||||
{ id: 'listenings', icon: 'music' },
|
||||
{ id: 'follows', icon: 'users' },
|
||||
{ id: 'playlists', icon: 'list' },
|
||||
{ id: 'radios', icon: 'rss' },
|
||||
{ id: 'filters', icon: 'eye slash' },
|
||||
{ id: 'notifications', icon: 'bell' },
|
||||
{ id: 'edits', icon: 'pencil alternate' },
|
||||
{ id: 'security', icon: 'lock' },
|
||||
{ id: 'reports', icon: 'warning sign' }
|
||||
] as { id: ScopeId, icon: string }[]
|
|
@ -1,4 +1,5 @@
|
|||
import type { PrivacyLevel, ImportStatus } from '~/types'
|
||||
import type { ScopeId } from '~/composables/auth/useScopes'
|
||||
|
||||
import { gettext } from '~/init/locale'
|
||||
|
||||
|
@ -144,5 +145,5 @@ export default () => ({
|
|||
label: $pgettext('*/Moderation/*/Noun', 'Reports'),
|
||||
description: $pgettext('Content/OAuth Scopes/Paragraph', 'Access to moderation reports')
|
||||
}
|
||||
}
|
||||
} as Record<ScopeId, { label: string, description: string }>
|
||||
})
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import type { NavigationGuardNext, RouteLocationNamedRaw, RouteLocationNormalized } from 'vue-router'
|
||||
import type { Permission } from '~/store/auth'
|
||||
|
||||
import router from '~/router'
|
||||
import store from '~/store'
|
||||
|
||||
export const hasPermissions = (permission: Permission) => (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
|
@ -13,9 +14,9 @@ export const hasPermissions = (permission: Permission) => (to: RouteLocationNorm
|
|||
next({ name: 'library.index' })
|
||||
}
|
||||
|
||||
export const requireLoggedIn = (fallbackLocation: RouteLocationNamedRaw) => (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
export const requireLoggedIn = (fallbackLocation?: RouteLocationNamedRaw) => (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
if (store.state.auth.authenticated) return next()
|
||||
return next(fallbackLocation)
|
||||
return next(fallbackLocation ?? { name: 'login', query: { next: router.currentRoute.value.fullPath } })
|
||||
}
|
||||
|
||||
export const requireLoggedOut = (fallbackLocation: RouteLocationNamedRaw) => (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import { requireLoggedOut } from '../guards'
|
||||
import { requireLoggedOut, requireLoggedIn } from '../guards'
|
||||
|
||||
export default [
|
||||
{
|
||||
|
@ -52,7 +52,8 @@ export default [
|
|||
responseType: route.query.response_type,
|
||||
nonce: route.query.nonce,
|
||||
state: route.query.state
|
||||
})
|
||||
}),
|
||||
beforeEnter: requireLoggedIn()
|
||||
},
|
||||
{
|
||||
path: '/signup',
|
||||
|
|
|
@ -7,6 +7,7 @@ import manage from './manage'
|
|||
import store from '~/store'
|
||||
import auth from './auth'
|
||||
import user from './user'
|
||||
import { requireLoggedIn } from '../guards'
|
||||
|
||||
export default [
|
||||
{
|
||||
|
@ -71,7 +72,8 @@ export default [
|
|||
props: route => ({
|
||||
defaultOrdering: route.query.ordering,
|
||||
defaultPage: route.query.page ? +route.query.page : undefined
|
||||
})
|
||||
}),
|
||||
beforeEnter: requireLoggedIn()
|
||||
},
|
||||
...content,
|
||||
...manage,
|
||||
|
|
|
@ -111,12 +111,9 @@ const store: Module<State, RootState> = {
|
|||
},
|
||||
getters: {
|
||||
artistFilters: (state) => () => {
|
||||
const f = state.filters.filter((f) => {
|
||||
return f.target.type === 'artist'
|
||||
})
|
||||
const p = sortBy(f, [(e) => { return e.creation_date }])
|
||||
p.reverse()
|
||||
return p
|
||||
const filters = state.filters.filter((filter) => filter.target.type === 'artist')
|
||||
const sorted = sortBy(filters, [(e) => { return e.creation_date }])
|
||||
return sorted.reverse()
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
|
|
@ -7,3 +7,7 @@ html {
|
|||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=search]::-webkit-search-cancel-button {
|
||||
appearance: none;
|
||||
}
|
|
@ -472,3 +472,16 @@ export interface Notification {
|
|||
id: number
|
||||
is_read: boolean
|
||||
}
|
||||
|
||||
// Tags stuff
|
||||
export interface Tag {
|
||||
name: string
|
||||
}
|
||||
|
||||
// Application stuff
|
||||
export interface Application {
|
||||
client_id: string
|
||||
name: string
|
||||
redirect_uris: string
|
||||
scopes: string
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { startCase } from 'lodash-es'
|
||||
import type { Store } from 'vuex'
|
||||
import type { Router } from 'vue-router'
|
||||
import type { APIErrorResponse } from '~/types'
|
||||
import type { RootState } from '~/store'
|
||||
|
||||
import { startCase } from 'lodash-es'
|
||||
|
||||
export function parseAPIErrors (responseData: APIErrorResponse, parentField?: string): string[] {
|
||||
const errors = []
|
||||
|
@ -40,13 +38,6 @@ export function getCookie (name: string) {
|
|||
?.split('=')[1]
|
||||
}
|
||||
|
||||
// TODO (wvffle): Use navigation guards
|
||||
export async function checkRedirectToLogin (store: Store<RootState>, router: Router) {
|
||||
if (!store.state.auth.authenticated) {
|
||||
return router.push({ name: 'login', query: { next: router.currentRoute.value.fullPath } })
|
||||
}
|
||||
}
|
||||
|
||||
export function getDomain (url: string) {
|
||||
const parser = document.createElement('a')
|
||||
parser.href = url
|
||||
|
|
Loading…
Reference in New Issue