Version 2.1 #41
@ -3,6 +3,7 @@
|
|||||||
open System
|
open System
|
||||||
open MyWebLog
|
open MyWebLog
|
||||||
open NodaTime
|
open NodaTime
|
||||||
|
open NodaTime.Text
|
||||||
|
|
||||||
/// Helper functions for view models
|
/// Helper functions for view models
|
||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
@ -71,6 +72,45 @@ type DisplayCategory = {
|
|||||||
PostCount: int
|
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
|
/// A display version of a custom feed definition
|
||||||
type DisplayCustomFeed = {
|
type DisplayCustomFeed = {
|
||||||
@ -984,6 +1024,25 @@ type LogOnModel = {
|
|||||||
{ EmailAddress = ""; Password = ""; ReturnTo = None }
|
{ EmailAddress = ""; Password = ""; ReturnTo = None }
|
||||||
|
|
||||||
|
|
||||||
|
/// View model to manage chapters
|
||||||
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
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
|
/// View model to manage permalinks
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type ManagePermalinksModel = {
|
type ManagePermalinksModel = {
|
||||||
|
@ -227,14 +227,15 @@ let register () =
|
|||||||
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>
|
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>
|
||||||
typeof<RedirectRule>; typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
|
typeof<RedirectRule>; typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
|
||||||
// View models
|
// View models
|
||||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>
|
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayChapter>
|
||||||
typeof<DisplayPage>; typeof<DisplayRevision>; typeof<DisplayTheme>
|
typeof<DisplayCustomFeed>; typeof<DisplayPage>; typeof<DisplayRevision>
|
||||||
typeof<DisplayUpload>; typeof<DisplayUser>; typeof<EditCategoryModel>
|
typeof<DisplayTheme>; typeof<DisplayUpload>; typeof<DisplayUser>
|
||||||
typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>; typeof<EditPageModel>
|
typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>
|
||||||
typeof<EditPostModel>; typeof<EditRedirectRuleModel>; typeof<EditRssModel>
|
typeof<EditPageModel>; typeof<EditPostModel>; typeof<EditRedirectRuleModel>
|
||||||
typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel>
|
typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>
|
||||||
typeof<ManagePermalinksModel>; typeof<ManageRevisionsModel>; typeof<PostDisplay>
|
typeof<LogOnModel>; typeof<ManageChaptersModel>; typeof<ManagePermalinksModel>
|
||||||
typeof<PostListItem>; typeof<SettingsModel>; typeof<UserMessage>
|
typeof<ManageRevisionsModel>; typeof<PostDisplay>; typeof<PostListItem>
|
||||||
|
typeof<SettingsModel>; typeof<UserMessage>
|
||||||
// Framework types
|
// Framework types
|
||||||
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
|
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
|
||||||
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>
|
typeof<MetaItem list>; typeof<string list>; typeof<string option>; typeof<TagMap list>
|
||||||
|
@ -371,6 +371,21 @@ let deleteRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fu
|
|||||||
| _, None -> return! Error.notFound next ctx
|
| _, 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
|
// POST /admin/post/save
|
||||||
let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let save : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
let! model = ctx.BindFormAsync<EditPostModel>()
|
let! model = ctx.BindFormAsync<EditPostModel>()
|
||||||
|
@ -129,6 +129,7 @@ let router : HttpHandler = choose [
|
|||||||
routef "/%s/permalinks" Post.editPermalinks
|
routef "/%s/permalinks" Post.editPermalinks
|
||||||
routef "/%s/revision/%s/preview" Post.previewRevision
|
routef "/%s/revision/%s/preview" Post.previewRevision
|
||||||
routef "/%s/revisions" Post.editRevisions
|
routef "/%s/revisions" Post.editRevisions
|
||||||
|
routef "/%s/chapters" Post.chapters
|
||||||
])
|
])
|
||||||
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
|
subRoute "/settings" (requireAccess WebLogAdmin >=> choose [
|
||||||
route "" >=> Admin.WebLog.settings
|
route "" >=> Admin.WebLog.settings
|
||||||
|
38
src/admin-theme/chapters.liquid
Normal file
38
src/admin-theme/chapters.liquid
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<h2 class=my-3>{{ page_title }}</h2>
|
||||||
|
<article>
|
||||||
|
<form method=post hx-target=body>
|
||||||
|
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
||||||
|
<input type=hidden name=Id value="{{ model.id }}">
|
||||||
|
<div class="container mb-3">
|
||||||
|
<div class=row>
|
||||||
|
<div class=col>
|
||||||
|
<p style="line-height:1.2rem;">
|
||||||
|
<strong>{{ model.title }}</strong><br>
|
||||||
|
<small class=text-muted>
|
||||||
|
<a href="{{ "admin/post/" | append: model.id | append: "/edit" | relative_link }}">
|
||||||
|
« Back to Edit Post
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mwl-table-heading">
|
||||||
|
<div class=col>Start</div>
|
||||||
|
<div class=col>Title</div>
|
||||||
|
<div class=col>Image?</div>
|
||||||
|
<div class=col>Location?</div>
|
||||||
|
</div>
|
||||||
|
{% for chapter in model.chapters %}
|
||||||
|
<div class="row pb-3 mwl-table-detail">
|
||||||
|
<div class=col>{{ chapter.start_time }}</div>
|
||||||
|
<div class=col>{{ chapter.title }}</div>
|
||||||
|
<div class=col>{% if chapter.image_url == "" %}N{% else %}Y{% endif %}</div>
|
||||||
|
<div class=col>{% if chapter.location %}Y{% else %}N{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="row pb-3 mwl-table-detail" id=chapter_new>
|
||||||
|
{% assign new_link = "admin/post/" | append: model.id | append: "/chapter/new" | relative_link %}
|
||||||
|
<a href="{{ new_link }}" hx-get="{{ new_link }}" hx-target=#chapter_new>Add a New Chapter</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</article>
|
Loading…
Reference in New Issue
Block a user