Complete main chapter functionality (#6)

- Edit page still needs UI tweaks
This commit is contained in:
Daniel J. Summers 2024-03-10 22:11:02 -04:00
parent 81039579ea
commit 5b8a632e9d
5 changed files with 80 additions and 22 deletions

View File

@ -164,7 +164,7 @@ type Location = {
Name: string Name: string
/// A geographic coordinate string (RFC 5870) /// A geographic coordinate string (RFC 5870)
Geo: string option Geo: string
/// An OpenStreetMap query /// An OpenStreetMap query
Osm: string option Osm: string option
@ -182,6 +182,9 @@ type Chapter = {
/// A URL for an image for this chapter /// A URL for an image for this chapter
ImageUrl: string option ImageUrl: string option
/// A URL with information pertaining to this chapter
Url: string option
/// Whether this chapter is hidden /// Whether this chapter is hidden
IsHidden: bool option IsHidden: bool option
@ -197,6 +200,7 @@ type Chapter = {
{ StartTime = Duration.Zero { StartTime = Duration.Zero
Title = None Title = None
ImageUrl = None ImageUrl = None
Url = None
IsHidden = None IsHidden = None
EndTime = None EndTime = None
Location = None } Location = None }

View File

@ -84,6 +84,9 @@ type DisplayChapter = {
/// An image to display for this chapter /// An image to display for this chapter
ImageUrl: string ImageUrl: string
/// A URL with information about this chapter
Url: string
/// Whether this chapter should be displayed in podcast players /// Whether this chapter should be displayed in podcast players
IsHidden: bool IsHidden: bool
@ -106,10 +109,11 @@ type DisplayChapter = {
{ StartTime = pattern.Format chapter.StartTime { StartTime = pattern.Format chapter.StartTime
Title = defaultArg chapter.Title "" Title = defaultArg chapter.Title ""
ImageUrl = defaultArg chapter.ImageUrl "" ImageUrl = defaultArg chapter.ImageUrl ""
Url = defaultArg chapter.Url ""
IsHidden = defaultArg chapter.IsHidden false IsHidden = defaultArg chapter.IsHidden false
EndTime = chapter.EndTime |> Option.map pattern.Format |> Option.defaultValue "" EndTime = chapter.EndTime |> Option.map pattern.Format |> Option.defaultValue ""
LocationName = chapter.Location |> Option.map _.Name |> Option.defaultValue "" LocationName = chapter.Location |> Option.map _.Name |> Option.defaultValue ""
LocationGeo = chapter.Location |> Option.map _.Geo |> Option.flatten |> Option.defaultValue "" LocationGeo = chapter.Location |> Option.map _.Geo |> Option.defaultValue ""
LocationOsm = chapter.Location |> Option.map _.Osm |> Option.flatten |> Option.defaultValue "" } LocationOsm = chapter.Location |> Option.map _.Osm |> Option.flatten |> Option.defaultValue "" }
@ -379,6 +383,9 @@ type EditChapterModel = {
/// An image to display for this chapter /// An image to display for this chapter
ImageUrl: string ImageUrl: string
/// A URL with information about this chapter
Url: string
/// Whether this chapter should be displayed in podcast players /// Whether this chapter should be displayed in podcast players
IsHidden: bool IsHidden: bool
@ -406,6 +413,7 @@ type EditChapterModel = {
StartTime = it.StartTime StartTime = it.StartTime
Title = it.Title Title = it.Title
ImageUrl = it.ImageUrl ImageUrl = it.ImageUrl
Url = it.Url
IsHidden = it.IsHidden IsHidden = it.IsHidden
EndTime = it.EndTime EndTime = it.EndTime
LocationName = it.LocationName LocationName = it.LocationName
@ -429,10 +437,11 @@ type EditChapterModel = {
let location = let location =
match noneIfBlank this.LocationName with match noneIfBlank this.LocationName with
| None -> None | None -> None
| Some name -> Some { Name = name; Geo = noneIfBlank this.LocationGeo; Osm = noneIfBlank this.LocationOsm } | Some name -> Some { Name = name; Geo = this.LocationGeo; Osm = noneIfBlank this.LocationOsm }
{ StartTime = parseDuration (nameof this.StartTime) this.StartTime { StartTime = parseDuration (nameof this.StartTime) this.StartTime
Title = noneIfBlank this.Title Title = noneIfBlank this.Title
ImageUrl = noneIfBlank this.ImageUrl ImageUrl = noneIfBlank this.ImageUrl
Url = noneIfBlank this.Url
IsHidden = if this.IsHidden then Some true else None IsHidden = if this.IsHidden then Some true else None
EndTime = noneIfBlank this.EndTime |> Option.map (parseDuration (nameof this.EndTime)) EndTime = noneIfBlank this.EndTime |> Option.map (parseDuration (nameof this.EndTime))
Location = location } Location = location }

View File

@ -421,6 +421,11 @@ open System.Threading.Tasks
/// Create a Task with a Some result for the given object /// Create a Task with a Some result for the given object
let someTask<'T> (it: 'T) = Task.FromResult(Some it) let someTask<'T> (it: 'T) = Task.FromResult(Some it)
/// Create an absolute URL from a string that may already be an absolute URL
let absoluteUrl (url: string) (ctx: HttpContext) =
if url.StartsWith "http" then url else ctx.WebLog.AbsoluteUrl (Permalink url)
open System.Collections.Generic open System.Collections.Generic
open MyWebLog.Data open MyWebLog.Data

View File

@ -208,18 +208,42 @@ let home : HttpHandler = fun next ctx -> task {
} }
// GET /{post-permalink}?chapters // GET /{post-permalink}?chapters
let chapters (post: Post) : HttpHandler = let chapters (post: Post) : HttpHandler = fun next ctx ->
match post.Episode with match post.Episode with
| Some ep -> | Some ep ->
match ep.Chapters with match ep.Chapters with
| Some chapters -> | Some chapters ->
let chapterData =
json chapters chapters
|> Seq.ofList
|> Seq.map (fun it ->
let dic = Dictionary<string, obj>()
dic["startTime"] <- Math.Round(it.StartTime.TotalSeconds, 2)
it.Title |> Option.iter (fun ttl -> dic["title"] <- ttl)
it.ImageUrl |> Option.iter (fun img -> dic["img"] <- absoluteUrl img ctx)
it.Url |> Option.iter (fun url -> dic["url"] <- absoluteUrl url ctx)
it.IsHidden |> Option.iter (fun toc -> dic["toc"] <- not toc)
it.EndTime |> Option.iter (fun ent -> dic["endTime"] <- Math.Round(ent.TotalSeconds, 2))
it.Location |> Option.iter (fun loc ->
let locData = Dictionary<string, obj>()
locData["name"] <- loc.Name
locData["geo"] <- loc.Geo
loc.Osm |> Option.iter (fun osm -> locData["osm"] <- osm)
dic["location"] <- locData)
dic)
|> ResizeArray
let jsonFile = Dictionary<string, obj>()
jsonFile["version"] <- "1.2.0"
jsonFile["title"] <- post.Title
jsonFile["fileName"] <- absoluteUrl ep.Media ctx
if defaultArg ep.ChapterWaypoints false then jsonFile["waypoints"] <- true
jsonFile["chapters"] <- chapterData
json jsonFile next ctx
| None -> | None ->
match ep.ChapterFile with match ep.ChapterFile with
| Some file -> redirectTo true file | Some file -> redirectTo true file next ctx
| None -> Error.notFound | None -> Error.notFound next ctx
| None -> Error.notFound | None -> Error.notFound next ctx
// ~~ ADMINISTRATION ~~ // ~~ ADMINISTRATION ~~

View File

@ -46,7 +46,7 @@ let chapterEdit (model: EditChapterModel) app = [
span [ _class "form-text" ] [ raw "Optional" ] span [ _class "form-text" ] [ raw "Optional" ]
] ]
] ]
div [ _class "col-12 col-lg-6 offset-xl-1 mb-3" ] [ div [ _class "col-12 col-lg-6 col-xl-5 mb-3" ] [
div [ _class "form-floating" ] [ div [ _class "form-floating" ] [
input [ _type "text"; _id "image_url"; _name "ImageUrl"; _class "form-control" input [ _type "text"; _id "image_url"; _name "ImageUrl"; _class "form-control"
_value model.ImageUrl; _placeholder "Image URL" ] _value model.ImageUrl; _placeholder "Image URL" ]
@ -56,7 +56,17 @@ let chapterEdit (model: EditChapterModel) app = [
] ]
] ]
] ]
div [ _class "col-12 col-lg-6 col-xl-4 mb-3 align-self-end d-flex flex-column" ] [ div [ _class "col-12 col-lg-6 col-xl-5 mb-3" ] [
div [ _class "form-floating" ] [
input [ _type "text"; _id "url"; _name "Url"; _class "form-control"; _value model.Url
_placeholder "URL" ]
label [ _for "url" ] [ raw "URL" ]
span [ _class "form-text" ] [
raw "Optional; informational link for this chapter"
]
]
]
div [ _class "col-12 col-lg-6 offset-lg-3 col-xl-2 offset-xl-0 mb-3 align-self-end d-flex flex-column" ] [
div [ _class "form-check form-switch mb-3" ] [ div [ _class "form-check form-switch mb-3" ] [
input [ _type "checkbox"; _id "is_hidden"; _name "IsHidden"; _class "form-check-input" input [ _type "checkbox"; _id "is_hidden"; _name "IsHidden"; _class "form-check-input"
_value "true" _value "true"
@ -87,11 +97,10 @@ let chapterEdit (model: EditChapterModel) app = [
div [ _class "col-6 col-lg-4 offset-lg-2 mb-3" ] [ div [ _class "col-6 col-lg-4 offset-lg-2 mb-3" ] [
div [ _class "form-floating" ] [ div [ _class "form-floating" ] [
input [ _type "text"; _id "location_geo"; _name "LocationGeo"; _class "form-control" input [ _type "text"; _id "location_geo"; _name "LocationGeo"; _class "form-control"
_value model.LocationGeo; _placeholder "Location Geo URL" _value model.LocationGeo; _placeholder "Location Geo URL"; _required
if not hasLoc then _disabled ] if not hasLoc then _disabled ]
label [ _for "location_geo" ] [ raw "Geo URL" ] label [ _for "location_geo" ] [ raw "Geo URL" ]
em [ _class "form-text" ] [ em [ _class "form-text" ] [
raw "Optional; "
a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/location/location.md#geo-recommended" a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/location/location.md#geo-recommended"
_target "_blank"; _rel "noopener" ] [ _target "_blank"; _rel "noopener" ] [
raw "see spec" raw "see spec"
@ -142,16 +151,19 @@ let chapterList withNew (model: ManageChaptersModel) app =
antiCsrf app antiCsrf app
input [ _type "hidden"; _name "Id"; _value model.Id ] input [ _type "hidden"; _name "Id"; _value model.Id ]
div [ _class "row mwl-table-heading" ] [ div [ _class "row mwl-table-heading" ] [
div [ _class "col" ] [ raw "Start" ] div [ _class "col-3 col-md-2" ] [ raw "Start" ]
div [ _class "col" ] [ raw "Title" ] div [ _class "col-3 col-md-6 col-lg-8" ] [ raw "Title" ]
div [ _class "col" ] [ raw "Image?" ] div [ _class "col-3 col-md-2 col-lg-1 text-center" ] [ raw "Image?" ]
div [ _class "col" ] [ raw "Location?" ] div [ _class "col-3 col-md-2 col-lg-1 text-center" ] [ raw "Location?" ]
] ]
yield! model.Chapters |> List.mapi (fun idx chapter -> yield! model.Chapters |> List.mapi (fun idx chapter ->
div [ _class "row mwl-table-detail"; _id $"chapter{idx}" ] [ div [ _class "row mwl-table-detail"; _id $"chapter{idx}" ] [
div [ _class "col" ] [ txt (startTimePattern.Format chapter.StartTime) ] div [ _class "col-3 col-md-2" ] [ txt (startTimePattern.Format chapter.StartTime) ]
div [ _class "col" ] [ div [ _class "col-3 col-md-6 col-lg-8" ] [
txt (defaultArg chapter.Title ""); br [] match chapter.Title with
| Some title -> txt title
| None -> em [ _class "text-muted" ] [ raw "no title" ]
br []
small [] [ small [] [
if withNew then if withNew then
raw "&nbsp;" raw "&nbsp;"
@ -167,8 +179,12 @@ let chapterList withNew (model: ManageChaptersModel) app =
] ]
] ]
] ]
div [ _class "col" ] [ raw (if Option.isSome chapter.ImageUrl then "Y" else "N") ] div [ _class "col-3 col-md-2 col-lg-1 text-center" ] [
div [ _class "col" ] [ raw (if Option.isSome chapter.Location then "Y" else "N") ] raw (match chapter.ImageUrl with Some _ -> "Y" | None -> "N")
]
div [ _class "col-3 col-md-2 col-lg-1 text-center" ] [
raw (match chapter.Location with Some _ -> "Y" | None -> "N")
]
]) ])
div [ _class "row pb-3"; _id "chapter-1" ] [ div [ _class "row pb-3"; _id "chapter-1" ] [
let newLink = relUrl app $"admin/post/{model.Id}/chapter/-1" let newLink = relUrl app $"admin/post/{model.Id}/chapter/-1"