Version 2.1 #41
@ -384,7 +384,7 @@ module Theme =
|
|||||||
if ctx.Request.HasFormContentType && ctx.Request.Form.Files.Count > 0 then
|
if ctx.Request.HasFormContentType && ctx.Request.Form.Files.Count > 0 then
|
||||||
let themeFile = Seq.head ctx.Request.Form.Files
|
let themeFile = Seq.head ctx.Request.Form.Files
|
||||||
match deriveIdFromFileName themeFile.FileName with
|
match deriveIdFromFileName themeFile.FileName with
|
||||||
| Ok themeId when themeId <> adminTheme ->
|
| Ok themeId when themeId <> ThemeId "admin" ->
|
||||||
let data = ctx.Data
|
let data = ctx.Data
|
||||||
let! exists = data.Theme.Exists themeId
|
let! exists = data.Theme.Exists themeId
|
||||||
let isNew = not exists
|
let isNew = not exists
|
||||||
|
@ -355,13 +355,6 @@ let themedView template next ctx hash = task {
|
|||||||
return! viewForTheme (hash[ViewContext.WebLog] :?> WebLog).ThemeId template next ctx hash
|
return! viewForTheme (hash[ViewContext.WebLog] :?> WebLog).ThemeId template next ctx hash
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The ID for the admin theme
|
|
||||||
let adminTheme = ThemeId "admin"
|
|
||||||
|
|
||||||
/// Display a bare view for the admin theme
|
|
||||||
let adminBareView template =
|
|
||||||
bareForTheme adminTheme template
|
|
||||||
|
|
||||||
/// Display a page for an admin endpoint
|
/// Display a page for an admin endpoint
|
||||||
let adminPage pageTitle includeCsrf next ctx (content: AppViewContext -> XmlNode list) = task {
|
let adminPage pageTitle includeCsrf next ctx (content: AppViewContext -> XmlNode list) = task {
|
||||||
let! messages = getCurrentMessages ctx
|
let! messages = getCurrentMessages ctx
|
||||||
|
@ -115,18 +115,9 @@ let private findPageRevision pgId revDate (ctx: HttpContext) = task {
|
|||||||
let previewRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let previewRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
match! findPageRevision pgId revDate ctx with
|
match! findPageRevision pgId revDate ctx with
|
||||||
| Some pg, Some rev when canEdit pg.AuthorId ctx ->
|
| Some pg, Some rev when canEdit pg.AuthorId ctx ->
|
||||||
return! {|
|
return! adminBarePage "" false next ctx (Views.Helpers.commonPreview rev)
|
||||||
content =
|
|
||||||
[ """<div class="mwl-revision-preview mb-3">"""
|
|
||||||
rev.Text.AsHtml() |> addBaseToRelativeUrls ctx.WebLog.ExtraPath
|
|
||||||
"</div>"
|
|
||||||
]
|
|
||||||
|> String.concat ""
|
|
||||||
|}
|
|
||||||
|> makeHash |> adminBareView "" next ctx
|
|
||||||
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None, _
|
| None, _ | _, None -> return! Error.notFound next ctx
|
||||||
| _, None -> return! Error.notFound next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/page/{id}/revision/{revision-date}/restore
|
// POST /admin/page/{id}/revision/{revision-date}/restore
|
||||||
@ -150,7 +141,7 @@ let deleteRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun
|
|||||||
| Some pg, Some rev when canEdit pg.AuthorId ctx ->
|
| Some pg, Some rev when canEdit pg.AuthorId ctx ->
|
||||||
do! ctx.Data.Page.Update { pg with Revisions = pg.Revisions |> List.filter (fun r -> r.AsOf <> rev.AsOf) }
|
do! ctx.Data.Page.Update { pg with Revisions = pg.Revisions |> List.filter (fun r -> r.AsOf <> rev.AsOf) }
|
||||||
do! addMessage ctx { UserMessage.Success with Message = "Revision deleted successfully" }
|
do! addMessage ctx { UserMessage.Success with Message = "Revision deleted successfully" }
|
||||||
return! adminBareView "" next ctx (makeHash {| content = "" |})
|
return! adminBarePage "" false next ctx (fun _ -> [])
|
||||||
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None, _
|
| None, _
|
||||||
| _, None -> return! Error.notFound next ctx
|
| _, None -> return! Error.notFound next ctx
|
||||||
|
@ -358,18 +358,9 @@ let private findPostRevision postId revDate (ctx: HttpContext) = task {
|
|||||||
let previewRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
let previewRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||||
match! findPostRevision postId revDate ctx with
|
match! findPostRevision postId revDate ctx with
|
||||||
| Some post, Some rev when canEdit post.AuthorId ctx ->
|
| Some post, Some rev when canEdit post.AuthorId ctx ->
|
||||||
return! {|
|
return! adminBarePage "" false next ctx (Views.Helpers.commonPreview rev)
|
||||||
content =
|
|
||||||
[ """<div class="mwl-revision-preview mb-3">"""
|
|
||||||
rev.Text.AsHtml() |> addBaseToRelativeUrls ctx.WebLog.ExtraPath
|
|
||||||
"</div>"
|
|
||||||
]
|
|
||||||
|> String.concat ""
|
|
||||||
|}
|
|
||||||
|> makeHash |> adminBareView "" next ctx
|
|
||||||
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None, _
|
| None, _ | _, None -> return! Error.notFound next ctx
|
||||||
| _, None -> return! Error.notFound next ctx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /admin/post/{id}/revision/{revision-date}/restore
|
// POST /admin/post/{id}/revision/{revision-date}/restore
|
||||||
@ -393,7 +384,7 @@ let deleteRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fu
|
|||||||
| Some post, Some rev when canEdit post.AuthorId ctx ->
|
| Some post, Some rev when canEdit post.AuthorId ctx ->
|
||||||
do! ctx.Data.Post.Update { post with Revisions = post.Revisions |> List.filter (fun r -> r.AsOf <> rev.AsOf) }
|
do! ctx.Data.Post.Update { post with Revisions = post.Revisions |> List.filter (fun r -> r.AsOf <> rev.AsOf) }
|
||||||
do! addMessage ctx { UserMessage.Success with Message = "Revision deleted successfully" }
|
do! addMessage ctx { UserMessage.Success with Message = "Revision deleted successfully" }
|
||||||
return! adminBareView "" next ctx (makeHash {| content = "" |})
|
return! adminBarePage "" false next ctx (fun _ -> [])
|
||||||
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
| Some _, Some _ -> return! Error.notAuthorized next ctx
|
||||||
| None, _
|
| None, _
|
||||||
| _, None -> return! Error.notFound next ctx
|
| _, None -> return! Error.notFound next ctx
|
||||||
|
@ -393,6 +393,14 @@ let commonMetaItems (model: EditCommonModel) =
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
/// Revision preview template
|
||||||
|
let commonPreview (rev: Revision) app =
|
||||||
|
div [ _class "mwl-revision-preview mb-3" ] [
|
||||||
|
rev.Text.AsHtml() |> addBaseToRelativeUrls app.WebLog.ExtraPath |> raw
|
||||||
|
]
|
||||||
|
|> List.singleton
|
||||||
|
|
||||||
|
|
||||||
/// Form to manage permalinks for pages or posts
|
/// Form to manage permalinks for pages or posts
|
||||||
let managePermalinks (model: ManagePermalinksModel) app = [
|
let managePermalinks (model: ManagePermalinksModel) app = [
|
||||||
let baseUrl = relUrl app $"admin/{model.Entity}/"
|
let baseUrl = relUrl app $"admin/{model.Entity}/"
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
<div class="form-floating pb-3">
|
|
||||||
<input type=text name=Title id=title class=form-control placeholder=Title autofocus required
|
|
||||||
value="{{ model.title }}">
|
|
||||||
<label for=title>Title</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating pb-3">
|
|
||||||
<input type=text name=Permalink id=permalink class=form-control placeholder=Permalink required
|
|
||||||
value="{{ model.permalink }}">
|
|
||||||
<label for=permalink>Permalink</label>
|
|
||||||
{%- unless model.is_new %}
|
|
||||||
{%- assign entity_url_base = "admin/" | append: entity | append: "/" | append: entity_id -%}
|
|
||||||
<span class=form-text>
|
|
||||||
<a href="{{ entity_url_base | append: "/permalinks" | relative_link }}">Manage Permalinks</a>
|
|
||||||
<span class=text-muted> • </span>
|
|
||||||
<a href="{{ entity_url_base | append: "/revisions" | relative_link }}">Manage Revisions</a>
|
|
||||||
{% if model.chapter_source == "internal" %}
|
|
||||||
<span id="chapterEditLink">
|
|
||||||
<span class=text-muted> • </span>
|
|
||||||
<a href="{{ entity_url_base | append: "/chapters" | relative_link }}">Manage Chapters</a>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
{%- endunless -%}
|
|
||||||
</div>
|
|
||||||
<div class=mb-2>
|
|
||||||
<label for=text>Text</label>
|
|
||||||
<div class="btn-group btn-group-sm" role=group aria-label="Text format button group">
|
|
||||||
<input type=radio name=Source id=source_html class=btn-check value=HTML
|
|
||||||
{%- if model.source == "HTML" %} checked{% endif %}>
|
|
||||||
<label class="btn btn-sm btn-outline-secondary" for=source_html>HTML</label>
|
|
||||||
<input type=radio name=Source id=source_md class=btn-check value=Markdown
|
|
||||||
{%- if model.source == "Markdown" %} checked{% endif %}>
|
|
||||||
<label class="btn btn-sm btn-outline-secondary" for=source_md>Markdown</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=pb-3>
|
|
||||||
<textarea name=Text id=text class=form-control rows=20>{{ model.text }}</textarea>
|
|
||||||
</div>
|
|
@ -1,80 +0,0 @@
|
|||||||
<header>
|
|
||||||
<nav class="navbar navbar-dark bg-dark navbar-expand-md justify-content-start px-2 position-fixed top-0 w-100">
|
|
||||||
<div class=container-fluid>
|
|
||||||
<a class=navbar-brand href="{{ "" | relative_link }}" hx-boost=false>{{ web_log.name }}</a>
|
|
||||||
<button class=navbar-toggler type=button data-bs-toggle=collapse data-bs-target=#navbarText
|
|
||||||
aria-controls=navbarText aria-expanded=false aria-label="Toggle navigation">
|
|
||||||
<span class=navbar-toggler-icon></span>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id=navbarText>
|
|
||||||
{%- if is_logged_on %}
|
|
||||||
<ul class=navbar-nav>
|
|
||||||
{{ "admin/dashboard" | nav_link: "Dashboard" }}
|
|
||||||
{%- if is_author %}
|
|
||||||
{{ "admin/pages" | nav_link: "Pages" }}
|
|
||||||
{{ "admin/posts" | nav_link: "Posts" }}
|
|
||||||
{{ "admin/uploads" | nav_link: "Uploads" }}
|
|
||||||
{%- endif %}
|
|
||||||
{%- if is_web_log_admin %}
|
|
||||||
{{ "admin/categories" | nav_link: "Categories" }}
|
|
||||||
{{ "admin/settings" | nav_link: "Settings" }}
|
|
||||||
{%- endif %}
|
|
||||||
{%- if is_administrator %}
|
|
||||||
{{ "admin/administration" | nav_link: "Admin" }}
|
|
||||||
{%- endif %}
|
|
||||||
</ul>
|
|
||||||
{%- endif %}
|
|
||||||
<ul class="navbar-nav flex-grow-1 justify-content-end">
|
|
||||||
{%- if is_logged_on %}
|
|
||||||
{{ "admin/my-info" | nav_link: "My Info" }}
|
|
||||||
<li class=nav-item>
|
|
||||||
<a class=nav-link href=https://bitbadger.solutions/open-source/myweblog/#how-to-use-myweblog target=_blank>
|
|
||||||
Docs
|
|
||||||
</a>
|
|
||||||
<li class=nav-item>
|
|
||||||
<a class=nav-link href="{{ "user/log-off" | relative_link }}" hx-boost=false>Log Off</a>
|
|
||||||
{%- else -%}
|
|
||||||
<li class=nav-item>
|
|
||||||
<a class=nav-link href=https://bitbadger.solutions/open-source/myweblog/#how-to-use-myweblog target=_blank>
|
|
||||||
Docs
|
|
||||||
</a>
|
|
||||||
{{ "user/log-on" | nav_link: "Log On" }}
|
|
||||||
{%- endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<div id=toastHost class="position-fixed top-0 w-100" aria-live=polite aria-atomic=true>
|
|
||||||
<div id=toasts class="toast-container position-absolute p-3 mt-5 top-0 end-0">
|
|
||||||
{% for msg in messages %}
|
|
||||||
<div class=toast role=alert aria-live=assertive aria-atomic=true
|
|
||||||
{%- unless msg.level == "success" %} data-bs-autohide="false"{% endunless %}>
|
|
||||||
<div class="toast-header bg-{{ msg.level }}{% unless msg.level == "warning" %} text-white{% endunless %}">
|
|
||||||
<strong class="me-auto text-uppercase">
|
|
||||||
{% if msg.level == "danger" %}error{% else %}{{ msg.level}}{% endif %}
|
|
||||||
</strong>
|
|
||||||
<button type=button class=btn-close data-bs-dismiss=toast aria-label=Close></button>
|
|
||||||
</div>
|
|
||||||
<div class="toast-body bg-{{ msg.level }} bg-opacity-25">
|
|
||||||
{{ msg.message }}
|
|
||||||
{%- if msg.detail %}
|
|
||||||
<hr>
|
|
||||||
{{ msg.detail.value }}
|
|
||||||
{%- endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<main class="mx-3 mt-3">
|
|
||||||
<div class="load-overlay p-5" id=loadOverlay><h1 class=p-3>Loading…</h1></div>
|
|
||||||
{{ content }}
|
|
||||||
</main>
|
|
||||||
<footer class="position-fixed bottom-0 w-100">
|
|
||||||
<div class="text-end text-white me-2">
|
|
||||||
{%- assign version = generator | split: " " -%}
|
|
||||||
<small class="me-1 align-baseline">v{{ version[1] }}</small>
|
|
||||||
<img src="{{ "themes/admin/logo-light.png" | relative_link }}" alt=myWebLog width=120 height=34>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
@ -1,5 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang=en>
|
|
||||||
<title></title>
|
|
||||||
{{ content }}
|
|
||||||
</html>
|
|
@ -1,5 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang=en>
|
|
||||||
<title>{{ page_title | strip_html }} « Admin « {{ web_log.name | strip_html }}</title>
|
|
||||||
{% include_template "_layout" %}
|
|
||||||
</html>
|
|
@ -1,17 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang=en>
|
|
||||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
|
||||||
<meta name=generator content="{{ generator }}">
|
|
||||||
<title>{{ page_title | strip_html }} « Admin « {{ web_log.name | strip_html }}</title>
|
|
||||||
<link rel=stylesheet href=https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css
|
|
||||||
integrity=sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3 crossorigin=anonymous>
|
|
||||||
<link rel=stylesheet href="{{ "themes/admin/admin.css" | relative_link }}">
|
|
||||||
<body hx-boost=true hx-indicator=#loadOverlay>
|
|
||||||
{% include_template "_layout" %}
|
|
||||||
<script src=https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js
|
|
||||||
integrity=sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p
|
|
||||||
crossorigin=anonymous></script>
|
|
||||||
{{ htmx_script }}
|
|
||||||
<script src="{{ "themes/admin/admin.js" | relative_link }}"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,82 +0,0 @@
|
|||||||
<h2 class=my-3>{{ page_title }}</h2>
|
|
||||||
<article>
|
|
||||||
<form action="{{ "admin/page/save" | relative_link }}" method="post" hx-push-url="true">
|
|
||||||
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
|
||||||
<input type=hidden name=PageId value="{{ model.page_id }}">
|
|
||||||
<div class=container>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class=col-9>
|
|
||||||
{%- assign entity = "page" -%}
|
|
||||||
{%- assign entity_id = model.page_id -%}
|
|
||||||
{% include_template "_edit-common" %}
|
|
||||||
</div>
|
|
||||||
<div class=col-3>
|
|
||||||
<div class="form-floating pb-3">
|
|
||||||
<select name=Template id=template class=form-control>
|
|
||||||
{% for tmpl in templates -%}
|
|
||||||
<option value="{{ tmpl[0] }}"{% if model.template == tmpl[0] %} selected{% endif %}>
|
|
||||||
{{ tmpl[1] }}
|
|
||||||
</option>
|
|
||||||
{%- endfor %}
|
|
||||||
</select>
|
|
||||||
<label for=template>Page Template</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input type=checkbox name=IsShownInPageList id=showList class=form-check-input value=true
|
|
||||||
{%- if model.is_shown_in_page_list %} checked{% endif %}>
|
|
||||||
<label for=showList class=form-check-label>Show in Page List</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class=col>
|
|
||||||
<button type=submit class="btn btn-primary">Save Changes</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class=col>
|
|
||||||
<fieldset>
|
|
||||||
<legend>
|
|
||||||
Metadata
|
|
||||||
<button type=button class="btn btn-sm btn-secondary" data-bs-toggle=collapse
|
|
||||||
data-bs-target=#metaItemContainer>
|
|
||||||
show
|
|
||||||
</button>
|
|
||||||
</legend>
|
|
||||||
<div id=metaItemContainer class=collapse>
|
|
||||||
<div id=metaItems class=container>
|
|
||||||
{%- for meta in metadata %}
|
|
||||||
<div id="meta_{{ meta[0] }}" class="row mb-3">
|
|
||||||
<div class="col-1 text-center align-self-center">
|
|
||||||
<button type=button class="btn btn-sm btn-danger" onclick="Admin.removeMetaItem({{ meta[0] }})">
|
|
||||||
−
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class=col-3>
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=MetaNames id="metaNames_{{ meta[0] }}" class=form-control
|
|
||||||
placeholder=Name value="{{ meta[1] }}">
|
|
||||||
<label for="metaNames_{{ meta[0] }}">Name</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=col-8>
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=MetaValues id="metaValues_{{ meta[0] }}" class=form-control
|
|
||||||
placeholder=Value value="{{ meta[2] }}">
|
|
||||||
<label for="metaValues_{{ meta[0] }}">Value</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor -%}
|
|
||||||
</div>
|
|
||||||
<button type=button class="btn btn-sm btn-secondary" onclick="Admin.addMetaItem()">Add an Item</button>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", () => Admin.setNextMetaIndex({{ metadata | size }}))
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
@ -1,341 +0,0 @@
|
|||||||
<h2 class=my-3>{{ page_title }}</h2>
|
|
||||||
<article>
|
|
||||||
<form action="{{ "admin/post/save" | relative_link }}" method=post hx-push-url=true>
|
|
||||||
<input type=hidden name="{{ csrf.form_field_name }}" value="{{ csrf.request_token }}">
|
|
||||||
<input type=hidden name=PostId value="{{ model.post_id }}">
|
|
||||||
<div class=container>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-12 col-lg-9">
|
|
||||||
{%- assign entity = "post" -%}
|
|
||||||
{%- assign entity_id = model.post_id -%}
|
|
||||||
{% include_template "_edit-common" %}
|
|
||||||
<div class="form-floating pb-3">
|
|
||||||
<input type=text name=Tags id=tags class=form-control placeholder=Tags value="{{ model.tags }}">
|
|
||||||
<label for=tags>Tags</label>
|
|
||||||
<div class=form-text>comma-delimited</div>
|
|
||||||
</div>
|
|
||||||
{% if model.status == "Draft" %}
|
|
||||||
<div class="form-check form-switch pb-2">
|
|
||||||
<input type=checkbox name=DoPublish id=doPublish class=form-check-input value=true>
|
|
||||||
<label for=doPublish class=form-check-label>Publish This Post</label>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<button type=submit class="btn btn-primary pb-2">Save Changes</button>
|
|
||||||
<hr class=mb-3>
|
|
||||||
<fieldset class=mb-3>
|
|
||||||
<legend>
|
|
||||||
<span class="form-check form-switch">
|
|
||||||
<small>
|
|
||||||
<input type=checkbox name=IsEpisode id=isEpisode class=form-check-input value=true
|
|
||||||
data-bs-toggle=collapse data-bs-target=#episodeItems onclick="Admin.toggleEpisodeFields()"
|
|
||||||
{%- if model.is_episode %} checked{% endif %}>
|
|
||||||
</small>
|
|
||||||
<label for=isEpisode>Podcast Episode</label>
|
|
||||||
</span>
|
|
||||||
</legend>
|
|
||||||
<div id=episodeItems class="container p-0 collapse{% if model.is_episode %} show{% endif %}">
|
|
||||||
<div class=row>
|
|
||||||
<div class="col-12 col-md-8 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=Media id=media class=form-control placeholder=Media required
|
|
||||||
value="{{ model.media }}">
|
|
||||||
<label for=media>Media File</label>
|
|
||||||
<div class=form-text>
|
|
||||||
Relative URL will be appended to base media path (if set) or served from this web log
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=MediaType id=mediaType class=form-control placeholder="Media Type"
|
|
||||||
value="{{ model.media_type }}">
|
|
||||||
<label for=mediaType>Media MIME Type</label>
|
|
||||||
<div class=form-text>Optional; overrides podcast default</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row pb-3">
|
|
||||||
<div class=col>
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=number name=Length id=length class=form-control placeholder=Length required
|
|
||||||
value="{{ model.length }}">
|
|
||||||
<label for=length>Media Length (bytes)</label>
|
|
||||||
<div class=form-text>TODO: derive from above file name</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=col>
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=Duration id=duration class=form-control placeholder=Duration
|
|
||||||
value="{{ model.duration }}">
|
|
||||||
<label for=duration>Duration</label>
|
|
||||||
<div class=form-text>Recommended; enter in <code>HH:MM:SS</code> format</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row pb-3">
|
|
||||||
<div class=col>
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=Subtitle id=subtitle class=form-control placeholder=Subtitle
|
|
||||||
value="{{ model.subtitle }}">
|
|
||||||
<label for=subtitle>Subtitle</label>
|
|
||||||
<div class=form-text>Optional; a subtitle for this episode</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=row>
|
|
||||||
<div class="col-12 col-md-8 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=ImageUrl id=imageUrl class=form-control placeholder="Image URL"
|
|
||||||
value="{{ model.image_url }}">
|
|
||||||
<label for=imageUrl>Image URL</label>
|
|
||||||
<div class=form-text>
|
|
||||||
Optional; overrides podcast default; relative URL served from this web log
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<select name=Explicit id=explicit class=form-control>
|
|
||||||
{% for exp_value in explicit_values %}
|
|
||||||
<option value="{{ exp_value[0] }}"{% if model.explicit == exp_value[0] %} selected{% endif -%}>
|
|
||||||
{{ exp_value[1] }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<label for=explicit>Explicit Rating</label>
|
|
||||||
<div class=form-text>Optional; overrides podcast default</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=row>
|
|
||||||
<div class="col-12 col-md-8 pb-3">
|
|
||||||
<div class="form-text">Chapters</div>
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input type=radio name=ChapterSource id=chapterSourceNone value=none
|
|
||||||
class=form-check-input{% if model.chapter_source == "none" %} checked{% endif %}
|
|
||||||
onclick="Admin.setChapterSource('none')">
|
|
||||||
<label for=chapterSourceNone>None</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input type=radio name=ChapterSource id=chapterSourceInternal value=internal
|
|
||||||
class=form-check-input{% if model.chapter_source == "internal" %} checked{% endif %}
|
|
||||||
onclick="Admin.setChapterSource('internal')">
|
|
||||||
<label for=chapterSourceInternal>Defined Here</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<input type=radio name=ChapterSource id=chapterSourceExternal value=none
|
|
||||||
class=form-check-input{% if model.chapter_source == "external" %} checked{% endif %}
|
|
||||||
onclick="Admin.setChapterSource('external')">
|
|
||||||
<label for=chapterSourceExternal>Separate File</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 d-flex justify-content-center">
|
|
||||||
<div class="form-check form-switch align-self-center pb-3">
|
|
||||||
<input type=checkbox name=ContainsWaypoints id=containsWaypoints class=form-check-input
|
|
||||||
value=true{% if model.contains_waypoints %} checked{% endif %}>
|
|
||||||
<label for=containsWaypoints>Chapters contain waypoints</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=row>
|
|
||||||
<div class="col-12 col-md-8 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=ChapterFile id=chapterFile class=form-control placeholder="Chapter File"
|
|
||||||
value="{{ model.chapter_file }}">
|
|
||||||
<label for=chapterFile>Chapter File</label>
|
|
||||||
<div class=form-text>Relative URL served from this web log</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=ChapterType id=chapterType class=form-control placeholder="Chapter Type"
|
|
||||||
value="{{ model.chapter_type }}">
|
|
||||||
<label for=chapterType>Chapter MIME Type</label>
|
|
||||||
<div class=form-text>
|
|
||||||
Optional; <code>application/json+chapters</code> assumed if chapter file ends with
|
|
||||||
<code>.json</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=row>
|
|
||||||
<div class="col-12 col-md-8 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=TranscriptUrl id=transcriptUrl class=form-control placeholder="Transcript URL"
|
|
||||||
value="{{ model.transcript_url }}" onkeyup="Admin.requireTranscriptType()">
|
|
||||||
<label for=transcriptUrl>Transcript URL</label>
|
|
||||||
<div class=form-text>Optional; relative URL served from this web log</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4 pb-3">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=TranscriptType id=transcriptType class=form-control
|
|
||||||
placeholder="Transcript Type" value="{{ model.transcript_type }}"
|
|
||||||
{%- if model.transcript_url != "" %} required{% endif %}>
|
|
||||||
<label for=transcriptType>Transcript MIME Type</label>
|
|
||||||
<div class=form-text>Required if transcript URL provided</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row pb-3">
|
|
||||||
<div class=col>
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=TranscriptLang id=transcriptLang class=form-control
|
|
||||||
placeholder="Transcript Language" value="{{ model.transcript_lang }}">
|
|
||||||
<label for=transcriptLang>Transcript Language</label>
|
|
||||||
<div class=form-text>Optional; overrides podcast default</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col d-flex justify-content-center">
|
|
||||||
<div class="form-check form-switch align-self-center pb-3">
|
|
||||||
<input type=checkbox name=TranscriptCaptions id=transcriptCaptions class=form-check-input
|
|
||||||
value=true{% if model.transcript_captions %} checked{% endif %}>
|
|
||||||
<label for=transcriptCaptions>This is a captions file</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row pb-3">
|
|
||||||
<div class="col col-md-4">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=number name=SeasonNumber id=seasonNumber class=form-control placeholder="Season Number"
|
|
||||||
value="{{ model.season_number }}">
|
|
||||||
<label for=seasonNumber>Season Number</label>
|
|
||||||
<div class=form-text>Optional</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-8">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=SeasonDescription id=seasonDescription class=form-control
|
|
||||||
placeholder="Season Description" maxlength=128 value="{{ model.season_description }}">
|
|
||||||
<label for=seasonDescription>Season Description</label>
|
|
||||||
<div class=form-text>Optional</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row pb-3">
|
|
||||||
<div class="col col-md-4">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=number name=EpisodeNumber id=episodeNumber class=form-control step=0.01
|
|
||||||
placeholder="Episode Number" value="{{ model.episode_number }}">
|
|
||||||
<label for=episodeNumber>Episode Number</label>
|
|
||||||
<div class=form-text>Optional; up to 2 decimal points</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-8">
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=EpisodeDescription id=episodeDescription class=form-control
|
|
||||||
placeholder="Episode Description" maxlength=128 value="{{ model.episode_description }}">
|
|
||||||
<label for=episodeDescription>Episode Description</label>
|
|
||||||
<div class=form-text>Optional</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", () => Admin.toggleEpisodeFields())
|
|
||||||
</script>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class=pb-3>
|
|
||||||
<legend>
|
|
||||||
Metadata
|
|
||||||
<button type=button class="btn btn-sm btn-secondary" data-bs-toggle=collapse
|
|
||||||
data-bs-target="#metaItemContainer">
|
|
||||||
show
|
|
||||||
</button>
|
|
||||||
</legend>
|
|
||||||
<div id=metaItemContainer class=collapse>
|
|
||||||
<div id=metaItems class=container>
|
|
||||||
{%- for meta in metadata %}
|
|
||||||
<div id="meta_{{ meta[0] }}" class="row mb-3">
|
|
||||||
<div class="col-1 text-center align-self-center">
|
|
||||||
<button type=button class="btn btn-sm btn-danger" onclick="Admin.removeMetaItem({{ meta[0] }})">
|
|
||||||
−
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class=col-3>
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=MetaNames id="metaNames_{{ meta[0] }}" class=form-control
|
|
||||||
placeholder=Name value="{{ meta[1] }}">
|
|
||||||
<label for="metaNames_{{ meta[0] }}">Name</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=col-8>
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=text name=MetaValues id="metaValues_{{ meta[0] }}" class=form-control
|
|
||||||
placeholder=Value value="{{ meta[2] }}">
|
|
||||||
<label for="metaValues_{{ meta[0] }}">Value</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor -%}
|
|
||||||
</div>
|
|
||||||
<button type=button class="btn btn-sm btn-secondary" onclick="Admin.addMetaItem()">Add an Item</button>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", () => Admin.setNextMetaIndex({{ metadata | size }}))
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{% if model.status == "Published" %}
|
|
||||||
<fieldset class=pb-3>
|
|
||||||
<legend>Maintenance</legend>
|
|
||||||
<div class=container>
|
|
||||||
<div class=row>
|
|
||||||
<div class="col align-self-center">
|
|
||||||
<div class="form-check form-switch pb-2">
|
|
||||||
<input type=checkbox name=SetPublished id=setPublished class=form-check-input value=true>
|
|
||||||
<label for=setPublished class=form-check-label>Set Published Date</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class=col-4>
|
|
||||||
<div class=form-floating>
|
|
||||||
<input type=datetime-local name=PubOverride id=pubOverride class=form-control
|
|
||||||
placeholder="Override Date"
|
|
||||||
{%- if model.pub_override -%}
|
|
||||||
value="{{ model.pub_override | date: "yyyy-MM-dd\THH:mm" }}"
|
|
||||||
{%- endif %}>
|
|
||||||
<label for=pubOverride class=form-label>Published On</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-5 align-self-center">
|
|
||||||
<div class="form-check form-switch pb-2">
|
|
||||||
<input type=checkbox name=SetUpdated id=setUpdated class=form-check-input value=true>
|
|
||||||
<label for=setUpdated class=form-check-label>
|
|
||||||
Purge revisions and<br>set as updated date as well
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-lg-3">
|
|
||||||
<div class="form-floating pb-3">
|
|
||||||
<select name=Template id=template class=form-control>
|
|
||||||
{% for tmpl in templates -%}
|
|
||||||
<option value="{{ tmpl[0] }}"{% if model.template == tmpl[0] %} selected{% endif %}>
|
|
||||||
{{ tmpl[1] }}
|
|
||||||
</option>
|
|
||||||
{%- endfor %}
|
|
||||||
</select>
|
|
||||||
<label for=template>Post Template</label>
|
|
||||||
</div>
|
|
||||||
<fieldset>
|
|
||||||
<legend>Categories</legend>
|
|
||||||
{% for cat in categories %}
|
|
||||||
<div class=form-check>
|
|
||||||
<input type=checkbox name=CategoryIds id="categoryId_{{ cat.id }}" class=form-check-input
|
|
||||||
value="{{ cat.id }}"{% if model.category_ids contains cat.id %} checked{% endif %}>
|
|
||||||
<label for="categoryId_{{ cat.id }}" class=form-check-label
|
|
||||||
{%- if cat.description %} title="{{ cat.description.value | strip_html | escape }}"{% endif %}>
|
|
||||||
{%- for it in cat.parent_names %} ⟩ {% endfor %}{{ cat.name }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
<script>window.setTimeout(() => Admin.toggleEpisodeFields(), 500)</script>
|
|
@ -217,13 +217,13 @@ this.Admin = {
|
|||||||
* @param {"none"|"internal"|"external"} src The source for chapters for this episode
|
* @param {"none"|"internal"|"external"} src The source for chapters for this episode
|
||||||
*/
|
*/
|
||||||
setChapterSource(src) {
|
setChapterSource(src) {
|
||||||
document.getElementById("containsWaypoints").disabled = src === "none"
|
document.getElementById("ContainsWaypoints").disabled = src === "none"
|
||||||
const isDisabled = src === "none" || src === "internal"
|
const isDisabled = src === "none" || src === "internal"
|
||||||
const chapterFile = document.getElementById("chapterFile")
|
const chapterFile = document.getElementById("ChapterFile")
|
||||||
chapterFile.disabled = isDisabled
|
chapterFile.disabled = isDisabled
|
||||||
chapterFile.required = !isDisabled
|
chapterFile.required = !isDisabled
|
||||||
document.getElementById("chapterType").disabled = isDisabled
|
document.getElementById("ChapterType").disabled = isDisabled
|
||||||
const link = document.getElementById("chapterEditLink")
|
const link = document.getElementById("ChapterEditLink")
|
||||||
if (link) link.style.display = src === "none" || src === "external" ? "none" : ""
|
if (link) link.style.display = src === "none" || src === "external" ? "none" : ""
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -231,13 +231,13 @@ this.Admin = {
|
|||||||
* Enable or disable podcast fields
|
* Enable or disable podcast fields
|
||||||
*/
|
*/
|
||||||
toggleEpisodeFields() {
|
toggleEpisodeFields() {
|
||||||
const disabled = !document.getElementById("isEpisode").checked
|
const disabled = !document.getElementById("IsEpisode").checked
|
||||||
let fields = [
|
let fields = [
|
||||||
"media", "mediaType", "length", "duration", "subtitle", "imageUrl", "explicit", "transcriptUrl", "transcriptType",
|
"Media", "MediaType", "Length", "Duration", "Subtitle", "ImageUrl", "Explicit", "TranscriptUrl", "TranscriptType",
|
||||||
"transcriptLang", "transcriptCaptions", "seasonNumber", "seasonDescription", "episodeNumber", "episodeDescription"
|
"TranscriptLang", "TranscriptCaptions", "SeasonNumber", "SeasonDescription", "EpisodeNumber", "EpisodeDescription"
|
||||||
]
|
]
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
fields.push("chapterFile", "chapterType", "containsWaypoints")
|
fields.push("ChapterFile", "ChapterType", "ContainsWaypoints")
|
||||||
} else {
|
} else {
|
||||||
const src = [...document.getElementsByName("ChapterSource")].filter(it => it.checked)[0].value
|
const src = [...document.getElementsByName("ChapterSource")].filter(it => it.checked)[0].value
|
||||||
this.setChapterSource(src)
|
this.setChapterSource(src)
|
||||||
@ -302,7 +302,7 @@ this.Admin = {
|
|||||||
* Require transcript type if transcript URL is present
|
* Require transcript type if transcript URL is present
|
||||||
*/
|
*/
|
||||||
requireTranscriptType() {
|
requireTranscriptType() {
|
||||||
document.getElementById("transcriptType").required = document.getElementById("transcriptUrl").value.trim() !== ""
|
document.getElementById("TranscriptType").required = document.getElementById("TranscriptUrl").value.trim() !== ""
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user