diff --git a/src/MyWebLog.Domain/SupportTypes.fs b/src/MyWebLog.Domain/SupportTypes.fs index acbb7d9..2ba8266 100644 --- a/src/MyWebLog.Domain/SupportTypes.fs +++ b/src/MyWebLog.Domain/SupportTypes.fs @@ -164,7 +164,7 @@ type Location = { Name: string /// A geographic coordinate string (RFC 5870) - Geo: string option + Geo: string /// An OpenStreetMap query Osm: string option @@ -182,6 +182,9 @@ type Chapter = { /// A URL for an image for this chapter ImageUrl: string option + /// A URL with information pertaining to this chapter + Url: string option + /// Whether this chapter is hidden IsHidden: bool option @@ -197,6 +200,7 @@ type Chapter = { { StartTime = Duration.Zero Title = None ImageUrl = None + Url = None IsHidden = None EndTime = None Location = None } diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index 5a2afcd..bb411aa 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -84,6 +84,9 @@ type DisplayChapter = { /// An image to display for this chapter ImageUrl: string + /// A URL with information about this chapter + Url: string + /// Whether this chapter should be displayed in podcast players IsHidden: bool @@ -106,10 +109,11 @@ type DisplayChapter = { { StartTime = pattern.Format chapter.StartTime Title = defaultArg chapter.Title "" ImageUrl = defaultArg chapter.ImageUrl "" + Url = defaultArg chapter.Url "" IsHidden = defaultArg chapter.IsHidden false EndTime = chapter.EndTime |> Option.map pattern.Format |> 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 "" } @@ -379,6 +383,9 @@ type EditChapterModel = { /// An image to display for this chapter ImageUrl: string + /// A URL with information about this chapter + Url: string + /// Whether this chapter should be displayed in podcast players IsHidden: bool @@ -406,6 +413,7 @@ type EditChapterModel = { StartTime = it.StartTime Title = it.Title ImageUrl = it.ImageUrl + Url = it.Url IsHidden = it.IsHidden EndTime = it.EndTime LocationName = it.LocationName @@ -429,10 +437,11 @@ type EditChapterModel = { let location = match noneIfBlank this.LocationName with | 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 Title = noneIfBlank this.Title ImageUrl = noneIfBlank this.ImageUrl + Url = noneIfBlank this.Url IsHidden = if this.IsHidden then Some true else None EndTime = noneIfBlank this.EndTime |> Option.map (parseDuration (nameof this.EndTime)) Location = location } diff --git a/src/MyWebLog/Handlers/Helpers.fs b/src/MyWebLog/Handlers/Helpers.fs index 7f01db7..00f7274 100644 --- a/src/MyWebLog/Handlers/Helpers.fs +++ b/src/MyWebLog/Handlers/Helpers.fs @@ -421,6 +421,11 @@ open System.Threading.Tasks /// Create a Task with a Some result for the given object 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 MyWebLog.Data diff --git a/src/MyWebLog/Handlers/Post.fs b/src/MyWebLog/Handlers/Post.fs index 9f0a508..279ca22 100644 --- a/src/MyWebLog/Handlers/Post.fs +++ b/src/MyWebLog/Handlers/Post.fs @@ -208,18 +208,42 @@ let home : HttpHandler = fun next ctx -> task { } // GET /{post-permalink}?chapters -let chapters (post: Post) : HttpHandler = +let chapters (post: Post) : HttpHandler = fun next ctx -> match post.Episode with | Some ep -> match ep.Chapters with | Some chapters -> - - json chapters + let chapterData = + chapters + |> Seq.ofList + |> Seq.map (fun it -> + let dic = Dictionary() + 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() + 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() + 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 -> match ep.ChapterFile with - | Some file -> redirectTo true file - | None -> Error.notFound - | None -> Error.notFound + | Some file -> redirectTo true file next ctx + | None -> Error.notFound next ctx + | None -> Error.notFound next ctx // ~~ ADMINISTRATION ~~ diff --git a/src/MyWebLog/Views/Post.fs b/src/MyWebLog/Views/Post.fs index f64eb48..9878310 100644 --- a/src/MyWebLog/Views/Post.fs +++ b/src/MyWebLog/Views/Post.fs @@ -46,7 +46,7 @@ let chapterEdit (model: EditChapterModel) app = [ 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" ] [ input [ _type "text"; _id "image_url"; _name "ImageUrl"; _class "form-control" _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" ] [ input [ _type "checkbox"; _id "is_hidden"; _name "IsHidden"; _class "form-check-input" _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 "form-floating" ] [ 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 ] label [ _for "location_geo" ] [ raw "Geo URL" ] em [ _class "form-text" ] [ - raw "Optional; " a [ _href "https://github.com/Podcastindex-org/podcast-namespace/blob/main/location/location.md#geo-recommended" _target "_blank"; _rel "noopener" ] [ raw "see spec" @@ -142,16 +151,19 @@ let chapterList withNew (model: ManageChaptersModel) app = antiCsrf app input [ _type "hidden"; _name "Id"; _value model.Id ] div [ _class "row mwl-table-heading" ] [ - div [ _class "col" ] [ raw "Start" ] - div [ _class "col" ] [ raw "Title" ] - div [ _class "col" ] [ raw "Image?" ] - div [ _class "col" ] [ raw "Location?" ] + div [ _class "col-3 col-md-2" ] [ raw "Start" ] + div [ _class "col-3 col-md-6 col-lg-8" ] [ raw "Title" ] + div [ _class "col-3 col-md-2 col-lg-1 text-center" ] [ raw "Image?" ] + div [ _class "col-3 col-md-2 col-lg-1 text-center" ] [ raw "Location?" ] ] yield! model.Chapters |> List.mapi (fun idx chapter -> div [ _class "row mwl-table-detail"; _id $"chapter{idx}" ] [ - div [ _class "col" ] [ txt (startTimePattern.Format chapter.StartTime) ] - div [ _class "col" ] [ - txt (defaultArg chapter.Title ""); br [] + div [ _class "col-3 col-md-2" ] [ txt (startTimePattern.Format chapter.StartTime) ] + div [ _class "col-3 col-md-6 col-lg-8" ] [ + match chapter.Title with + | Some title -> txt title + | None -> em [ _class "text-muted" ] [ raw "no title" ] + br [] small [] [ if withNew then raw " " @@ -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" ] [ raw (if Option.isSome chapter.Location then "Y" else "N") ] + div [ _class "col-3 col-md-2 col-lg-1 text-center" ] [ + 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" ] [ let newLink = relUrl app $"admin/post/{model.Id}/chapter/-1"