mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-13 15:34:50 +02:00
Add:Podcast create form #225
This commit is contained in:
parent
705bbaccc1
commit
379fa21571
8 changed files with 582 additions and 12 deletions
182
components/forms/NewPodcastForm.vue
Normal file
182
components/forms/NewPodcastForm.vue
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full px-2">
|
||||||
|
<img v-if="podcast.imageUrl" :src="podcast.imageUrl" class="h-36 w-36 object-contain mx-auto mb-2" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="podcast.title" :label="'Title'" class="mb-2 text-sm" @input="titleUpdated" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="podcast.author" :label="'Author'" class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="podcast.feedUrl" :label="'Feed URL'" readonly class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="'Genres'" class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-textarea-with-label v-model="podcast.description" :label="'Description'" :rows="3" class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" :label="'Folder'" class="mb-2 text-sm" @input="folderUpdated" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="fullPath" :label="'Podcast Path'" input-class="h-10" readonly class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<div class="flex items-center py-4 px-2">
|
||||||
|
<ui-checkbox v-model="podcast.autoDownloadEpisodes" :label="'Auto Download Episodes'" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-sm font-semibold" />
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn color="success" @click="submit">Submit</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Path from 'path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
processing: Boolean,
|
||||||
|
podcastData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
podcastFeedData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedFolderId: null,
|
||||||
|
fullPath: null,
|
||||||
|
podcast: {
|
||||||
|
title: '',
|
||||||
|
author: '',
|
||||||
|
description: '',
|
||||||
|
releaseDate: '',
|
||||||
|
genres: [],
|
||||||
|
feedUrl: '',
|
||||||
|
feedImageUrl: '',
|
||||||
|
itunesPageUrl: '',
|
||||||
|
itunesId: '',
|
||||||
|
itunesArtistId: '',
|
||||||
|
autoDownloadEpisodes: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
_processing: {
|
||||||
|
get() {
|
||||||
|
return this.processing
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:processing', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this._podcastData.title
|
||||||
|
},
|
||||||
|
currentLibrary() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
|
},
|
||||||
|
folders() {
|
||||||
|
if (!this.currentLibrary) return []
|
||||||
|
return this.currentLibrary.folders || []
|
||||||
|
},
|
||||||
|
folderItems() {
|
||||||
|
return this.folders.map((fold) => {
|
||||||
|
return {
|
||||||
|
value: fold.id,
|
||||||
|
text: fold.fullPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
_podcastData() {
|
||||||
|
return this.podcastData || {}
|
||||||
|
},
|
||||||
|
feedMetadata() {
|
||||||
|
if (!this.podcastFeedData) return {}
|
||||||
|
return this.podcastFeedData.metadata || {}
|
||||||
|
},
|
||||||
|
episodes() {
|
||||||
|
if (!this.podcastFeedData) return []
|
||||||
|
return this.podcastFeedData.episodes || []
|
||||||
|
},
|
||||||
|
selectedFolder() {
|
||||||
|
return this.folders.find((f) => f.id === this.selectedFolderId)
|
||||||
|
},
|
||||||
|
selectedFolderPath() {
|
||||||
|
if (!this.selectedFolder) return ''
|
||||||
|
return this.selectedFolder.fullPath
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
titleUpdated() {
|
||||||
|
this.folderUpdated()
|
||||||
|
},
|
||||||
|
folderUpdated() {
|
||||||
|
if (!this.selectedFolderPath || !this.podcast.title) {
|
||||||
|
this.fullPath = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.fullPath = Path.join(this.selectedFolderPath, this.$sanitizeFilename(this.podcast.title))
|
||||||
|
},
|
||||||
|
submit() {
|
||||||
|
const podcastPayload = {
|
||||||
|
path: this.fullPath,
|
||||||
|
folderId: this.selectedFolderId,
|
||||||
|
libraryId: this.currentLibrary.id,
|
||||||
|
media: {
|
||||||
|
metadata: {
|
||||||
|
title: this.podcast.title,
|
||||||
|
author: this.podcast.author,
|
||||||
|
description: this.podcast.description,
|
||||||
|
releaseDate: this.podcast.releaseDate,
|
||||||
|
genres: [...this.podcast.genres],
|
||||||
|
feedUrl: this.podcast.feedUrl,
|
||||||
|
imageUrl: this.podcast.imageUrl,
|
||||||
|
itunesPageUrl: this.podcast.itunesPageUrl,
|
||||||
|
itunesId: this.podcast.itunesId,
|
||||||
|
itunesArtistId: this.podcast.itunesArtistId,
|
||||||
|
language: this.podcast.language
|
||||||
|
},
|
||||||
|
autoDownloadEpisodes: this.podcast.autoDownloadEpisodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Podcast payload', podcastPayload)
|
||||||
|
|
||||||
|
this._processing = true
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/podcasts', podcastPayload)
|
||||||
|
.then((libraryItem) => {
|
||||||
|
this._processing = false
|
||||||
|
this.$toast.success('Podcast added')
|
||||||
|
this.$router.push(`/item/${libraryItem.id}`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to add podcast'
|
||||||
|
console.error('Failed to create podcast', error)
|
||||||
|
this._processing = false
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
// Prefer using itunes podcast data but not always passed in if manually entering rss feed
|
||||||
|
this.podcast.title = this._podcastData.title || this.feedMetadata.title || ''
|
||||||
|
this.podcast.author = this._podcastData.artistName || this.feedMetadata.author || ''
|
||||||
|
this.podcast.description = this._podcastData.description || this.feedMetadata.descriptionPlain || ''
|
||||||
|
this.podcast.releaseDate = this._podcastData.releaseDate || ''
|
||||||
|
this.podcast.genres = this._podcastData.genres || this.feedMetadata.categories || []
|
||||||
|
this.podcast.feedUrl = this._podcastData.feedUrl || this.feedMetadata.feedUrl || ''
|
||||||
|
this.podcast.imageUrl = this._podcastData.cover || this.feedMetadata.image || ''
|
||||||
|
this.podcast.itunesPageUrl = this._podcastData.pageUrl || ''
|
||||||
|
this.podcast.itunesId = this._podcastData.id || ''
|
||||||
|
this.podcast.itunesArtistId = this._podcastData.artistId || ''
|
||||||
|
this.podcast.language = this._podcastData.language || ''
|
||||||
|
this.podcast.autoDownloadEpisodes = false
|
||||||
|
|
||||||
|
if (this.folderItems[0]) {
|
||||||
|
this.selectedFolderId = this.folderItems[0].value
|
||||||
|
this.folderUpdated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
261
components/ui/MultiSelect.vue
Normal file
261
components/ui/MultiSelect.vue
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
||||||
|
<div ref="wrapper" class="relative">
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
|
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
|
||||||
|
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 px-1 bg-bg bg-opacity-75 flex items-center justify-end opacity-0 hover:opacity-100">
|
||||||
|
<span v-if="showEdit" class="material-icons text-white hover:text-warning cursor-pointer" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span>
|
||||||
|
<span class="material-icons text-white hover:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
||||||
|
</div>
|
||||||
|
{{ item }}
|
||||||
|
</div>
|
||||||
|
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ul ref="menu" v-show="showMenu" class="absolute z-50 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
|
||||||
|
<template v-for="item in itemsToShow">
|
||||||
|
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="font-normal ml-3 block truncate">{{ item }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="selected.includes(item)" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
|
<span class="material-icons text-xl">checkmark</span>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<li v-if="!itemsToShow.length" class="text-gray-50 select-none relative py-2 pr-9" role="option">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<span class="font-normal">No Items</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
label: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
readonly: Boolean,
|
||||||
|
showEdit: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
textInput: null,
|
||||||
|
currentSearch: null,
|
||||||
|
typingTimeout: null,
|
||||||
|
isFocused: false,
|
||||||
|
menu: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
showMenu(newVal) {
|
||||||
|
if (newVal) this.setListener()
|
||||||
|
else this.removeListener()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selected: {
|
||||||
|
get() {
|
||||||
|
return this.value || []
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showMenu() {
|
||||||
|
return this.isFocused
|
||||||
|
},
|
||||||
|
wrapperClass() {
|
||||||
|
var classes = []
|
||||||
|
if (this.disabled) classes.push('bg-black-300')
|
||||||
|
else classes.push('bg-primary')
|
||||||
|
if (!this.readonly) classes.push('cursor-text')
|
||||||
|
return classes.join(' ')
|
||||||
|
},
|
||||||
|
itemsToShow() {
|
||||||
|
if (!this.currentSearch || !this.textInput) {
|
||||||
|
return this.items
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.items.filter((i) => {
|
||||||
|
var iValue = String(i).toLowerCase()
|
||||||
|
return iValue.includes(this.currentSearch.toLowerCase())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editItem(item) {
|
||||||
|
this.$emit('edit', item)
|
||||||
|
},
|
||||||
|
keydownInput() {
|
||||||
|
clearTimeout(this.typingTimeout)
|
||||||
|
this.typingTimeout = setTimeout(() => {
|
||||||
|
this.currentSearch = this.textInput
|
||||||
|
}, 100)
|
||||||
|
this.setInputWidth()
|
||||||
|
},
|
||||||
|
setInputWidth() {
|
||||||
|
setTimeout(() => {
|
||||||
|
var value = this.$refs.input.value
|
||||||
|
var len = value.length * 7 + 24
|
||||||
|
this.$refs.input.style.width = len + 'px'
|
||||||
|
this.recalcMenuPos()
|
||||||
|
}, 50)
|
||||||
|
},
|
||||||
|
recalcMenuPos() {
|
||||||
|
if (!this.menu) return
|
||||||
|
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
||||||
|
if (boundingBox.y > window.innerHeight - 8) {
|
||||||
|
// Input is off the page
|
||||||
|
return this.forceBlur()
|
||||||
|
}
|
||||||
|
var menuHeight = this.menu.clientHeight
|
||||||
|
var top = boundingBox.y + boundingBox.height - 4
|
||||||
|
if (top + menuHeight > window.innerHeight - 20) {
|
||||||
|
// Reverse menu to open upwards
|
||||||
|
top = boundingBox.y - menuHeight - 4
|
||||||
|
}
|
||||||
|
|
||||||
|
this.menu.style.top = top + 'px'
|
||||||
|
this.menu.style.left = boundingBox.x + 'px'
|
||||||
|
this.menu.style.width = boundingBox.width + 'px'
|
||||||
|
},
|
||||||
|
unmountMountMenu() {
|
||||||
|
if (!this.$refs.menu) return
|
||||||
|
this.menu = this.$refs.menu
|
||||||
|
|
||||||
|
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
|
||||||
|
this.menu.remove()
|
||||||
|
document.body.appendChild(this.menu)
|
||||||
|
this.menu.style.top = boundingBox.y + boundingBox.height - 4 + 'px'
|
||||||
|
this.menu.style.left = boundingBox.x + 'px'
|
||||||
|
this.menu.style.width = boundingBox.width + 'px'
|
||||||
|
},
|
||||||
|
inputFocus() {
|
||||||
|
if (!this.menu) {
|
||||||
|
this.unmountMountMenu()
|
||||||
|
}
|
||||||
|
this.isFocused = true
|
||||||
|
this.$nextTick(this.recalcMenuPos)
|
||||||
|
},
|
||||||
|
inputBlur() {
|
||||||
|
if (!this.isFocused) return
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.activeElement === this.$refs.input) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.isFocused = false
|
||||||
|
if (this.textInput) this.submitForm()
|
||||||
|
}, 50)
|
||||||
|
},
|
||||||
|
focus() {
|
||||||
|
if (this.$refs.input) this.$refs.input.focus()
|
||||||
|
},
|
||||||
|
blur() {
|
||||||
|
if (this.$refs.input) this.$refs.input.blur()
|
||||||
|
},
|
||||||
|
forceBlur() {
|
||||||
|
this.isFocused = false
|
||||||
|
if (this.textInput) this.submitForm()
|
||||||
|
if (this.$refs.input) this.$refs.input.blur()
|
||||||
|
},
|
||||||
|
clickedOption(e, itemValue) {
|
||||||
|
if (e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
if (this.$refs.input) this.$refs.input.focus()
|
||||||
|
|
||||||
|
var newSelected = null
|
||||||
|
if (this.selected.includes(itemValue)) {
|
||||||
|
newSelected = this.selected.filter((s) => s !== itemValue)
|
||||||
|
this.$emit('removedItem', itemValue)
|
||||||
|
} else {
|
||||||
|
newSelected = this.selected.concat([itemValue])
|
||||||
|
}
|
||||||
|
this.textInput = null
|
||||||
|
this.currentSearch = null
|
||||||
|
this.$emit('input', newSelected)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.recalcMenuPos()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clickWrapper() {
|
||||||
|
if (this.disabled) return
|
||||||
|
if (this.showMenu) {
|
||||||
|
return this.blur()
|
||||||
|
}
|
||||||
|
this.focus()
|
||||||
|
},
|
||||||
|
removeItem(item) {
|
||||||
|
var remaining = this.selected.filter((i) => i !== item)
|
||||||
|
this.$emit('input', remaining)
|
||||||
|
this.$emit('removedItem', item)
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.recalcMenuPos()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
insertNewItem(item) {
|
||||||
|
this.selected.push(item)
|
||||||
|
this.$emit('input', this.selected)
|
||||||
|
this.$emit('newItem', item)
|
||||||
|
this.textInput = null
|
||||||
|
this.currentSearch = null
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.blur()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
if (!this.textInput) return
|
||||||
|
|
||||||
|
var cleaned = this.textInput.trim()
|
||||||
|
var matchesItem = this.items.find((i) => {
|
||||||
|
return i === cleaned
|
||||||
|
})
|
||||||
|
if (matchesItem) {
|
||||||
|
this.clickedOption(null, matchesItem)
|
||||||
|
} else {
|
||||||
|
this.insertNewItem(this.textInput)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scroll() {
|
||||||
|
this.recalcMenuPos()
|
||||||
|
},
|
||||||
|
setListener() {
|
||||||
|
document.addEventListener('scroll', this.scroll, true)
|
||||||
|
},
|
||||||
|
removeListener() {
|
||||||
|
document.removeEventListener('scroll', this.scroll, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.menu) this.menu.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
input {
|
||||||
|
border-style: inherit !important;
|
||||||
|
}
|
||||||
|
input:read-only {
|
||||||
|
color: #aaa;
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input v-model="input" ref="input" autofocus :type="type" :disabled="disabled" autocorrect="off" autocapitalize="none" autocomplete="off" :placeholder="placeholder" class="py-2 w-full outline-none" :class="inputClass" @keyup="keyup" />
|
<input v-model="input" ref="input" autofocus :type="type" :disabled="disabled" autocorrect="off" autocapitalize="none" autocomplete="off" :placeholder="placeholder" class="py-2 w-full outline-none bg-primary" :class="inputClass" @keyup="keyup" />
|
||||||
<div v-if="prependIcon" class="absolute top-0 left-0 h-full px-2 flex items-center justify-center">
|
<div v-if="prependIcon" class="absolute top-0 left-0 h-full px-2 flex items-center justify-center">
|
||||||
<span class="material-icons text-lg">{{ prependIcon }}</span>
|
<span class="material-icons text-lg">{{ prependIcon }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,10 +26,6 @@ export default {
|
||||||
prependIcon: {
|
prependIcon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
|
||||||
textSize: {
|
|
||||||
type: String,
|
|
||||||
default: 'lg'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -45,7 +41,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
inputClass() {
|
inputClass() {
|
||||||
var classes = [`bg-${this.bg}`, `rounded-${this.rounded}`, `text-${this.textSize}`]
|
var classes = [`bg-${this.bg}`, `rounded-${this.rounded}`]
|
||||||
if (this.disabled) classes.push('text-gray-300')
|
if (this.disabled) classes.push('text-gray-300')
|
||||||
else classes.push('text-white')
|
else classes.push('text-white')
|
||||||
|
|
||||||
|
|
53
components/ui/TextareaInput.vue
Normal file
53
components/ui/TextareaInput.vue
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<textarea ref="input" v-model="inputValue" :rows="rows" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @change="change" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: [String, Number],
|
||||||
|
placeholder: String,
|
||||||
|
readonly: Boolean,
|
||||||
|
rows: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
},
|
||||||
|
transparent: Boolean,
|
||||||
|
disabled: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
inputValue: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
change(e) {
|
||||||
|
this.$emit('change', e.target.value)
|
||||||
|
},
|
||||||
|
blur() {
|
||||||
|
if (this.$refs.input && this.$refs.input.blur) {
|
||||||
|
this.$refs.input.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
textarea {
|
||||||
|
border-style: inherit !important;
|
||||||
|
}
|
||||||
|
textarea:read-only {
|
||||||
|
color: #aaa;
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
</style>
|
41
components/ui/TextareaWithLabel.vue
Normal file
41
components/ui/TextareaWithLabel.vue
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
||||||
|
<ui-textarea-input ref="input" v-model="inputValue" :disabled="disabled" :rows="rows" class="w-full" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: [String, Number],
|
||||||
|
label: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
rows: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
inputValue: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
blur() {
|
||||||
|
if (this.$refs.input && this.$refs.input.blur) {
|
||||||
|
this.$refs.input.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -31,15 +31,15 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="flex items-center mb-4 py-4 px-2">
|
<div class="flex items-center py-4 px-2">
|
||||||
<div class="flex items-center" @click="clearSelected">
|
<div class="flex items-center" @click="clearSelected">
|
||||||
<span class="material-icons text-2xl text-gray-300">arrow_back</span>
|
<span class="material-icons text-2xl text-gray-300">arrow_back</span>
|
||||||
<p class="pl-2 uppercase text-sm font-semibold text-gray-300 leading-4 pb-px">Back</p>
|
<p class="pl-2 uppercase text-sm font-semibold text-gray-300 leading-4 pb-px">Back</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-2">
|
<div class="w-full py-2 search-results-container overflow-y-auto overflow-x-hidden">
|
||||||
<p>Selected Podcast Feed</p>
|
<forms-new-podcast-form :podcast-data="selectedPodcast" :podcast-feed-data="selectedPodcastFeed" :processing.sync="processing" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -5,21 +5,21 @@ export default function ({ $axios, store }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var customHeaders = store.getters['user/getCustomHeaders']
|
const customHeaders = store.getters['user/getCustomHeaders']
|
||||||
if (customHeaders) {
|
if (customHeaders) {
|
||||||
for (const key in customHeaders) {
|
for (const key in customHeaders) {
|
||||||
config.headers.common[key] = customHeaders[key]
|
config.headers.common[key] = customHeaders[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var bearerToken = store.getters['user/getToken']
|
const bearerToken = store.getters['user/getToken']
|
||||||
if (bearerToken) {
|
if (bearerToken) {
|
||||||
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
|
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
|
||||||
} else {
|
} else {
|
||||||
console.warn('[Axios] No Bearer Token for request')
|
console.warn('[Axios] No Bearer Token for request')
|
||||||
}
|
}
|
||||||
|
|
||||||
var serverUrl = store.getters['user/getServerAddress']
|
const serverUrl = store.getters['user/getServerAddress']
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
config.url = `${serverUrl}${config.url}`
|
config.url = `${serverUrl}${config.url}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,43 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
|
||||||
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
|
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
||||||
|
if (typeof input !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max is actually 255-260 for windows but this leaves padding incase ext wasnt put on yet
|
||||||
|
const MAX_FILENAME_LEN = 240
|
||||||
|
|
||||||
|
var replacement = ''
|
||||||
|
var illegalRe = /[\/\?<>\\:\*\|"]/g
|
||||||
|
var controlRe = /[\x00-\x1f\x80-\x9f]/g
|
||||||
|
var reservedRe = /^\.+$/
|
||||||
|
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
||||||
|
var windowsTrailingRe = /[\. ]+$/
|
||||||
|
var lineBreaks = /[\n\r]/g
|
||||||
|
|
||||||
|
var sanitized = input
|
||||||
|
.replace(':', colonReplacement) // Replace first occurrence of a colon
|
||||||
|
.replace(illegalRe, replacement)
|
||||||
|
.replace(controlRe, replacement)
|
||||||
|
.replace(reservedRe, replacement)
|
||||||
|
.replace(lineBreaks, replacement)
|
||||||
|
.replace(windowsReservedRe, replacement)
|
||||||
|
.replace(windowsTrailingRe, replacement)
|
||||||
|
|
||||||
|
|
||||||
|
if (sanitized.length > MAX_FILENAME_LEN) {
|
||||||
|
var lenToRemove = sanitized.length - MAX_FILENAME_LEN
|
||||||
|
var ext = Path.extname(sanitized)
|
||||||
|
var basename = Path.basename(sanitized, ext)
|
||||||
|
basename = basename.slice(0, basename.length - lenToRemove)
|
||||||
|
sanitized = basename + ext
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
|
|
||||||
function isClickedOutsideEl(clickEvent, elToCheckOutside, ignoreSelectors = [], ignoreElems = []) {
|
function isClickedOutsideEl(clickEvent, elToCheckOutside, ignoreSelectors = [], ignoreElems = []) {
|
||||||
const isDOMElement = (element) => {
|
const isDOMElement = (element) => {
|
||||||
return element instanceof Element || element instanceof HTMLDocument
|
return element instanceof Element || element instanceof HTMLDocument
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue