Version 2.1 #41
|
@ -3,6 +3,7 @@
|
|||
open System
|
||||
open MyWebLog
|
||||
open NodaTime
|
||||
open NodaTime.Text
|
||||
|
||||
/// Helper functions for view models
|
||||
[<AutoOpen>]
|
||||
|
@ -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
|
||||
[<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
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type ManagePermalinksModel = {
|
||||
|
|
|
@ -227,14 +227,15 @@ let register () =
|
|||
typeof<CustomFeed>; typeof<Episode>; typeof<Episode option>; typeof<MetaItem>; typeof<Page>
|
||||
typeof<RedirectRule>; typeof<RssOptions>; typeof<TagMap>; typeof<UploadDestination>; typeof<WebLog>
|
||||
// View models
|
||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayCustomFeed>
|
||||
typeof<DisplayPage>; typeof<DisplayRevision>; typeof<DisplayTheme>
|
||||
typeof<DisplayUpload>; typeof<DisplayUser>; typeof<EditCategoryModel>
|
||||
typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>; typeof<EditPageModel>
|
||||
typeof<EditPostModel>; typeof<EditRedirectRuleModel>; typeof<EditRssModel>
|
||||
typeof<EditTagMapModel>; typeof<EditUserModel>; typeof<LogOnModel>
|
||||
typeof<ManagePermalinksModel>; typeof<ManageRevisionsModel>; typeof<PostDisplay>
|
||||
typeof<PostListItem>; typeof<SettingsModel>; typeof<UserMessage>
|
||||
typeof<DashboardModel>; typeof<DisplayCategory>; typeof<DisplayChapter>
|
||||
typeof<DisplayCustomFeed>; typeof<DisplayPage>; typeof<DisplayRevision>
|
||||
typeof<DisplayTheme>; typeof<DisplayUpload>; typeof<DisplayUser>
|
||||
typeof<EditCategoryModel>; typeof<EditCustomFeedModel>; typeof<EditMyInfoModel>
|
||||
typeof<EditPageModel>; typeof<EditPostModel>; typeof<EditRedirectRuleModel>
|
||||
typeof<EditRssModel>; typeof<EditTagMapModel>; typeof<EditUserModel>
|
||||
typeof<LogOnModel>; typeof<ManageChaptersModel>; typeof<ManagePermalinksModel>
|
||||
typeof<ManageRevisionsModel>; typeof<PostDisplay>; typeof<PostListItem>
|
||||
typeof<SettingsModel>; typeof<UserMessage>
|
||||
// Framework types
|
||||
typeof<AntiforgeryTokenSet>; typeof<DateTime option>; typeof<int option>; typeof<KeyValuePair>
|
||||
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
|
||||
}
|
||||
|
||||
// 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<EditPostModel>()
|
||||
|
|
|
@ -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
|
||||
|
|
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