Add episode fields to UI (#9)

- Add chapter fields (#5)
- Add transcript fields (#8)
This commit is contained in:
Daniel J. Summers 2022-06-27 17:47:00 -04:00
parent 9fbb1bb14d
commit 80c65bcad6
5 changed files with 244 additions and 78 deletions

View File

@ -141,7 +141,7 @@ type SQLitePostData (conn : SqliteConnection) =
let updatePostEpisode (post : Post) = backgroundTask {
use cmd = conn.CreateCommand ()
cmd.CommandText <- "SELECT COUNT(post_id) FROM post_episode WHERE post_id = @postId"
cmd.Parameters.AddWithValue ("@postId", post.id) |> ignore
cmd.Parameters.AddWithValue ("@postId", PostId.toString post.id) |> ignore
let! count = count cmd
if count = 1 then
match post.episode with
@ -176,13 +176,13 @@ type SQLitePostData (conn : SqliteConnection) =
| Some ep ->
cmd.CommandText <-
"""INSERT INTO post_episode (
post_id, media, length, duration, media_type, image_url, subtitle, explicit, chapter_file,
chapter_type, transcript_url, transcript_type, transcript_lang, transcript_captions,
season_number, season_description, episode_number, episode_description
post_id, media, length, duration, media_type, image_url, subtitle, explicit,
chapter_file, chapter_type, transcript_url, transcript_type, transcript_lang,
transcript_captions, season_number, season_description, episode_number, episode_description
) VALUES (
@media, @length, @duration, @mediaType, @imageUrl, @subtitle, @explicit, @chapterFile,
@chapterType, @transcriptUrl, @transcriptType, @transcriptLang, @transcriptCaptions,
@seasonNumber, @seasonDescription, @episodeNumber, @episodeDescription
@postId, @media, @length, @duration, @mediaType, @imageUrl, @subtitle, @explicit,
@chapterFile, @chapterType, @transcriptUrl, @transcriptType, @transcriptLang,
@transcriptCaptions, @seasonNumber, @seasonDescription, @episodeNumber, @episodeDescription
)"""
addEpisodeParameters cmd ep
do! write cmd
@ -530,7 +530,7 @@ type SQLitePostData (conn : SqliteConnection) =
use cmd = conn.CreateCommand ()
cmd.CommandText <-
"""UPDATE post
SET author_id = @author_id,
SET author_id = @authorId,
status = @status,
title = @title,
permalink = @permalink,

View File

@ -9,7 +9,7 @@ module private Helpers =
/// Create a string option if a string is blank
let noneIfBlank (it : string) =
match it.Trim () with "" -> None | trimmed -> Some trimmed
match (defaultArg (Option.ofObj it) "").Trim () with "" -> None | trimmed -> Some trimmed
/// Details about a category, used to display category lists
@ -448,10 +448,10 @@ type EditPostModel =
transcriptLang : string
/// Whether the provided transcript should be presented as captions
transcriptCaptions : bool option
transcriptCaptions : bool
/// The season number (optional)
seasonNumber : int option
seasonNumber : int
/// A description of this season (optional, ignored if season number is not provided)
seasonDescription : string
@ -489,7 +489,7 @@ type EditPostModel =
isEpisode = Option.isSome post.episode
media = episode.media
length = episode.length
duration = defaultArg (episode.duration |> Option.map (fun it -> it.ToString "HH:mm:SS")) ""
duration = defaultArg (episode.duration |> Option.map (fun it -> it.ToString """hh\:mm\:ss""")) ""
mediaType = defaultArg episode.mediaType ""
imageUrl = defaultArg episode.imageUrl ""
subtitle = defaultArg episode.subtitle ""
@ -499,8 +499,8 @@ type EditPostModel =
transcriptUrl = defaultArg episode.transcriptUrl ""
transcriptType = defaultArg episode.transcriptType ""
transcriptLang = defaultArg episode.transcriptLang ""
transcriptCaptions = episode.transcriptCaptions
seasonNumber = episode.seasonNumber
transcriptCaptions = defaultArg episode.transcriptCaptions false
seasonNumber = defaultArg episode.seasonNumber 0
seasonDescription = defaultArg episode.seasonDescription ""
episodeNumber = defaultArg (episode.episodeNumber |> Option.map string) ""
episodeDescription = defaultArg episode.episodeDescription ""
@ -536,27 +536,24 @@ type EditPostModel =
Some {
media = this.media
length = this.length
duration = match this.duration.Trim () with
| "" -> None
| dur -> Some (TimeSpan.Parse dur)
mediaType = match this.mediaType.Trim () with "" -> None | mt -> Some mt
imageUrl = match this.imageUrl.Trim () with "" -> None | iu -> Some iu
subtitle = match this.subtitle.Trim () with "" -> None | sub -> Some sub
explicit = match this.explicit.Trim () with
| "" -> None
| exp -> Some (ExplicitRating.parse exp)
chapterFile = match this.chapterFile.Trim () with "" -> None | cf -> Some cf
chapterType = match this.chapterType.Trim () with "" -> None | ct -> Some ct
transcriptUrl = match this.transcriptUrl.Trim () with "" -> None | tu -> Some tu
transcriptType = match this.transcriptType.Trim () with "" -> None | tt -> Some tt
transcriptLang = match this.transcriptLang.Trim () with "" -> None | tl -> Some tl
transcriptCaptions = this.transcriptCaptions
seasonNumber = this.seasonNumber
seasonDescription = match this.seasonDescription.Trim () with "" -> None | sd -> Some sd
episodeNumber = match this.episodeNumber.Trim () with
| "" -> None
| en -> Some (Double.Parse en)
episodeDescription = match this.episodeDescription.Trim () with "" -> None | ed -> Some ed
duration = noneIfBlank this.duration |> Option.map TimeSpan.Parse
mediaType = noneIfBlank this.mediaType
imageUrl = noneIfBlank this.imageUrl
subtitle = noneIfBlank this.subtitle
explicit = noneIfBlank this.explicit |> Option.map ExplicitRating.parse
chapterFile = noneIfBlank this.chapterFile
chapterType = noneIfBlank this.chapterType
transcriptUrl = noneIfBlank this.transcriptUrl
transcriptType = noneIfBlank this.transcriptType
transcriptLang = noneIfBlank this.transcriptLang
transcriptCaptions = if this.transcriptCaptions then Some true else None
seasonNumber = if this.seasonNumber = 0 then None else Some this.seasonNumber
seasonDescription = noneIfBlank this.seasonDescription
episodeNumber = match noneIfBlank this.episodeNumber |> Option.map Double.Parse with
| Some it when it = 0.0 -> None
| Some it -> Some (double it)
| None -> None
episodeDescription = noneIfBlank this.episodeDescription
}
else
None

View File

@ -2,6 +2,7 @@
module MyWebLog.Handlers.Post
open System
open System.Collections.Generic
open MyWebLog
/// Parse a slug and page number from an "everything else" URL
@ -237,13 +238,19 @@ let edit postId : HttpHandler = fun next ctx -> task {
let model = EditPostModel.fromPost webLog post
return!
Hash.FromAnonymousObject {|
csrf = csrfToken ctx
model = model
metadata = Array.zip model.metaNames model.metaValues
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |])
page_title = title
templates = templates
categories = cats
csrf = csrfToken ctx
model = model
metadata = Array.zip model.metaNames model.metaValues
|> Array.mapi (fun idx (name, value) -> [| string idx; name; value |])
page_title = title
templates = templates
categories = cats
explicit_values = [|
KeyValuePair.Create ("", "&ndash; Default &ndash;")
KeyValuePair.Create (ExplicitRating.toString Yes, "Yes")
KeyValuePair.Create (ExplicitRating.toString No, "No")
KeyValuePair.Create (ExplicitRating.toString Clean, "Clean")
|]
|}
|> viewForTheme "admin" "post-edit" next ctx
| None -> return! Error.notFound next ctx

View File

@ -22,12 +22,14 @@
</div>
<div class="mb-2">
<label for="text">Text</label> &nbsp; &nbsp;
<input type="radio" name="source" id="source_html" class="btn-check" value="HTML"
{%- if model.source == "HTML" %} checked="checked"{% endif %}>
<label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label>
<input type="radio" name="source" id="source_md" class="btn-check" value="Markdown"
{%- if model.source == "Markdown" %} checked="checked"{% endif %}>
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
<div class="btn-group btn-group-sm" role="group" aria-label="Text format button group">
<input type="radio" name="source" id="source_html" class="btn-check" value="HTML"
{%- if model.source == "HTML" %} checked="checked"{% endif %}>
<label class="btn btn-sm btn-outline-secondary" for="source_html">HTML</label>
<input type="radio" name="source" id="source_md" class="btn-check" value="Markdown"
{%- if model.source == "Markdown" %} checked="checked"{% endif %}>
<label class="btn btn-sm btn-outline-secondary" for="source_md">Markdown</label>
</div>
</div>
<div class="pb-3">
<textarea name="text" id="text" class="form-control" rows="20">{{ model.text }}</textarea>
@ -39,13 +41,194 @@
<div class="form-text">comma-delimited</div>
</div>
{% if model.status == "Draft" %}
<div class="form-check pb-2">
<div class="form-check form-switch pb-2">
<input type="checkbox" name="doPublish" id="doPublish" class="form-check-input" value="true">
<label for="doPublish" class="form-check-label">Publish This Post</label>
</div>
{% endif %}
<button type="submit" class="btn btn-primary pb-2">Save Changes</button>
<hr class="mb-3">
<fieldset class="mb-3">
<legend>
<span class="form-check form-switch">
<small>
<input type="checkbox" name="isEpisode" id="isEpisode" class="form-check-input" value="true"
data-bs-toggle="collapse" data-bs-target="#episodeItems" onclick="Admin.toggleEpisodeFields()"
{%- if model.is_episode %}checked="checked"{% endif %}>
</small>
<label for="isEpisode">Podcast Episode</label>
</span>
</legend>
<div id="episodeItems" class="container p-0 collapse{% if model.is_episode %} show{% endif %}">
<div class="row">
<div class="col-12 col-md-8 pb-3">
<div class="form-floating">
<input type="text" name="media" id="media" class="form-control" placeholder="Media" required
value="{{ model.media }}">
<label for="media">Media File</label>
<div class="form-text">Path to media; relative URL will be appended to base media path</div>
</div>
</div>
<div class="col-12 col-md-4 pb-3">
<div class="form-floating">
<input type="text" name="mediaType" id="mediaType" class="form-control" placeholder="Media Type"
value="{{ model.media_type }}">
<label for="mediaType">Media MIME Type</label>
<div class="form-text">Optional; overrides podcast default</div>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col">
<div class="form-floating">
<input type="number" name="length" id="length" class="form-control" placeholder="Length" required
value="{{ model.length }}">
<label for="length">Media Length (bytes)</label>
<div class="form-text">TODO: derive from above file name</div>
</div>
</div>
<div class="col">
<div class="form-floating">
<input type="text" name="duration" id="duration" class="form-control" placeholder="Duration"
value="{{ model.duration }}">
<label for="duration">Duration</label>
<div class="form-text">Recommended; enter in <code>HH:MM:SS</code> format</div>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col">
<div class="form-floating">
<input type="text" name="subtitle" id="subtitle" class="form-control" placeholder="Subtitle"
value="{{ model.subtitle }}">
<label for="subtitle">Subtitle</label>
<div class="form-text">Optional; a subtitle for this episode</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-8 pb-3">
<div class="form-floating">
<input type="text" name="imageUrl" id="imageUrl" class="form-control" placeholder="Image URL"
value="{{ model.image_url }}">
<label for="imageUrl">Image URL</label>
<div class="form-text">
Optional; overrides podcast default; relative URL served from this web log
</div>
</div>
</div>
<div class="col-12 col-md-4 pb-3">
<div class="form-floating">
<select name="explicit" id="explicit" class="form-control">
{% for exp_value in explicit_values %}
<option value="{{ exp_value[0] }}"
{%- if model.explicit == exp_value[0] %} selected="selected"{% endif -%}>
{{ exp_value[1] }}
</option>
{% endfor %}
</select>
<label for="explicit">Explicit Rating</label>
<div class="form-text">Optional; overrides podcast default</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-8 pb-3">
<div class="form-floating">
<input type="text" name="chapterFile" id="chapterFile" class="form-control"
placeholder="Chapter File" value="{{ model.chapter_file }}">
<label for="chapterFile">Chapter File</label>
<div class="form-text">Optional; relative URL served from this web log</div>
</div>
</div>
<div class="col-12 col-md-4 pb-3">
<div class="form-floating">
<input type="text" name="chapterType" id="chapterType" class="form-control"
placeholder="Chapter Type" value="{{ model.chapter_type }}">
<label for="chapterType">Chapter MIME Type</label>
<div class="form-text">
Optional; <code>application/json+chapters</code> assumed if chapter file ends with
<code>.json</code>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-8 pb-3">
<div class="form-floating">
<input type="text" name="transcriptUrl" id="transcriptUrl" class="form-control"
placeholder="Transcript URL" value="{{ model.transcript_url }}">
<label for="transcriptUrl">Transcript URL</label>
<div class="form-text">Optional; relative URL served from this web log</div>
</div>
</div>
<div class="col-12 col-md-4 pb-3">
<div class="form-floating">
<input type="text" name="transcriptType" id="transcriptType" class="form-control"
placeholder="Transcript Type" value="{{ model.transcript_type }}">
<label for="transcriptType">Transcript MIME Type</label>
<div class="form-text">Recommended if transcript file provided</div>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col">
<div class="form-floating">
<input type="text" name="transcriptLang" id="transcriptLang" class="form-control"
placeholder="Transcript Language" value="{{ model.transcript_lang }}">
<label for="transcriptLang">Transcript Language</label>
<div class="form-text">Optional; overrides podcast default</div>
</div>
</div>
<div class="col d-flex justify-content-center">
<div class="form-check form-switch align-self-center pb-3">
<input type="checkbox" name="transcriptCaptions" id="transcriptCaptions" class="form-check-input"
value="true" {% if model.transcript_captions %} checked="checked"{% endif %}>
<label for="transcriptCaptions">This is a captions file</label>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col col-md-4">
<div class="form-floating">
<input type="number" name="seasonNumber" id="seasonNumber" class="form-control"
placeholder="Season Number" value="{{ model.season_number }}">
<label for="seasonNumber">Season Number</label>
<div class="form-text">Optional</div>
</div>
</div>
<div class="col col-md-8">
<div class="form-floating">
<input type="text" name="seasonDescription" id="seasonDescription" class="form-control"
placeholder="Season Description" maxlength="128" value="{{ model.season_description }}">
<label for="seasonDescription">Season Description</label>
<div class="form-text">Optional</div>
</div>
</div>
</div>
<div class="row pb-3">
<div class="col col-md-4">
<div class="form-floating">
<input type="number" name="episodeNumber" id="episodeNumber" class="form-control" step="0.01"
placeholder="Episode Number" value="{{ model.episode_number }}">
<label for="episodeNumber">Episode Number</label>
<div class="form-text">Optional; up to 2 decimal points</div>
</div>
</div>
<div class="col col-md-8">
<div class="form-floating">
<input type="text" name="episodeDescription" id="episodeDescription" class="form-control"
placeholder="Episode Description" maxlength="128" value="{{ model.episode_description }}">
<label for="episodeDescription">Episode Description</label>
<div class="form-text">Optional</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => Admin.toggleEpisodeFields())
</script>
</fieldset>
<fieldset class="pb-3">
<legend>
Metadata
@ -55,11 +238,6 @@
</button>
</legend>
<div id="metaItemContainer" class="collapse">
{%- unless model.meta_names contains "episode_media_file" %}
<button class="btn btn-sm btn-secondary" onclick="Admin.addPodcastFields()" id="addPodcastFieldButton">
Add Podcast Episode Fields
</button>
{%- endunless %}
<div id="metaItems" class="container">
{%- for meta in metadata %}
<div id="meta_{{ meta[0] }}" class="row mb-3">

View File

@ -204,30 +204,14 @@
},
/**
* Add podcast episode metadata fields
* Enable or disable podcast fields
*/
addPodcastFields() {
[ [ "episode_media_file", true, "required; relative URL will be appended to configured base media path" ],
[ "episode_media_length", true, "required; file size in bytes" ],
[ "episode_duration", false, "suggested; format is HH:MM:SS" ],
[ "episode_media_type", false, "optional; blank uses podcast default" ],
[ "episode_image", false, "optional; relative URLs are served from this web log" ],
[ "episode_subtitle", false, "optional" ],
[ "episode_explicit", false, "optional; blank uses podcast default"]
].forEach(([fieldName, isRequired, hintText]) => {
const nameField = this.createMetaNameField()
nameField.value = fieldName
nameField.readOnly = true
const valueField = this.createMetaValueField()
valueField.required = isRequired
this.createMetaRow(
this.createMetaRemoveColumn(),
this.createMetaNameColumn(nameField),
this.createMetaValueColumn(valueField, hintText))
})
document.getElementById("addPodcastFieldButton").remove()
toggleEpisodeFields() {
const disabled = !document.getElementById("isEpisode").checked
;[ "media", "mediaType", "length", "duration", "subtitle", "imageUrl", "explicit", "chapterFile", "chapterType",
"transcriptUrl", "transcriptType", "transcriptLang", "transcriptCaptions", "seasonNumber", "seasonDescription",
"episodeNumber", "episodeDescription"
].forEach(it => document.getElementById(it).disabled = disabled)
},
/**