Complete main chapter functionality (#6)
- Edit page still needs UI tweaks
This commit is contained in:
parent
81039579ea
commit
5b8a632e9d
@ -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 }
|
||||||
|
@ -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 }
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 ~~
|
||||||
|
@ -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 " "
|
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-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"
|
||||||
|
Loading…
Reference in New Issue
Block a user