aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-09-30 21:43:19 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-09-30 21:43:19 -0400
commit95d60952b3ea470495003b0d0e51840cd8e68605 (patch)
tree79f5179c4dd9349c7b88b7b1d2ed13b53152d166
parent62da31eb31baf7e27bf4c6d3d15e2c69617e2f74 (diff)
configure html podcast description support and minor ui tweaks
-rw-r--r--back-end/src/FeedGenerator.cs37
-rw-r--r--back-end/src/Model/BlogPost.cs5
-rw-r--r--back-end/src/Model/PostMeta.cs3
-rw-r--r--front-end/src/views/Blog/ckeditor/Editor.vue32
-rw-r--r--front-end/src/views/Blog/components/ContentSearch.vue8
-rw-r--r--front-end/src/views/Blog/components/Posts/PostEdit.vue26
-rw-r--r--front-end/src/views/Blog/components/podcast-helpers/EpisodeAdder.vue36
-rw-r--r--front-end/src/views/Blog/components/podcast-helpers/podcast-form.ts12
-rw-r--r--lib/admin/src/types.ts1
9 files changed, 131 insertions, 29 deletions
diff --git a/back-end/src/FeedGenerator.cs b/back-end/src/FeedGenerator.cs
index 2aed945..ae8fd52 100644
--- a/back-end/src/FeedGenerator.cs
+++ b/back-end/src/FeedGenerator.cs
@@ -27,20 +27,27 @@ using System.Collections.Generic;
using VNLib.Utils.IO;
using VNLib.Plugins;
+using VNLib.Plugins.Extensions.Loading;
using Content.Publishing.Blog.Admin.Model;
namespace Content.Publishing.Blog.Admin
{
+ [ConfigurationName("rss_feed", Required = false)]
internal sealed class FeedGenerator : IRssFeedGenerator
{
const int defaultMaxItems = 20;
const string ITUNES_XML_ATTR = "http://www.itunes.com/dtds/podcast-1.0.dtd";
const string CONTENT_XML_ATTR = "http://purl.org/rss/1.0/modules/content/";
+ const string PODCAST_INDEX_ATTR = "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md";
+ const string GENERATOR_NAME = "CMNext";
public FeedGenerator(PluginBase pbase)
{ }
+ public FeedGenerator(PluginBase pbase, IConfigScope config)
+ { }
+
public void BuildFeed(IChannelContext context, IEnumerable<PostMeta> posts, VnMemoryStream output)
{
_ = context.Feed ?? throw new ArgumentNullException(nameof(context.Feed));
@@ -66,7 +73,7 @@ namespace Content.Publishing.Blog.Admin
writer.WriteAttributeString("version", "2.0");
writer.WriteAttributeString("xmlns", "itunes", null, ITUNES_XML_ATTR);
writer.WriteAttributeString("xmlns", "content", null, CONTENT_XML_ATTR);
-
+ writer.WriteAttributeString("xmlns", "podcast", null, PODCAST_INDEX_ATTR);
//Channel element
writer.WriteStartElement("channel");
@@ -94,6 +101,12 @@ namespace Content.Publishing.Blog.Admin
{
PrintExtendedProps(prop, writer);
}
+
+ //Add generator tag if not set by user
+ if(!context.Feed.ExtendedProperties.Any(static p => "generator".Equals(p.Name, StringComparison.OrdinalIgnoreCase)))
+ {
+ writer.WriteElementString("generator", GENERATOR_NAME);
+ }
}
//Author
@@ -127,9 +140,25 @@ namespace Content.Publishing.Blog.Admin
writer.WriteElementString("itunes", "author", null, post.Author);
//Description is just the post summary
- writer.WriteElementString("description", post.Summary);
writer.WriteElementString("itunes", "summary", null, post.Summary);
+ //Allow an html description from the post meta itself
+ if (post.HtmlDescription != null)
+ {
+ writer.WriteStartElement("description");
+ writer.WriteCData(post.HtmlDescription);
+ writer.WriteEndElement();
+
+ //Add content encoded tag
+ writer.WriteStartElement("content", "encoded", null);
+ writer.WriteCData(post.HtmlDescription);
+ writer.WriteEndElement();
+ }
+ else
+ {
+ writer.WriteElementString("description", post.Summary);
+ }
+
//Time as iso string from unix seconds timestamp
string pubDate = DateTimeOffset.FromUnixTimeSeconds(post.Created).ToString("R");
@@ -190,6 +219,10 @@ namespace Content.Publishing.Blog.Admin
PrintExtendedProps(child, writer);
}
}
+ else if(prop.Value != null && prop.Value.StartsWith("<![CDATA", StringComparison.OrdinalIgnoreCase))
+ {
+ writer.WriteCData(prop.Value);
+ }
else
{
//Write the value
diff --git a/back-end/src/Model/BlogPost.cs b/back-end/src/Model/BlogPost.cs
index 0adea40..86cc289 100644
--- a/back-end/src/Model/BlogPost.cs
+++ b/back-end/src/Model/BlogPost.cs
@@ -44,6 +44,11 @@ namespace Content.Publishing.Blog.Admin.Model
.IllegalCharacters()
.MaximumLength(200);
+ //Allow an custom html description to be stored on the post object
+ validator.RuleFor(x => x.HtmlDescription)
+ .MaximumLength(4000)
+ .When(p => p.HtmlDescription != null);
+
validator.RuleFor(x => x.Author!)
.NotEmpty()
.IllegalCharacters()
diff --git a/back-end/src/Model/PostMeta.cs b/back-end/src/Model/PostMeta.cs
index 2bb963e..ce92c7e 100644
--- a/back-end/src/Model/PostMeta.cs
+++ b/back-end/src/Model/PostMeta.cs
@@ -43,6 +43,9 @@ namespace Content.Publishing.Blog.Admin.Model
[JsonPropertyName("summary")]
public string? Summary { get; set; }
+ [JsonPropertyName("html_description")]
+ public string? HtmlDescription { get; set; }
+
[JsonPropertyName("tags")]
public string[]? Tags { get; set; }
diff --git a/front-end/src/views/Blog/ckeditor/Editor.vue b/front-end/src/views/Blog/ckeditor/Editor.vue
index 3cd9a8c..4fc534b 100644
--- a/front-end/src/views/Blog/ckeditor/Editor.vue
+++ b/front-end/src/views/Blog/ckeditor/Editor.vue
@@ -1,7 +1,21 @@
<template>
<div class="pt-6">
<div class="flex justify-end w-full gap-2 my-2">
- <div class="w-fit">
+ <div class="w-fit">
+ <div class="flex flex-row py-2 mr-auto">
+ <Switch v-model="podcastMode"
+ :class="$props.podcastMode ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-500'"
+ class="relative inline-flex items-center w-10 h-5 my-auto duration-75 rounded-full">
+ <span class="sr-only">Podcast Mode</span>
+ <span :class="$props.podcastMode ? 'translate-x-6' : 'translate-x-1'"
+ class="inline-block w-3 h-3 transition transform bg-white rounded-full" />
+ </Switch>
+ <div class="my-auto ml-3">
+ Podcast Mode
+ </div>
+ </div>
+ </div>
+ <div class="w-fit">
<Popover class="relative">
<PopoverButton class="btn">
Add
@@ -56,11 +70,11 @@
<script setup lang="ts">
import { debounce, defer } from 'lodash-es';
-import { ref } from 'vue';
+import { computed, ref, toRefs } from 'vue';
import { useSessionStorage } from '@vueuse/core';
import { tryOnMounted } from '@vueuse/shared';
import { apiCall } from '@vnuge/vnlib.browser';
-import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
+import { Popover, PopoverButton, PopoverPanel, Switch } from '@headlessui/vue'
import { BlogState } from '../blog-api'
import { Converter } from 'showdown'
@@ -68,18 +82,24 @@ import { Converter } from 'showdown'
import { config } from './build.ts'
import ContentSearch from '../components/ContentSearch.vue';
-const emit = defineEmits(['change', 'load'])
+const emit = defineEmits(['change', 'load', 'mode-change'])
-defineProps<{
- blog: BlogState
+const props = defineProps<{
+ blog: BlogState,
+ podcastMode: boolean
}>()
let editor = {}
+const propRefs = toRefs(props)
//Init new shodown converter
const showdownConverter = new Converter()
const mdBuffer = ref('')
const editorFrame = ref(null)
const crashBuffer = useSessionStorage('post-crash', '')
+const podcastMode = computed({
+ get: () => propRefs.podcastMode.value,
+ set: (v) => emit('mode-change', v)
+})
const recoverFromCrash = () => {
//Set editor content from crash buffer
diff --git a/front-end/src/views/Blog/components/ContentSearch.vue b/front-end/src/views/Blog/components/ContentSearch.vue
index 03cb432..78cfa1a 100644
--- a/front-end/src/views/Blog/components/ContentSearch.vue
+++ b/front-end/src/views/Blog/components/ContentSearch.vue
@@ -79,8 +79,8 @@ const searchResults = computed<ContentResult[]>(() => {
return {
...content,
//truncate the id and name for display
- shortId: truncate(content.id, { length: 15 }),
- shortName: truncate(content.name, { length: 24 }),
+ shortId: truncate(content.id, { length: 18 }),
+ shortName: truncate(content.name, { length: 36 }),
copyLink: () => copyLink(content, copy),
copied
}
@@ -110,6 +110,10 @@ const onSelected = (result: ContentResult) => {
.controls{
@apply min-w-[4rem] text-center;
}
+
+ &.name{
+ @apply text-sm;
+ }
}
</style> \ No newline at end of file
diff --git a/front-end/src/views/Blog/components/Posts/PostEdit.vue b/front-end/src/views/Blog/components/Posts/PostEdit.vue
index e55deee..6ea0bac 100644
--- a/front-end/src/views/Blog/components/Posts/PostEdit.vue
+++ b/front-end/src/views/Blog/components/Posts/PostEdit.vue
@@ -9,9 +9,6 @@
</div>
<div class="mx-auto">
<h4 class="text-center">Edit Post</h4>
- <p>
- Add or edit a post to publish to your blog.
- </p>
</div>
<div class="relative">
<div class="absolute top-2 right-10">
@@ -28,7 +25,7 @@
/>
<div id="post-content-editor" class="px-6" :class="{'invalid':v$.content.$invalid}">
- <Editor @change="onContentChanged" :blog="$props.blog" @load="onEditorLoad" />
+ <Editor :podcast-mode="podcastMode" :blog="$props.blog" @change="onContentChanged" @mode-change="onModeChange" @load="onEditorLoad" />
</div>
<FeedFields :properties="postProperties" :blog="$props.blog" />
@@ -44,7 +41,7 @@
</div>
</template>
<script setup lang="ts">
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
import { BlogState } from '../../blog-api';
import { reactiveComputed } from '@vueuse/core';
import { isNil, isString, split } from 'lodash-es';
@@ -64,6 +61,7 @@ const { getProfile } = useUser();
const { schema, getValidator } = getPostForm();
const { posts, content } = props.blog;
+const podcastMode = ref(false)
const isNew = computed(() => isNil(posts.selectedItem.value));
@@ -98,6 +96,14 @@ const onSubmit = async () =>{
//Remove the content from the post object
delete post.content;
+ //Store the post content on the html descrption if in podcast mode
+ if(podcastMode.value){
+ post.html_description = v$.value.content.$model;
+ }
+ else{
+ delete post.html_description;
+ }
+
//Convert the tags string to an array of strings
post.tags = isString(post.tags) ? split(post.tags, ',') : post.tags;
@@ -136,6 +142,10 @@ const setMeAsAuthor = () => {
})
}
+const onModeChange = (e: boolean) => {
+ podcastMode.value = e;
+}
+
const onEditorLoad = async (editor : any) =>{
//Get the initial content
@@ -146,6 +156,12 @@ const onEditorLoad = async (editor : any) =>{
onContentChanged(postContent);
editor.setData(postContent);
}
+
+ //If the post has an html description, set podcast mode
+ if(postBuffer.html_description){
+ //Set podcast mode to true
+ podcastMode.value = true;
+ }
}
</script>
diff --git a/front-end/src/views/Blog/components/podcast-helpers/EpisodeAdder.vue b/front-end/src/views/Blog/components/podcast-helpers/EpisodeAdder.vue
index d4adb6f..6d1e473 100644
--- a/front-end/src/views/Blog/components/podcast-helpers/EpisodeAdder.vue
+++ b/front-end/src/views/Blog/components/podcast-helpers/EpisodeAdder.vue
@@ -13,12 +13,26 @@
<div class="fixed inset-0 flex justify-center pt-[8rem]">
<DialogPanel class="dialog">
<div class="">
- <DialogTitle>Set feed enclosure</DialogTitle>
- <DialogDescription>
+ <DialogTitle class="text-2xl font-bold">Add Media Enclosure</DialogTitle>
+ <DialogDescription class="text-xs">
You may set a podcast episode or other rss enclosure from
content stored in the cms.
</DialogDescription>
- <div class="my-3 ml-auto w-fit">
+ <div class="flex flex-row gap-3 my-3 ml-auto w-fit">
+ <div class="flex flex-row gap-2 my-auto">
+ <div class="text-sm">
+ Explicit
+ </div>
+ <div class="">
+ <Switch v-model="isExplicit"
+ :class="isExplicit ? 'bg-red-500' : 'bg-gray-300 dark:bg-dark-500'"
+ class="relative inline-flex items-center w-10 h-5 my-auto duration-75 rounded-full">
+ <span class="sr-only">Podcast Mode</span>
+ <span :class="isExplicit ? 'translate-x-6' : 'translate-x-1'"
+ class="inline-block w-3 h-3 transition transform bg-white rounded-full" />
+ </Switch>
+ </div>
+ </div>
<Popover class="relative">
<PopoverButton class="btn">
Add media
@@ -57,7 +71,7 @@
</template>
<script setup lang="ts">
-import { ref, reactive } from 'vue';
+import { ref, reactive, computed } from 'vue';
import { BlogState } from '../../blog-api';
import { PodcastEntity, getPodcastForm } from './podcast-form'
import {
@@ -68,6 +82,7 @@ import {
PopoverButton,
PopoverPanel,
Popover,
+ Switch
} from '@headlessui/vue'
import ContentSearch from '../ContentSearch.vue'
import { apiCall, debugLog } from '@vnuge/vnlib.browser';
@@ -87,6 +102,11 @@ const buffer = reactive<PodcastEntity>({} as PodcastEntity)
const { v$, validate } = getValidator(buffer)
+const isExplicit = computed({
+ get : () => buffer.explicit,
+ set : (v: boolean) => buffer.explicit = v
+});
+
const setIsOpen = (value: boolean) => isOpen.value = value
const onFormSubmit = async () =>{
@@ -102,15 +122,13 @@ const onFormSubmit = async () =>{
setIsOpen(false)
}
-const onCancel = () =>{
- setIsOpen(false)
-}
+const onCancel = () => setIsOpen(false)
const onContentSelected = (content: ContentMeta) =>{
apiCall(async () =>{
//Get the content link from the server
const url = await getPublicUrl(content)
-
+
//set the form content
setEnclosureContent(buffer, content, `/${url}`)
})
@@ -147,7 +165,7 @@ const onContentSelected = (content: ContentMeta) =>{
@apply py-1.5 bg-transparent;
&:disabled{
- @apply bg-gray-100 dark:bg-transparent dark:border-transparent;
+ @apply bg-gray-100 py-0.5 bg-transparent disabled:text-sm disabled:border-0 dark:text-gray-400 text-black;
}
&:focus{
diff --git a/front-end/src/views/Blog/components/podcast-helpers/podcast-form.ts b/front-end/src/views/Blog/components/podcast-helpers/podcast-form.ts
index ab8ad8a..56b898c 100644
--- a/front-end/src/views/Blog/components/podcast-helpers/podcast-form.ts
+++ b/front-end/src/views/Blog/components/podcast-helpers/podcast-form.ts
@@ -25,6 +25,7 @@ export interface EnclosureEntity{
contentUrl: string;
contentLength: number;
contentType: string;
+ explicit: boolean;
}
export interface PodcastEntity extends EnclosureEntity{
@@ -58,7 +59,6 @@ export const getPodcastForm = (editMode?: Ref<boolean>) => {
label: 'File Id',
name: 'fileId',
placeholder: '',
- description: 'The file id of the episode already in the channel',
disabled: true,
},
{
@@ -67,7 +67,6 @@ export const getPodcastForm = (editMode?: Ref<boolean>) => {
label: 'Content url',
name: 'contentUrl',
placeholder: '',
- description: 'This the relative url to the episode content file',
disabled: true,
},
{
@@ -76,16 +75,14 @@ export const getPodcastForm = (editMode?: Ref<boolean>) => {
label: 'Content length',
name: 'contentLength',
placeholder: '',
- description: 'This the length in bytes of the episode content file',
disabled: true,
},
{
id: 'content-type',
type: 'text',
- label: 'The MIME content type',
+ label: 'MIME content type',
name: 'contentType',
placeholder: '',
- description: 'The MIME content type for the episode content file',
disabled: true,
}
]
@@ -159,6 +156,11 @@ export const getPodcastForm = (editMode?: Ref<boolean>) => {
length: podcast.contentLength?.toString(),
type: podcast.contentType
},
+ },
+ {
+ name: 'explicit',
+ namespace: 'itunes',
+ value: podcast.explicit ? 'true' : 'false'
}
]
}
diff --git a/lib/admin/src/types.ts b/lib/admin/src/types.ts
index f94932a..f03c008 100644
--- a/lib/admin/src/types.ts
+++ b/lib/admin/src/types.ts
@@ -107,6 +107,7 @@ export interface PostMeta extends NamedBlogEntity, XmlPropertyContainer {
author?: string;
tags?: string[];
image?: string;
+ html_description?: string;
}
/**