From 90bca34be30163758fae45cf55a82e78496d22f8 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 7 Feb 2024 21:59:43 -0500 Subject: [PATCH] WIP on chapter list page (#6) --- src/MyWebLog.Domain/ViewModels.fs | 59 +++++++++++++++++++++++++++++++ src/MyWebLog/DotLiquidBespoke.fs | 17 ++++----- src/MyWebLog/Handlers/Post.fs | 15 ++++++++ src/MyWebLog/Handlers/Routes.fs | 1 + src/admin-theme/chapters.liquid | 38 ++++++++++++++++++++ 5 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 src/admin-theme/chapters.liquid diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index fdec2ca..8352ce9 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -3,6 +3,7 @@ open System open MyWebLog open NodaTime +open NodaTime.Text /// Helper functions for view models [] @@ -71,6 +72,45 @@ type DisplayCategory = { PostCount: int } +/// A display version of an episode chapter +type DisplayChapter = { + /// The start time of the chapter (HH:MM:SS.FF format) + StartTime: string + + /// The title of the chapter + Title: string + + /// An image to display for this chapter + ImageUrl: string + + /// Whether this chapter should be displayed in podcast players + IsHidden: bool + + /// The end time of the chapter (HH:MM:SS.FF format) + EndTime: string + + /// The name of a location + LocationName: string + + /// The geographic coordinates of the location + LocationGeo: string + + /// An OpenStreetMap query for this location + LocationOsm: string +} with + + /// Create a display chapter from a chapter + static member FromChapter (chapter: Chapter) = + let pattern = DurationPattern.CreateWithInvariantCulture("H:mm:ss.ff") + { StartTime = pattern.Format(chapter.StartTime) + Title = defaultArg chapter.Title "" + ImageUrl = defaultArg chapter.ImageUrl "" + IsHidden = defaultArg chapter.IsHidden false + EndTime = chapter.EndTime |> Option.map pattern.Format |> Option.defaultValue "" + LocationName = chapter.Location |> Option.map (fun l -> l.Name) |> Option.defaultValue "" + LocationGeo = chapter.Location |> Option.map (fun l -> l.Geo) |> Option.flatten |> Option.defaultValue "" + LocationOsm = chapter.Location |> Option.map (fun l -> l.Osm) |> Option.flatten |> Option.defaultValue "" } + /// A display version of a custom feed definition type DisplayCustomFeed = { @@ -984,6 +1024,25 @@ type LogOnModel = { { EmailAddress = ""; Password = ""; ReturnTo = None } +/// View model to manage chapters +[] +type ManageChaptersModel = { + /// The post ID for the chapters being edited + Id: string + + /// The title of the post for which chapters are being edited + Title: string + + Chapters: Chapter array +} with + + /// Create a model from a post and its episode's chapters + static member Create (post: Post) = + { Id = string post.Id + Title = post.Title + Chapters = Array.ofList post.Episode.Value.Chapters.Value } + + /// View model to manage permalinks [] type ManagePermalinksModel = { diff --git a/src/MyWebLog/DotLiquidBespoke.fs b/src/MyWebLog/DotLiquidBespoke.fs index d5edcca..9ef5b45 100644 --- a/src/MyWebLog/DotLiquidBespoke.fs +++ b/src/MyWebLog/DotLiquidBespoke.fs @@ -227,14 +227,15 @@ let register () = typeof; typeof; typeof; typeof; typeof typeof; typeof; typeof; typeof; typeof // View models - typeof; typeof; typeof - typeof; typeof; typeof - typeof; typeof; typeof - typeof; typeof; typeof - typeof; typeof; typeof - typeof; typeof; typeof - typeof; typeof; typeof - typeof; typeof; typeof + typeof; typeof; typeof + typeof; typeof; typeof + typeof; typeof; typeof + typeof; typeof; typeof + typeof; typeof; typeof + typeof; typeof; typeof + typeof; typeof; typeof + typeof; typeof; typeof + typeof; typeof // Framework types typeof; typeof; typeof; typeof typeof; typeof; typeof; typeof diff --git a/src/MyWebLog/Handlers/Post.fs b/src/MyWebLog/Handlers/Post.fs index c5d6bec..ca68ebb 100644 --- a/src/MyWebLog/Handlers/Post.fs +++ b/src/MyWebLog/Handlers/Post.fs @@ -371,6 +371,21 @@ let deleteRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fu | _, None -> return! Error.notFound next ctx } +// GET /admin/post/{id}/chapters +let chapters postId : HttpHandler = requireAccess Author >=> fun next ctx -> task { + match! ctx.Data.Post.FindById (PostId postId) ctx.WebLog.Id with + | Some post + when Option.isSome post.Episode + && Option.isSome post.Episode.Value.Chapters + && canEdit post.AuthorId ctx -> + return! + hashForPage "Manage Chapters" + |> withAntiCsrf ctx + |> addToHash ViewContext.Model (ManageChaptersModel.Create post) + |> adminView "chapters" next ctx + | Some _ | None -> return! Error.notFound next ctx +} + // POST /admin/post/save let save : HttpHandler = requireAccess Author >=> fun next ctx -> task { let! model = ctx.BindFormAsync() diff --git a/src/MyWebLog/Handlers/Routes.fs b/src/MyWebLog/Handlers/Routes.fs index 996553c..7028f04 100644 --- a/src/MyWebLog/Handlers/Routes.fs +++ b/src/MyWebLog/Handlers/Routes.fs @@ -129,6 +129,7 @@ let router : HttpHandler = choose [ routef "/%s/permalinks" Post.editPermalinks routef "/%s/revision/%s/preview" Post.previewRevision routef "/%s/revisions" Post.editRevisions + routef "/%s/chapters" Post.chapters ]) subRoute "/settings" (requireAccess WebLogAdmin >=> choose [ route "" >=> Admin.WebLog.settings diff --git a/src/admin-theme/chapters.liquid b/src/admin-theme/chapters.liquid new file mode 100644 index 0000000..fa8a38a --- /dev/null +++ b/src/admin-theme/chapters.liquid @@ -0,0 +1,38 @@ +

{{ page_title }}

+
+
+ + +
+
+
+

+ {{ model.title }}
+ + + « Back to Edit Post + + +

+
+
+
Start
+
Title
+
Image?
+
Location?
+
+ {% for chapter in model.chapters %} +
+
{{ chapter.start_time }}
+
{{ chapter.title }}
+
{% if chapter.image_url == "" %}N{% else %}Y{% endif %}
+
{% if chapter.location %}Y{% else %}N{% endif %}
+
+ {% endfor %} +
+ {% assign new_link = "admin/post/" | append: model.id | append: "/chapter/new" | relative_link %} + Add a New Chapter +
+
+
+