Files
FabledCurator/frontend/src/components/subscriptions/SourceFormDialog.vue
T

220 lines
7.3 KiB
Vue

<template>
<v-dialog :model-value="modelValue" max-width="640"
@update:model-value="$emit('update:modelValue', $event)">
<v-card>
<v-card-title>{{ source ? 'Edit source' : 'Add subscription' }}</v-card-title>
<v-card-text>
<!-- Artist picker (only when adding without a pre-selected artist) -->
<div v-if="!source && !initialArtist" class="mb-3">
<v-combobox
v-model="artistChoice"
:items="artistMatches"
item-title="name" item-value="id"
label="Artist (type to search or create)"
:loading="artistLoading"
return-object hide-no-data
@update:search="onArtistSearch"
/>
</div>
<div v-else-if="initialArtist && !source" class="text-caption mb-3">
Adding source for <strong>{{ initialArtist.name }}</strong>
</div>
<v-select
v-model="platform"
:items="platformsItems"
item-value="value"
item-title="title"
label="Platform"
/>
<v-text-field v-model="url" label="URL" :error-messages="urlError" />
<v-switch v-model="enabled" label="Enabled" color="accent" hide-details />
<v-tabs v-model="configTab" class="mt-4">
<v-tab value="structured">Structured</v-tab>
<v-tab value="advanced">Advanced JSON</v-tab>
</v-tabs>
<v-window v-model="configTab" class="mt-2">
<v-window-item value="structured">
<v-switch
v-model="structuredVideos"
label="Include videos" color="accent" hide-details
/>
<v-text-field
v-model="structuredSince" label="Skip posts older than (YYYY-MM-DD)"
placeholder="2024-01-01" hide-details class="mt-2"
/>
<p class="text-caption mt-2" style="opacity: 0.75">
More per-platform fields land here over time. Use Advanced JSON for everything else.
</p>
</v-window-item>
<v-window-item value="advanced">
<v-textarea
v-model="jsonText" :error-messages="jsonError"
label="config_overrides (JSON object)"
auto-grow rows="6" spellcheck="false"
/>
</v-window-item>
</v-window>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="$emit('update:modelValue', false)">Cancel</v-btn>
<v-btn color="accent" variant="flat" :loading="busy" :disabled="!canSave" @click="save">
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useSourcesStore } from '../../stores/sources.js'
import { usePlatformsStore } from '../../stores/platforms.js'
const props = defineProps({
modelValue: { type: Boolean, default: false },
source: { type: Object, default: null },
initialArtist: { type: Object, default: null },
})
const emit = defineEmits(['update:modelValue', 'saved'])
const store = useSourcesStore()
const platformsStore = usePlatformsStore()
const platformsItems = computed(() =>
platformsStore.list.map(p => ({ value: p.key, title: p.name }))
)
const artistChoice = ref(null)
const artistMatches = ref([])
const artistLoading = ref(false)
const platform = ref('patreon')
const url = ref('')
const enabled = ref(true)
const urlError = ref('')
const configTab = ref('structured')
const structuredVideos = ref(true)
const structuredSince = ref('')
const jsonText = ref('{}')
const jsonError = ref('')
const busy = ref(false)
// Sync config_overrides between the two views.
const config = computed(() => {
const out = {}
if (!structuredVideos.value) out.videos = false
if (structuredSince.value) out.since = structuredSince.value
return out
})
watch([structuredVideos, structuredSince], () => {
if (configTab.value === 'structured') {
jsonText.value = JSON.stringify(config.value, null, 2)
jsonError.value = ''
}
})
watch(jsonText, (txt) => {
if (configTab.value !== 'advanced') return
try {
const parsed = JSON.parse(txt || '{}')
if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
jsonError.value = 'Must be a JSON object'
return
}
jsonError.value = ''
// Reflect recognized keys into the structured view.
structuredVideos.value = parsed.videos !== false
structuredSince.value = parsed.since ?? ''
} catch {
jsonError.value = 'Invalid JSON'
}
})
const canSave = computed(() => !jsonError.value && !!url.value.trim())
watch(() => props.modelValue, async (open) => {
if (!open) return
urlError.value = ''; jsonError.value = ''
await platformsStore.loadAll()
if (props.source) {
platform.value = props.source.platform
url.value = props.source.url
enabled.value = props.source.enabled
const co = props.source.config_overrides || {}
structuredVideos.value = co.videos !== false
structuredSince.value = co.since ?? ''
jsonText.value = JSON.stringify(co, null, 2)
artistChoice.value = { id: props.source.artist_id, name: props.source.artist_name }
} else {
platform.value = platformsStore.list[0]?.key || 'patreon'
url.value = ''; enabled.value = true
structuredVideos.value = true; structuredSince.value = ''
jsonText.value = '{}'
artistChoice.value = props.initialArtist
? { id: props.initialArtist.id, name: props.initialArtist.name }
: null
}
})
async function onArtistSearch(q) {
if (!q || (typeof q !== 'string')) return
artistLoading.value = true
try {
artistMatches.value = await store.autocompleteArtist(q)
} finally {
artistLoading.value = false
}
}
function _parseConfig() {
if (configTab.value === 'structured') return Object.keys(config.value).length ? config.value : null
const parsed = JSON.parse(jsonText.value || '{}')
return Object.keys(parsed).length ? parsed : null
}
async function save() {
urlError.value = ''
if (!url.value.trim()) { urlError.value = 'URL is required'; return }
let configOut
try { configOut = _parseConfig() } catch { jsonError.value = 'Invalid JSON'; return }
busy.value = true
try {
if (props.source) {
await store.update(props.source.id, {
platform: platform.value, url: url.value.trim(),
enabled: enabled.value, config_overrides: configOut,
}, props.source.artist_id)
} else {
let artistId
if (props.initialArtist) artistId = props.initialArtist.id
else if (artistChoice.value && typeof artistChoice.value === 'object') artistId = artistChoice.value.id
else if (typeof artistChoice.value === 'string') {
const { artist } = await store.findOrCreateArtist(artistChoice.value)
artistId = artist.id
} else {
urlError.value = 'Pick or create an artist first'; busy.value = false; return
}
await store.create({
artist_id: artistId, platform: platform.value, url: url.value.trim(),
enabled: enabled.value, config_overrides: configOut,
})
}
emit('saved')
} catch (e) {
if (e?.body?.error === 'duplicate') {
urlError.value = `Duplicate (existing source id ${e.body.existing_id})`
} else {
urlError.value = String(e?.detail || e?.message || e)
}
} finally {
busy.value = false
}
}
</script>