Move *.js to *.ts

This commit is contained in:
Kasper Seweryn 2022-05-02 17:06:44 +02:00 committed by Georg Krause
parent 8ff0bb937b
commit 9e0596d136
78 changed files with 626 additions and 788 deletions

View File

@ -28,11 +28,7 @@ module.exports = {
// NOTE: Handled by typescript // NOTE: Handled by typescript
'no-undef': 'off', 'no-undef': 'off',
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'no-use-before-define': 'off',
// TODO (wvffle): Migrate to VUI
// We're using `// @ts-ignore` in jQuery extensions
// and gettext for vue 2
'@typescript-eslint/ban-ts-comment': 'off',
// TODO (wvffle): Enable these rules later // TODO (wvffle): Enable these rules later
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',

View File

@ -1,23 +0,0 @@
const Album = {
clean (album) {
// we manually rebind the album and artist to each child track
album.tracks = album.tracks.map((track) => {
track.album = album
return track
})
return album
}
}
const Artist = {
clean (artist) {
// clean data as given by the API
artist.albums = artist.albums.map((album) => {
return Album.clean(album)
})
return artist
}
}
export default {
Artist: Artist,
Album: Album
}

View File

@ -1,11 +0,0 @@
export default {
formats: [
// 'audio/ogg',
'audio/mpeg'
],
formatsMap: {
'audio/ogg': 'ogg',
'audio/mpeg': 'mp3',
'audio/x-flac': 'flac'
}
}

View File

@ -1,23 +0,0 @@
const DYNAMIC_RANGE = 40 // dB
export function toLinearVolumeScale (v) {
if (v <= 0.0) {
return 0.0
}
// (1.0; 0.0) -> (0; -DYNAMIC_RANGE) dB
const dB = (v - 1) * DYNAMIC_RANGE
return Math.pow(10, dB / 20)
}
export function toLogarithmicVolumeScale (v) {
if (v <= 0.0) {
return 0.0
}
const dB = 20 * Math.log10(v)
// (0; -DYNAMIC_RANGE) [dB] -> (1.0; 0.0)
return 1 - (dB / -DYNAMIC_RANGE)
}

View File

@ -51,8 +51,8 @@
</template> </template>
<content-form <content-form
v-if="setting.fieldType === 'markdown'" v-if="setting.fieldType === 'markdown'"
v-model="values[setting.identifier]"
v-bind="setting.fieldParams" v-bind="setting.fieldParams"
v-model="values[setting.identifier]"
/> />
<signup-form-builder <signup-form-builder
v-else-if="setting.fieldType === 'formBuilder'" v-else-if="setting.fieldType === 'formBuilder'"

View File

@ -321,7 +321,7 @@
<script> <script>
import { useStore, mapState, mapGetters, mapActions } from 'vuex' import { useStore, mapState, mapGetters, mapActions } from 'vuex'
import { toLinearVolumeScale } from '~/audio/volume.js' import toLinearVolumeScale from '~/composables/audio/toLinearVolumeScale'
import { Howl, Howler } from 'howler' import { Howl, Howler } from 'howler'
import { throttle, reverse } from 'lodash-es' import { throttle, reverse } from 'lodash-es'
import axios from 'axios' import axios from 'axios'

View File

@ -27,17 +27,15 @@
class="ui center aligned basic segment desktop-and-up" class="ui center aligned basic segment desktop-and-up"
> >
<pagination <pagination
v-bind="$attrs"
:total="total" :total="total"
:current="page" :current="page"
:paginate-by="paginateBy" :paginate-by="paginateBy"
v-bind="$attrs"
/> />
</div> </div>
</div> </div>
<div <div :class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']">
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
>
<div <div
v-if="isLoading" v-if="isLoading"
class="ui inverted active dimmer" class="ui inverted active dimmer"
@ -66,10 +64,10 @@
> >
<pagination <pagination
v-if="paginateResults" v-if="paginateResults"
v-bind="$attrs"
:total="total" :total="total"
:current="page" :current="page"
:compact="true" :compact="true"
v-bind="$attrs"
/> />
</div> </div>
</div> </div>

View File

@ -121,7 +121,7 @@
<script> <script>
import { uniq } from 'lodash-es' import { uniq } from 'lodash-es'
import axios from 'axios' import axios from 'axios'
import useSharedLabels from '../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
props: { props: {

View File

@ -145,7 +145,7 @@
import axios from 'axios' import axios from 'axios'
import { checkRedirectToLogin } from '~/utils' import { checkRedirectToLogin } from '~/utils'
import useSharedLabels from '../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
props: { props: {
clientId: { type: String, required: true }, clientId: { type: String, required: true },

View File

@ -717,7 +717,7 @@ import PasswordInput from '~/components/forms/PasswordInput.vue'
import SubsonicTokenForm from '~/components/auth/SubsonicTokenForm.vue' import SubsonicTokenForm from '~/components/auth/SubsonicTokenForm.vue'
import AttachmentInput from '~/components/common/AttachmentInput.vue' import AttachmentInput from '~/components/common/AttachmentInput.vue'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import useSharedLabels from '../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
const logger = useLogger() const logger = useLogger()

View File

@ -77,7 +77,7 @@ const remove = async (uuid: string, sendEvent = true) => {
const initialValue = ref(props.initialValue ?? props.modelValue) const initialValue = ref(props.initialValue ?? props.modelValue)
watch(value, (to, from) => { watch(value, (to, from) => {
// NOTE: Remove old attachment if it's not the original one // NOTE: Remove old attachment if it's not the original one
if (from !== initialValue.value) { if (from !== initialValue.value && typeof from === 'string') {
remove(from, false) remove(from, false)
} }

View File

@ -1,16 +1,33 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useToggle } from '@vueuse/core'
interface Props {
content: string
length?: number
}
const props = withDefaults(defineProps<Props>(), {
length: 150
})
const [expanded, toggleExpanded] = useToggle(false)
const truncated = computed(() => props.content.slice(0, props.length))
</script>
<template> <template>
<div class="expandable-wrapper"> <div class="expandable-wrapper">
<div :class="['expandable-content', {expandable: truncated.length < content.length}, {expanded: isExpanded}]"> <div :class="['expandable-content', { expandable: truncated.length < content.length, expanded }]">
<slot>{{ content }}</slot> <slot>{{ content }}</slot>
</div> </div>
<a <a
v-if="truncated.length < content.length" v-if="truncated.length < content.length"
role="button" role="button"
@click.prevent="isExpanded = !isExpanded" @click.prevent="toggleExpanded()"
> >
<br> <br>
<translate <translate
v-if="isExpanded" v-if="expanded"
key="1" key="1"
translate-context="*/*/Button,Label" translate-context="*/*/Button,Label"
>Show less</translate> >Show less</translate>
@ -22,23 +39,3 @@
</a> </a>
</div> </div>
</template> </template>
<script>
// import sanitize from "~/sanitize"
export default {
props: {
content: { type: String, required: true },
length: { type: Number, default: 150, required: false }
},
data () {
return {
isExpanded: false
}
},
computed: {
truncated () {
return this.content.substring(0, this.length)
}
}
}
</script>

View File

@ -7,7 +7,8 @@ interface Props {
duration: number duration: number
} }
const { duration } = toRefs(defineProps<Props>()) const props = defineProps<Props>()
const { duration } = toRefs(props)
const parsedDuration = computed(() => time.parse(duration.value)) const parsedDuration = computed(() => time.parse(duration.value))
</script> </script>

View File

@ -23,7 +23,7 @@ onMounted(() => {
...props.message ...props.message
} }
// @ts-expect-error // @ts-expect-error toast is from semantic ui
$('body').toast(params) $('body').toast(params)
$('.ui.toast.visible').last().attr('role', 'alert') $('.ui.toast.visible').last().attr('role', 'alert')
}) })

View File

@ -142,7 +142,7 @@ import PaginationMixin from '~/components/mixins/Pagination.vue'
import { checkRedirectToLogin } from '~/utils' import { checkRedirectToLogin } from '~/utils'
import TrackTable from '~/components/audio/track/Table.vue' import TrackTable from '~/components/audio/track/Table.vue'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import useSharedLabels from '../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
const logger = useLogger() const logger = useLogger()

View File

@ -157,7 +157,7 @@ import AlbumCard from '~/components/audio/album/Card.vue'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import TagsSelector from '~/components/library/TagsSelector.vue' import TagsSelector from '~/components/library/TagsSelector.vue'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import useSharedLabels from '../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
const logger = useLogger() const logger = useLogger()

View File

@ -173,7 +173,7 @@ import ArtistCard from '~/components/audio/artist/Card.vue'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import TagsSelector from '~/components/library/TagsSelector.vue' import TagsSelector from '~/components/library/TagsSelector.vue'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import useSharedLabels from '../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
const logger = useLogger() const logger = useLogger()

View File

@ -219,7 +219,7 @@
import axios from 'axios' import axios from 'axios'
import { diffWordsWithSpace } from 'diff' import { diffWordsWithSpace } from 'diff'
import edits from '~/edits.js' import useEditConfigs from '~/composables/moderation/useEditConfigs'
function castValue (value) { function castValue (value) {
if (value === null || value === undefined) { if (value === null || value === undefined) {
@ -233,15 +233,28 @@ export default {
obj: { type: Object, required: true }, obj: { type: Object, required: true },
currentState: { type: Object, required: false, default: function () { return { } } } currentState: { type: Object, required: false, default: function () { return { } } }
}, },
setup () {
return { configs: useEditConfigs() }
},
data () { data () {
return { return {
isLoading: false isLoading: false
} }
}, },
computed: { computed: {
configs: edits.getConfigs, canApprove () {
canApprove: edits.getCanApprove, if (this.obj.is_applied) return false
canDelete: edits.getCanDelete, if (!this.$store.state.auth.authenticated) return false
return this.$store.state.auth.availablePermissions.library
},
canDelete () {
if (this.obj.is_applied || this.obj.is_approved) return false
if (!this.$store.state.auth.authenticated) return false
// TODO (wvffle): Is it better to compare ids? Is full_username unique?
return this.obj.created_by.full_username === this.$store.state.auth.fullUsername ||
this.$store.state.auth.availablePermissions.library
},
previousState () { previousState () {
if (this.obj.is_applied) { if (this.obj.is_applied) {
// mutation was applied, we use the previous state that is stored // mutation was applied, we use the previous state that is stored
@ -279,7 +292,7 @@ export default {
const fields = Object.keys(payload) const fields = Object.keys(payload)
const self = this const self = this
return fields.map((f) => { return fields.map((f) => {
const fieldConfig = edits.getFieldConfig(self.configs, this.obj.target.type, f) const fieldConfig = configs[this.obj.target.type].fields.find(({ id }) => id === f)
const dummyRepr = (v) => { return v } const dummyRepr = (v) => { return v }
const getValueRepr = fieldConfig.getValueRepr || dummyRepr const getValueRepr = fieldConfig.getValueRepr || dummyRepr
const d = { const d = {

View File

@ -1,5 +1,46 @@
<script setup lang="ts">
import axios from 'axios'
import useEditConfigs, { EditObject, EditObjectType } from '~/composables/moderation/useEditConfigs'
import EditCard from '~/components/library/EditCard.vue'
import { computed, ref } from 'vue'
interface Props {
object: EditObject
objectType: EditObjectType
editId: number
}
const props = defineProps<Props>()
const configs = useEditConfigs()
const config = computed(() => configs[props.objectType])
const currentState = computed(() => config.value.fields.reduce((state: Record<string, unknown>, field) => {
state[field.id] = { value: field.getValue(props.object) }
return state
}, {}))
const isLoading = ref(false)
const obj = ref()
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`mutations/${props.editId}/`)
obj.value = response.data
} catch (error) {
// TODO (wvffle): Handle error
} finally {
isLoading.value = false
}
}
fetchData()
// TODO (wvffle): Check if we want to watch for editId change and refetch data
</script>
<template> <template>
<section :class="['ui', 'vertical', 'stripe', {loading: isLoading}, 'segment']"> <section :class="['ui', 'vertical', 'stripe', { loading: isLoading }, 'segment']">
<div class="ui text container"> <div class="ui text container">
<edit-card <edit-card
v-if="obj" v-if="obj"
@ -9,50 +50,3 @@
</div> </div>
</section> </section>
</template> </template>
<script>
import axios from 'axios'
import edits from '~/edits.js'
import EditCard from '~/components/library/EditCard.vue'
export default {
components: {
EditCard
},
props: {
object: { type: Object, required: true },
objectType: { type: String, required: true },
editId: { type: Number, required: true }
},
data () {
return {
isLoading: true,
obj: null
}
},
computed: {
configs: edits.getConfigs,
config: edits.getConfig,
currentState () {
const self = this
const s = {}
this.config.fields.forEach(f => {
s[f.id] = { value: f.getValue(self.object) }
})
return s
}
},
created () {
this.fetchData()
},
methods: {
fetchData () {
const self = this
this.isLoading = true
axios.get(`mutations/${this.editId}/`).then(response => {
self.obj = response.data
self.isLoading = false
})
}
}
}
</script>

View File

@ -249,7 +249,8 @@ import AttachmentInput from '~/components/common/AttachmentInput.vue'
import EditList from '~/components/library/EditList.vue' import EditList from '~/components/library/EditList.vue'
import EditCard from '~/components/library/EditCard.vue' import EditCard from '~/components/library/EditCard.vue'
import TagsSelector from '~/components/library/TagsSelector.vue' import TagsSelector from '~/components/library/TagsSelector.vue'
import edits from '~/edits.js' import useEditConfigs from '~/composables/useEditConfigs'
import { computed } from 'vue/dist/vue'
export default { export default {
components: { components: {
@ -263,6 +264,16 @@ export default {
object: { type: Object, required: true }, object: { type: Object, required: true },
licenses: { type: Array, required: true } licenses: { type: Array, required: true }
}, },
setup (props) {
const configs = useEditConfigs()
const config = computed(() => configs[props.objectType])
const currentState = computed(() => config.value.fields.reduce((state/*: Record<string, unknown> */, field) => {
state[field.id] = { value: field.getValue(props.object) }
return state
}, {}))
return { config, currentState, configs }
},
data () { data () {
return { return {
isLoading: false, isLoading: false,
@ -275,10 +286,15 @@ export default {
} }
}, },
computed: { computed: {
configs: edits.getConfigs, canEdit () {
config: edits.getConfig, if (!this.$store.state.auth.authenticated) return false
currentState: edits.getCurrentState,
canEdit: edits.getCanEdit, const isOwner = this.object.attributed_to &&
// TODO (wvffle): Is it better to compare ids? Is full_username unique?
this.$store.state.auth.fullUsername === this.object.attributed_to.full_username
return isOwner || this.$store.state.auth.availablePermissions.library
},
labels () { labels () {
return { return {
summaryPlaceholder: this.$pgettext('*/*/Placeholder', 'A short summary describing your changes.') summaryPlaceholder: this.$pgettext('*/*/Placeholder', 'A short summary describing your changes.')

View File

@ -207,7 +207,7 @@ import TagsSelector from '~/components/library/TagsSelector.vue'
import Modal from '~/components/semantic/Modal.vue' import Modal from '~/components/semantic/Modal.vue'
import RemoteSearchForm from '~/components/RemoteSearchForm.vue' import RemoteSearchForm from '~/components/RemoteSearchForm.vue'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import useSharedLabels from '../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
const logger = useLogger() const logger = useLogger()

View File

@ -182,7 +182,7 @@ import PaginationMixin from '~/components/mixins/Pagination.vue'
import RadioCard from '~/components/radios/Card.vue' import RadioCard from '~/components/radios/Card.vue'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import useSharedLabels from '../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
const logger = useLogger() const logger = useLogger()

View File

@ -202,12 +202,12 @@
import axios from 'axios' import axios from 'axios'
import { merge } from 'lodash-es' import { merge } from 'lodash-es'
import time from '~/utils/time' import time from '~/utils/time'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import useSharedLabels from '../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -187,12 +187,12 @@
import axios from 'axios' import axios from 'axios'
import { merge } from 'lodash-es' import { merge } from 'lodash-es'
import time from '~/utils/time' import time from '~/utils/time'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -186,12 +186,12 @@
import axios from 'axios' import axios from 'axios'
import { merge } from 'lodash-es' import { merge } from 'lodash-es'
import time from '~/utils/time' import time from '~/utils/time'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -136,11 +136,11 @@ import time from '~/utils/time'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import EditCard from '~/components/library/EditCard.vue' import EditCard from '~/components/library/EditCard.vue'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import edits from '~/edits' import useEditConfigs from '~/composables/useEditConfigs'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {
@ -153,7 +153,8 @@ export default {
}, },
setup () { setup () {
const sharedLabels = useSharedLabels() const sharedLabels = useSharedLabels()
return { sharedLabels } const configs = useEditConfigs()
return { sharedLabels, configs }
}, },
data () { data () {
return { return {
@ -244,7 +245,10 @@ export default {
response.data.results.forEach((e) => { response.data.results.forEach((e) => {
self.targets[k][e.id] = { self.targets[k][e.id] = {
payload: e, payload: e,
currentState: edits.getCurrentStateForObj(e, edits.getConfigs.bind(self)()[k]) currentState: configs[k].fields.reduce((state/*: Record<string, unknown> */, field) => {
state[field.id] = { value: field.getValue(e) }
return state
}, {})
} }
}) })
}, error => { }, error => {

View File

@ -217,12 +217,12 @@
import axios from 'axios' import axios from 'axios'
import { merge } from 'lodash-es' import { merge } from 'lodash-es'
import time from '~/utils/time' import time from '~/utils/time'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -148,14 +148,14 @@
import axios from 'axios' import axios from 'axios'
import { merge } from 'lodash-es' import { merge } from 'lodash-es'
import time from '~/utils/time' import time from '~/utils/time'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import ImportStatusModal from '~/components/library/ImportStatusModal.vue' import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
import { truncate } from '~/utils/filters' import { truncate } from '~/utils/filters'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -200,12 +200,12 @@
import axios from 'axios' import axios from 'axios'
import { merge } from 'lodash-es' import { merge } from 'lodash-es'
import time from '~/utils/time' import time from '~/utils/time'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -309,14 +309,14 @@
import axios from 'axios' import axios from 'axios'
import { merge } from 'lodash-es' import { merge } from 'lodash-es'
import time from '~/utils/time' import time from '~/utils/time'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import ImportStatusModal from '~/components/library/ImportStatusModal.vue' import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
import { humanSize, truncate } from '~/utils/filters' import { humanSize, truncate } from '~/utils/filters'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -173,12 +173,12 @@
import axios from 'axios' import axios from 'axios'
import { merge } from 'lodash-es' import { merge } from 'lodash-es'
import time from '~/utils/time' import time from '~/utils/time'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -187,7 +187,7 @@ import time from '~/utils/time'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -396,7 +396,7 @@ import NoteForm from '~/components/manage/moderation/NoteForm.vue'
import NotesThread from '~/components/manage/moderation/NotesThread.vue' import NotesThread from '~/components/manage/moderation/NotesThread.vue'
import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue' import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue'
import InstancePolicyModal from '~/components/manage/moderation/InstancePolicyModal.vue' import InstancePolicyModal from '~/components/manage/moderation/InstancePolicyModal.vue'
import entities from '~/entities' import useReportConfigs from '~/composables/moderation/useReportConfigs.ts'
import { setUpdate } from '~/utils' import { setUpdate } from '~/utils'
import showdown from 'showdown' import showdown from 'showdown'
@ -418,6 +418,9 @@ export default {
initObj: { type: Object, required: true }, initObj: { type: Object, required: true },
currentState: { type: String, required: false, default: '' } currentState: { type: String, required: false, default: '' }
}, },
setup () {
return { configs: useReportConfigs() }
},
data () { data () {
return { return {
obj: this.initObj, obj: this.initObj,
@ -430,7 +433,6 @@ export default {
} }
}, },
computed: { computed: {
configs: entities.getConfigs,
previousState () { previousState () {
if (this.obj.is_applied) { if (this.obj.is_applied) {
// mutation was applied, we use the previous state that is stored // mutation was applied, we use the previous state that is stored
@ -466,15 +468,13 @@ export default {
const payload = this.obj.target_state const payload = this.obj.target_state
const fields = this.configs[this.target.type].moderatedFields const fields = this.configs[this.target.type].moderatedFields
return fields.map((fieldConfig) => { return fields.map((fieldConfig) => {
const dummyRepr = (v) => { return v } const getValueRepr = fieldConfig.getValueRepr ?? (i => i)
const getValueRepr = fieldConfig.getValueRepr || dummyRepr return {
const d = {
id: fieldConfig.id, id: fieldConfig.id,
label: fieldConfig.label, label: fieldConfig.label,
value: payload[fieldConfig.id], value: payload[fieldConfig.id],
repr: castValue(getValueRepr(payload[fieldConfig.id])) repr: castValue(getValueRepr(payload[fieldConfig.id]))
} }
return d
}) })
}, },
target () { target () {

View File

@ -161,7 +161,7 @@ import { merge } from 'lodash-es'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -204,7 +204,7 @@ import time from '~/utils/time'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -1,6 +1,6 @@
<script> <script>
import { normalizeQuery, parseTokens, compileTokens } from '~/search' import { normalizeQuery, parseTokens, compileTokens } from '~/utils/search'
export default { export default {
props: { props: {

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useGettext } from 'vue3-gettext' import { useGettext } from 'vue3-gettext'
import useSharedLabels from '~/composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
interface Props { interface Props {

View File

@ -102,7 +102,7 @@ import $ from 'jquery'
import axios from 'axios' import axios from 'axios'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import useSharedLabels from '~/composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
const logger = useLogger() const logger = useLogger()

View File

@ -27,7 +27,7 @@ const show = useVModel(props, 'show', emit)
const control = ref() const control = ref()
const initModal = () => { const initModal = () => {
// @ts-expect-error // @ts-expect-error modal is from semantic ui
control.value = $(modal.value).modal({ control.value = $(modal.value).modal({
duration: 100, duration: 100,
onApprove: () => emit('approved'), onApprove: () => emit('approved'),

View File

@ -0,0 +1,11 @@
export const DYNAMIC_RANGE = 40 // dB
export default (volume: number) => {
if (volume <= 0.0) {
return 0.0
}
// (1.0; 0.0) -> (0; -DYNAMIC_RANGE) dB
const dB = (volume - 1) * DYNAMIC_RANGE
return Math.pow(10, dB / 20)
}

View File

@ -0,0 +1,120 @@
import { gettext } from '~/init/locale'
import { Album, Artist, Content, Track } from '~/types'
interface ConfigField {
id: string
label: string
type: 'content' | 'attachment' | 'tags' | 'text' | 'license'
inputType?: 'text' | 'number'
required: boolean
getValue: (obj: EditObject) => unknown
getValueRepr?: (obj: any) => string
}
export type EditObject = Artist | Album | Track
export type EditObjectType = 'artist' | 'album' | 'track'
type Configs = Record<EditObjectType, { fields: ConfigField[] }>
const { $pgettext } = gettext
const getContentValueRepr = (val: Content) => val.text
const description: ConfigField = {
id: 'description',
type: 'content',
required: true,
label: $pgettext('*/*/*/Noun', 'Description'),
getValue: (obj) => obj.description ?? { text: '', content_type: 'text/markdown' },
getValueRepr: getContentValueRepr
}
const cover: ConfigField = {
id: 'cover',
type: 'attachment',
required: false,
label: $pgettext('Content/*/*/Noun', 'Cover'),
getValue: (obj) => obj.cover?.uuid ?? null
}
const tags: ConfigField = {
id: 'tags',
type: 'tags',
required: true,
label: $pgettext('*/*/*/Noun', 'Tags'),
getValue: (obj) => { return obj.tags },
getValueRepr: (tags: string[]) => tags.slice().sort().join('\n')
}
// TODO: Get params from typescript type somehow?
export default (): Configs => ({
artist: {
fields: [
{
id: 'name',
type: 'text',
required: true,
label: $pgettext('*/*/*/Noun', 'Name'),
getValue: (artist) => (artist as Artist).name
},
description,
cover,
tags
]
},
album: {
fields: [
{
id: 'title',
type: 'text',
required: true,
label: $pgettext('*/*/*/Noun', 'Title'),
getValue: (album) => (album as Album).title
},
description,
{
id: 'release_date',
type: 'text',
required: false,
label: $pgettext('Content/*/*/Noun', 'Release date'),
getValue: (album) => (album as Album).release_date
},
cover,
tags
]
},
track: {
fields: [
{
id: 'title',
type: 'text',
required: true,
label: $pgettext('*/*/*/Noun', 'Title'),
getValue: (track) => (track as Track).title
},
description,
cover,
{
id: 'position',
type: 'text',
inputType: 'number',
required: false,
label: $pgettext('*/*/*/Short, Noun', 'Position'),
getValue: (track) => (track as Track).position
},
{
id: 'copyright',
type: 'text',
required: false,
label: $pgettext('Content/Track/*/Noun', 'Copyright'),
getValue: (track) => (track as Track).copyright
},
{
id: 'license',
type: 'license',
required: false,
label: $pgettext('Content/*/*/Noun', 'License'),
getValue: (track) => (track as Track).license
},
tags
]
}
})

View File

@ -0,0 +1,183 @@
import { gettext } from '~/init/locale'
import { RouteLocationRaw } from 'vue-router'
interface ModeratedField {
id: string
label: string
getValueRepr?: (obj: any) => string
}
export interface Entity {
label: string
icon: string
getDeleteUrl?: (object: any) => string
urls: {
getDetail?: (object: any) => RouteLocationRaw
getAdminDetail?: (object: any) => RouteLocationRaw
}
moderatedFields: ModeratedField[]
}
export type EntityObjectType = 'artist' | 'album' | 'track' | 'library' | 'playlist' | 'account' | 'channel'
type Configs = Record<EntityObjectType, Entity>
const { $pgettext } = gettext
const tags: ModeratedField = {
id: 'tags',
label: $pgettext('*/*/*/Noun', 'Tags'),
getValueRepr: (tags: string[]) => tags.slice().sort().join('\n')
}
const name: ModeratedField = {
id: 'name',
label: $pgettext('*/*/*/Noun', 'Name')
}
const creationDate: ModeratedField = {
id: 'creation_date',
label: $pgettext('Content/*/*/Noun', 'Creation date')
}
const musicBrainzId: ModeratedField = {
id: 'mbid',
label: $pgettext('*/*/*/Noun', 'MusicBrainz ID')
}
const visibility: ModeratedField = {
id: 'privacy_level',
label: $pgettext('*/*/*', 'Visibility')
}
export default (): Configs => ({
artist: {
label: $pgettext('*/*/*/Noun', 'Artist'),
icon: 'users',
getDeleteUrl: (obj) => {
return `manage/library/artists/${obj.id}/`
},
urls: {
getDetail: (obj) => ({ name: 'library.artists.detail', params: { id: obj.id } }),
getAdminDetail: (obj) => ({ name: 'manage.library.artists.detail', params: { id: obj.id } })
},
moderatedFields: [
name,
creationDate,
tags,
musicBrainzId
]
},
album: {
label: $pgettext('*/*/*', 'Album'),
icon: 'play',
getDeleteUrl: (obj) => {
return `manage/library/albums/${obj.id}/`
},
urls: {
getDetail: (obj) => ({ name: 'library.albums.detail', params: { id: obj.id } }),
getAdminDetail: (obj) => ({ name: 'manage.library.albums.detail', params: { id: obj.id } })
},
moderatedFields: [
{
id: 'title',
label: $pgettext('*/*/*/Noun', 'Title')
},
creationDate,
{
id: 'release_date',
label: $pgettext('Content/*/*/Noun', 'Release date')
},
tags,
musicBrainzId
]
},
track: {
label: $pgettext('*/*/*/Noun', 'Track'),
icon: 'music',
getDeleteUrl: (obj) => {
return `manage/library/tracks/${obj.id}/`
},
urls: {
getDetail: (obj) => ({ name: 'library.tracks.detail', params: { id: obj.id } }),
getAdminDetail: (obj) => ({ name: 'manage.library.tracks.detail', params: { id: obj.id } })
},
moderatedFields: [
{
id: 'title',
label: $pgettext('*/*/*/Noun', 'Title')
},
{
id: 'position',
label: $pgettext('*/*/*/Short, Noun', 'Position')
},
{
id: 'copyright',
label: $pgettext('Content/Track/*/Noun', 'Copyright')
},
{
id: 'license',
label: $pgettext('Content/*/*/Noun', 'License')
},
tags,
musicBrainzId
]
},
library: {
label: $pgettext('*/*/*/Noun', 'Library'),
icon: 'book',
getDeleteUrl: (obj) => {
return `manage/library/libraries/${obj.uuid}/`
},
urls: {
getAdminDetail: (obj) => ({ name: 'manage.library.libraries.detail', params: { id: obj.uuid } })
},
moderatedFields: [
name,
{
id: 'description',
label: $pgettext('*/*/*/Noun', 'Description')
},
visibility
]
},
playlist: {
label: $pgettext('*/*/*', 'Playlist'),
icon: 'list',
urls: {
getDetail: (obj) => ({ name: 'library.playlists.detail', params: { id: obj.id } })
// getAdminDetail: (obj) => ({name: 'manage.playlists.detail', params: {id: obj.id}}}
},
moderatedFields: [
name,
visibility
]
},
account: {
label: $pgettext('*/*/*/Noun', 'Account'),
icon: 'user',
urls: {
getDetail: (obj) => ({ name: 'profile.full.overview', params: { username: obj.preferred_username, domain: obj.domain } }),
getAdminDetail: (obj) => ({ name: 'manage.moderation.accounts.detail', params: { id: `${obj.preferred_username}@${obj.domain}` } })
},
moderatedFields: [
name,
{
id: 'summary',
label: $pgettext('*/*/*/Noun', 'Bio')
}
]
},
channel: {
label: $pgettext('*/*/*', 'Channel'),
icon: 'stream',
urls: {
getDetail: (obj) => ({ name: 'channels.detail', params: { id: obj.uuid } }),
getAdminDetail: (obj) => ({ name: 'manage.channels.detail', params: { id: obj.uuid } })
},
moderatedFields: [
name,
creationDate,
tags
]
}
})

View File

@ -1,191 +0,0 @@
function getTagsValueRepr (val) {
if (!val) {
return ''
}
return val.slice().sort().join('\n')
}
function getContentValueRepr (val) {
return val.text
}
export default {
getConfigs () {
const description = {
id: 'description',
type: 'content',
required: true,
label: this.$pgettext('*/*/*/Noun', 'Description'),
getValue: (obj) => { return obj.description || { text: null, content_type: 'text/markdown' } },
getValueRepr: getContentValueRepr
}
const cover = {
id: 'cover',
type: 'attachment',
required: false,
label: this.$pgettext('Content/*/*/Noun', 'Cover'),
getValue: (obj) => {
if (obj.cover) {
return obj.cover.uuid
} else {
return null
}
}
}
return {
artist: {
fields: [
{
id: 'name',
type: 'text',
required: true,
label: this.$pgettext('*/*/*/Noun', 'Name'),
getValue: (obj) => { return obj.name }
},
description,
cover,
{
id: 'tags',
type: 'tags',
required: true,
label: this.$pgettext('*/*/*/Noun', 'Tags'),
getValue: (obj) => { return obj.tags },
getValueRepr: getTagsValueRepr
}
]
},
album: {
fields: [
{
id: 'title',
type: 'text',
required: true,
label: this.$pgettext('*/*/*/Noun', 'Title'),
getValue: (obj) => { return obj.title }
},
description,
{
id: 'release_date',
type: 'text',
required: false,
label: this.$pgettext('Content/*/*/Noun', 'Release date'),
getValue: (obj) => { return obj.release_date }
},
cover,
{
id: 'tags',
type: 'tags',
required: true,
label: this.$pgettext('*/*/*/Noun', 'Tags'),
getValue: (obj) => { return obj.tags },
getValueRepr: getTagsValueRepr
}
]
},
track: {
fields: [
{
id: 'title',
type: 'text',
required: true,
label: this.$pgettext('*/*/*/Noun', 'Title'),
getValue: (obj) => { return obj.title }
},
description,
cover,
{
id: 'position',
type: 'text',
inputType: 'number',
required: false,
label: this.$pgettext('*/*/*/Short, Noun', 'Position'),
getValue: (obj) => { return obj.position }
},
{
id: 'copyright',
type: 'text',
required: false,
label: this.$pgettext('Content/Track/*/Noun', 'Copyright'),
getValue: (obj) => { return obj.copyright }
},
{
id: 'license',
type: 'license',
required: false,
label: this.$pgettext('Content/*/*/Noun', 'License'),
getValue: (obj) => { return obj.license }
},
{
id: 'tags',
type: 'tags',
required: true,
label: this.$pgettext('*/*/*/Noun', 'Tags'),
getValue: (obj) => { return obj.tags },
getValueRepr: getTagsValueRepr
}
]
}
}
},
getConfig () {
return this.configs[this.objectType]
},
getFieldConfig (configs, type, fieldId) {
const c = configs[type]
return c.fields.filter((f) => {
return f.id === fieldId
})[0]
},
getCurrentState () {
const self = this
const s = {}
this.config.fields.forEach(f => {
s[f.id] = { value: f.getValue(self.object) }
})
return s
},
getCurrentStateForObj (obj, config) {
const s = {}
config.fields.forEach(f => {
s[f.id] = { value: f.getValue(obj) }
})
return s
},
getCanDelete () {
if (this.obj.is_applied || this.obj.is_approved) {
return false
}
if (!this.$store.state.auth.authenticated) {
return false
}
return (
this.obj.created_by.full_username === this.$store.state.auth.fullUsername ||
this.$store.state.auth.availablePermissions.library
)
},
getCanApprove () {
if (this.obj.is_applied) {
return false
}
if (!this.$store.state.auth.authenticated) {
return false
}
return this.$store.state.auth.availablePermissions.library
},
getCanEdit () {
if (!this.$store.state.auth.authenticated) {
return false
}
const libraryPermission = this.$store.state.auth.availablePermissions.library
const objData = this.object || {}
let isOwner = false
if (objData.attributed_to) {
isOwner = this.$store.state.auth.fullUsername === objData.attributed_to.full_username
}
return libraryPermission || isOwner
}
}

View File

@ -1,7 +1,7 @@
import EmbedFrame from './EmbedFrame.vue' import EmbedFrame from './EmbedFrame.vue'
import { createApp } from 'vue' import { createApp } from 'vue'
// @ts-expect-error // @ts-expect-error vue-plyr has no types defined
import VuePlyr from 'vue-plyr' import VuePlyr from 'vue-plyr'
const app = createApp(EmbedFrame) const app = createApp(EmbedFrame)

View File

@ -1,245 +0,0 @@
function getTagsValueRepr (val) {
if (!val) {
return ''
}
return val.slice().sort().join('\n')
}
export default {
getConfigs () {
return {
artist: {
label: this.$pgettext('*/*/*/Noun', 'Artist'),
icon: 'users',
getDeleteUrl: (obj) => {
return `manage/library/artists/${obj.id}/`
},
urls: {
getDetail: (obj) => { return { name: 'library.artists.detail', params: { id: obj.id } } },
getAdminDetail: (obj) => { return { name: 'manage.library.artists.detail', params: { id: obj.id } } }
},
moderatedFields: [
{
id: 'name',
label: this.$pgettext('*/*/*/Noun', 'Name'),
getValue: (obj) => { return obj.name }
},
{
id: 'creation_date',
label: this.$pgettext('Content/*/*/Noun', 'Creation date'),
getValue: (obj) => { return obj.creation_date }
},
{
id: 'tags',
type: 'tags',
label: this.$pgettext('*/*/*/Noun', 'Tags'),
getValue: (obj) => { return obj.tags },
getValueRepr: getTagsValueRepr
},
{
id: 'mbid',
label: this.$pgettext('*/*/*/Noun', 'MusicBrainz ID'),
getValue: (obj) => { return obj.mbid }
}
]
},
album: {
label: this.$pgettext('*/*/*', 'Album'),
icon: 'play',
getDeleteUrl: (obj) => {
return `manage/library/albums/${obj.id}/`
},
urls: {
getDetail: (obj) => { return { name: 'library.albums.detail', params: { id: obj.id } } },
getAdminDetail: (obj) => { return { name: 'manage.library.albums.detail', params: { id: obj.id } } }
},
moderatedFields: [
{
id: 'title',
label: this.$pgettext('*/*/*/Noun', 'Title'),
getValue: (obj) => { return obj.title }
},
{
id: 'creation_date',
label: this.$pgettext('Content/*/*/Noun', 'Creation date'),
getValue: (obj) => { return obj.creation_date }
},
{
id: 'release_date',
label: this.$pgettext('Content/*/*/Noun', 'Release date'),
getValue: (obj) => { return obj.release_date }
},
{
id: 'tags',
type: 'tags',
required: true,
label: this.$pgettext('*/*/*/Noun', 'Tags'),
getValue: (obj) => { return obj.tags },
getValueRepr: getTagsValueRepr
},
{
id: 'mbid',
label: this.$pgettext('*/*/*/Noun', 'MusicBrainz ID'),
getValue: (obj) => { return obj.mbid }
}
]
},
track: {
label: this.$pgettext('*/*/*/Noun', 'Track'),
icon: 'music',
getDeleteUrl: (obj) => {
return `manage/library/tracks/${obj.id}/`
},
urls: {
getDetail: (obj) => { return { name: 'library.tracks.detail', params: { id: obj.id } } },
getAdminDetail: (obj) => { return { name: 'manage.library.tracks.detail', params: { id: obj.id } } }
},
moderatedFields: [
{
id: 'title',
label: this.$pgettext('*/*/*/Noun', 'Title'),
getValue: (obj) => { return obj.title }
},
{
id: 'position',
label: this.$pgettext('*/*/*/Short, Noun', 'Position'),
getValue: (obj) => { return obj.position }
},
{
id: 'copyright',
label: this.$pgettext('Content/Track/*/Noun', 'Copyright'),
getValue: (obj) => { return obj.copyright }
},
{
id: 'license',
label: this.$pgettext('Content/*/*/Noun', 'License'),
getValue: (obj) => { return obj.license }
},
{
id: 'tags',
label: this.$pgettext('*/*/*/Noun', 'Tags'),
getValue: (obj) => { return obj.tags },
getValueRepr: getTagsValueRepr
},
{
id: 'mbid',
label: this.$pgettext('*/*/*/Noun', 'MusicBrainz ID'),
getValue: (obj) => { return obj.mbid }
}
]
},
library: {
label: this.$pgettext('*/*/*/Noun', 'Library'),
icon: 'book',
getDeleteUrl: (obj) => {
return `manage/library/libraries/${obj.uuid}/`
},
urls: {
getAdminDetail: (obj) => { return { name: 'manage.library.libraries.detail', params: { id: obj.uuid } } }
},
moderatedFields: [
{
id: 'name',
label: this.$pgettext('*/*/*/Noun', 'Name'),
getValue: (obj) => { return obj.name }
},
{
id: 'description',
label: this.$pgettext('*/*/*/Noun', 'Description'),
getValue: (obj) => { return obj.position }
},
{
id: 'privacy_level',
label: this.$pgettext('*/*/*', 'Visibility'),
getValue: (obj) => { return obj.privacy_level }
}
]
},
playlist: {
label: this.$pgettext('*/*/*', 'Playlist'),
icon: 'list',
urls: {
getDetail: (obj) => { return { name: 'library.playlists.detail', params: { id: obj.id } } }
// getAdminDetail: (obj) => { return {name: 'manage.playlists.detail', params: {id: obj.id}}}
},
moderatedFields: [
{
id: 'name',
label: this.$pgettext('*/*/*/Noun', 'Name'),
getValue: (obj) => { return obj.name }
},
{
id: 'privacy_level',
label: this.$pgettext('*/*/*', 'Visibility'),
getValue: (obj) => { return obj.privacy_level }
}
]
},
account: {
label: this.$pgettext('*/*/*/Noun', 'Account'),
icon: 'user',
urls: {
getDetail: (obj) => { return { name: 'profile.full.overview', params: { username: obj.preferred_username, domain: obj.domain } } },
getAdminDetail: (obj) => { return { name: 'manage.moderation.accounts.detail', params: { id: `${obj.preferred_username}@${obj.domain}` } } }
},
moderatedFields: [
{
id: 'name',
label: this.$pgettext('*/*/*/Noun', 'Name'),
getValue: (obj) => { return obj.name }
},
{
id: 'summary',
label: this.$pgettext('*/*/*/Noun', 'Bio'),
getValue: (obj) => { return obj.summary }
}
]
},
channel: {
label: this.$pgettext('*/*/*', 'Channel'),
icon: 'stream',
urls: {
getDetail: (obj) => { return { name: 'channels.detail', params: { id: obj.uuid } } },
getAdminDetail: (obj) => { return { name: 'manage.channels.detail', params: { id: obj.uuid } } }
},
moderatedFields: [
{
id: 'name',
label: this.$pgettext('*/*/*/Noun', 'Name'),
getValue: (obj) => { return obj.name }
},
{
id: 'creation_date',
label: this.$pgettext('Content/*/*/Noun', 'Creation date'),
getValue: (obj) => { return obj.creation_date }
},
{
id: 'tags',
type: 'tags',
label: this.$pgettext('*/*/*/Noun', 'Tags'),
getValue: (obj) => { return obj.tags },
getValueRepr: getTagsValueRepr
}
]
}
}
},
getConfig () {
return this.configs[this.objectType]
},
getFieldConfig (configs, type, fieldId) {
const c = configs[type]
return c.fields.filter((f) => {
return f.id === fieldId
})[0]
},
getCurrentStateForObj (obj, config) {
const s = {}
config.fields.forEach(f => {
s[f.id] = { value: f.getValue(obj) }
})
return s
}
}

View File

@ -7,14 +7,14 @@ export const install: InitModule = ({ app, store }) => {
}) })
app.directive('dropdown', function (el, binding) { app.directive('dropdown', function (el, binding) {
// @ts-expect-error // @ts-expect-error dropdown is from semantic ui
jQuery(el).dropdown({ jQuery(el).dropdown({
selectOnKeydown: false, selectOnKeydown: false,
action (text: string, value: string, $el: JQuery<HTMLElement>) { action (text: string, value: string, $el: JQuery<HTMLElement>) {
// used to ensure focusing the dropdown and clicking via keyboard // used to ensure focusing the dropdown and clicking via keyboard
// works as expected // works as expected
$el[0]?.click() $el[0]?.click()
// @ts-expect-error // @ts-expect-error dropdown is from semantic ui
jQuery(el).find('.ui.dropdown').dropdown('hide') jQuery(el).find('.ui.dropdown').dropdown('hide')
}, },
...(binding.value || {}) ...(binding.value || {})

View File

@ -1,5 +1,6 @@
import router from '~/router' import router from '~/router'
import store from '~/store' import store from '~/store'
// @ts-expect-error typescript does not know about configureCompat
import { configureCompat, createApp, defineAsyncComponent, h } from 'vue' import { configureCompat, createApp, defineAsyncComponent, h } from 'vue'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import useTheme from '~/composables/useTheme' import useTheme from '~/composables/useTheme'
@ -49,10 +50,8 @@ Promise.all(modules).finally(() => {
logger.info('Everything loaded!') logger.info('Everything loaded!')
}) })
// TODO (wvffle): Migrate to pinia
// TODO (wvffle): Check for mixin merging: https://v3-migration.vuejs.org/breaking-changes/data-option.html#mixin-merge-behavior-change= // TODO (wvffle): Check for mixin merging: https://v3-migration.vuejs.org/breaking-changes/data-option.html#mixin-merge-behavior-change=
// TODO (wvffle): Use emits options: https://v3-migration.vuejs.org/breaking-changes/emits-option.html // TODO (wvffle): Use emits options: https://v3-migration.vuejs.org/breaking-changes/emits-option.html
// TODO (wvffle): Find all array watchers and make them deep // TODO (wvffle): Find all array watchers and make them deep
// TODO (wvffle): Migrate to <script setup> // TODO (wvffle): Migrate to <script setup>
// TODO (wvffle): Replace `from '(../)+` with `from '~/` // TODO (wvffle): Replace `from '(../)+` with `from '~/`
// TODO (wvffle): Remove `allowJs` from tsconfig.json

View File

@ -1,43 +0,0 @@
import sanitizeHtml from 'sanitize-html'
const allowedTags = [
'h3',
'h4',
'h5',
'h6',
'blockquote',
'p',
'a',
'ul',
'ol',
'nl',
'li',
'b',
'i',
'strong',
'em',
'strike',
'code',
'hr',
'br',
'div',
'table',
'thead',
'caption',
'tbody',
'tr',
'th',
'td',
'pre'
]
const allowedAttributes = {
a: ['href', 'name', 'target'],
// We don't currently allow img itself by default, but this
// would make sense if we did. You could add srcset here,
// and if you do the URL is checked for safety
img: ['src']
}
export default function sanitize (input) {
return sanitizeHtml(input, { allowedAttributes, allowedTags })
}

View File

@ -1,69 +0,0 @@
export function normalizeQuery (query) {
// given a string such as 'this is "my query" go', returns
// an array of tokens like this: ['this', 'is', 'my query', 'go']
if (!query) {
return []
}
return query.match(/\\?.|^$/g).reduce((p, c) => {
if (c === '"') {
p.quote ^= 1
} else if (!p.quote && c === ' ') {
p.a.push('')
} else {
p.a[p.a.length - 1] += c.replace(/\\(.)/, '$1')
}
return p
}, { a: [''] }).a
}
export function parseTokens (tokens) {
// given an array of tokens as returned by normalizeQuery,
// returns a list of objects such as [
// {
// field: 'status',
// value: 'pending'
// },
// {
// field: null,
// value: 'hello'
// }
// ]
return tokens.map(t => {
// we split the token on ":"
const parts = t.split(/:(.+)/)
if (parts.length === 1) {
// no field specified
return { field: null, value: t }
}
// first item is the field, second is the value, possibly quoted
const field = parts[0]
let rawValue = parts[1]
// we remove surrounding quotes if any
if (rawValue[0] === '"') {
rawValue = rawValue.substring(1)
}
if (rawValue.slice(-1) === '"') {
rawValue = rawValue.substring(0, rawValue.length - 1)
}
return { field, value: rawValue }
})
}
export function compileTokens (tokens) {
// given a list of tokens as returned by parseTokens,
// returns a string query
const parts = tokens.map(t => {
let v = t.value
const k = t.field
if (v.indexOf(' ') > -1) {
v = `"${v}"`
}
if (k) {
return `${k}:${v}`
} else {
return v
}
})
return parts.join(' ')
}

7
front/src/shims-vuex.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import { Store } from 'vuex'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store<any>
}
}

View File

@ -1,4 +1,4 @@
import { createStore } from 'vuex' import { createStore, Store } from 'vuex'
import createPersistedState from 'vuex-persistedstate' import createPersistedState from 'vuex-persistedstate'
import favorites from './favorites' import favorites from './favorites'
@ -12,8 +12,14 @@ import radios from './radios'
import player from './player' import player from './player'
import playlists from './playlists' import playlists from './playlists'
import ui from './ui' import ui from './ui'
import { InjectionKey } from 'vue'
export default createStore({ export interface RootState {
}
export const key: InjectionKey<Store<RootState>> = Symbol('vuex state injection key')
export default createStore<RootState>({
modules: { modules: {
ui, ui,
auth, auth,

View File

@ -33,20 +33,41 @@ export type ContentCategory = 'podcast'
export interface Artist { export interface Artist {
id: string id: string
name: string name: string
description: Content
cover?: Cover
tags: string[]
content_category: ContentCategory content_category: ContentCategory
albums: Album[]
} }
export interface Album { export interface Album {
id: string id: string
title: string
description: Content
release_date?: string
cover?: Cover
tags: string[]
artist: Artist artist: Artist
tracks_count: number tracks_count: number
title: string tracks: Track[]
} }
export interface Track { export interface Track {
id: string id: string
title: string title: string
description: Content
cover?: Cover
position?: number
copyright?: string
license?: License
tags: string[]
album?: Album album?: Album
artist?: Artist artist?: Artist
} }
@ -56,6 +77,16 @@ export interface Channel {
artist?: Artist artist?: Artist
} }
export interface Cover {
uuid: string
}
export interface License {
code: string
name: string
url: string
}
// API stuff // API stuff
export interface APIErrorResponse { export interface APIErrorResponse {
[key: string]: APIErrorResponse | string[] [key: string]: APIErrorResponse | string[]
@ -111,12 +142,13 @@ export interface FileSystem {
content: FSEntry[] content: FSEntry[]
} }
// Form stuff // Content stuff
export interface FormHelpText { export interface Content {
content_type: string content_type: 'text/plain' | 'text/markdown'
text?: string text: string // TODO (wvffle): Ensure it's not nullable from backend side
} }
// Form stuff
export interface FormField { export interface FormField {
label: string label: string
input_type: 'short_text' | 'long_text' input_type: 'short_text' | 'long_text'
@ -125,7 +157,7 @@ export interface FormField {
export interface Form { export interface Form {
fields: FormField[] fields: FormField[]
help_text: FormHelpText help_text: Content
} }
// Yet uncategorized stuff // Yet uncategorized stuff
@ -135,9 +167,3 @@ export interface Actor {
is_local: boolean is_local: boolean
domain: string domain: string
} }
export interface License {
code: string
name: string
url: string
}

View File

@ -1,4 +1,5 @@
export function hashCode (str) { // java String#hashCode // java String#hashCode
export function hashCode (str: string) {
let hash = 0 let hash = 0
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash) hash = str.charCodeAt(i) + ((hash << 5) - hash)
@ -6,7 +7,7 @@ export function hashCode (str) { // java String#hashCode
return hash return hash
} }
export function intToRGB (i) { export function intToRGB (i: number) {
const c = (i & 0x00FFFFFF).toString(16).toUpperCase() const c = (i & 0x00FFFFFF).toString(16).toUpperCase()
return '00000'.substring(0, 6 - c.length) + c return '00000'.substring(0, 6 - c.length) + c
} }

82
front/src/utils/search.ts Normal file
View File

@ -0,0 +1,82 @@
interface Token {
field: string | null
value: string
}
export function normalizeQuery (query: string): string[] {
// given a string such as 'this is "my query" go', returns
// an array of tokens like this: ['this', 'is', 'my query', 'go']
if (!query) return []
const match = query.match(/\\?.|^$/g)
if (!match) return []
const { tokens } = match.reduce((state, c) => {
if (c === '"') {
state.quote ^= 1
} else if (!state.quote && c === ' ') {
state.tokens.push('')
} else {
state.tokens[state.tokens.length - 1] += c.replace(/\\(.)/, '$1')
}
return state
}, { tokens: [''], quote: 0 })
return tokens
}
const unquote = (str: string) => {
if (str[0] === '"') str = str.slice(1)
if (str[str.length - 1] === '"') str = str.slice(0, -1)
return str
}
export function parseTokens (normalizedQuery: string[]): Token[] {
// given an array of tokens as returned by normalizeQuery,
// returns a list of objects such as [
// {
// field: 'status',
// value: 'pending'
// },
// {
// field: null,
// value: 'hello'
// }
// ]
return normalizedQuery.map(t => {
// we split the token on ":"
const parts = t.split(/:(.+)/)
if (parts.length === 1) {
// no field specified
return { field: null, value: t }
}
// first item is the field, second is the value, possibly quoted
const [field, value] = parts
// we remove surrounding quotes if any
return { field, value: unquote(value) }
})
}
export function compileTokens (tokens: Token[]) {
// given a list of tokens as returned by parseTokens,
// returns a string query
const parts = tokens.map(token => {
const { field } = token
let { value } = token
if (value.includes(' ')) {
value = `"${value}"`
}
if (field) {
return `${field}:${value}`
}
return value
})
return parts.join(' ')
}

View File

@ -366,7 +366,7 @@
import axios from 'axios' import axios from 'axios'
import { humanSize, truncate } from '~/utils/filters' import { humanSize, truncate } from '~/utils/filters'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
const logger = useLogger() const logger = useLogger()

View File

@ -386,7 +386,7 @@ import axios from 'axios'
import ImportStatusModal from '~/components/library/ImportStatusModal.vue' import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
import time from '~/utils/time' import time from '~/utils/time'
import { humanSize, truncate } from '~/utils/filters' import { humanSize, truncate } from '~/utils/filters'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -133,9 +133,9 @@ import Pagination from '~/components/Pagination.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import ReportCard from '~/components/manage/moderation/ReportCard.vue' import ReportCard from '~/components/manage/moderation/ReportCard.vue'
import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue' import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -130,9 +130,9 @@ import time from '~/utils/time'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import OrderingMixin from '~/components/mixins/Ordering.vue' import OrderingMixin from '~/components/mixins/Ordering.vue'
import UserRequestCard from '~/components/manage/moderation/UserRequestCard.vue' import UserRequestCard from '~/components/manage/moderation/UserRequestCard.vue'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -78,7 +78,7 @@
<script> <script>
import { humanSize } from '~/utils/filters' import { humanSize } from '~/utils/filters'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
props: { library: { type: Object, required: true } }, props: { library: { type: Object, required: true } },

View File

@ -268,7 +268,7 @@
import axios from 'axios' import axios from 'axios'
import { merge } from 'lodash-es' import { merge } from 'lodash-es'
import time from '~/utils/time' import time from '~/utils/time'
import { normalizeQuery, parseTokens } from '~/search' import { normalizeQuery, parseTokens } from '~/utils/search'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import ActionTable from '~/components/common/ActionTable.vue' import ActionTable from '~/components/common/ActionTable.vue'
@ -276,7 +276,7 @@ import OrderingMixin from '~/components/mixins/Ordering.vue'
import SmartSearchMixin from '~/components/mixins/SmartSearch.vue' import SmartSearchMixin from '~/components/mixins/SmartSearch.vue'
import ImportStatusModal from '~/components/library/ImportStatusModal.vue' import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
import { humanSize, truncate } from '~/utils/filters' import { humanSize, truncate } from '~/utils/filters'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
components: { components: {

View File

@ -121,7 +121,7 @@
<script> <script>
import axios from 'axios' import axios from 'axios'
import useSharedLabels from '../../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
export default { export default {
props: { library: { type: Object, default: null } }, props: { library: { type: Object, default: null } },

View File

@ -213,7 +213,7 @@
<script> <script>
import axios from 'axios' import axios from 'axios'
import { humanSize } from '~/utils/filters' import { humanSize } from '~/utils/filters'
import { compileTokens } from '~/search' import { compileTokens } from '~/utils/search'
export default { export default {
data () { data () {

View File

@ -144,7 +144,7 @@ import OrderingMixin from '~/components/mixins/Ordering.vue'
import PaginationMixin from '~/components/mixins/Pagination.vue' import PaginationMixin from '~/components/mixins/Pagination.vue'
import PlaylistCardList from '~/components/playlists/CardList.vue' import PlaylistCardList from '~/components/playlists/CardList.vue'
import Pagination from '~/components/Pagination.vue' import Pagination from '~/components/Pagination.vue'
import useSharedLabels from '../../composables/useSharedLabels' import useSharedLabels from '~/composables/locale/useSharedLabels'
const FETCH_URL = 'playlists/' const FETCH_URL = 'playlists/'

View File

@ -14,8 +14,6 @@
"skipLibCheck": true, "skipLibCheck": true,
"lib": ["dom", "esnext", "webworker"], "lib": ["dom", "esnext", "webworker"],
"allowJs": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"strictNullChecks": true, "strictNullChecks": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
@ -33,6 +31,5 @@
"vueCompilerOptions": { "vueCompilerOptions": {
"experimentalCompatMode": 2 "experimentalCompatMode": 2
}, },
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.vue"], "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
"references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -1,8 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}