From d378f690e422da68fc7d71fb0824fb2af65b7737 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 5 Feb 2024 22:28:07 -0500 Subject: [PATCH] Add chapter source fields (#6) --- src/MyWebLog.Domain/ViewModels.fs | 23 ++++++++-- src/MyWebLog.Tests/Domain/ViewModelsTests.fs | 47 +++++++++++++++++++- src/admin-theme/_edit-common.liquid | 6 +++ src/admin-theme/post-edit.liquid | 32 ++++++++++++- src/admin-theme/wwwroot/admin.js | 30 +++++++++++-- 5 files changed, 127 insertions(+), 11 deletions(-) diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index f0036ee..fdec2ca 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -648,6 +648,9 @@ type EditPostModel = { /// The explicit rating for this episode (optional, defaults to podcast setting) Explicit: string + /// The chapter source ("internal" for chapters defined here, "external" for a file link, "none" if none defined) + ChapterSource: string + /// The URL for the chapter file for the episode (may be permalink; optional) ChapterFile: string @@ -713,6 +716,9 @@ type EditPostModel = { ImageUrl = defaultArg episode.ImageUrl "" Subtitle = defaultArg episode.Subtitle "" Explicit = defaultArg (episode.Explicit |> Option.map string) "" + ChapterSource = if Option.isSome episode.Chapters then "internal" + elif Option.isSome episode.ChapterFile then "external" + else "none" ChapterFile = defaultArg episode.ChapterFile "" ChapterType = defaultArg episode.ChapterType "" ContainsWaypoints = defaultArg episode.ChapterWaypoints false @@ -773,10 +779,19 @@ type EditPostModel = { ImageUrl = noneIfBlank this.ImageUrl Subtitle = noneIfBlank this.Subtitle Explicit = noneIfBlank this.Explicit |> Option.map ExplicitRating.Parse - Chapters = match post.Episode with Some e -> e.Chapters | None -> None - ChapterFile = noneIfBlank this.ChapterFile - ChapterType = noneIfBlank this.ChapterType - ChapterWaypoints = if this.ContainsWaypoints then Some true else None + Chapters = if this.ChapterSource = "internal" then + match post.Episode with + | Some e when Option.isSome e.Chapters -> e.Chapters + | Some _ + | None -> Some [] + else None + ChapterFile = if this.ChapterSource = "external" then noneIfBlank this.ChapterFile + else None + ChapterType = if this.ChapterSource = "external" then noneIfBlank this.ChapterType + else None + ChapterWaypoints = if this.ChapterSource = "none" then None + elif this.ContainsWaypoints then Some true + else None TranscriptUrl = noneIfBlank this.TranscriptUrl TranscriptType = noneIfBlank this.TranscriptType TranscriptLang = noneIfBlank this.TranscriptLang diff --git a/src/MyWebLog.Tests/Domain/ViewModelsTests.fs b/src/MyWebLog.Tests/Domain/ViewModelsTests.fs index 6bc7ecf..969cf90 100644 --- a/src/MyWebLog.Tests/Domain/ViewModelsTests.fs +++ b/src/MyWebLog.Tests/Domain/ViewModelsTests.fs @@ -621,7 +621,7 @@ let editPostModelTests = testList "EditPostModel" [ ImageUrl = Some "uploads/podcast-cover.jpg" Subtitle = Some "Narration" Explicit = Some Clean - Chapters = None // for future implementation + Chapters = None ChapterFile = Some "uploads/1970/01/chapters.txt" ChapterType = Some "chapters" ChapterWaypoints = Some true @@ -661,6 +661,7 @@ let editPostModelTests = testList "EditPostModel" [ Expect.equal model.ImageUrl "" "ImageUrl not filled properly" Expect.equal model.Subtitle "" "Subtitle not filled properly" Expect.equal model.Explicit "" "Explicit not filled properly" + Expect.equal model.ChapterSource "none" "ChapterSource not filled properly" Expect.equal model.ChapterFile "" "ChapterFile not filled properly" Expect.equal model.ChapterType "" "ChapterType not filled properly" Expect.isFalse model.ContainsWaypoints "ContainsWaypoints should not have been set" @@ -673,7 +674,7 @@ let editPostModelTests = testList "EditPostModel" [ Expect.equal model.EpisodeNumber "" "EpisodeNumber not filled properly" Expect.equal model.EpisodeDescription "" "EpisodeDescription not filled properly" } - test "succeeds for full post" { + test "succeeds for full post with external chapters" { let model = EditPostModel.FromPost { WebLog.Empty with TimeZone = "Etc/GMT+1" } fullPost Expect.equal model.PostId "a-post" "PostId not filled properly" Expect.equal model.Title "A Post" "Title not filled properly" @@ -704,6 +705,7 @@ let editPostModelTests = testList "EditPostModel" [ Expect.equal model.ImageUrl "uploads/podcast-cover.jpg" "ImageUrl not filled properly" Expect.equal model.Subtitle "Narration" "Subtitle not filled properly" Expect.equal model.Explicit "clean" "Explicit not filled properly" + Expect.equal model.ChapterSource "external" "ChapterSource not filled properly" Expect.equal model.ChapterFile "uploads/1970/01/chapters.txt" "ChapterFile not filled properly" Expect.equal model.ChapterType "chapters" "ChapterType not filled properly" Expect.isTrue model.ContainsWaypoints "ContainsWaypoints should have been set" @@ -716,6 +718,19 @@ let editPostModelTests = testList "EditPostModel" [ Expect.equal model.EpisodeNumber "322" "EpisodeNumber not filled properly" Expect.equal model.EpisodeDescription "Episode 322" "EpisodeDescription not filled properly" } + test "succeeds for full post with internal chapters" { + let model = + EditPostModel.FromPost + { WebLog.Empty with TimeZone = "Etc/GMT+1" } + { fullPost with + Episode = + Some + { fullPost.Episode.Value with + Chapters = Some [] + ChapterFile = None + ChapterType = None } } + Expect.equal model.ChapterSource "internal" "ChapterSource not filled properly" + } ] testList "IsNew" [ test "succeeds for a new post" { @@ -747,6 +762,7 @@ let editPostModelTests = testList "EditPostModel" [ ImageUrl = "updated-cover.png" Subtitle = "Talking" Explicit = "no" + ChapterSource = "external" ChapterFile = "updated-chapters.txt" ChapterType = "indexes" TranscriptUrl = "updated-transcript.txt" @@ -787,6 +803,7 @@ let editPostModelTests = testList "EditPostModel" [ Expect.equal ep.ImageUrl (Some "updated-cover.png") "ImageUrl not filled properly" Expect.equal ep.Subtitle (Some "Talking") "Subtitle not filled properly" Expect.equal ep.Explicit (Some No) "ExplicitRating not filled properly" + Expect.isNone ep.Chapters "Chapters should have had no value" Expect.equal ep.ChapterFile (Some "updated-chapters.txt") "ChapterFile not filled properly" Expect.equal ep.ChapterType (Some "indexes") "ChapterType not filled properly" Expect.equal ep.ChapterWaypoints (Some true) "ChapterWaypoints should have been set" @@ -840,6 +857,32 @@ let editPostModelTests = testList "EditPostModel" [ Expect.isNone ep.EpisodeNumber "EpisodeNumber not filled properly" Expect.isNone ep.EpisodeDescription "EpisodeDescription not filled properly" } + test "succeeds for a podcast episode with internal chapters" { + let minModel = + { updatedModel with + ChapterSource = "internal" + ChapterFile = "" + ChapterType = "" } + let post = minModel.UpdatePost fullPost (Noda.epoch + Duration.FromDays 500) + Expect.isSome post.Episode "There should have been a podcast episode" + let ep = post.Episode.Value + Expect.equal ep.Chapters (Some []) "Chapters not filled properly" + Expect.isNone ep.ChapterFile "ChapterFile not filled properly" + Expect.isNone ep.ChapterType "ChapterType not filled properly" + } + test "succeeds for a podcast episode with no chapters" { + let minModel = { updatedModel with ChapterSource = "none" } + let post = + minModel.UpdatePost + { fullPost with Episode = Some { fullPost.Episode.Value with Chapters = Some [] } } + (Noda.epoch + Duration.FromDays 500) + Expect.isSome post.Episode "There should have been a podcast episode" + let ep = post.Episode.Value + Expect.isNone ep.Chapters "Chapters not filled properly" + Expect.isNone ep.ChapterFile "ChapterFile not filled properly" + Expect.isNone ep.ChapterType "ChapterType not filled properly" + Expect.isNone ep.ChapterWaypoints "ChapterWaypoints not filled properly" + } test "succeeds for no podcast episode and no template" { let post = { updatedModel with IsEpisode = false; Template = "" }.UpdatePost fullPost Noda.epoch Expect.isNone post.Template "Template not filled properly" diff --git a/src/admin-theme/_edit-common.liquid b/src/admin-theme/_edit-common.liquid index 214de29..e72622b 100644 --- a/src/admin-theme/_edit-common.liquid +++ b/src/admin-theme/_edit-common.liquid @@ -13,6 +13,12 @@ Manage Permalinks Manage Revisions + {% if model.chapter_source == "internal" %} + + + Manage Chapters + + {% endif %} {%- endunless -%} diff --git a/src/admin-theme/post-edit.liquid b/src/admin-theme/post-edit.liquid index 4582fcc..03dcdd7 100644 --- a/src/admin-theme/post-edit.liquid +++ b/src/admin-theme/post-edit.liquid @@ -107,13 +107,43 @@ +
+
+
Chapters
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
-
Optional; relative URL served from this web log
+
Relative URL served from this web log
diff --git a/src/admin-theme/wwwroot/admin.js b/src/admin-theme/wwwroot/admin.js index 308b78a..9575a54 100644 --- a/src/admin-theme/wwwroot/admin.js +++ b/src/admin-theme/wwwroot/admin.js @@ -212,15 +212,37 @@ this.Admin = { this.nextPermalink++ }, + /** + * Set the chapter type for a podcast episode + * @param {"none"|"internal"|"external"} src The source for chapters for this episode + */ + setChapterSource(src) { + document.getElementById("containsWaypoints").disabled = src === "none" + const isDisabled = src === "none" || src === "internal" + const chapterFile = document.getElementById("chapterFile") + chapterFile.disabled = isDisabled + chapterFile.required = !isDisabled + document.getElementById("chapterType").disabled = isDisabled + const link = document.getElementById("chapterEditLink") + if (link) link.style.display = src === "none" || src === "external" ? "none" : "" + }, + /** * Enable or disable podcast fields */ 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) + let fields = [ + "media", "mediaType", "length", "duration", "subtitle", "imageUrl", "explicit", "transcriptUrl", "transcriptType", + "transcriptLang", "transcriptCaptions", "seasonNumber", "seasonDescription", "episodeNumber", "episodeDescription" + ] + if (disabled) { + fields.push("chapterFile", "chapterType", "containsWaypoints") + } else { + const src = [...document.getElementsByName("ChapterSource")].filter(it => it.checked)[0].value + this.setChapterSource(src) + } + fields.forEach(it => document.getElementById(it).disabled = disabled) }, /**