feat(submit): add video cover picker with playable video preview
Featured video preview now renders a real DPlayer (video-url/video-pic) like the reference theme instead of a static image, so the cover video plays on the submit page. Top-right bar gains the video-cover button shown only when a video cover is loaded; backend honors pic_id and no longer deletes the thumbnail before cover validation. Drop dead i18n keys and data-can-* attributes.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
/.git/
|
||||
/.claude/
|
||||
/.compare/
|
||||
/.zcode/
|
||||
|
||||
# Project planning, internal docs, and local-only development files
|
||||
/.specs/
|
||||
@@ -52,6 +53,9 @@
|
||||
!/tests/widget-system/circular-button-icon-margin-contract.test.cjs
|
||||
!/tests/widget-system/single-cover-height-contract.test.cjs
|
||||
!/tests/widget-system/frontend-lifecycle-contract.test.cjs
|
||||
!/tests/front-submit/
|
||||
/tests/front-submit/*
|
||||
!/tests/front-submit/video-cover-contract.test.cjs
|
||||
!/tests/asset-ledger/
|
||||
!/tests/asset-ledger/*.php
|
||||
!/tests/visual/docker-compose.zibll.yml
|
||||
|
||||
@@ -498,23 +498,13 @@ body.aksrt-editor-media-open {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.featured-edit .featured-preview > img,
|
||||
.featured-edit .featured-video-placeholder {
|
||||
.featured-edit .featured-preview > img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 2 / 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.featured-edit .featured-video-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: var(--muted-2-color, #999);
|
||||
}
|
||||
|
||||
.featured-edit .featured-type-badge {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
@@ -557,6 +547,14 @@ body.aksrt-editor-media-open {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.featured-edit .btns-full .btn-video-pic {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.featured-loaded.video .btns-full .btn-video-pic {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.featured-loaded.image .btn-video,
|
||||
.featured-loaded.image .btn-slide,
|
||||
.featured-loaded.slide .btn-video,
|
||||
|
||||
@@ -400,6 +400,22 @@
|
||||
this.isDirty = true;
|
||||
},
|
||||
|
||||
destroyFeaturedPlayer: function (preview) {
|
||||
var old = qs('.new-dplayer', preview);
|
||||
if (!old || !window.jQuery) {
|
||||
return;
|
||||
}
|
||||
var dp = window.jQuery(old).data('dplayer');
|
||||
if (dp) {
|
||||
if (typeof dp.pause === 'function') {
|
||||
dp.pause();
|
||||
}
|
||||
if (typeof dp.destroy === 'function') {
|
||||
dp.destroy();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
renderFeaturedPreview: function () {
|
||||
var preview = qs('[data-featured-preview]');
|
||||
if (!preview) {
|
||||
@@ -424,12 +440,20 @@
|
||||
'" alt=""><span class="featured-type-badge"><i class="fa fas fa-image"></i></span>';
|
||||
} else if (t === 'video') {
|
||||
html =
|
||||
(d.pic
|
||||
? '<img src="' + escapeHtml(d.pic) + '" alt="">'
|
||||
: '<div class="featured-video-placeholder"><i class="fa fas fa-play"></i></div>') +
|
||||
'<div class="new-dplayer dplayer-scale-height" style="--scale-height:50%;" video-url="' +
|
||||
escapeHtml(d.url || '') +
|
||||
'" video-pic="' +
|
||||
escapeHtml(d.pic || '') +
|
||||
'"><div class="graphic" style="padding-bottom:50%;">' +
|
||||
'<div class="abs-center text-center"><i class="fa fas fa-play-circle fa-4x muted-3-color opacity5" aria-hidden="true"></i></div>' +
|
||||
'</div></div>' +
|
||||
'<span class="featured-type-badge"><i class="fa fas fa-video"></i></span>';
|
||||
}
|
||||
this.destroyFeaturedPlayer(preview);
|
||||
preview.innerHTML = html;
|
||||
if (t === 'video' && typeof window.auto_fun === 'function') {
|
||||
window.auto_fun();
|
||||
}
|
||||
var clearBtn = qs('[data-featured-action="close"]');
|
||||
if (clearBtn) {
|
||||
clearBtn.hidden = !t || t === 'close';
|
||||
@@ -489,6 +513,27 @@
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (act === 'video_pic') {
|
||||
var current = self.featured;
|
||||
if (current.type !== 'video' || !current.data || !current.data.url) {
|
||||
return;
|
||||
}
|
||||
self.openMediaPicker({
|
||||
type: 'image',
|
||||
multiple: false,
|
||||
allowExternal: false,
|
||||
title: i18n.setVideoCoverTitle || '为视频设置首图封面',
|
||||
onSelect: function (cover) {
|
||||
self.setFeatured('video', {
|
||||
url: current.data.url,
|
||||
pic: cover && cover.url ? cover.url : '',
|
||||
pic_id: cover ? (cover.attachment_id || cover.id || 0) : 0,
|
||||
id: current.data.id || 0,
|
||||
});
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (act === 'video') {
|
||||
self.openMediaPicker({
|
||||
type: 'video',
|
||||
@@ -509,7 +554,7 @@
|
||||
type: 'image',
|
||||
multiple: false,
|
||||
allowExternal: false,
|
||||
title: i18n.pickVideoCover || '选择视频封面(可选)',
|
||||
title: i18n.setVideoCoverTitle || '为视频设置首图封面',
|
||||
onSelect: function (cover) {
|
||||
self.setFeatured('video', {
|
||||
url: video.url,
|
||||
|
||||
@@ -48,21 +48,36 @@ trait FrontSubmitFeatured {
|
||||
if ($video_id > 0 && !$this->can_use_front_submit_video($video_id, $user_id)) {
|
||||
$video_id = 0;
|
||||
}
|
||||
$pic = esc_url_raw((string) ($payload['pic'] ?? ''));
|
||||
if ($pic !== '' && !$this->user_can_cap('upload_submit_image', $user_id)) {
|
||||
$pic_id = absint($payload['pic_id'] ?? 0);
|
||||
if ($pic_id > 0 && !$this->can_use_front_submit_image($pic_id, $user_id)) {
|
||||
$pic_id = 0;
|
||||
}
|
||||
$pic = '';
|
||||
if ($pic_id > 0) {
|
||||
$pic = (string) wp_get_attachment_image_url($pic_id, 'full');
|
||||
}
|
||||
if ($pic === '') {
|
||||
$pic = esc_url_raw((string) ($payload['pic'] ?? ''));
|
||||
if ($pic !== '' && !$this->can_use_front_submit_image_url($pic, $user_id)) {
|
||||
$pic = '';
|
||||
}
|
||||
}
|
||||
$post_meta['featured_video'] = $video;
|
||||
$post_meta['featured_video_attachment_id'] = $video_id;
|
||||
$post_meta['featured_slide'] = '';
|
||||
$post_meta['thumbnail_url'] = '';
|
||||
$post_meta['cover_image'] = '';
|
||||
delete_post_thumbnail($post_id);
|
||||
if ($pic !== '' && $this->can_use_front_submit_image_url($pic, $user_id)) {
|
||||
if ($pic !== '') {
|
||||
$post_meta['thumbnail_url'] = $pic;
|
||||
$post_meta['cover_image'] = $pic;
|
||||
if ($pic_id > 0) {
|
||||
set_post_thumbnail($post_id, $pic_id);
|
||||
} else {
|
||||
$this->maybe_set_thumbnail_url($post_id, $pic);
|
||||
}
|
||||
} else {
|
||||
delete_post_thumbnail($post_id);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'slide':
|
||||
|
||||
@@ -147,9 +147,8 @@ if (file_exists($js)) {
|
||||
'fileTooLarge' => __('文件大小超过限制', 'aksrt'),
|
||||
'lastSaved' => __('最后保存:', 'aksrt'),
|
||||
'preview' => __('预览文章', 'aksrt'),
|
||||
'pickImage' => __('选择图片', 'aksrt'),
|
||||
'pickVideo' => __('选择视频封面图', 'aksrt'),
|
||||
'videoUrl' => __('请输入视频地址', 'aksrt'),
|
||||
'setVideoCover' => __('设置视频首图封面', 'aksrt'),
|
||||
'setVideoCoverTitle' => __('为视频设置首图封面', 'aksrt'),
|
||||
'close' => __('关闭', 'aksrt'),
|
||||
'insertImage' => __('插入图片', 'aksrt'),
|
||||
'uploadImage' => __('上传图片', 'aksrt'),
|
||||
@@ -198,8 +197,6 @@ if (file_exists($js)) {
|
||||
'maxMediaItems' => __('最多选择 %d 个文件', 'aksrt'),
|
||||
'uploadingClose' => __('上传中,请稍候再关闭', 'aksrt'),
|
||||
'mediaPickerUnavailable' => __('媒体选择器加载失败,请刷新页面后重试', 'aksrt'),
|
||||
'pickVideoResource' => __('选择视频资源', 'aksrt'),
|
||||
'pickVideoCover' => __('选择视频封面(可选)', 'aksrt'),
|
||||
'download' => __('下载', 'aksrt'),
|
||||
|
||||
'quote' => __('引言', 'aksrt'),
|
||||
|
||||
@@ -22,10 +22,7 @@ $can_slide = !empty($ctx['can_cover_slide']);
|
||||
$can_video = !empty($ctx['can_cover_video']);
|
||||
?>
|
||||
<div class="featured-edit<?php echo $type !== '' ? ' featured-loaded ' . esc_attr($type) : ''; ?>"
|
||||
data-featured-args="<?php echo esc_attr(wp_json_encode($args)); ?>"
|
||||
data-can-image="<?php echo $can_image ? '1' : '0'; ?>"
|
||||
data-can-slide="<?php echo $can_slide ? '1' : '0'; ?>"
|
||||
data-can-video="<?php echo $can_video ? '1' : '0'; ?>">
|
||||
data-featured-args="<?php echo esc_attr(wp_json_encode($args)); ?>">
|
||||
<div class="featured-preview" data-featured-preview>
|
||||
<?php if ($type === 'image' && !empty($data['url'])) : ?>
|
||||
<img src="<?php echo esc_url((string) $data['url']); ?>" alt="">
|
||||
@@ -33,11 +30,13 @@ $can_video = !empty($ctx['can_cover_video']);
|
||||
<img src="<?php echo esc_url((string) $data[0]['url']); ?>" alt="">
|
||||
<span class="featured-type-badge"><i class="fa fas fa-image"></i></span>
|
||||
<?php elseif ($type === 'video') : ?>
|
||||
<?php if (!empty($data['pic'])) : ?>
|
||||
<img src="<?php echo esc_url((string) $data['pic']); ?>" alt="">
|
||||
<?php else : ?>
|
||||
<div class="featured-video-placeholder"><i class="fa fas fa-play"></i></div>
|
||||
<?php endif; ?>
|
||||
<div class="new-dplayer dplayer-scale-height" style="--scale-height:50%;"
|
||||
video-url="<?php echo esc_url((string) ($data['url'] ?? '')); ?>"
|
||||
video-pic="<?php echo esc_url((string) ($data['pic'] ?? '')); ?>">
|
||||
<div class="graphic" style="padding-bottom:50%;">
|
||||
<div class="abs-center text-center"><i class="fa fas fa-play-circle fa-4x muted-3-color opacity5" aria-hidden="true"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="featured-type-badge"><i class="fa fas fa-video"></i></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -47,6 +46,11 @@ $can_video = !empty($ctx['can_cover_video']);
|
||||
<i class="fa fas fa-image"></i> <?php esc_html_e('图片', 'aksrt'); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($can_video) : ?>
|
||||
<button type="button" class="but btn-video-pic" data-featured-action="video_pic">
|
||||
<i class="fa fas fa-image"></i> <?php esc_html_e('设置视频首图封面', 'aksrt'); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($can_slide) : ?>
|
||||
<button type="button" class="but btn-slide" data-featured-action="slide">
|
||||
<i class="fa fas fa-image"></i> <?php esc_html_e('幻灯片', 'aksrt'); ?>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
|
||||
test('featured box renders a playable video preview and a video-cover button', () => {
|
||||
const featured = read('templates/parts/submit/featured.php');
|
||||
|
||||
assert.match(featured, /class="new-dplayer dplayer-scale-height"/);
|
||||
assert.match(featured, /video-url="<\?php echo esc_url\(\(string\) \(\$data\['url'\] \?\? ''\)\); \?>"/);
|
||||
assert.match(featured, /video-pic="<\?php echo esc_url\(\(string\) \(\$data\['pic'\] \?\? ''\)\); \?>"/);
|
||||
assert.match(featured, /fa fas fa-play-circle/);
|
||||
|
||||
// The video-cover button is capability-gated like the video button.
|
||||
assert.match(featured, /if \(\$can_video\) : \?>\s*\n\s*<button type="button" class="but btn-video-pic" data-featured-action="video_pic">/);
|
||||
|
||||
// Dead capability attributes are gone.
|
||||
assert.doesNotMatch(featured, /data-can-(image|slide|video)=/);
|
||||
|
||||
// The static image fallback for video covers must not come back.
|
||||
assert.doesNotMatch(featured, /featured-video-placeholder/);
|
||||
});
|
||||
|
||||
test('front-submit.js re-renders the preview as a DPlayer and reuses the runtime initializer', () => {
|
||||
const js = read('assets/js/front-submit.js');
|
||||
|
||||
assert.match(js, /new-dplayer dplayer-scale-height/);
|
||||
assert.match(js, /video-url="' \+\s*escapeHtml\(d\.url \|\| ''\)/);
|
||||
assert.match(js, /video-pic="' \+\s*escapeHtml\(d\.pic \|\| ''\)/);
|
||||
assert.match(js, /typeof window\.auto_fun === 'function'/);
|
||||
assert.match(js, /destroyFeaturedPlayer/);
|
||||
assert.match(js, /data\('dplayer'\)/);
|
||||
|
||||
// The dedicated video-cover picker action.
|
||||
assert.match(js, /act === 'video_pic'/);
|
||||
assert.match(js, /i18n\.setVideoCoverTitle/);
|
||||
assert.match(js, /pic_id: cover \? \(cover\.attachment_id \|\| cover\.id \|\| 0\) : 0/);
|
||||
});
|
||||
|
||||
test('submit.css toggles the video-cover button like the reference theme', () => {
|
||||
const css = read('assets/css/frontend/pages/submit.css');
|
||||
|
||||
assert.match(css, /\.featured-edit \.btns-full \.btn-video-pic \{\s*\n display: none;\s*\n\}/);
|
||||
assert.match(css, /\.featured-loaded\.video \.btns-full \.btn-video-pic \{\s*\n display: inline-flex;\s*\n\}/);
|
||||
assert.doesNotMatch(css, /featured-video-placeholder/);
|
||||
});
|
||||
|
||||
test('the backend accepts pic_id and never wipes the thumbnail before validation', () => {
|
||||
const featured = read('src/Core/FrontSubmitFeatured.php');
|
||||
const videoBranch = featured.slice(featured.indexOf("case 'video':"), featured.indexOf("case 'slide':"));
|
||||
|
||||
assert.match(videoBranch, /absint\(\$payload\['pic_id'\] \?\? 0\)/);
|
||||
assert.match(videoBranch, /!\$this->can_use_front_submit_image\(\$pic_id, \$user_id\)/);
|
||||
assert.match(videoBranch, /wp_get_attachment_image_url\(\$pic_id, 'full'\)/);
|
||||
assert.match(videoBranch, /set_post_thumbnail\(\$post_id, \$pic_id\)/);
|
||||
|
||||
// delete_post_thumbnail only runs when no valid cover picture remains.
|
||||
const deletePos = videoBranch.indexOf('delete_post_thumbnail($post_id)');
|
||||
const picGuard = videoBranch.indexOf("if ($pic !== '') {");
|
||||
assert.ok(picGuard !== -1 && deletePos > picGuard, 'delete_post_thumbnail must be guarded by the pic check');
|
||||
assert.doesNotMatch(videoBranch, /upload_submit_image/);
|
||||
});
|
||||
|
||||
test('i18n exposes the video-cover strings and dead keys are removed', () => {
|
||||
const page = read('templates/pages/page-submit.php');
|
||||
const editorJs = read('assets/js/front-submit-editor.js');
|
||||
const submitJs = read('assets/js/front-submit.js');
|
||||
|
||||
assert.match(page, /'setVideoCover' => __\('设置视频首图封面', 'aksrt'\)/);
|
||||
assert.match(page, /'setVideoCoverTitle' => __\('为视频设置首图封面', 'aksrt'\)/);
|
||||
assert.doesNotMatch(page, /'(pickImage|pickVideo|pickVideoCover|pickVideoResource|videoUrl)'/);
|
||||
|
||||
// data-video-name / mediaUrl stay: pinned by tinymce.css and the
|
||||
// front-submit-attachment contract respectively.
|
||||
assert.match(editorJs, /data-video-name/);
|
||||
assert.match(editorJs, /cfg\.mediaUrl/);
|
||||
assert.doesNotMatch(submitJs, /i18n\.pickVideoCover/);
|
||||
});
|
||||
|
||||
test('the unused aksrt_update_post ajax endpoint is removed', () => {
|
||||
const frontSubmit = read('src/Core/FrontSubmit.php');
|
||||
|
||||
assert.doesNotMatch(frontSubmit, /aksrt_update_post/);
|
||||
assert.doesNotMatch(frontSubmit, /ajax_update_post/);
|
||||
});
|
||||
Reference in New Issue
Block a user