Migrate rest of the components

This commit is contained in:
wvffle 2022-08-30 17:56:04 +00:00 committed by Georg Krause
parent 6431d0285c
commit 74d1a0a03e
47 changed files with 2295 additions and 2341 deletions

View File

@ -86,6 +86,7 @@
"sinon": "14.0.0",
"ts-jest": "28.0.7",
"typescript": "4.7.4",
"utility-types": "^3.10.0",
"vite": "3.0.3",
"vite-plugin-pwa": "0.12.3",
"vite-plugin-vue-inspector": "1.0.1",

View File

@ -1,3 +1,45 @@
<script setup lang="ts">
import type { EditObject, EditObjectType } from '~/composables/moderation/useEditConfigs'
import type { Library } from '~/types'
import { ref } from 'vue'
import store from '~/store'
import axios from 'axios'
import useErrorHandler from '~/composables/useErrorHandler'
import EditForm from '~/components/library/EditForm.vue'
interface Props {
objectType: EditObjectType
object: EditObject
libraries: Library[] | null
}
withDefaults(defineProps<Props>(), {
libraries: null
})
const canEdit = store.state.auth.availablePermissions.library
const isLoadingLicenses = ref(false)
const licenses = ref([])
const fetchLicenses = async () => {
isLoadingLicenses.value = true
try {
const response = await axios.get('licenses/')
licenses.value = response.data.results
} catch (error) {
useErrorHandler(error as Error)
}
isLoadingLicenses.value = false
}
fetchLicenses()
</script>
<template>
<section class="ui vertical stripe segment">
<div class="ui text container">
@ -39,44 +81,3 @@
</div>
</section>
</template>
<script>
import axios from 'axios'
import EditForm from '~/components/library/EditForm.vue'
export default {
components: {
EditForm
},
props: {
objectType: { type: String, required: true },
object: { type: Object, required: true },
libraries: { type: Array, default: null }
},
data () {
return {
id: this.object.id,
isLoadingLicenses: false,
licenses: []
}
},
computed: {
canEdit () {
return true
}
},
created () {
this.fetchLicenses()
},
methods: {
fetchLicenses () {
const self = this
self.isLoadingLicenses = true
axios.get('licenses/').then((response) => {
self.isLoadingLicenses = false
self.licenses = response.data.results
})
}
}
}
</script>

View File

@ -1,3 +1,45 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ref } from 'vue'
import axios from 'axios'
import useErrorHandler from '~/composables/useErrorHandler'
interface Props {
id: number
}
const props = defineProps<Props>()
const router = useRouter()
const isLoading = ref(false)
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`uploads/${props.id}/`, {
params: {
refresh: 'true',
include_channels: 'true'
}
})
router.replace({
name: 'library.tracks.detail',
params: { id: response.data.track.id }
})
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
fetchData()
</script>
<template>
<main>
<div
@ -8,23 +50,3 @@
</div>
</main>
</template>
<script>
import axios from 'axios'
export default {
props: { id: { type: Number, required: true } },
async created () {
const upload = await this.fetchData()
this.$router.replace({ name: 'library.tracks.detail', params: { id: upload.track.id } })
},
methods: {
async fetchData () {
this.isLoading = true
const response = await axios.get(`uploads/${this.id}/`, { params: { refresh: 'true', include_channels: 'true' } })
this.isLoading = false
return response.data
}
}
}
</script>

View File

@ -1,3 +1,200 @@
<script setup lang="ts">
import { computed, ref, reactive, watch, watchEffect, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useGettext } from 'vue3-gettext'
import axios from 'axios'
import $ from 'jquery'
import useErrorHandler from '~/composables/useErrorHandler'
import TrackTable from '~/components/audio/track/Table.vue'
import RadioButton from '~/components/radios/Button.vue'
import BuilderFilter from './Filter.vue'
export interface BuilderFilter {
type: string
label: string
help_text: string
fields: FilterField[]
}
export interface FilterField {
name: string
placeholder: string
type: 'list'
subtype: 'number'
autocomplete?: string
autocomplete_qs: string
autocomplete_fields: {
remoteValues?: unknown
}
}
export interface FilterConfig extends Record<string, unknown> {
type: string
not: boolean
names: string[]
}
interface Filter {
hash: number
config: FilterConfig
filter: BuilderFilter
}
interface Props {
id?: number
}
const props = withDefaults(defineProps<Props>(), {
id: 0
})
const { $pgettext } = useGettext()
const router = useRouter()
const labels = computed(() => ({
title: $pgettext('Head/Radio/Title', 'Radio Builder'),
placeholder: {
description: $pgettext('Content/Radio/Input.Placeholder', 'My awesome description'),
name: $pgettext('Content/Radio/Input.Placeholder', 'My awesome radio')
}
}))
const filters = reactive([] as Filter[])
const checkResult = ref()
const fetchCandidates = async () => {
// TODO (wvffle): Add loader
try {
const response = await axios.post('radios/radios/validate/', {
filters: [{
type: 'group',
filters: filters.map(filter => ({
...filter.config,
type: filter.filter.type
}))
}]
})
checkResult.value = response.data.filters[0]
} catch (error) {
useErrorHandler(error as Error)
}
}
watch(filters, fetchCandidates)
const checkErrors = computed(() => checkResult.value?.errors ?? [])
const isPublic = ref(true)
const radioName = ref('')
const radioDesc = ref('')
const canSave = computed(() => radioName.value.length > 0 && checkErrors.value.length === 0)
const currentFilterType = ref()
const availableFilters = reactive([] as BuilderFilter[])
const currentFilter = computed(() => availableFilters.find(filter => filter.type === currentFilterType.value))
const fetchFilters = async () => {
// TODO (wvffle): Add loader
try {
const response = await axios.get('radios/radios/filters/')
availableFilters.length = 0
availableFilters.push(...response.data)
} catch (error) {
useErrorHandler(error as Error)
}
}
const isLoading = ref(false)
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`radios/radios/${props.id}/`)
filters.length = 0
filters.push(...response.data.config.map((filter: FilterConfig) => ({
config: filter,
filter: availableFilters.find(available => available.type === filter.type),
hash: +new Date()
})))
radioName.value = response.data.name
radioDesc.value = response.data.description
isPublic.value = response.data.is_public
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
fetchFilters().then(() => watchEffect(fetchData))
const add = async () => {
if (currentFilter.value) {
filters.push({
config: {} as FilterConfig,
filter: currentFilter.value,
hash: +new Date()
})
}
return fetchCandidates()
}
const updateConfig = async (index: number, field: keyof FilterConfig, value: unknown) => {
filters[index].config[field] = value
return fetchCandidates()
}
const deleteFilter = async (index: number) => {
filters.splice(index, 1)
return fetchCandidates()
}
const success = ref(false)
const save = async () => {
success.value = false
isLoading.value = true
try {
const data = {
name: radioName.value,
description: radioDesc.value,
is_public: isPublic.value,
config: filters.map(filter => ({
...filter.config,
type: filter.filter.type
}))
}
const response = props.id
? await axios.put(`radios/radios/${props.id}/`, data)
: await axios.post('radios/radios/', data)
success.value = true
if (!props.id) {
router.push({
name: 'library.radios.detail',
params: {
id: response.data.id
}
})
}
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
onMounted(() => {
$('.ui.dropdown').dropdown()
})
</script>
<template>
<div
v-title="labels.title"
@ -64,7 +261,7 @@
</div>
<div class="ui hidden divider" />
<button
:disabled="!canSave || null"
:disabled="!canSave"
:class="['ui', 'success', {loading: isLoading}, 'button']"
@click="save"
>
@ -96,8 +293,8 @@
</translate>
</option>
<option
v-for="(f, key) in availableFilters"
:key="key"
v-for="f in availableFilters"
:key="f.label"
:value="f.type"
>
{{ f.label }}
@ -105,7 +302,7 @@
</select>
<button
id="addFilter"
:disabled="!currentFilterType || null"
:disabled="!currentFilterType"
class="ui button"
@click="add"
>
@ -151,7 +348,7 @@
<tbody>
<builder-filter
v-for="(f, index) in filters"
:key="(f, index, f.hash)"
:key="f.hash"
:index="index"
:config="f.config"
:filter="f.filter"
@ -183,179 +380,3 @@
</div>
</div>
</template>
<script>
import axios from 'axios'
import $ from 'jquery'
import { clone } from 'lodash-es'
import BuilderFilter from './Filter.vue'
import TrackTable from '~/components/audio/track/Table.vue'
import RadioButton from '~/components/radios/Button.vue'
export default {
components: {
BuilderFilter,
TrackTable,
RadioButton
},
props: {
id: { type: Number, required: false, default: 0 }
},
data: function () {
return {
isLoading: false,
success: false,
availableFilters: [],
currentFilterType: null,
filters: [],
checkResult: null,
radioName: '',
radioDesc: '',
isPublic: true
}
},
computed: {
labels () {
const title = this.$pgettext('Head/Radio/Title', 'Radio Builder')
const placeholder = {
name: this.$pgettext('Content/Radio/Input.Placeholder', 'My awesome radio'),
description: this.$pgettext('Content/Radio/Input.Placeholder', 'My awesome description')
}
return {
title,
placeholder
}
},
canSave: function () {
return this.radioName.length > 0 && this.checkErrors.length === 0
},
checkErrors: function () {
if (!this.checkResult) {
return []
}
const errors = this.checkResult.errors
return errors
},
currentFilter: function () {
const self = this
return this.availableFilters.filter(e => {
return e.type === self.currentFilterType
})[0]
}
},
watch: {
filters: {
handler: function () {
this.fetchCandidates()
},
deep: true
}
},
created: function () {
const self = this
this.fetchFilters().then(() => {
if (self.id) {
self.fetch()
}
})
},
mounted () {
$('.ui.dropdown').dropdown()
},
methods: {
fetchFilters: function () {
const self = this
const url = 'radios/radios/filters/'
return axios.get(url).then(response => {
self.availableFilters = response.data
})
},
add () {
this.filters.push({
config: {},
filter: this.currentFilter,
hash: +new Date()
})
this.fetchCandidates()
},
updateConfig (index, field, value) {
this.filters[index].config[field] = value
this.fetchCandidates()
},
deleteFilter (index) {
this.filters.splice(index, 1)
this.fetchCandidates()
},
fetch: function () {
const self = this
self.isLoading = true
const url = 'radios/radios/' + this.id + '/'
axios.get(url).then(response => {
self.filters = response.data.config.map(f => {
return {
config: f,
filter: this.availableFilters.filter(e => {
return e.type === f.type
})[0],
hash: +new Date()
}
})
self.radioName = response.data.name
self.radioDesc = response.data.description
self.isPublic = response.data.is_public
self.isLoading = false
})
},
fetchCandidates: function () {
const self = this
const url = 'radios/radios/validate/'
let final = this.filters.map(f => {
const c = clone(f.config)
c.type = f.filter.type
return c
})
final = {
filters: [{ type: 'group', filters: final }]
}
axios.post(url, final).then(response => {
self.checkResult = response.data.filters[0]
})
},
save: function () {
const self = this
self.success = false
self.isLoading = true
let final = this.filters.map(f => {
const c = clone(f.config)
c.type = f.filter.type
return c
})
final = {
name: this.radioName,
description: this.radioDesc,
is_public: this.isPublic,
config: final
}
if (this.id) {
const url = 'radios/radios/' + this.id + '/'
axios.put(url, final).then(response => {
self.isLoading = false
self.success = true
})
} else {
const url = 'radios/radios/'
axios.post(url, final).then(response => {
self.success = true
self.isLoading = false
self.$router.push({
name: 'library.radios.detail',
params: {
id: response.data.id
}
})
})
}
}
}
}
</script>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
// TODO (wvffle): SORT IMPORTS LIKE SO EVERYWHERE
import type { Track } from '~/types'
import type { BuilderFilter, FilterConfig } from './Builder.vue'
import axios from 'axios'
import $ from 'jquery'
@ -18,26 +19,8 @@ import useErrorHandler from '~/composables/useErrorHandler'
interface Props {
index: number
filter: {
type: string
label: string
fields: {
name: string
placeholder: string
type: 'list'
subtype: 'number'
autocomplete?: string
autocomplete_qs: string
autocomplete_fields: {
remoteValues?: unknown
}
}[]
}
config: {
not: boolean
names: string[]
}
filter: BuilderFilter
config: FilterConfig
}
type Filter = { candidates: { count: number, sample: Track[] } }
@ -153,18 +136,18 @@ watch(exclude, fetchCandidates)
{{ f.placeholder }}
</div>
<input
v-if="f.type === 'list' && config[f.name]"
v-if="f.type === 'list' && config[f.name as keyof FilterConfig]"
:id="f.name"
:value="config[f.name].join(',')"
:value="(config[f.name as keyof FilterConfig] as string[]).join(',')"
type="hidden"
>
<div
v-if="config[f.name]"
v-if="typeof config[f.name as keyof FilterConfig] === 'object'"
class="ui menu"
>
<div
v-for="(v, i) in config[f.name]"
:key="v"
v-for="(v, i) in config[f.name as keyof FilterConfig] as object"
:key="i"
class="ui item"
:data-value="v"
>

View File

@ -1,3 +1,121 @@
<script setup lang="ts">
import type { BackendError, InstancePolicy } from '~/types'
import { computed, ref, reactive } from 'vue'
import { whenever } from '@vueuse/core'
import { useGettext } from 'vue3-gettext'
import axios from 'axios'
interface Emits {
(e: 'save', data: InstancePolicy): void
(e: 'delete'): void
(e: 'cancel'): void
}
interface Props {
type: string
target: string
object?: InstancePolicy | null
}
const emit = defineEmits<Emits>()
const props = withDefaults(defineProps<Props>(), {
object: null
})
const { $pgettext } = useGettext()
const labels = computed(() => ({
summaryHelp: $pgettext('Content/Moderation/Help text', "Explain why you're applying this policy: this will help you remember why you added this rule. Depending on your pod configuration, this may be displayed publicly to help users understand the moderation rules in place."),
isActiveHelp: $pgettext('Content/Moderation/Help text', 'Use this setting to temporarily enable/disable the policy without completely removing it.'),
blockAllHelp: $pgettext('Content/Moderation/Help text', 'Block everything from this account or domain. This will prevent any interaction with the entity, and purge related content (uploads, libraries, follows, etc.)'),
silenceActivity: {
help: $pgettext('Content/Moderation/Help text', 'Hide account or domain content, except from followers.'),
label: $pgettext('Content/Moderation/*/Verb', 'Mute activity')
},
silenceNotifications: {
help: $pgettext('Content/Moderation/Help text', 'Prevent account or domain from triggering notifications, except from followers.'),
label: $pgettext('Content/Moderation/*/Verb', 'Mute notifications')
},
rejectMedia: {
help: $pgettext('Content/Moderation/Help text', 'Do not download any media file (audio, album cover, account avatar…) from this account or domain. This will purge existing content as well.'),
label: $pgettext('Content/Moderation/*/Verb', 'Reject media')
}
}))
const current = reactive({
summary: props.object?.summary ?? '',
isActive: props.object?.is_active ?? true,
blockAll: props.object?.block_all ?? true,
silenceActivity: props.object?.silence_activity ?? false,
silenceNotifications: props.object?.silence_notifications ?? false,
rejectMedia: props.object?.reject_media ?? false
})
const fieldConfig = [
// TODO: We hide those until we actually have the related features implemented :)
// { id: 'silenceActivity', icon: 'feed' },
// { id: 'silenceNotifications', icon: 'bell' },
{ id: 'rejectMedia', icon: 'file' }
] as const
whenever(() => current.silenceNotifications, () => (current.blockAll = false))
whenever(() => current.silenceActivity, () => (current.blockAll = false))
whenever(() => current.rejectMedia, () => (current.blockAll = false))
whenever(() => current.blockAll, () => {
for (const config of fieldConfig) {
current[config.id] = false
}
})
const isLoading = ref(false)
const errors = ref([] as string[])
const createOrUpdate = async () => {
isLoading.value = true
errors.value = []
try {
const data = {
summary: current.summary,
is_active: current.isActive,
block_all: current.blockAll,
silence_activity: current.silenceActivity,
silence_notifications: current.silenceNotifications,
reject_media: current.rejectMedia,
target: {
type: props.type,
id: props.target
}
}
const response = props.object
? await axios.patch(`manage/moderation/instance-policies/${props.object.id}/`, data)
: await axios.post('manage/moderation/instance-policies/', data)
emit('save', response.data)
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
isLoading.value = false
}
const remove = async () => {
isLoading.value = true
errors.value = []
try {
await axios.delete(`manage/moderation/instance-policies/${props.object?.id}/`)
emit('delete')
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
isLoading.value = false
}
</script>
<template>
<form
class="ui form"
@ -111,7 +229,7 @@
<div class="ui hidden divider" />
<button
class="ui basic left floated button"
@click.prevent="$emit('cancel')"
@click.prevent="emit('cancel')"
>
<translate translate-context="*/*/Button.Label/Verb">
Cancel
@ -119,7 +237,7 @@
</button>
<button
:class="['ui', 'right', 'floated', 'success', {'disabled loading': isLoading}, 'button']"
:disabled="isLoading || null"
:disabled="isLoading"
>
<translate
v-if="object"
@ -166,131 +284,3 @@
</dangerous-button>
</form>
</template>
<script>
import axios from 'axios'
import { get } from 'lodash-es'
export default {
props: {
type: { type: String, required: true },
object: { type: Object, default: null },
target: { type: String, required: true }
},
data () {
const current = this.object || {}
return {
isLoading: false,
errors: [],
current: {
summary: get(current, 'summary', ''),
isActive: get(current, 'is_active', true),
blockAll: get(current, 'block_all', true),
silenceActivity: get(current, 'silence_activity', false),
silenceNotifications: get(current, 'silence_notifications', false),
rejectMedia: get(current, 'reject_media', false)
},
fieldConfig: [
// we hide those until we actually have the related features implemented :)
// {id: "silenceActivity", icon: "feed"},
// {id: "silenceNotifications", icon: "bell"},
{ id: 'rejectMedia', icon: 'file' }
]
}
},
computed: {
labels () {
return {
summaryHelp: this.$pgettext('Content/Moderation/Help text', "Explain why you're applying this policy: this will help you remember why you added this rule. Depending on your pod configuration, this may be displayed publicly to help users understand the moderation rules in place."),
isActiveHelp: this.$pgettext('Content/Moderation/Help text', 'Use this setting to temporarily enable/disable the policy without completely removing it.'),
blockAllHelp: this.$pgettext('Content/Moderation/Help text', 'Block everything from this account or domain. This will prevent any interaction with the entity, and purge related content (uploads, libraries, follows, etc.)'),
silenceActivity: {
help: this.$pgettext('Content/Moderation/Help text', 'Hide account or domain content, except from followers.'),
label: this.$pgettext('Content/Moderation/*/Verb', 'Mute activity')
},
silenceNotifications: {
help: this.$pgettext('Content/Moderation/Help text', 'Prevent account or domain from triggering notifications, except from followers.'),
label: this.$pgettext('Content/Moderation/*/Verb', 'Mute notifications')
},
rejectMedia: {
help: this.$pgettext('Content/Moderation/Help text', 'Do not download any media file (audio, album cover, account avatar…) from this account or domain. This will purge existing content as well.'),
label: this.$pgettext('Content/Moderation/*/Verb', 'Reject media')
}
}
}
},
watch: {
'current.silenceActivity': function (v) {
if (v) {
this.current.blockAll = false
}
},
'current.silenceNotifications': function (v) {
if (v) {
this.current.blockAll = false
}
},
'current.rejectMedia': function (v) {
if (v) {
this.current.blockAll = false
}
},
'current.blockAll': function (v) {
if (v) {
const self = this
this.fieldConfig.forEach((f) => {
self.current[f.id] = false
})
}
}
},
methods: {
createOrUpdate () {
const self = this
this.isLoading = true
this.errors = []
let url, method
const data = {
summary: this.current.summary,
is_active: this.current.isActive,
block_all: this.current.blockAll,
silence_activity: this.current.silenceActivity,
silence_notifications: this.current.silenceNotifications,
reject_media: this.current.rejectMedia,
target: {
type: this.type,
id: this.target
}
}
if (this.object) {
url = `manage/moderation/instance-policies/${this.object.id}/`
method = 'patch'
} else {
url = 'manage/moderation/instance-policies/'
method = 'post'
}
axios[method](url, data).then((response) => {
this.isLoading = false
self.$emit('save', response.data)
}, (error) => {
self.isLoading = false
self.errors = error.backendErrors
})
},
remove () {
const self = this
this.isLoading = true
this.errors = []
const url = `manage/moderation/instance-policies/${this.object.id}/`
axios.delete(url).then((response) => {
this.isLoading = false
self.$emit('delete')
}, (error) => {
self.isLoading = false
self.errors = error.backendErrors
})
}
}
}
</script>

View File

@ -1,3 +1,49 @@
<script setup lang="ts">
import type { BackendError } from '~/types'
import { computed, ref, reactive } from 'vue'
import { useGettext } from 'vue3-gettext'
import { useRouter } from 'vue-router'
import { useStore } from '~/store'
import axios from 'axios'
interface Invitation {
code: string
}
const { $pgettext } = useGettext()
const router = useRouter()
const store = useStore()
const labels = computed(() => ({
placeholder: $pgettext('Content/Admin/Input.Placeholder', 'Leave empty for a random code')
}))
const invitations = reactive([] as Invitation[])
const code = ref('')
const isLoading = ref(false)
const errors = ref([] as string[])
const submit = async () => {
isLoading.value = true
errors.value = []
try {
const response = await axios.post('manage/users/invitations/', { code: code.value })
invitations.unshift(response.data)
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
isLoading.value = false
}
const getUrl = (code: string) => store.getters['instance/absoluteUrl'](router.resolve({
name: 'signup',
query: { invitation: code.toUpperCase() }
}).href)
</script>
<template>
<div>
<form
@ -37,7 +83,7 @@
<div class="ui field">
<button
:class="['ui', {loading: isLoading}, 'button']"
:disabled="isLoading || null"
:disabled="isLoading"
type="submit"
>
<translate translate-context="Content/Admin/Button.Label/Verb">
@ -90,46 +136,3 @@
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
data () {
return {
isLoading: false,
code: null,
invitations: [],
errors: []
}
},
computed: {
labels () {
return {
placeholder: this.$pgettext('Content/Admin/Input.Placeholder', 'Leave empty for a random code')
}
}
},
methods: {
submit () {
const self = this
this.isLoading = true
this.errors = []
const url = 'manage/users/invitations/'
const payload = {
code: this.code
}
axios.post(url, payload).then((response) => {
self.isLoading = false
self.invitations.unshift(response.data)
}, (error) => {
self.isLoading = false
self.errors = error.backendErrors
})
},
getUrl (code) {
return this.$store.getters['instance/absoluteUrl'](this.$router.resolve({ name: 'signup', query: { invitation: code.toUpperCase() } }).href)
}
}
}
</script>

View File

@ -1,3 +1,113 @@
<script setup lang="ts">
import type { Notification, LibraryFollow } from '~/types'
import { computed, ref, watchEffect } from 'vue'
import { useGettext } from 'vue3-gettext'
import { useStore } from '~/store'
import axios from 'axios'
interface Props {
initialItem: Notification
}
const props = defineProps<Props>()
const { $pgettext, $gettext } = useGettext()
const store = useStore()
const labels = computed(() => ({
libraryFollowMessage: $pgettext('Content/Notifications/Paragraph', '%{ username } followed your library "%{ library }"'),
libraryAcceptFollowMessage: $pgettext('Content/Notifications/Paragraph', '%{ username } accepted your follow on library "%{ library }"'),
libraryRejectMessage: $pgettext('Content/Notifications/Paragraph', 'You rejected %{ username }&#39;s request to follow "%{ library }"'),
libraryPendingFollowMessage: $pgettext('Content/Notifications/Paragraph', '%{ username } wants to follow your library "%{ library }"'),
markRead: $pgettext('Content/Notifications/Button.Tooltip/Verb', 'Mark as read'),
markUnread: $pgettext('Content/Notifications/Button.Tooltip/Verb', 'Mark as unread')
}))
const item = ref(props.initialItem)
watchEffect(() => (item.value = props.initialItem))
const username = computed(() => props.initialItem.activity.actor.preferred_username)
const notificationData = computed(() => {
const activity = props.initialItem.activity
if (activity.type === 'Follow') {
if (activity.object && activity.object.type === 'music.Library') {
const detailUrl = { name: 'content.libraries.detail', params: { id: activity.object.uuid } }
if (activity.related_object?.approved === null) {
return {
detailUrl,
message: $gettext(labels.value.libraryPendingFollowMessage, { username: username.value, library: activity.object.name }),
acceptFollow: {
buttonClass: 'success',
icon: 'check',
label: $pgettext('Content/*/Button.Label/Verb', 'Approve'),
handler: () => approveLibraryFollow(activity.related_object)
},
rejectFollow: {
buttonClass: 'danger',
icon: 'x',
label: $pgettext('Content/*/Button.Label/Verb', 'Reject'),
handler: () => rejectLibraryFollow(activity.related_object)
}
}
} else if (activity.related_object?.approved) {
return {
detailUrl,
message: $gettext(labels.value.libraryFollowMessage, { username: username.value, library: activity.object.name })
}
}
return {
detailUrl,
message: $gettext(labels.value.libraryRejectMessage, { username: username.value, library: activity.object.name })
}
}
}
if (activity.type === 'Accept') {
if (activity.object?.type === 'federation.LibraryFollow') {
return {
detailUrl: { name: 'content.remote.index' },
message: $gettext(labels.value.libraryAcceptFollowMessage, { username: username.value, library: activity.related_object.name })
}
}
}
return {}
})
const read = ref(false)
watchEffect(async () => {
await axios.patch(`federation/inbox/${item.value.id}/`, { is_read: read.value })
item.value.is_read = read.value
store.commit('ui/incrementNotifications', { type: 'inbox', count: read.value ? -1 : 1 })
})
const handleAction = (handler?: () => void) => {
// call handler then mark notification as read
handler?.()
read.value = true
}
const approveLibraryFollow = async (follow: LibraryFollow) => {
follow.isLoading = true
await axios.post(`federation/follows/library/${follow.uuid}/accept/`)
follow.isLoading = false
follow.approved = true
}
const rejectLibraryFollow = async (follow: LibraryFollow) => {
follow.isLoading = true
await axios.post(`federation/follows/library/${follow.uuid}/reject/`)
follow.isLoading = false
follow.approved = false
}
</script>
<template>
<tr :class="[{'disabled-row': item.is_read}]">
<td>
@ -29,7 +139,7 @@
&nbsp;
<button
:class="['ui', 'basic', 'tiny', notificationData.acceptFollow.buttonClass || '', 'button']"
@click="handleAction(notificationData.acceptFollow.handler)"
@click="handleAction(notificationData.acceptFollow?.handler)"
>
<i
v-if="notificationData.acceptFollow.icon"
@ -39,7 +149,7 @@
</button>
<button
:class="['ui', 'basic', 'tiny', notificationData.rejectFollow.buttonClass || '', 'button']"
@click="handleAction(notificationData.rejectFollow.handler)"
@click="handleAction(notificationData.rejectFollow?.handler)"
>
<i
v-if="notificationData.rejectFollow.icon"
@ -57,7 +167,7 @@
:aria-label="labels.markUnread"
class="discrete link"
:title="labels.markUnread"
@click.prevent="markRead(false)"
@click.prevent="read = false"
>
<i class="redo icon" />
</a>
@ -67,128 +177,10 @@
:aria-label="labels.markRead"
class="discrete link"
:title="labels.markRead"
@click.prevent="markRead(true)"
@click.prevent="read = true"
>
<i class="check icon" />
</a>
</td>
</tr>
</template>
<script>
import axios from 'axios'
export default {
props: { initialItem: { type: Object, required: true } },
data: function () {
return {
item: this.initialItem
}
},
computed: {
message () {
return 'plop'
},
labels () {
const libraryFollowMessage = this.$pgettext('Content/Notifications/Paragraph', '%{ username } followed your library "%{ library }"')
const libraryAcceptFollowMessage = this.$pgettext('Content/Notifications/Paragraph', '%{ username } accepted your follow on library "%{ library }"')
const libraryRejectMessage = this.$pgettext('Content/Notifications/Paragraph', 'You rejected %{ username }&#39;s request to follow "%{ library }"')
const libraryPendingFollowMessage = this.$pgettext('Content/Notifications/Paragraph', '%{ username } wants to follow your library "%{ library }"')
return {
libraryFollowMessage,
libraryAcceptFollowMessage,
libraryRejectMessage,
libraryPendingFollowMessage,
markRead: this.$pgettext('Content/Notifications/Button.Tooltip/Verb', 'Mark as read'),
markUnread: this.$pgettext('Content/Notifications/Button.Tooltip/Verb', 'Mark as unread')
}
},
username () {
return this.item.activity.actor.preferred_username
},
notificationData () {
const self = this
const a = this.item.activity
if (a.type === 'Follow') {
if (a.object && a.object.type === 'music.Library') {
let acceptFollow = null
let rejectFollow = null
let message = null
if (a.related_object && a.related_object.approved === null) {
message = this.labels.libraryPendingFollowMessage
acceptFollow = {
buttonClass: 'success',
icon: 'check',
label: this.$pgettext('Content/*/Button.Label/Verb', 'Approve'),
handler: () => { self.approveLibraryFollow(a.related_object) }
}
rejectFollow = {
buttonClass: 'danger',
icon: 'x',
label: this.$pgettext('Content/*/Button.Label/Verb', 'Reject'),
handler: () => { self.rejectLibraryFollow(a.related_object) }
}
} else if (a.related_object && a.related_object.approved) {
message = this.labels.libraryFollowMessage
} else {
message = this.labels.libraryRejectMessage
}
return {
acceptFollow,
rejectFollow,
detailUrl: { name: 'content.libraries.detail', params: { id: a.object.uuid } },
message: this.$gettextInterpolate(
message,
{ username: this.username, library: a.object.name }
)
}
}
}
if (a.type === 'Accept') {
if (a.object && a.object.type === 'federation.LibraryFollow') {
return {
detailUrl: { name: 'content.remote.index' },
message: this.$gettextInterpolate(
this.labels.libraryAcceptFollowMessage,
{ username: this.username, library: a.related_object.name }
)
}
}
}
return {}
}
},
methods: {
handleAction (handler) {
// call handler then mark notification as read
handler()
this.markRead(true)
},
approveLibraryFollow (follow) {
const action = 'accept'
axios.post(`federation/follows/library/${follow.uuid}/${action}/`).then((response) => {
follow.isLoading = false
follow.approved = true
})
},
rejectLibraryFollow (follow) {
const action = 'reject'
axios.post(`federation/follows/library/${follow.uuid}/${action}/`).then((response) => {
follow.isLoading = false
follow.approved = false
})
},
markRead (value) {
const self = this
axios.patch(`federation/inbox/${this.item.id}/`, { is_read: value }).then((response) => {
self.item.is_read = value
if (value) {
self.$store.commit('ui/incrementNotifications', { type: 'inbox', count: -1 })
} else {
self.$store.commit('ui/incrementNotifications', { type: 'inbox', count: 1 })
}
})
}
}
}
</script>

View File

@ -1,3 +1,28 @@
<script setup lang="ts">
import type { Track } from '~/types'
import { useGettext } from 'vue3-gettext'
import { computed } from 'vue'
interface Props {
track?: Track | null
button?: boolean
border?: boolean
}
withDefaults(defineProps<Props>(), {
track: null,
button: false,
border: false
})
const { $pgettext } = useGettext()
const labels = computed(() => ({
addToPlaylist: $pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Add to playlist…')
}))
</script>
<template>
<button
v-if="button"
@ -19,26 +44,3 @@
<i :class="['list', 'basic', 'icon']" />
</button>
</template>
<script>
export default {
props: {
track: { type: Object, default: function () { return {} } },
button: { type: Boolean, default: false },
border: { type: Boolean, default: false }
},
data () {
return {
showModal: false
}
},
computed: {
labels () {
return {
addToPlaylist: this.$pgettext('Sidebar/Player/Icon.Tooltip/Verb', 'Add to playlist…')
}
}
}
}
</script>

View File

@ -1,3 +1,53 @@
<script setup lang="ts">
import type { Playlist } from '~/types'
import { ref, reactive, watch } from 'vue'
import { useStore } from '~/store'
import axios from 'axios'
import useErrorHandler from '~/composables/useErrorHandler'
import PlaylistCard from '~/components/playlists/Card.vue'
interface Props {
filters: Record<string, unknown>
url: string
}
const props = defineProps<Props>()
const store = useStore()
const objects = reactive([] as Playlist[])
const isLoading = ref(false)
const nextPage = ref('')
const fetchData = async (url = props.url) => {
isLoading.value = true
try {
const params = {
...props.filters,
page_size: props.filters.limit ?? 3
}
const response = await axios.get(url, { params })
nextPage.value = response.data.next
objects.push(...response.data.results)
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
watch(
() => store.state.moderation.lastUpdate,
() => fetchData(),
{ immediate: true }
)
</script>
<template>
<div>
<h3
@ -13,7 +63,7 @@
<div class="ui loader" />
</div>
<div
v-if="playlistsExist"
v-if="objects.length > 0"
class="ui cards app-cards"
>
<playlist-card
@ -57,73 +107,3 @@
</template>
</div>
</template>
<script>
import { clone } from 'lodash-es'
import axios from 'axios'
import PlaylistCard from '~/components/playlists/Card.vue'
export default {
components: {
PlaylistCard
},
props: {
filters: { type: Object, required: true },
url: { type: String, required: true }
},
data () {
return {
objects: [],
limit: this.filters.limit || 3,
isLoading: false,
errors: null,
previousPage: null,
nextPage: null
}
},
computed: {
playlistsExist: function () {
return this.objects.length > 0
}
},
watch: {
offset () {
this.fetchData()
},
'$store.state.moderation.lastUpdate': function () {
this.fetchData(this.url)
}
},
created () {
this.fetchData(this.url)
},
methods: {
fetchData (url) {
if (!url) {
return
}
this.isLoading = true
const self = this
const params = clone(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]
}, 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>

View File

@ -1,3 +1,34 @@
<script setup lang="ts">
import type { Radio } from '~/types'
import { ref, computed } from 'vue'
import { useStore } from '~/store'
import RadioButton from './Button.vue'
interface Props {
type: string
customRadio?: Radio | null
objectId?: string | null
}
const props = withDefaults(defineProps<Props>(), {
customRadio: null,
objectId: null
})
const store = useStore()
const isDescriptionExpanded = ref(false)
const radio = computed(() => props.customRadio
? props.customRadio
: store.getters['radios/types'][props.type]
)
const customRadioId = computed(() => props.customRadio?.id ?? null)
</script>
<template>
<div class="ui card">
<div class="content">
@ -35,7 +66,7 @@
:object-id="objectId"
/>
<router-link
v-if="$store.state.auth.authenticated && type === 'custom' && radio.user.id === $store.state.auth.profile.id"
v-if="$store.state.auth.authenticated && type === 'custom' && radio.user.id === $store.state.auth.profile?.id"
class="ui success button right floated"
:to="{name: 'library.radios.edit', params: {id: customRadioId }}"
>
@ -46,37 +77,3 @@
</div>
</div>
</template>
<script>
import RadioButton from './Button.vue'
export default {
components: {
RadioButton
},
props: {
type: { type: String, required: true, default: '' },
customRadio: { type: Object, required: false, default: () => { return {} } },
objectId: { type: String, required: false, default: null }
},
data () {
return {
isDescriptionExpanded: false
}
},
computed: {
radio () {
if (Object.keys(this.customRadio).length > 0) {
return this.customRadio
}
return this.$store.getters['radios/types'][this.type]
},
customRadioId: function () {
if (this.customRadio) {
return this.customRadio.id
}
return null
}
}
}
</script>

View File

@ -21,9 +21,11 @@ const store: Module<State, RootState> = {
playlists (state, value) {
state.playlists = value
},
chooseTrack (state, value) {
state.showModal = true
state.modalTrack = value
chooseTrack (state, value: Track | null) {
if (value !== null) {
state.showModal = true
state.modalTrack = value
}
},
showModal (state, value) {
state.showModal = value

View File

@ -161,6 +161,10 @@ export interface LibraryFollow {
uuid: string
approved: boolean
name: string
type?: 'music.Library' | 'federation.LibraryFollow'
target: Library
// TODO (wvffle): Check if it's not added only on frontend side
isLoading?: boolean
}
@ -199,7 +203,7 @@ export interface PlaylistTrack {
}
export interface Radio {
id: string
id: number
name: string
user: User
}
@ -466,9 +470,18 @@ export interface UserRequest {
}
// Notification stuff
export type Activity = {
actor: Actor
creation_date: string
related_object: LibraryFollow
type: 'Follow' | 'Accept'
object: LibraryFollow
}
export interface Notification {
id: number
is_read: boolean
activity: Activity
}
// Tags stuff

View File

@ -1,3 +1,78 @@
<script setup lang="ts">
import { humanSize, truncate } from '~/utils/filters'
import { useGettext } from 'vue3-gettext'
import { useRouter } from 'vue-router'
import { computed, ref } from 'vue'
import axios from 'axios'
import FetchButton from '~/components/federation/FetchButton.vue'
import TagsList from '~/components/tags/List.vue'
import useErrorHandler from '~/composables/useErrorHandler'
interface Props {
id: string
}
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const router = useRouter()
const labels = computed(() => ({
statsWarning: $pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object')
}))
const isLoading = ref(false)
const object = ref()
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`manage/channels/${props.id}/`)
object.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const isLoadingStats = ref(false)
const stats = ref()
const fetchStats = async () => {
isLoadingStats.value = true
try {
const response = await axios.get(`manage/channels/${props.id}/stats/`)
stats.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoadingStats.value = true
}
fetchStats()
fetchData()
const remove = async () => {
isLoading.value = true
try {
await axios.delete(`manage/channels/${props.id}/`)
router.push({ name: 'manage.channels' })
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const getQuery = (field: string, value: string) => `${field}:"${value}"`
</script>
<template>
<main>
<div
@ -421,72 +496,3 @@
</template>
</main>
</template>
<script>
import axios from 'axios'
import TagsList from '~/components/tags/List.vue'
import FetchButton from '~/components/federation/FetchButton.vue'
import { humanSize, truncate } from '~/utils/filters'
export default {
components: {
FetchButton,
TagsList
},
props: { id: { type: String, required: true } },
setup () {
return { humanSize, truncate }
},
data () {
return {
isLoading: true,
isLoadingStats: false,
object: null,
stats: null
}
},
computed: {
labels () {
return {
statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object')
}
}
},
created () {
this.fetchData()
this.fetchStats()
},
methods: {
fetchData () {
const self = this
this.isLoading = true
const url = `manage/channels/${this.id}/`
axios.get(url).then(response => {
self.object = response.data
self.isLoading = false
})
},
fetchStats () {
const self = this
this.isLoadingStats = true
const url = `manage/channels/${this.id}/stats/`
axios.get(url).then(response => {
self.stats = response.data
self.isLoadingStats = false
})
},
remove () {
const self = this
this.isLoading = true
const url = `manage/channels/${this.id}/`
axios.delete(url).then(response => {
self.$router.push({ name: 'manage.channels' })
})
},
getQuery (field, value) {
return `${field}:"${value}"`
}
}
}
</script>

View File

@ -1,3 +1,78 @@
<script setup lang="ts">
import { humanSize, truncate } from '~/utils/filters'
import { useGettext } from 'vue3-gettext'
import { useRouter } from 'vue-router'
import { computed, ref } from 'vue'
import axios from 'axios'
import FetchButton from '~/components/federation/FetchButton.vue'
import TagsList from '~/components/tags/List.vue'
import useErrorHandler from '~/composables/useErrorHandler'
interface Props {
id: number
}
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const router = useRouter()
const labels = computed(() => ({
statsWarning: $pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object')
}))
const isLoading = ref(false)
const object = ref()
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`manage/library/albums/${props.id}/`)
object.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const isLoadingStats = ref(false)
const stats = ref()
const fetchStats = async () => {
isLoadingStats.value = true
try {
const response = await axios.get(`manage/library/albums/${props.id}/stats/`)
stats.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoadingStats.value = true
}
fetchStats()
fetchData()
const remove = async () => {
isLoading.value = true
try {
await axios.delete(`manage/library/albums/${props.id}/`)
router.push({ name: 'manage.library.albums' })
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const getQuery = (field: string, value: string) => `${field}:"${value}"`
</script>
<template>
<main>
<div
@ -405,71 +480,3 @@
</template>
</main>
</template>
<script>
import axios from 'axios'
import FetchButton from '~/components/federation/FetchButton.vue'
import TagsList from '~/components/tags/List.vue'
import { humanSize, truncate } from '~/utils/filters'
export default {
components: {
FetchButton,
TagsList
},
props: { id: { type: Number, required: true } },
setup () {
return { humanSize, truncate }
},
data () {
return {
isLoading: true,
isLoadingStats: false,
object: null,
stats: null
}
},
computed: {
labels () {
return {
statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object')
}
}
},
created () {
this.fetchData()
this.fetchStats()
},
methods: {
fetchData () {
const self = this
this.isLoading = true
const url = `manage/library/albums/${this.id}/`
axios.get(url).then(response => {
self.object = response.data
self.isLoading = false
})
},
fetchStats () {
const self = this
this.isLoadingStats = true
const url = `manage/library/albums/${this.id}/stats/`
axios.get(url).then(response => {
self.stats = response.data
self.isLoadingStats = false
})
},
remove () {
const self = this
this.isLoading = true
const url = `manage/library/albums/${this.id}/`
axios.delete(url).then(response => {
self.$router.push({ name: 'manage.library.albums' })
})
},
getQuery (field, value) {
return `${field}:"${value}"`
}
}
}
</script>

View File

@ -1,3 +1,78 @@
<script setup lang="ts">
import { humanSize, truncate } from '~/utils/filters'
import { useGettext } from 'vue3-gettext'
import { useRouter } from 'vue-router'
import { computed, ref } from 'vue'
import axios from 'axios'
import FetchButton from '~/components/federation/FetchButton.vue'
import TagsList from '~/components/tags/List.vue'
import useErrorHandler from '~/composables/useErrorHandler'
interface Props {
id: number
}
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const router = useRouter()
const labels = computed(() => ({
statsWarning: $pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object')
}))
const isLoading = ref(false)
const object = ref()
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`manage/library/artists/${props.id}/`)
object.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const isLoadingStats = ref(false)
const stats = ref()
const fetchStats = async () => {
isLoadingStats.value = true
try {
const response = await axios.get(`manage/library/artists/${props.id}/stats/`)
stats.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoadingStats.value = true
}
fetchStats()
fetchData()
const remove = async () => {
isLoading.value = true
try {
await axios.delete(`manage/library/artists/${props.id}/`)
router.push({ name: 'manage.library.artists' })
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const getQuery = (field: string, value: string) => `${field}:"${value}"`
</script>
<template>
<main>
<div
@ -416,76 +491,3 @@
</template>
</main>
</template>
<script>
import axios from 'axios'
import TagsList from '~/components/tags/List.vue'
import FetchButton from '~/components/federation/FetchButton.vue'
import { humanSize, truncate } from '~/utils/filters'
export default {
components: {
FetchButton,
TagsList
},
props: { id: { type: Number, required: true } },
setup () {
return { humanSize, truncate }
},
data () {
return {
isLoading: true,
isLoadingStats: false,
object: null,
stats: null
}
},
computed: {
labels () {
return {
statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object')
}
}
},
created () {
this.fetchData()
this.fetchStats()
},
methods: {
fetchData () {
const self = this
this.isLoading = true
const url = `manage/library/artists/${this.id}/`
axios.get(url).then(response => {
if (response.data.channel) {
self.$router.push({ name: 'manage.channels.detail', params: { id: response.data.channel } })
} else {
self.object = response.data
self.isLoading = false
}
})
},
fetchStats () {
const self = this
this.isLoadingStats = true
const url = `manage/library/artists/${this.id}/stats/`
axios.get(url).then(response => {
self.stats = response.data
self.isLoadingStats = false
})
},
remove () {
const self = this
this.isLoading = true
const url = `manage/library/artists/${this.id}/`
axios.delete(url).then(response => {
self.$router.push({ name: 'manage.library.artists' })
})
},
getQuery (field, value) {
return `${field}:"${value}"`
}
}
}
</script>

View File

@ -1,3 +1,98 @@
<script setup lang="ts">
import type { PrivacyLevel } from '~/types'
import { humanSize, truncate } from '~/utils/filters'
import { useGettext } from 'vue3-gettext'
import { useRouter } from 'vue-router'
import { computed, ref } from 'vue'
import axios from 'axios'
import useSharedLabels from '~/composables/locale/useSharedLabels'
import useErrorHandler from '~/composables/useErrorHandler'
import useLogger from '~/composables/useLogger'
const PRIVACY_LEVELS = ['me', 'instance', 'everyone'] as PrivacyLevel[]
interface Props {
id: string
}
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const sharedLabels = useSharedLabels()
const router = useRouter()
const logger = useLogger()
const labels = computed(() => ({
statsWarning: $pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object')
}))
const isLoading = ref(false)
const object = ref()
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`manage/library/libraries/${props.id}/`)
object.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const isLoadingStats = ref(false)
const stats = ref()
const fetchStats = async () => {
isLoadingStats.value = true
try {
const response = await axios.get(`manage/library/libraries/${props.id}/stats/`)
stats.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoadingStats.value = true
}
fetchStats()
fetchData()
const remove = async () => {
isLoading.value = true
try {
await axios.delete(`manage/library/libraries/${props.id}/`)
router.push({ name: 'manage.library.libraries' })
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const getQuery = (field: string, value: string) => `${field}:"${value}"`
const updateObj = async (attr: string) => {
const params = {
[attr]: object.value[attr]
}
try {
await axios.patch(`manage/library/libraries/${props.id}/`, params)
logger.info(`${attr} was updated succcessfully to ${params[attr]}`)
} catch (error) {
logger.error(`Error while setting ${attr} to ${params[attr]}`, error)
// TODO (wvffle): Use error handler with custom msg
}
}
</script>
<template>
<main>
<div
@ -148,15 +243,15 @@
@change="updateObj('privacy_level')"
>
<option
v-for="(p, key) in ['me', 'instance', 'everyone']"
:key="key"
v-for="p in PRIVACY_LEVELS"
:key="p"
:value="p"
>
{{ sharedLabels.fields.privacy_level.shortChoices[p] }}
</option>
</select>
<template v-else>
{{ sharedLabels.fields.privacy_level.shortChoices[object.privacy_level] }}
{{ sharedLabels.fields.privacy_level.shortChoices[object.privacy_level as PrivacyLevel] }}
</template>
</td>
</tr>
@ -361,91 +456,3 @@
</template>
</main>
</template>
<script>
import axios from 'axios'
import { humanSize, truncate } from '~/utils/filters'
import useLogger from '~/composables/useLogger'
import useSharedLabels from '~/composables/locale/useSharedLabels'
const logger = useLogger()
export default {
props: { id: { type: String, required: true } },
setup () {
const sharedLabels = useSharedLabels()
return { sharedLabels, humanSize, truncate }
},
data () {
return {
isLoading: true,
isLoadingStats: false,
object: null,
stats: null
}
},
computed: {
labels () {
return {
statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object')
}
}
},
created () {
this.fetchData()
this.fetchStats()
},
methods: {
fetchData () {
const self = this
this.isLoading = true
const url = `manage/library/libraries/${this.id}/`
axios.get(url).then(response => {
self.object = response.data
self.isLoading = false
})
},
fetchStats () {
const self = this
this.isLoadingStats = true
const url = `manage/library/libraries/${this.id}/stats/`
axios.get(url).then(response => {
self.stats = response.data
self.isLoadingStats = false
})
},
remove () {
const self = this
this.isLoading = true
const url = `manage/library/libraries/${this.id}/`
axios.delete(url).then(response => {
self.$router.push({ name: 'manage.library.libraries' })
})
},
getQuery (field, value) {
return `${field}:"${value}"`
},
updateObj (attr, toNull) {
let newValue = this.object[attr]
if (toNull && !newValue) {
newValue = null
}
const params = {}
params[attr] = newValue
axios.patch(`manage/library/libraries/${this.id}/`, params).then(
response => {
logger.info(
`${attr} was updated succcessfully to ${newValue}`
)
},
error => {
logger.error(
`Error while setting ${attr} to ${newValue}`,
error
)
}
)
}
}
}
</script>

View File

@ -1,3 +1,53 @@
<script setup lang="ts">
import { truncate } from '~/utils/filters'
import { useRouter } from 'vue-router'
import { ref } from 'vue'
import axios from 'axios'
import useErrorHandler from '~/composables/useErrorHandler'
interface Props {
id: number
}
const props = defineProps<Props>()
const router = useRouter()
const isLoading = ref(false)
const object = ref()
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`manage/tags/${props.id}/`)
object.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
fetchData()
const remove = async () => {
isLoading.value = true
try {
await axios.delete(`manage/tags/${props.id}/`)
router.push({ name: 'manage.library.tags' })
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const getQuery = (field: string, value: string) => `${field}:"${value}"`
</script>
<template>
<main>
<div
@ -124,22 +174,9 @@
<translate translate-context="Content/Moderation/Title">
Activity
</translate>&nbsp;
<span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span>
</div>
</h3>
<div
v-if="isLoadingStats"
class="ui placeholder"
>
<div class="full line" />
<div class="short line" />
<div class="medium line" />
<div class="long line" />
</div>
<table
v-else
class="ui very basic table"
>
<table class="ui very basic table">
<tbody>
<tr>
<td>
@ -163,7 +200,6 @@
<translate translate-context="Content/Moderation/Title">
Audio content
</translate>&nbsp;
<span :data-tooltip="labels.statsWarning"><i class="question circle icon" /></span>
</div>
</h3>
<table class="ui very basic table">
@ -213,55 +249,3 @@
</template>
</main>
</template>
<script>
import axios from 'axios'
import { truncate } from '~/utils/filters'
export default {
props: { id: { type: Number, required: true } },
setup () {
return { truncate }
},
data () {
return {
isLoading: true,
isLoadingStats: false,
object: null,
stats: null
}
},
computed: {
labels () {
return {
statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object')
}
}
},
created () {
this.fetchData()
},
methods: {
fetchData () {
const self = this
this.isLoading = true
const url = `manage/tags/${this.id}/`
axios.get(url).then(response => {
self.object = response.data
self.isLoading = false
})
},
remove () {
const self = this
this.isLoading = true
const url = `manage/tags/${this.id}/`
axios.delete(url).then(response => {
self.$router.push({ name: 'manage.library.tags' })
})
},
getQuery (field, value) {
return `${field}:"${value}"`
}
}
}
</script>

View File

@ -1,3 +1,66 @@
<script setup lang="ts">
import type { PrivacyLevel, ImportStatus } from '~/types'
import { humanSize, truncate } from '~/utils/filters'
import { useRouter } from 'vue-router'
import { computed, ref } from 'vue'
import time from '~/utils/time'
import axios from 'axios'
import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
import useSharedLabels from '~/composables/locale/useSharedLabels'
import useErrorHandler from '~/composables/useErrorHandler'
interface Props {
id: string
}
const props = defineProps<Props>()
const sharedLabels = useSharedLabels()
const router = useRouter()
const privacyLevels = computed(() => sharedLabels.fields.privacy_level.shortChoices[object.value.library.privacy_level as PrivacyLevel])
const importStatus = computed(() => sharedLabels.fields.import_status.choices[object.value.import_status as ImportStatus].label)
const isLoading = ref(false)
const object = ref()
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`manage/library/uploads/${props.id}/`)
object.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
fetchData()
const remove = async () => {
isLoading.value = true
try {
await axios.delete(`manage/uploads/${props.id}/`)
router.push({ name: 'manage.library.uploads' })
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const getQuery = (field: string, value: string) => `${field}:"${value}"`
const displayName = (object: any) => object.filename ?? object.source ?? object.uuid
const showUploadDetailModal = ref(false)
</script>
<template>
<main>
<div
@ -156,7 +219,7 @@
</router-link>
</td>
<td>
{{ sharedLabels.fields.privacy_level.shortChoices[object.library.privacy_level] }}
{{ privacyLevels }}
</td>
</tr>
<tr>
@ -192,11 +255,11 @@
</router-link>
</td>
<td>
{{ sharedLabels.fields.import_status.choices[object.import_status].label }}
{{ importStatus }}
<button
class="ui tiny basic icon button"
:title="sharedLabels.fields.import_status.detailTitle"
@click="detailedUpload = object; showUploadDetailModal = true"
@click="showUploadDetailModal = true"
>
<i class="question circle outline icon" />
</button>
@ -380,72 +443,3 @@
</template>
</main>
</template>
<script>
import axios from 'axios'
import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
import time from '~/utils/time'
import { humanSize, truncate } from '~/utils/filters'
import useSharedLabels from '~/composables/locale/useSharedLabels'
export default {
components: {
ImportStatusModal
},
props: { id: { type: Number, required: true } },
setup () {
const sharedLabels = useSharedLabels()
return { sharedLabels, humanSize, time, truncate }
},
data () {
return {
detailedUpload: {},
showUploadDetailModal: false,
isLoading: true,
object: null,
stats: null
}
},
computed: {
labels () {
return {
statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object')
}
}
},
created () {
this.fetchData()
},
methods: {
fetchData () {
const self = this
this.isLoading = true
const url = `manage/library/uploads/${this.id}/`
axios.get(url).then(response => {
self.object = response.data
self.isLoading = false
})
},
remove () {
const self = this
this.isLoading = true
const url = `manage/library/uploads/${this.id}/`
axios.delete(url).then(response => {
self.$router.push({ name: 'manage.library.uploads' })
})
},
getQuery (field, value) {
return `${field}:"${value}"`
},
displayName (upload) {
if (upload.filename) {
return upload.filename
}
if (upload.source) {
return upload.source
}
return upload.uuid
}
}
}
</script>

View File

@ -1,3 +1,148 @@
<script setup lang="ts">
import type { InstancePolicy } from '~/types'
import { computed, ref, reactive, nextTick, watch } from 'vue'
import { useCurrentElement } from '@vueuse/core'
import { humanSize } from '~/utils/filters'
import { useGettext } from 'vue3-gettext'
import axios from 'axios'
import $ from 'jquery'
import InstancePolicyForm from '~/components/manage/moderation/InstancePolicyForm.vue'
import InstancePolicyCard from '~/components/manage/moderation/InstancePolicyCard.vue'
import useErrorHandler from '~/composables/useErrorHandler'
import useLogger from '~/composables/useLogger'
interface Props {
id: number
}
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const logger = useLogger()
const labels = computed(() => ({
statsWarning: $pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object'),
uploadQuota: $pgettext('Content/Moderation/Help text', 'Determine how much content the user can upload. Leave empty to use the default value of the instance.')
}))
const allPermissions = computed(() => [
{ code: 'library', label: $pgettext('*/*/*/Noun', 'Library') },
{ code: 'moderation', label: $pgettext('*/Moderation/*', 'Moderation') },
{ code: 'settings', label: $pgettext('*/*/*/Noun', 'Settings') }
])
const isLoadingPolicy = ref(false)
const policy = ref()
const fetchPolicy = async (id: string) => {
isLoadingPolicy.value = true
try {
const response = await axios.get(`manage/moderation/instance-policies/${id}/`)
policy.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoadingPolicy.value = false
}
const permissions = reactive([] as string[])
const isLoading = ref(false)
const object = ref()
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`manage/accounts/${props.id}/`)
object.value = response.data
if (response.data.instance_policy) {
fetchPolicy(response.data.instance_policy)
}
if (response.data.user) {
for (const { code } of allPermissions.value) {
if (response.data.user.permissions[code]) {
permissions.push(code)
}
}
}
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const isLoadingStats = ref(false)
const stats = ref()
const fetchStats = async () => {
isLoadingStats.value = true
try {
const response = await axios.get(`manage/accounts/${props.id}/stats/`)
stats.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoadingStats.value = true
}
fetchStats()
fetchData()
const el = useCurrentElement()
watch(object, async () => {
await nextTick()
$(el.value).find('select.dropdown').dropdown()
})
const getQuery = (field: string, value: string) => `${field}:"${value}"`
const updating = reactive(new Set<string>())
const updateUser = async (attr: string, toNull = false) => {
let newValue = object.value.user[attr]
if (toNull && !newValue) {
newValue = null
}
updating.add(attr)
const params = {
[attr]: newValue
}
if (attr === 'permissions') {
params.permissions = allPermissions.value.reduce((acc, { code }) => {
acc[code] = permissions.includes(code)
return acc
}, {} as Record<string, boolean>)
}
try {
await axios.patch(`manage/users/users/${object.value.user.id}/`, params)
logger.info(`${attr} was updated succcessfully to ${newValue}`)
} catch (error) {
logger.error(`Error while setting ${attr} to ${newValue}`, error)
// TODO: Use error handler
}
updating.delete(attr)
}
const showPolicyForm = ref(false)
const updatePolicy = (newPolicy: InstancePolicy) => {
policy.value = newPolicy
showPolicyForm.value = false
}
</script>
<template>
<main class="page-admin-account-detail">
<div
@ -207,7 +352,7 @@
</td>
<td>
<div
v-if="object.user.username != $store.state.auth.profile.username"
v-if="object.user.username != $store.state.auth.profile?.username"
class="ui toggle checkbox"
>
<input
@ -262,7 +407,7 @@
{{ p.label }}
</option>
</select>
<action-feedback :is-loading="updating.permissions" />
<action-feedback :is-loading="updating.has('permissions')" />
</td>
</tr>
<tr>
@ -470,7 +615,7 @@
<action-feedback
class="ui basic label"
size="tiny"
:is-loading="updating.upload_quota"
:is-loading="updating.has('upload_quota')"
/>
</div>
</td>
@ -560,155 +705,3 @@
</template>
</main>
</template>
<script>
import axios from 'axios'
import $ from 'jquery'
import InstancePolicyForm from '~/components/manage/moderation/InstancePolicyForm.vue'
import InstancePolicyCard from '~/components/manage/moderation/InstancePolicyCard.vue'
import useLogger from '~/composables/useLogger'
import { humanSize } from '~/utils/filters'
const logger = useLogger()
export default {
components: {
InstancePolicyForm,
InstancePolicyCard
},
props: { id: { type: Number, required: true } },
setup () {
return { humanSize }
},
data () {
return {
isLoading: true,
isLoadingStats: false,
isLoadingPolicy: false,
object: null,
stats: null,
showPolicyForm: false,
permissions: [],
updating: {
permissions: false,
upload_quota: false
}
}
},
computed: {
labels () {
return {
statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this account'),
uploadQuota: this.$pgettext('Content/Moderation/Help text', 'Determine how much content the user can upload. Leave empty to use the default value of the instance.')
}
},
allPermissions () {
return [
{
code: 'library',
label: this.$pgettext('*/*/*/Noun', 'Library')
},
{
code: 'moderation',
label: this.$pgettext('*/Moderation/*', 'Moderation')
},
{
code: 'settings',
label: this.$pgettext('*/*/*/Noun', 'Settings')
}
]
}
},
watch: {
object () {
this.$nextTick(() => {
$(this.$el).find('select.dropdown').dropdown()
})
}
},
created () {
this.fetchData()
this.fetchStats()
},
methods: {
fetchData () {
const self = this
this.isLoading = true
const url = 'manage/accounts/' + this.id + '/'
axios.get(url).then(response => {
self.object = response.data
self.isLoading = false
if (self.object.instance_policy) {
self.fetchPolicy(self.object.instance_policy)
}
if (response.data.user) {
self.allPermissions.forEach(p => {
if (self.object.user.permissions[p.code]) {
self.permissions.push(p.code)
}
})
}
})
},
fetchPolicy (id) {
const self = this
this.isLoadingPolicy = true
const url = `manage/moderation/instance-policies/${id}/`
axios.get(url).then(response => {
self.policy = response.data
self.isLoadingPolicy = false
})
},
fetchStats () {
const self = this
this.isLoadingStats = true
const url = 'manage/accounts/' + this.id + '/stats/'
axios.get(url).then(response => {
self.stats = response.data
self.isLoadingStats = false
})
},
refreshNodeInfo (data) {
this.object.nodeinfo = data
this.object.nodeinfo_fetch_date = new Date()
},
updateUser (attr, toNull) {
let newValue = this.object.user[attr]
if (toNull && !newValue) {
newValue = null
}
const self = this
this.updating[attr] = true
const params = {}
if (attr === 'permissions') {
params.permissions = {}
this.allPermissions.forEach(p => {
params.permissions[p.code] = this.permissions.indexOf(p.code) > -1
})
} else {
params[attr] = newValue
}
axios.patch(`manage/users/users/${this.object.user.id}/`, params).then(
response => {
logger.info(
`${attr} was updated succcessfully to ${newValue}`
)
self.updating[attr] = false
},
error => {
logger.error(
`Error while setting ${attr} to ${newValue}`,
error
)
self.updating[attr] = false
}
)
},
getQuery (field, value) {
return `${field}:"${value}"`
}
}
}
</script>

View File

@ -1,3 +1,26 @@
<script setup lang="ts">
import { useGettext } from 'vue3-gettext'
import { computed, ref } from 'vue'
import { get } from 'lodash-es'
import axios from 'axios'
const { $pgettext } = useGettext()
const allowListEnabled = ref(false)
const labels = computed(() => ({
moderation: $pgettext('*/Moderation/*', 'Moderation'),
secondaryMenu: $pgettext('Menu/*/Hidden text', 'Secondary menu')
}))
const fetchNodeInfo = async () => {
const response = await axios.get('instance/nodeinfo/2.0/')
allowListEnabled.value = get(response.data, 'metadata.allowList.enabled', false)
}
fetchNodeInfo()
</script>
<template>
<div
v-title="labels.moderation"
@ -59,35 +82,3 @@
/>
</div>
</template>
<script>
import { get } from 'lodash-es'
import axios from 'axios'
export default {
data () {
return {
allowListEnabled: false
}
},
computed: {
labels () {
return {
moderation: this.$pgettext('*/Moderation/*', 'Moderation'),
secondaryMenu: this.$pgettext('Menu/*/Hidden text', 'Secondary menu')
}
}
},
created () {
this.fetchNodeInfo()
},
methods: {
fetchNodeInfo () {
const self = this
axios.get('instance/nodeinfo/2.0/').then(response => {
self.allowListEnabled = get(response.data, 'metadata.allowList.enabled', false)
})
}
}
}
</script>

View File

@ -1,3 +1,111 @@
<script setup lang="ts">
import type { InstancePolicy } from '~/types'
import { humanSize } from '~/utils/filters'
import { useGettext } from 'vue3-gettext'
import { computed, ref } from 'vue'
import { get } from 'lodash-es'
import axios from 'axios'
import InstancePolicyForm from '~/components/manage/moderation/InstancePolicyForm.vue'
import InstancePolicyCard from '~/components/manage/moderation/InstancePolicyCard.vue'
import useErrorHandler from '~/composables/useErrorHandler'
interface Props {
id: number
allowListEnabled: boolean
}
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const labels = computed(() => ({
statsWarning: $pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this object')
}))
const isLoadingPolicy = ref(false)
const policy = ref()
const fetchPolicy = async (id: string) => {
isLoadingPolicy.value = true
try {
const response = await axios.get(`manage/moderation/instance-policies/${id}/`)
policy.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoadingPolicy.value = false
}
const isLoading = ref(false)
const object = ref()
const externalUrl = computed(() => `https://${object.value?.name}`)
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`manage/federation/domains/${props.id}/`)
object.value = response.data
if (response.data.instance_policy) {
fetchPolicy(response.data.instance_policy)
}
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = true
}
const isLoadingStats = ref(false)
const stats = ref()
const fetchStats = async () => {
isLoadingStats.value = true
try {
const response = await axios.get(`manage/federation/domains/${props.id}/stats/`)
stats.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoadingStats.value = true
}
fetchStats()
fetchData()
const refreshNodeInfo = (data: any) => {
object.value.nodeinfo = data
object.value.nodeinfo_fetch_date = new Date()
}
const getQuery = (field: string, value: string) => `${field}:"${value}"`
const showPolicyForm = ref(false)
const updatePolicy = (newPolicy: InstancePolicy) => {
policy.value = newPolicy
showPolicyForm.value = false
}
const isLoadingAllowList = ref(false)
const setAllowList = async (value: boolean) => {
isLoadingAllowList.value = true
try {
const response = await axios.patch(`manage/federation/domains/${props.id}/`, { allowed: value })
object.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoadingAllowList.value = false
}
</script>
<template>
<main class="page-admin-domain-detail">
<div
@ -34,7 +142,7 @@
<div class="header-buttons">
<div class="ui icon buttons">
<a
v-if="$store.state.auth.profile.is_superuser"
v-if="$store.state.auth.profile?.is_superuser"
class="ui labeled icon button"
:href="$store.getters['instance/absoluteUrl'](`/api/admin/federation/domain/${object.name}`)"
target="_blank"
@ -455,103 +563,3 @@
</template>
</main>
</template>
<script>
import axios from 'axios'
import { get } from 'lodash-es'
import InstancePolicyForm from '~/components/manage/moderation/InstancePolicyForm.vue'
import InstancePolicyCard from '~/components/manage/moderation/InstancePolicyCard.vue'
import { humanSize } from '~/utils/filters'
export default {
components: {
InstancePolicyForm,
InstancePolicyCard
},
props: { id: { type: String, required: true }, allowListEnabled: { type: Boolean, required: true } },
setup () {
return { humanSize }
},
data () {
return {
get,
isLoading: true,
isLoadingStats: false,
isLoadingPolicy: false,
isLoadingAllowList: false,
policy: null,
object: null,
stats: null,
showPolicyForm: false,
permissions: []
}
},
computed: {
labels () {
return {
statsWarning: this.$pgettext('Content/Moderation/Help text', 'Statistics are computed from known activity and content on your instance, and do not reflect general activity for this domain')
}
},
externalUrl () {
return `https://${this.object.name}`
}
},
created () {
this.fetchData()
this.fetchStats()
},
methods: {
fetchData () {
const self = this
this.isLoading = true
const url = 'manage/federation/domains/' + this.id + '/'
axios.get(url).then(response => {
self.object = response.data
self.isLoading = false
if (self.object.instance_policy) {
self.fetchPolicy(self.object.instance_policy)
}
})
},
fetchStats () {
const self = this
this.isLoadingStats = true
const url = 'manage/federation/domains/' + this.id + '/stats/'
axios.get(url).then(response => {
self.stats = response.data
self.isLoadingStats = false
})
},
fetchPolicy (id) {
const self = this
this.isLoadingPolicy = true
const url = `manage/moderation/instance-policies/${id}/`
axios.get(url).then(response => {
self.policy = response.data
self.isLoadingPolicy = false
})
},
setAllowList (value) {
const self = this
this.isLoadingAllowList = true
const url = `manage/federation/domains/${this.id}/`
axios.patch(url, { allowed: value }).then(response => {
self.object = response.data
self.isLoadingAllowList = false
})
},
refreshNodeInfo (data) {
this.object.nodeinfo = data
this.object.nodeinfo_fetch_date = new Date()
},
updatePolicy (policy) {
this.policy = policy
this.showPolicyForm = false
},
getQuery (field, value) {
return `${field}:"${value}"`
}
}
}
</script>

View File

@ -1,3 +1,51 @@
<script setup lang="ts">
import type { BackendError } from '~/types'
import { useGettext } from 'vue3-gettext'
import { useRouter } from 'vue-router'
import { computed, ref } from 'vue'
import axios from 'axios'
import DomainsTable from '~/components/manage/moderation/DomainsTable.vue'
interface Props {
allowListEnabled: boolean
}
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const router = useRouter()
const labels = computed(() => ({
domains: $pgettext('*/Moderation/*/Noun', 'Domains')
}))
const domainName = ref('')
const domainAllowed = ref(props.allowListEnabled || undefined)
const isCreating = ref(false)
const errors = ref([] as string[])
const createDomain = async () => {
isCreating.value = true
errors.value = []
try {
const response = await axios.post('manage/federation/domains/', { name: domainName.value, allowed: domainAllowed.value })
router.push({
name: 'manage.moderation.domains.detail',
params: { id: response.data.name }
})
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
isCreating.value = false
}
</script>
<template>
<main v-title="labels.domains">
<section class="ui vertical stripe segment">
@ -55,7 +103,7 @@
<button
:class="['ui', {'loading': isCreating}, 'success', 'button']"
type="submit"
:disabled="isCreating || null"
:disabled="isCreating"
>
<translate translate-context="Content/Moderation/Button/Verb">
Add
@ -65,51 +113,10 @@
</div>
</form>
<div class="ui clearing hidden divider" />
<domains-table :allow-list-enabled="allowListEnabled" />
<domains-table
:ordering-config-name="null"
:allow-list-enabled="allowListEnabled"
/>
</section>
</main>
</template>
<script>
import axios from 'axios'
import DomainsTable from '~/components/manage/moderation/DomainsTable.vue'
export default {
components: {
DomainsTable
},
props: { allowListEnabled: { type: Boolean, required: true } },
data () {
return {
domainName: '',
domainAllowed: this.allowListEnabled ? true : null,
isCreating: false,
errors: []
}
},
computed: {
labels () {
return {
domains: this.$pgettext('*/Moderation/*/Noun', 'Domains')
}
}
},
methods: {
createDomain () {
const self = this
this.isCreating = true
this.errors = []
axios.post('manage/federation/domains/', { name: this.domainName, allowed: this.domainAllowed }).then((response) => {
this.isCreating = false
this.$router.push({
name: 'manage.moderation.domains.detail',
params: { id: response.data.name }
})
}, (error) => {
self.isCreating = false
self.errors = error.backendErrors
})
}
}
}
</script>

View File

@ -1,3 +1,36 @@
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import ReportCard from '~/components/manage/moderation/ReportCard.vue'
import useErrorHandler from '~/composables/useErrorHandler'
interface Props {
id: number
}
const props = defineProps<Props>()
const isLoading = ref(false)
const object = ref()
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`manage/moderation/reports/${props.id}/`)
object.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
fetchData()
</script>
<template>
<main>
<div
@ -13,36 +46,3 @@
</template>
</main>
</template>
<script>
import axios from 'axios'
import ReportCard from '~/components/manage/moderation/ReportCard.vue'
export default {
components: {
ReportCard
},
props: { id: { type: Number, required: true } },
data () {
return {
isLoading: true,
object: null
}
},
created () {
this.fetchData()
},
methods: {
fetchData () {
const self = this
this.isLoading = true
const url = `manage/moderation/reports/${this.id}/`
axios.get(url).then(response => {
self.object = response.data
self.isLoading = false
})
}
}
}
</script>

View File

@ -1,3 +1,36 @@
<script setup lang="ts">
import { ref } from 'vue'
import axios from 'axios'
import UserRequestCard from '~/components/manage/moderation/UserRequestCard.vue'
import useErrorHandler from '~/composables/useErrorHandler'
interface Props {
id: number
}
const props = defineProps<Props>()
const isLoading = ref(false)
const object = ref()
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get(`manage/moderation/requests/${props.id}/`)
object.value = response.data
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
fetchData()
</script>
<template>
<main>
<div
@ -13,36 +46,3 @@
</template>
</main>
</template>
<script>
import axios from 'axios'
import UserRequestCard from '~/components/manage/moderation/UserRequestCard.vue'
export default {
components: {
UserRequestCard
},
props: { id: { type: Number, required: true } },
data () {
return {
isLoading: true,
object: null
}
},
created () {
this.fetchData()
},
methods: {
fetchData () {
const self = this
this.isLoading = true
const url = `manage/moderation/requests/${this.id}/`
axios.get(url).then(response => {
self.object = response.data
self.isLoading = false
})
}
}
}
</script>

View File

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

View File

@ -1,3 +1,24 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useStore } from '~/store'
import { onMounted } from 'vue'
interface Props {
state: string
code: string
}
const props = defineProps<Props>()
const router = useRouter()
const store = useStore()
onMounted(async () => {
await store.dispatch('auth/handleOauthCallback', props.code)
router.push(props.state ?? '/library')
})
</script>
<template>
<main class="main pusher">
<section class="ui vertical stripe segment">
@ -16,17 +37,3 @@
</section>
</main>
</template>
<script>
export default {
props: {
state: { type: String, required: true },
code: { type: String, required: true }
},
async mounted () {
await this.$store.dispatch('auth/handleOauthCallback', this.code)
this.$router.push(this.state || '/library')
}
}
</script>

View File

@ -1,3 +1,46 @@
<script setup lang="ts">
import type { BackendError } from '~/types'
import { computed, ref, onMounted } from 'vue'
import { useGettext } from 'vue3-gettext'
import axios from 'axios'
interface Props {
defaultKey: string
}
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const labels = computed(() => ({
confirm: $pgettext('Head/Signup/Title', 'Confirm your e-mail address')
}))
const errors = ref([] as string[])
const key = ref(props.defaultKey)
const isLoading = ref(false)
const success = ref(false)
const submit = async () => {
isLoading.value = true
errors.value = []
try {
await axios.post('auth/registration/verify-email/', { key: key.value })
success.value = true
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
isLoading.value = false
}
onMounted(() => {
if (key.value) submit()
})
</script>
<template>
<main
v-title="labels.confirm"
@ -76,51 +119,3 @@
</section>
</main>
</template>
<script>
import axios from 'axios'
export default {
props: { defaultKey: { type: String, required: true } },
data () {
return {
isLoading: false,
errors: [],
key: this.defaultKey,
success: false
}
},
computed: {
labels () {
return {
confirm: this.$pgettext('Head/Signup/Title', 'Confirm your e-mail address')
}
}
},
mounted () {
if (this.key) {
this.submit()
}
},
methods: {
submit () {
const self = this
self.isLoading = true
self.errors = []
const payload = {
key: this.key
}
return axios.post('auth/registration/verify-email/', payload).then(
response => {
self.isLoading = false
self.success = true
},
error => {
self.errors = error.backendErrors
self.isLoading = false
}
)
}
}
}
</script>

View File

@ -1,3 +1,48 @@
<script setup lang="ts">
import type { BackendError } from '~/types'
import { computed, ref, onMounted } from 'vue'
import { useGettext } from 'vue3-gettext'
import { useRouter } from 'vue-router'
import axios from 'axios'
interface Props {
defaultEmail: string
}
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const router = useRouter()
const labels = computed(() => ({
placeholder: $pgettext('Content/Signup/Input.Placeholder', 'Enter the e-mail address linked to your account'),
reset: $pgettext('*/Login/*/Verb', 'Reset your password')
}))
const email = ref(props.defaultEmail)
const errors = ref([] as string[])
const isLoading = ref(false)
const submit = async () => {
isLoading.value = true
errors.value = []
try {
await axios.post('auth/password/reset/', { email: email.value })
router.push({ name: 'auth.password-reset-confirm' })
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
isLoading.value = false
}
const emailInput = ref()
onMounted(() => emailInput.value.focus())
</script>
<template>
<main
v-title="labels.reset"
@ -42,7 +87,7 @@
<label for="account-email"><translate translate-context="Content/Signup/Input.Label">Account's e-mail address</translate></label>
<input
id="account-email"
ref="email"
ref="emailInput"
v-model="email"
required
type="email"
@ -69,54 +114,3 @@
</section>
</main>
</template>
<script>
import axios from 'axios'
export default {
props: { defaultEmail: { type: String, required: true } },
data () {
return {
email: this.defaultEmail,
isLoading: false,
errors: []
}
},
computed: {
labels () {
const reset = this.$pgettext('*/Login/*/Verb', 'Reset your password')
const placeholder = this.$pgettext('Content/Signup/Input.Placeholder', 'Enter the e-mail address linked to your account'
)
return {
reset,
placeholder
}
}
},
mounted () {
this.$refs.email.focus()
},
methods: {
submit () {
const self = this
self.isLoading = true
self.errors = []
const payload = {
email: this.email
}
return axios.post('auth/password/reset/', payload).then(
response => {
self.isLoading = false
self.$router.push({
name: 'auth.password-reset-confirm'
})
},
error => {
self.errors = error.backendErrors
self.isLoading = false
}
)
}
}
}
</script>

View File

@ -1,3 +1,54 @@
<script setup lang="ts">
import type { BackendError } from '~/types'
import { useGettext } from 'vue3-gettext'
import { computed, ref } from 'vue'
import axios from 'axios'
import PasswordInput from '~/components/forms/PasswordInput.vue'
interface Props {
defaultToken: string
defaultUid: string
}
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const labels = computed(() => ({
changePassword: $pgettext('*/Signup/Title', 'Change your password')
}))
const newPassword = ref('')
const token = ref(props.defaultToken)
const uid = ref(props.defaultUid)
const errors = ref([] as string[])
const isLoading = ref(false)
const success = ref(false)
const submit = async () => {
isLoading.value = true
errors.value = []
try {
await axios.post('auth/password/reset/confirm/', {
uid: uid.value,
token: token.value,
new_password1: newPassword.value,
new_password2: newPassword.value
})
success.value = true
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
isLoading.value = false
}
</script>
<template>
<main
v-title="labels.changePassword"
@ -84,58 +135,3 @@
</section>
</main>
</template>
<script>
import axios from 'axios'
import PasswordInput from '~/components/forms/PasswordInput.vue'
export default {
components: {
PasswordInput
},
props: {
defaultToken: { type: String, required: true },
defaultUid: { type: String, required: true }
},
data () {
return {
newPassword: '',
isLoading: false,
errors: [],
token: this.defaultToken,
uid: this.defaultUid,
success: false
}
},
computed: {
labels () {
return {
changePassword: this.$pgettext('*/Signup/Title', 'Change your password')
}
}
},
methods: {
submit () {
const self = this
self.isLoading = true
self.errors = []
const payload = {
uid: this.uid,
token: this.token,
new_password1: this.newPassword,
new_password2: this.newPassword
}
return axios.post('auth/password/reset/confirm/', payload).then(
response => {
self.isLoading = false
self.success = true
},
error => {
self.errors = error.backendErrors
self.isLoading = false
}
)
}
}
}
</script>

View File

@ -1,3 +1,43 @@
<script setup lang="ts">
import { useGettext } from 'vue3-gettext'
import { computed, ref } from 'vue'
import axios from 'axios'
import PluginForm from '~/components/auth/Plugin.vue'
import useErrorHandler from '~/composables/useErrorHandler'
const { $pgettext } = useGettext()
const labels = computed(() => ({
title: $pgettext('Head/Login/Title', 'Manage plugins')
}))
const isLoading = ref(false)
const plugins = ref()
const libraries = ref()
const fetchData = async () => {
isLoading.value = true
try {
const [pluginsResponse, librariesResponse] = await Promise.all([
axios.get('plugins'),
axios.get('libraries', { params: { scope: 'me', page_size: 50 } })
])
plugins.value = pluginsResponse.data
libraries.value = librariesResponse.data.results
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
fetchData()
</script>
<template>
<main
v-title="labels.title"
@ -26,42 +66,3 @@
</section>
</main>
</template>
<script>
import axios from 'axios'
import PluginForm from '~/components/auth/Plugin.vue'
export default {
components: {
PluginForm
},
data () {
return {
isLoading: true,
plugins: null,
libraries: null
}
},
computed: {
labels () {
const title = this.$pgettext('Head/Login/Title', 'Manage plugins')
return {
title
}
}
},
async created () {
await this.fetchData()
},
methods: {
async fetchData () {
this.isLoading = true
let response = await axios.get('plugins')
this.plugins = response.data
response = await axios.get('libraries', { paramis: { scope: 'me', page_size: 50 } })
this.libraries = response.data.results
this.isLoading = false
}
}
}
</script>

View File

@ -1,3 +1,21 @@
<script setup lang="ts">
import type { Actor } from '~/types'
import { ref } from 'vue'
import PlaylistWidget from '~/components/playlists/Widget.vue'
import TrackWidget from '~/components/audio/track/Widget.vue'
import RadioButton from '~/components/radios/Button.vue'
interface Props {
object: Actor
}
defineProps<Props>()
const recentActivity = ref(0)
</script>
<template>
<section>
<div>
@ -48,19 +66,3 @@
</div>
</section>
</template>
<script>
import TrackWidget from '~/components/audio/track/Widget.vue'
import PlaylistWidget from '~/components/playlists/Widget.vue'
import RadioButton from '~/components/radios/Button.vue'
export default {
components: { TrackWidget, PlaylistWidget, RadioButton },
props: { object: { type: Object, required: true } },
data () {
return {
recentActivity: 0
}
}
}
</script>

View File

@ -1,3 +1,28 @@
<script setup lang="ts">
import type { RouteLocationRaw } from 'vue-router'
import { useGettext } from 'vue3-gettext'
import { computed } from 'vue'
import SignupForm from '~/components/auth/SignupForm.vue'
interface Props {
defaultInvitation?: string
next?: RouteLocationRaw
}
withDefaults(defineProps<Props>(), {
defaultInvitation: undefined,
next: '/'
})
const { $pgettext } = useGettext()
const labels = computed(() => ({
title: $pgettext('*/Signup/Title', 'Sign Up')
}))
</script>
<template>
<main
v-title="labels.title"
@ -18,37 +43,3 @@
</section>
</main>
</template>
<script>
import SignupForm from '~/components/auth/SignupForm.vue'
export default {
components: {
SignupForm
},
props: {
defaultInvitation: { type: String, required: false, default: null },
next: { type: String, default: '/' }
},
data () {
return {
username: '',
email: '',
password: '',
isLoadingInstanceSetting: true,
errors: [],
isLoading: false,
invitation: this.defaultInvitation
}
},
computed: {
labels () {
const title = this.$pgettext('*/Signup/Title', 'Sign Up')
return {
title
}
}
}
}
</script>

View File

@ -1,21 +1,22 @@
<script setup lang="ts">
import type { Channel } from '~/types'
import ChannelEntries from '~/components/audio/ChannelEntries.vue'
interface Props {
object: Channel
}
defineProps<Props>()
</script>
<template>
<section>
<channel-entries
:default-cover="object.artist.cover"
:is-podcast="object.artist.content_category === 'podcast'"
:default-cover="object.artist?.cover"
:is-podcast="object.artist?.content_category === 'podcast'"
:limit="25"
:filters="{channel: object.uuid, ordering: 'creation_date'}"
/>
</section>
</template>
<script>
import ChannelEntries from '~/components/audio/ChannelEntries.vue'
export default {
components: {
ChannelEntries
},
props: { object: { type: Object, required: true } }
}
</script>

View File

@ -1,3 +1,94 @@
<script setup lang="ts">
import type { Channel, Upload } from '~/types'
import { computed, ref, reactive, watch } from 'vue'
import { whenever } from '@vueuse/core'
import { useStore } from '~/store'
import axios from 'axios'
import qs from 'qs'
import ChannelEntries from '~/components/audio/ChannelEntries.vue'
import ChannelSeries from '~/components/audio/ChannelSeries.vue'
import AlbumModal from '~/components/channels/AlbumModal.vue'
import useWebSocketHandler from '~/composables/useWebSocketHandler'
interface Props {
object: Channel
}
const props = defineProps<Props>()
const store = useStore()
const isPodcast = computed(() => props.object.artist?.content_category === 'podcast')
const isOwner = computed(() => store.state.auth.authenticated && props.object.attributed_to.full_username === store.state.auth.fullUsername)
const seriesFilters = computed(() => ({
artist: props.object.artist?.id,
ordering: '-creation_date',
playable: isOwner.value
? undefined
: true
}))
const pendingUploads = reactive([] as Upload[])
const processedUploads = computed(() => pendingUploads.filter(upload => upload.import_status !== 'pending'))
const finishedUploads = computed(() => pendingUploads.filter(upload => upload.import_status === 'finished'))
const erroredUploads = computed(() => pendingUploads.filter(upload => upload.import_status === 'errored'))
const skippedUploads = computed(() => pendingUploads.filter(upload => upload.import_status === 'skipped'))
const pendingUploadsById = computed(() => pendingUploads.reduce((acc, upload) => {
acc[upload.uuid] = upload
return acc
}, {} as Record<string, Upload>))
const isOver = computed(() => pendingUploads.length === processedUploads.value.length)
const isSuccessfull = computed(() => pendingUploads.length === finishedUploads.value.length)
watch(() => store.state.channels.latestPublication, (value) => {
if (value?.channel.uuid === props.object.uuid && value.uploads.length > 0) {
pendingUploads.push(...value.uploads)
}
})
const episodesKey = ref(new Date())
const seriesKey = ref(new Date())
whenever(isOver, () => {
episodesKey.value = new Date()
seriesKey.value = new Date()
})
const fetchPendingUploads = async () => {
try {
const response = await axios.get('uploads/', {
params: { channel: props.object.uuid, import_status: ['pending', 'skipped', 'errored'], include_channels: 'true' },
paramsSerializer: function (params) {
return qs.stringify(params, { indices: false })
}
})
pendingUploads.length = 0
pendingUploads.push(...response.data.results)
} catch (error) {
}
}
if (isOwner.value) {
fetchPendingUploads()
.then(() => {
useWebSocketHandler('import.status_updated', (event) => {
if (!pendingUploadsById.value[event.upload.uuid]) return
Object.assign(pendingUploadsById.value[event.upload.uuid], event.upload)
})
})
}
const albumModal = ref()
</script>
<template>
<section>
<div
@ -68,7 +159,7 @@
</div>
<div v-if="$store.getters['ui/layoutVersion'] === 'small'">
<rendered-description
:content="object.artist.description"
:content="object.artist?.description"
:update-url="`channels/${object.uuid}/`"
:can-update="false"
/>
@ -77,7 +168,7 @@
<channel-entries
:key="String(episodesKey) + 'entries'"
:is-podcast="isPodcast"
:default-cover="object.artist.cover"
:default-cover="object.artist?.cover"
:limit="25"
:filters="{channel: object.uuid, ordering: '-creation_date', page_size: '25'}"
>
@ -119,7 +210,7 @@
v-if="isOwner"
class="actions"
>
<a @click.stop.prevent="$refs.albumModal.show = true">
<a @click.stop.prevent="albumModal.show = true">
<i class="plus icon" />
<translate translate-context="Content/Profile/Button">Add new</translate>
</a>
@ -130,126 +221,7 @@
v-if="isOwner"
ref="albumModal"
:channel="object"
@created="$refs.albumModal.show = false; seriesKey = new Date()"
@created="albumModal.show = false; seriesKey = new Date()"
/>
</section>
</template>
<script>
import axios from 'axios'
import qs from 'qs'
import ChannelEntries from '~/components/audio/ChannelEntries.vue'
import ChannelSeries from '~/components/audio/ChannelSeries.vue'
import AlbumModal from '~/components/channels/AlbumModal.vue'
export default {
components: {
ChannelEntries,
ChannelSeries,
AlbumModal
},
props: { object: { type: Object, required: true } },
data () {
return {
seriesKey: new Date(),
episodesKey: new Date(),
pendingUploads: []
}
},
computed: {
isPodcast () {
return this.object.artist.content_category === 'podcast'
},
isOwner () {
return this.$store.state.auth.authenticated && this.object.attributed_to.full_username === this.$store.state.auth.fullUsername
},
seriesFilters () {
const filters = { artist: this.object.artist.id, ordering: '-creation_date' }
if (!this.isOwner) {
filters.playable = 'true'
}
return filters
},
processedUploads () {
return this.pendingUploads.filter((u) => {
return u.import_status !== 'pending'
})
},
erroredUploads () {
return this.pendingUploads.filter((u) => {
return u.import_status === 'errored'
})
},
skippedUploads () {
return this.pendingUploads.filter((u) => {
return u.import_status === 'skipped'
})
},
finishedUploads () {
return this.pendingUploads.filter((u) => {
return u.import_status === 'finished'
})
},
pendingUploadsById () {
const d = {}
this.pendingUploads.forEach((u) => {
d[u.uuid] = u
})
return d
},
isOver () {
return this.pendingUploads && this.processedUploads.length === this.pendingUploads.length
},
isSuccessfull () {
return this.pendingUploads && this.finishedUploads.length === this.pendingUploads.length
}
},
watch: {
'$store.state.channels.latestPublication' (v) {
if (v && v.uploads && v.channel.uuid === this.object.uuid) {
this.pendingUploads = [...this.pendingUploads, ...v.uploads]
}
},
'isOver' (v) {
if (v) {
this.seriesKey = new Date()
this.episodesKey = new Date()
}
}
},
async created () {
if (this.isOwner) {
await this.fetchPendingUploads()
this.$store.commit('ui/addWebsocketEventHandler', {
eventName: 'import.status_updated',
id: 'fileUploadChannel',
handler: this.handleImportEvent
})
}
},
unmounted () {
this.$store.commit('ui/removeWebsocketEventHandler', {
eventName: 'import.status_updated',
id: 'fileUploadChannel'
})
},
methods: {
handleImportEvent (event) {
if (!this.pendingUploadsById[event.upload.uuid]) {
return
}
Object.assign(this.pendingUploadsById[event.upload.uuid], event.upload)
},
async fetchPendingUploads () {
const response = await axios.get('uploads/', {
params: { channel: this.object.uuid, import_status: ['pending', 'skipped', 'errored'], include_channels: 'true' },
paramsSerializer: function (params) {
return qs.stringify(params, { indices: false })
}
})
this.pendingUploads = response.data.results
}
}
}
</script>

View File

@ -1,3 +1,15 @@
<script setup lang="ts">
import { useGettext } from 'vue3-gettext'
import { computed } from 'vue'
const { $pgettext } = useGettext()
const labels = computed(() => ({
secondaryMenu: $pgettext('Menu/*/Hidden text', 'Secondary menu'),
title: $pgettext('*/Library/*/Verb', 'Add content')
}))
</script>
<template>
<main
v-title="labels.title"
@ -28,17 +40,3 @@
<router-view :key="$route.fullPath" />
</main>
</template>
<script>
export default {
computed: {
labels () {
const title = this.$pgettext('*/Library/*/Verb', 'Add content')
const secondaryMenu = this.$pgettext('Menu/*/Hidden text', 'Secondary menu')
return {
title,
secondaryMenu
}
}
}
}
</script>

View File

@ -1,3 +1,27 @@
<script setup lang="ts">
import type { Library, PrivacyLevel } from '~/types'
import { humanSize } from '~/utils/filters'
import { useGettext } from 'vue3-gettext'
import { computed } from 'vue'
import useSharedLabels from '~/composables/locale/useSharedLabels'
interface Props {
library: Library
}
defineProps<Props>()
const { $pgettext } = useGettext()
const sharedLabels = useSharedLabels()
const sizeLabel = computed(() => $pgettext('Content/Library/Card.Help text', 'Total size of the files in this library'))
const privacyTooltips = (level: PrivacyLevel) => `Visibility: ${sharedLabels.fields.privacy_level.choices[level].toLowerCase()}`
</script>
<template>
<div class="ui card">
<div class="content">
@ -6,21 +30,21 @@
<span
v-if="library.privacy_level === 'me'"
class="right floated"
:data-tooltip="privacy_tooltips('me')"
:data-tooltip="privacyTooltips('me')"
>
<i class="small lock icon" />
</span>
<span
v-else-if="library.privacy_level === 'instance'"
class="right floated"
:data-tooltip="privacy_tooltips('instance')"
:data-tooltip="privacyTooltips('instance')"
>
<i class="small circle outline icon" />
</span>
<span
v-else-if="library.privacy_level === 'everyone'"
class="right floated"
:data-tooltip="privacy_tooltips('everyone')"
:data-tooltip="privacyTooltips('everyone')"
>
<i class="small globe icon" />
</span>
@ -39,7 +63,7 @@
<span
v-if="library.size"
class="right floated"
:data-tooltip="size_label"
:data-tooltip="sizeLabel"
>
<i class="database icon" />
{{ humanSize(library.size) }}
@ -75,26 +99,3 @@
</div>
</div>
</template>
<script>
import { humanSize } from '~/utils/filters'
import useSharedLabels from '~/composables/locale/useSharedLabels'
export default {
props: { library: { type: Object, required: true } },
setup () {
const sharedLabels = useSharedLabels()
return { sharedLabels, humanSize }
},
computed: {
size_label () {
return this.$pgettext('Content/Library/Card.Help text', 'Total size of the files in this library')
}
},
methods: {
privacy_tooltips (level) {
return 'Visibility: ' + this.sharedLabels.fields.privacy_level.choices[level].toLowerCase()
}
}
}
</script>

View File

@ -1,16 +1,18 @@
<template>
<section class="ui vertical aligned stripe segment">
<library-files-table :default-query="query" />
</section>
</template>
<script>
<script setup lang="ts">
import LibraryFilesTable from './FilesTable.vue'
export default {
components: {
LibraryFilesTable
},
props: { query: { type: String, required: true } }
interface Props {
query: string
}
defineProps<Props>()
</script>
<template>
<section class="ui vertical aligned stripe segment">
<library-files-table
:ordering-config-name="null"
:default-query="query"
/>
</section>
</template>

View File

@ -1,3 +1,93 @@
<script setup lang="ts">
import type { Library, BackendError, PrivacyLevel } from '~/types'
import { useGettext } from 'vue3-gettext'
import { computed, ref } from 'vue'
import { useStore } from '~/store'
import axios from 'axios'
import useSharedLabels from '~/composables/locale/useSharedLabels'
const PRIVACY_LEVELS = ['me', 'instance', 'everyone'] as PrivacyLevel[]
interface Emits {
(e: 'updated', data: Library): void
(e: 'created', data: Library): void
(e: 'deleted'): void
}
interface Props {
library?: Library
}
const emit = defineEmits<Emits>()
const props = defineProps<Props>()
const { $pgettext } = useGettext()
const sharedLabels = useSharedLabels()
const store = useStore()
const labels = computed(() => ({
descriptionPlaceholder: $pgettext('Content/Library/Input.Placeholder', 'This library contains my personal music, I hope you like it.'),
namePlaceholder: $pgettext('Content/Library/Input.Placeholder', 'My awesome library')
}))
const currentVisibilityLevel = ref(props.library?.privacy_level ?? 'me')
const currentDescription = ref(props.library?.description ?? '')
const currentName = ref(props.library?.name ?? '')
const errors = ref([] as string[])
const isLoading = ref(false)
const submit = async () => {
isLoading.value = true
try {
const payload = {
name: currentName.value,
description: currentDescription.value,
privacy_level: currentVisibilityLevel.value
}
const response = props.library
? await axios.patch(`libraries/${props.library.uuid}/`, payload)
: await axios.post('libraries/', payload)
if (props.library) emit('updated', response.data)
else emit('created', response.data)
store.commit('ui/addMessage', {
content: props.library
? $pgettext('Content/Library/Message', 'Library updated')
: $pgettext('Content/Library/Message', 'Library created'),
date: new Date()
})
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
isLoading.value = false
}
const remove = async () => {
isLoading.value = true
try {
await axios.delete(`libraries/${props.library?.uuid}/`)
emit('deleted')
store.commit('ui/addMessage', {
content: $pgettext('Content/Library/Message', 'Library deleted'),
date: new Date()
})
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
isLoading.value = false
}
</script>
<template>
<form
class="ui form"
@ -60,8 +150,8 @@
class="ui dropdown"
>
<option
v-for="(c, key) in ['me', 'instance', 'everyone']"
:key="key"
v-for="c in PRIVACY_LEVELS"
:key="c"
:value="c"
>
{{ sharedLabels.fields.privacy_level.choices[c] }}
@ -89,7 +179,7 @@
v-if="library"
type="button"
class="ui right floated basic danger button"
@confirm="remove()"
@confirm="remove"
>
<translate translate-context="*/*/*/Verb">
Delete
@ -118,98 +208,3 @@
</dangerous-button>
</form>
</template>
<script>
import axios from 'axios'
import useSharedLabels from '~/composables/locale/useSharedLabels'
export default {
props: { library: { type: Object, default: null } },
setup () {
const sharedLabels = useSharedLabels()
return { sharedLabels }
},
data () {
const d = {
isLoading: false,
over: false,
errors: []
}
if (this.library) {
d.currentVisibilityLevel = this.library.privacy_level
d.currentName = this.library.name
d.currentDescription = this.library.description
} else {
d.currentVisibilityLevel = 'me'
d.currentName = ''
d.currentDescription = ''
}
return d
},
computed: {
labels () {
const namePlaceholder = this.$pgettext('Content/Library/Input.Placeholder', 'My awesome library')
const descriptionPlaceholder = this.$pgettext('Content/Library/Input.Placeholder', 'This library contains my personal music, I hope you like it.')
return {
namePlaceholder,
descriptionPlaceholder
}
}
},
methods: {
submit () {
const self = this
this.isLoading = true
const payload = {
name: this.currentName,
description: this.currentDescription,
privacy_level: this.currentVisibilityLevel
}
let promise
if (this.library) {
promise = axios.patch(`libraries/${this.library.uuid}/`, payload)
} else {
promise = axios.post('libraries/', payload)
}
promise.then((response) => {
self.isLoading = false
let msg
if (self.library) {
self.$emit('updated', response.data)
msg = this.$pgettext('Content/Library/Message', 'Library updated')
} else {
self.$emit('created', response.data)
msg = this.$pgettext('Content/Library/Message', 'Library created')
}
self.$store.commit('ui/addMessage', {
content: msg,
date: new Date()
})
}, error => {
self.isLoading = false
self.errors = error.backendErrors
})
},
reset () {
this.currentVisibilityLevel = 'me'
this.currentName = ''
this.currentDescription = ''
},
remove () {
const self = this
axios.delete(`libraries/${this.library.uuid}/`).then((response) => {
self.isLoading = false
const msg = this.$pgettext('Content/Library/Message', 'Library deleted')
self.$emit('deleted', {})
self.$store.commit('ui/addMessage', {
content: msg,
date: new Date()
})
}, error => {
self.isLoading = false
self.errors = error.backendErrors
})
}
}
}
</script>

View File

@ -1,3 +1,44 @@
<script setup lang="ts">
import type { Library } from '~/types'
import { useRouter } from 'vue-router'
import { ref } from 'vue'
import axios from 'axios'
import LibraryForm from './Form.vue'
import LibraryCard from './Card.vue'
import Quota from './Quota.vue'
import useErrorHandler from '~/composables/useErrorHandler'
const router = useRouter()
const libraries = ref([] as Library[])
const isLoading = ref(false)
const hiddenForm = ref(true)
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get('libraries/', { params: { scope: 'me' } })
libraries.value = response.data.results
if (libraries.value.length === 0) {
hiddenForm.value = false
}
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
fetchData()
const libraryCreated = (library: Library) => {
router.push({ name: 'library.detail', params: { id: library.uuid } })
}
</script>
<template>
<section class="ui vertical aligned stripe segment">
<div
@ -63,44 +104,3 @@
</div>
</section>
</template>
<script>
import axios from 'axios'
import LibraryForm from './Form.vue'
import LibraryCard from './Card.vue'
import Quota from './Quota.vue'
export default {
components: {
LibraryForm,
LibraryCard,
Quota
},
data () {
return {
isLoading: false,
hiddenForm: true,
libraries: []
}
},
created () {
this.fetch()
},
methods: {
fetch () {
this.isLoading = true
const self = this
axios.get('libraries/', { params: { scope: 'me' } }).then(response => {
self.isLoading = false
self.libraries = response.data.results
if (self.libraries.length === 0) {
self.hiddenForm = false
}
})
},
libraryCreated (library) {
this.$router.push({ name: 'library.detail', params: { id: library.uuid } })
}
}
}
</script>

View File

@ -1,3 +1,55 @@
<script setup lang="ts">
import type { ImportStatus } from '~/types'
import { compileTokens } from '~/utils/search'
import { humanSize } from '~/utils/filters'
import { computed, ref } from 'vue'
import useErrorHandler from '~/composables/useErrorHandler'
import axios from 'axios'
const quotaStatus = ref()
const progress = computed(() => !quotaStatus.value
? 0
: Math.min(quotaStatus.value.current * 100 / quotaStatus.value.max, 100)
)
const isLoading = ref(false)
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get('users/me/')
quotaStatus.value = response.data.quota_status
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
fetchData()
const purge = async (status: ImportStatus) => {
try {
await axios.post('uploads/action/', {
action: 'delete',
objects: 'all',
filters: { import_status: status }
})
fetchData()
} catch (error) {
useErrorHandler(error as Error)
}
}
const purgeSkippedFiles = () => purge('skipped')
const purgePendingFiles = () => purge('pending')
const purgeErroredFiles = () => purge('errored')
</script>
<template>
<div class="ui segment">
<h3 class="ui header">
@ -210,62 +262,3 @@
</div>
</div>
</template>
<script>
import axios from 'axios'
import { humanSize } from '~/utils/filters'
import { compileTokens } from '~/utils/search'
export default {
data () {
return {
quotaStatus: null,
isLoading: false,
humanSize,
compileTokens
}
},
computed: {
progress () {
if (!this.quotaStatus) {
return 0
}
return Math.min(parseInt(this.quotaStatus.current * 100 / this.quotaStatus.max), 100)
}
},
created () {
this.fetch()
},
methods: {
fetch () {
const self = this
self.isLoading = true
axios.get('users/me/').then((response) => {
self.quotaStatus = response.data.quota_status
self.isLoading = false
})
},
purge (status) {
const self = this
const payload = {
action: 'delete',
objects: 'all',
filters: {
import_status: status
}
}
axios.post('uploads/action/', payload).then((response) => {
self.fetch()
})
},
purgeSkippedFiles () {
this.purge('skipped')
},
purgePendingFiles () {
this.purge('pending')
},
purgeErroredFiles () {
this.purge('errored')
}
}
}
</script>

View File

@ -1,3 +1,45 @@
<script setup lang="ts">
import type { Library, LibraryFollow } from '~/types'
import { ref } from 'vue'
import axios from 'axios'
import ScanForm from './ScanForm.vue'
import LibraryCard from './Card.vue'
import useErrorHandler from '~/composables/useErrorHandler'
const existingFollows = ref()
const isLoading = ref(false)
const fetchData = async () => {
isLoading.value = true
try {
const response = await axios.get('federation/follows/library/', { params: { page_size: 100, ordering: '-creation_date' } })
existingFollows.value = response.data
for (const follow of existingFollows.value.results) {
follow.target.follow = follow
}
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
fetchData()
const getLibraryFromFollow = (follow: LibraryFollow) => {
const { target } = follow
target.follow = follow
return target as Library
}
const scanResult = ref()
</script>
<template>
<div class="ui vertical aligned stripe segment">
<div
@ -45,7 +87,7 @@
<a
href=""
class="discrete link"
@click.prevent="fetch()"
@click.prevent="fetchData"
>
<i :class="['ui', 'circular', 'refresh', 'icon']" /> <translate translate-context="Content/*/Button.Label/Short, Verb">Refresh</translate>
</a>
@ -55,56 +97,11 @@
v-for="follow in existingFollows.results"
:key="follow.fid"
:initial-library="getLibraryFromFollow(follow)"
@deleted="fetch()"
@followed="fetch()"
@deleted="fetchData"
@followed="fetchData"
/>
</div>
</template>
</div>
</div>
</template>
<script>
import axios from 'axios'
import ScanForm from './ScanForm.vue'
import LibraryCard from './Card.vue'
export default {
components: {
ScanForm,
LibraryCard
},
data () {
return {
isLoading: false,
scanResult: null,
existingFollows: null,
errors: []
}
},
created () {
this.fetch()
},
methods: {
fetch () {
this.isLoading = true
const self = this
axios.get('federation/follows/library/', { params: { page_size: 100, ordering: '-creation_date' } }).then((response) => {
self.existingFollows = response.data
self.existingFollows.results.forEach(f => {
f.target.follow = f
})
self.isLoading = false
}, error => {
self.isLoading = false
self.errors.push(error)
})
},
getLibraryFromFollow (follow) {
const d = follow.target
d.follow = follow
return d
}
}
}
</script>

View File

@ -1,3 +1,43 @@
<script setup lang="ts">
import type { BackendError } from '~/types'
import { useGettext } from 'vue3-gettext'
import { computed, ref } from 'vue'
import axios from 'axios'
interface Emits {
(e: 'scanned', data: object): void
}
const emit = defineEmits<Emits>()
const { $pgettext } = useGettext()
const labels = computed(() => ({
placeholder: $pgettext('Content/Library/Input.Placeholder', 'Enter a library URL'),
submitLibrarySearch: $pgettext('Content/Library/Input.Label', 'Submit search')
}))
const errors = ref([] as string[])
const isLoading = ref(false)
const query = ref('')
const scan = async () => {
if (!query.value) return
isLoading.value = true
errors.value = []
try {
const response = await axios.post('federation/libraries/fetch/', { fid: query.value })
emit('scanned', response.data)
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
isLoading.value = false
}
</script>
<template>
<form
class="ui form"
@ -43,41 +83,3 @@
</div>
</form>
</template>
<script>
import axios from 'axios'
export default {
data () {
return {
query: '',
isLoading: false,
errors: []
}
},
computed: {
labels () {
return {
placeholder: this.$pgettext('Content/Library/Input.Placeholder', 'Enter a library URL'),
submitLibrarySearch: this.$pgettext('Content/Library/Input.Label', 'Submit search')
}
}
},
methods: {
scan () {
if (!this.query) {
return
}
const self = this
self.errors = []
self.isLoading = true
axios.post('federation/libraries/fetch/', { fid: this.query }).then((response) => {
self.$emit('scanned', response.data)
self.isLoading = false
}, error => {
self.isLoading = false
self.errors = error.backendErrors
})
}
}
}
</script>

View File

@ -1,3 +1,16 @@
<script setup lang="ts">
import type { Library } from '~/types'
import AlbumWidget from '~/components/audio/album/Widget.vue'
interface Props {
object: Library
isOwner: boolean
}
defineProps<Props>()
</script>
<template>
<section>
<album-widget
@ -28,17 +41,3 @@
</album-widget>
</section>
</template>
<script>
import AlbumWidget from '~/components/audio/album/Widget.vue'
export default {
components: {
AlbumWidget
},
props: {
object: { type: Object, required: true },
isOwner: { type: Boolean, required: true }
}
}
</script>

View File

@ -1,3 +1,16 @@
<script setup lang="ts">
import type { Library } from '~/types'
import ArtistWidget from '~/components/audio/artist/Widget.vue'
interface Props {
object: Library
isOwner: boolean
}
defineProps<Props>()
</script>
<template>
<section>
<template v-if="$store.getters['ui/layoutVersion'] === 'small'">
@ -37,22 +50,3 @@
</artist-widget>
</section>
</template>
<script>
import ArtistWidget from '~/components/audio/artist/Widget.vue'
export default {
components: {
ArtistWidget
},
props: {
object: { type: Object, required: true },
isOwner: { type: Boolean, required: true }
},
data () {
return {
query: ''
}
}
}
</script>

View File

@ -1,3 +1,35 @@
<script setup lang="ts">
import type { Library } from '~/types'
import { onBeforeRouteLeave } from 'vue-router'
import { ref } from 'vue'
import FileUpload from '~/components/library/FileUpload.vue'
interface Props {
object: Library
defaultImportReference?: string
}
withDefaults(defineProps<Props>(), {
defaultImportReference: ''
})
const fileupload = ref()
onBeforeRouteLeave((to, from, next) => {
if (!fileupload.value.hasActiveUploads) {
return next()
}
const answer = window.confirm('This page is asking you to confirm that you want to leave - data you have entered may not be saved.')
if (answer) {
next()
} else {
next(false)
}
})
</script>
<template>
<section>
<file-upload
@ -8,31 +40,3 @@
/>
</section>
</template>
<script>
import FileUpload from '~/components/library/FileUpload.vue'
export default {
components: {
FileUpload
},
beforeRouteLeave (to, from, next) {
if (this.$refs.fileupload.hasActiveUploads) {
const answer = window.confirm('This page is asking you to confirm that you want to leave - data you have entered may not be saved.')
if (answer) {
next()
} else {
next(false)
}
} else {
next()
}
},
props: {
object: { type: Object, required: true },
defaultImportReference: { type: String, default: '' }
}
}
</script>

View File

@ -6642,6 +6642,11 @@ util-deprecate@^1.0.2:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
utility-types@^3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"