Migrate rest of the components
This commit is contained in:
parent
6431d0285c
commit
74d1a0a03e
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }'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 @@
|
|||
|
||||
<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 }'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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue