V2 #1
|
@ -278,13 +278,16 @@ type PodcastOptions =
|
|||
iTunesCategory : string
|
||||
|
||||
/// A further refinement of the categorization of this podcast (iTunes field / values)
|
||||
iTunesSubcategory : string
|
||||
iTunesSubcategory : string option
|
||||
|
||||
/// The explictness rating (iTunes field)
|
||||
explicit : ExplicitRating
|
||||
|
||||
/// The default media type for files in this podcast
|
||||
defaultMediaType : string option
|
||||
|
||||
/// The base URL for relative URL media files for this podcast (optional; defaults to web log base)
|
||||
mediaBaseUrl : string option
|
||||
}
|
||||
|
||||
|
||||
|
@ -303,6 +306,17 @@ type CustomFeed =
|
|||
podcast : PodcastOptions option
|
||||
}
|
||||
|
||||
/// Functions to support custom feeds
|
||||
module CustomFeed =
|
||||
|
||||
/// An empty custom feed
|
||||
let empty =
|
||||
{ id = CustomFeedId ""
|
||||
source = Category (CategoryId "")
|
||||
path = Permalink ""
|
||||
podcast = None
|
||||
}
|
||||
|
||||
|
||||
/// Really Simple Syndication (RSS) options for this web log
|
||||
type RssOptions =
|
||||
|
|
|
@ -164,6 +164,110 @@ type EditCategoryModel =
|
|||
}
|
||||
|
||||
|
||||
/// View model to edit a custom RSS feed
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditCustomFeedModel =
|
||||
{ /// The ID of the feed being editing
|
||||
id : string
|
||||
|
||||
/// The type of source for this feed ("category" or "tag")
|
||||
sourceType : string
|
||||
|
||||
/// The category ID or tag on which this feed is based
|
||||
sourceValue : string
|
||||
|
||||
/// The relative path at which this feed is served
|
||||
path : string
|
||||
|
||||
/// Whether this feed defines a podcast
|
||||
isPodcast : bool
|
||||
|
||||
/// The title of the podcast
|
||||
title : string
|
||||
|
||||
/// A subtitle for the podcast
|
||||
subtitle : string
|
||||
|
||||
/// The number of items in the podcast feed
|
||||
itemsInFeed : int
|
||||
|
||||
/// A summary of the podcast (iTunes field)
|
||||
summary : string
|
||||
|
||||
/// The display name of the podcast author (iTunes field)
|
||||
displayedAuthor : string
|
||||
|
||||
/// The e-mail address of the user who registered the podcast at iTunes
|
||||
email : string
|
||||
|
||||
/// The link to the image for the podcast
|
||||
imageUrl : string
|
||||
|
||||
/// The category from iTunes under which this podcast is categorized
|
||||
itunesCategory : string
|
||||
|
||||
/// A further refinement of the categorization of this podcast (iTunes field / values)
|
||||
itunesSubcategory : string
|
||||
|
||||
/// The explictness rating (iTunes field)
|
||||
explicit : string
|
||||
|
||||
/// The default media type for files in this podcast
|
||||
defaultMediaType : string
|
||||
|
||||
/// The base URL for relative URL media files for this podcast (optional; defaults to web log base)
|
||||
mediaBaseUrl : string
|
||||
}
|
||||
|
||||
/// An empty custom feed model
|
||||
static member empty =
|
||||
{ id = ""
|
||||
sourceType = "category"
|
||||
sourceValue = ""
|
||||
path = ""
|
||||
isPodcast = false
|
||||
title = ""
|
||||
subtitle = ""
|
||||
itemsInFeed = 25
|
||||
summary = ""
|
||||
displayedAuthor = ""
|
||||
email = ""
|
||||
imageUrl = ""
|
||||
itunesCategory = ""
|
||||
itunesSubcategory = ""
|
||||
explicit = "no"
|
||||
defaultMediaType = "audio/mpeg"
|
||||
mediaBaseUrl = ""
|
||||
}
|
||||
|
||||
/// Create a model from a custom feed
|
||||
static member fromFeed (feed : CustomFeed) =
|
||||
let rss =
|
||||
{ EditCustomFeedModel.empty with
|
||||
id = CustomFeedId.toString feed.id
|
||||
sourceType = match feed.source with Category _ -> "category" | Tag _ -> "tag"
|
||||
sourceValue = match feed.source with Category (CategoryId catId) -> catId | Tag tag -> tag
|
||||
path = Permalink.toString feed.path
|
||||
}
|
||||
match feed.podcast with
|
||||
| Some p ->
|
||||
{ rss with
|
||||
isPodcast = true
|
||||
title = p.title
|
||||
subtitle = defaultArg p.subtitle ""
|
||||
itemsInFeed = p.itemsInFeed
|
||||
summary = p.summary
|
||||
displayedAuthor = p.displayedAuthor
|
||||
email = p.email
|
||||
itunesCategory = p.iTunesCategory
|
||||
itunesSubcategory = defaultArg p.iTunesSubcategory ""
|
||||
explicit = ExplicitRating.toString p.explicit
|
||||
defaultMediaType = defaultArg p.defaultMediaType ""
|
||||
mediaBaseUrl = defaultArg p.mediaBaseUrl ""
|
||||
}
|
||||
| None -> rss
|
||||
|
||||
|
||||
/// View model to edit a page
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type EditPageModel =
|
||||
|
|
|
@ -180,9 +180,11 @@ let private addPodcast webLog (rssFeed : SyndicationFeed) (feed : CustomFeed) =
|
|||
let categoryXml = XmlDocument ()
|
||||
let catElt = categoryXml.CreateElement ("itunes", "category", "")
|
||||
catElt.SetAttribute ("text", podcast.iTunesCategory)
|
||||
let subCat = categoryXml.CreateElement ("itunes", "category", "")
|
||||
subCat.SetAttribute ("text", podcast.iTunesSubcategory)
|
||||
catElt.AppendChild subCat |> ignore
|
||||
podcast.iTunesSubcategory
|
||||
|> Option.iter (fun subCat ->
|
||||
let subCatElt = categoryXml.CreateElement ("itunes", "category", "")
|
||||
subCatElt.SetAttribute ("text", subCat)
|
||||
catElt.AppendChild subCatElt |> ignore)
|
||||
categoryXml.AppendChild catElt |> ignore
|
||||
|
||||
[ "dc", "http://purl.org/dc/elements/1.1/"
|
||||
|
@ -287,13 +289,12 @@ let editSettings : HttpHandler = fun next ctx -> task {
|
|||
webLog.rss.customFeeds
|
||||
|> List.map (DisplayCustomFeed.fromFeed (CategoryCache.get ctx))
|
||||
|> Array.ofList
|
||||
return!
|
||||
Hash.FromAnonymousObject
|
||||
{| csrf = csrfToken ctx
|
||||
page_title = "RSS Settings"
|
||||
model = EditRssModel.fromRssOptions webLog.rss
|
||||
custom_feeds = feeds
|
||||
|}
|
||||
return! Hash.FromAnonymousObject
|
||||
{| csrf = csrfToken ctx
|
||||
page_title = "RSS Settings"
|
||||
model = EditRssModel.fromRssOptions webLog.rss
|
||||
custom_feeds = feeds
|
||||
|}
|
||||
|> viewForTheme "admin" "rss-settings" next ctx
|
||||
}
|
||||
|
||||
|
@ -311,6 +312,30 @@ let saveSettings : HttpHandler = fun next ctx -> task {
|
|||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// GET: /admin/rss/{id}/edit
|
||||
let editCustomFeed feedId : HttpHandler = fun next ctx -> task {
|
||||
let customFeed =
|
||||
match feedId with
|
||||
| "new" -> Some CustomFeed.empty
|
||||
| _ -> ctx.WebLog.rss.customFeeds |> List.tryFind (fun f -> f.id = CustomFeedId feedId)
|
||||
match customFeed with
|
||||
| Some f ->
|
||||
return! Hash.FromAnonymousObject
|
||||
{| csrf = csrfToken ctx
|
||||
page_title = $"""{if feedId = "new" then "Add" else "Edit"} Custom RSS Feed"""
|
||||
model = EditCustomFeedModel.fromFeed f
|
||||
categories = CategoryCache.get ctx
|
||||
|}
|
||||
|> viewForTheme "admin" "custom-feed-edit" next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// POST: /admin/rss/save
|
||||
let saveCustomFeed : HttpHandler = fun next ctx -> task {
|
||||
// TODO: stub
|
||||
return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// POST /admin/rss/{id}/delete
|
||||
let deleteCustomFeed feedId : HttpHandler = fun next ctx -> task {
|
||||
let conn = ctx.Conn
|
||||
|
@ -329,7 +354,7 @@ let deleteCustomFeed feedId : HttpHandler = fun next ctx -> task {
|
|||
WebLogCache.set webLog
|
||||
do! addMessage ctx { UserMessage.success with message = "Custom feed deleted successfully" }
|
||||
else
|
||||
do! addMessage ctx { UserMessage.warning with message = "Post not found; nothing deleted" }
|
||||
do! addMessage ctx { UserMessage.warning with message = "Custom feed not found; no action taken" }
|
||||
return! redirectToGet (WebLog.relativeUrl webLog (Permalink "admin/settings/rss")) next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
|
|
@ -95,7 +95,10 @@ let router : HttpHandler = choose [
|
|||
])
|
||||
subRoute "/settings" (choose [
|
||||
route "" >=> Admin.settings
|
||||
route "/rss" >=> Feed.editSettings
|
||||
subRoute "/rss" (choose [
|
||||
route "" >=> Feed.editSettings
|
||||
routef "/%s/edit" Feed.editCustomFeed
|
||||
])
|
||||
subRoute "/tag-mapping" (choose [
|
||||
route "s" >=> Admin.tagMappings
|
||||
routef "/%s/edit" Admin.editMapping
|
||||
|
@ -122,6 +125,7 @@ let router : HttpHandler = choose [
|
|||
route "" >=> Admin.saveSettings
|
||||
subRoute "/rss" (choose [
|
||||
route "" >=> Feed.saveSettings
|
||||
route "/save" >=> Feed.saveCustomFeed
|
||||
routef "/%s/delete" Feed.deleteCustomFeed
|
||||
])
|
||||
subRoute "/tag-mapping" (choose [
|
||||
|
|
|
@ -220,10 +220,11 @@ let main args =
|
|||
[ // Domain types
|
||||
typeof<CustomFeed>; typeof<MetaItem>; typeof<Page>; typeof<RssOptions>; typeof<TagMap>; typeof<WebLog>
|
||||
// View models
|
||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
||||
typeof<EditCategoryModel>; typeof<EditPageModel>; typeof<EditPostModel>; typeof<EditRssModel>
|
||||
typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel>; typeof<ManagePermalinksModel>
|
||||
typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>; typeof<UserMessage>
|
||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>; typeof<DisplayPage>
|
||||
typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditPageModel>; typeof<EditPostModel>
|
||||
typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel>
|
||||
typeof<ManagePermalinksModel>; typeof<PostDisplay>; typeof<PostListItem>; typeof<SettingsModel>
|
||||
typeof<UserMessage>
|
||||
// Framework types
|
||||
typeof<AntiforgeryTokenSet>; typeof<int option>; typeof<KeyValuePair>; typeof<MetaItem list>
|
||||
typeof<string list>; typeof<string option>; typeof<TagMap list>
|
||||
|
|
186
src/MyWebLog/themes/admin/custom-feed-edit.liquid
Normal file
186
src/MyWebLog/themes/admin/custom-feed-edit.liquid
Normal file
|
@ -0,0 +1,186 @@
|
|||
<h2 class="my-3">{{ page_title }}</h2>
|
||||
<article>
|
||||
<form action="{{ "admin/settings/rss/save" | relative_link }}" method="post">
|
||||
<input type="hidden" name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||
{%- assign is_cat = model.source_type == "category" -%}
|
||||
<div class="container">
|
||||
<div class="row pb-3">
|
||||
<div class="col-12 col-xl-10 offset-xl-1">
|
||||
<fieldset class="container pb-0">
|
||||
<legend>Feed Source</legend>
|
||||
<div class="row d-flex align-items-center">
|
||||
<div class="col-1 d-flex justify-content-end pb-3">
|
||||
<div class="form-check form-check-inline me-0">
|
||||
<input type="radio" name="sourceType" id="sourceTypeCat" class="form-check-input" value="category"
|
||||
{% if is_cat %}checked="checked"{% endif %} onclick="Admin.customFeedBy('category')">
|
||||
<label for="sourceTypeCat" class="form-check-label d-none">Category</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-11 col-lg-5 pb-3">
|
||||
<div class="form-floating">
|
||||
<select name="sourceValue" id="sourceValueCat" class="form-control" required
|
||||
{% unless is_cat %}disabled="disabled"{% endunless %}>
|
||||
<option value="">– Select Category –</option>
|
||||
{% for cat in categories -%}
|
||||
<option value="{{ cat.id }}"
|
||||
{%- if is_cat and model.source_value == cat.id %} selected="selected"{% endif -%}>
|
||||
{% for it in cat.parent_names %}{{ it }} ⟩ {% endfor %}{{ cat.name }}
|
||||
</option>
|
||||
{%- endfor %}
|
||||
</select>
|
||||
<label for="sourceValueCat">Category</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-1 d-flex justify-content-end pb-3">
|
||||
<div class="form-check form-check-inline me-0">
|
||||
<input type="radio" name="sourceType" id="sourceTypeTag" class="form-check-input" value="tag"
|
||||
{%- unless is_cat %} checked="checked"{% endunless %} onclick="Admin.customFeedBy('tag')">
|
||||
<label for="sourceTypeTag" class="form-check-label d-none">Tag</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-11 col-lg-5 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="sourceValue" id="sourceValueTag" class="form-control" placeholder="Tag"
|
||||
{%- if is_cat %} disabled="disabled"{% endif %} required
|
||||
{%- unless is_cat %} value="{{ model.source_value }}"{% endunless %}>
|
||||
<label for="sourceValueTag">Tag</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-lg-5 offset-lg-1 col-xl-4 offset-xl-2 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="path" id="path" class="form-control" placeholder="Relative Feed Path"
|
||||
value="{{ model.path }}">
|
||||
<label for="path">Relative Feed Path</label>
|
||||
<span class="form-text fst-italic">Appended to {{ web_log.url_base }}/</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-lg-5 col-xl-4 pb-3 d-flex align-self-center justify-content-center">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" name="isPodcast" id="isPodcast" class="form-check-input" value="true"
|
||||
{%- if model.is_podcast %} checked="checked"{% endif %} onclick="Admin.checkPodcast()">
|
||||
<label for="isPodcast" class="form-check-label">Is Podcast Feed</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col">
|
||||
<fieldset class="container" id="podcastFields"{% unless model.is_podcast %} disabled="disabled"{%endunless%}>
|
||||
<legend>Podcast Settings</legend>
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="title" id="title" class="form-control" placeholder="Title" required
|
||||
value="{{ model.title }}">
|
||||
<label for="title">Title</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="subtitle" id="subtitle" class="form-control" placeholder="Subtitle"
|
||||
value="{{ model.subtitle }}">
|
||||
<label for="subtitle">Podcast Subtitle</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="summary" id="summary" class="form-control" placeholder="Summary" required
|
||||
value="{{ model.summary }}">
|
||||
<label for="summary">Summary</label>
|
||||
<span class="form-text fst-italic">Displayed in podcast directories</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-5 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="displayedAuthor" id="displayedAuthor" class="form-control"
|
||||
placeholder="Author" required value="{{ model.displayed_author }}">
|
||||
<label for="displayedAuthor">Displayed Author</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-5 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="email" id="email" class="form-control" placeholder="Email" required
|
||||
value="{{ model.email }}">
|
||||
<label for="email">Author E-mail</label>
|
||||
<span class="form-text fst-italic">For iTunes, must match registered e-mail</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-2 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="number" name="itemsInFeed" id="itemsInFeed" class="form-control" placeholder="Items"
|
||||
required value="{{ model.items_in_feed }}">
|
||||
<label for="itemsInFeed"># Episodes</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="itunesCategory" id="itunesCategory" class="form-control"
|
||||
placeholder="iTunes Category" required value="{{ model.itunes_category }}">
|
||||
<label for="itunesCategory">iTunes Category</label>
|
||||
<span class="form-text fst-italic">
|
||||
<a href="https://www.thepodcasthost.com/planning/itunes-podcast-categories/" target="_blank"
|
||||
rel="noopener">
|
||||
iTunes Category / Subcategory List
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 pb-3">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="itunesSubcategory" id="itunesSubcategory" class="form-control"
|
||||
placeholder="iTunes Subcategory" value="{{ model.itunes_subcategory }}">
|
||||
<label for="itunesSubcategory">iTunes Subcategory</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-6 col-lg-4">
|
||||
<div class="form-floating">
|
||||
<select name="explicit" id="explicit" class="form-control" required>
|
||||
<option value="yes"{% if model.explicit == "yes" %} selected="selected"{% endif %}>Yes</option>
|
||||
<option value="no"{% if model.explicit == "no" %} selected="selected"{% endif %}>No</option>
|
||||
<option value="clean"{% if model.explicit == "clean" %} selected="selected"{% endif %}>
|
||||
Clean
|
||||
</option>
|
||||
</select>
|
||||
<label for="explicit">Explicit Rating</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="defaultMediaType" id="defaultMediaType" class="form-control"
|
||||
placeholder="Media Type" value="{{ model.default_media_type }}">
|
||||
<label for="defaultMediaType">Default Media Type</label>
|
||||
<span class="form-text fst-italic">Optional; blank for no default</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-4">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="mediaBaseUrl" id="mediaBaseUrl" class="form-control"
|
||||
placeholder="Media Base URL" value="{{ model.media_base_url }}">
|
||||
<label for="mediaBaseUrl">Media Base URL</label>
|
||||
<span class="form-text fst-italic">Optional; prepended to episode media file if present</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pb-3">
|
||||
<div class="col text-center">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
|
@ -64,7 +64,9 @@
|
|||
</div>
|
||||
</form>
|
||||
<h3>Custom Feeds</h3>
|
||||
<a class="btn btn-sm btn-secondary" href="{{ 'admin/rss/new/edit' | relative_link }}">Add a New Custom Feed</a>
|
||||
<a class="btn btn-sm btn-secondary" href="{{ 'admin/settings/rss/new/edit' | relative_link }}">
|
||||
Add a New Custom Feed
|
||||
</a>
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
@ -139,6 +139,31 @@
|
|||
this.nextPermalink++
|
||||
},
|
||||
|
||||
/**
|
||||
* Check to enable or disable podcast fields
|
||||
*/
|
||||
checkPodcast() {
|
||||
document.getElementById("podcastFields").disabled = !document.getElementById("isPodcast").checked
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the source of a custom RSS feed
|
||||
* @param source The source that was selected
|
||||
*/
|
||||
customFeedBy(source) {
|
||||
const categoryInput = document.getElementById("sourceValueCat")
|
||||
const tagInput = document.getElementById("sourceValueTag")
|
||||
if (source === "category") {
|
||||
tagInput.value = ""
|
||||
tagInput.disabled = true
|
||||
categoryInput.disabled = false
|
||||
} else {
|
||||
categoryInput.selectedIndex = -1
|
||||
categoryInput.disabled = true
|
||||
tagInput.disabled = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a metadata item
|
||||
* @param idx The index of the metadata item to remove
|
||||
|
|
Loading…
Reference in New Issue
Block a user