feat(widgets): deepen buttons, video player, recent comments
Add go_link/hollow/radius/icon slots for buttons; loop/poster/hide controls for video player; exclude post/keywords on recent comments.
This commit is contained in:
@@ -377,3 +377,79 @@
|
||||
.aksrt-cover-card__bg.is-video video {
|
||||
width: 100%; height: 100%; object-fit: cover; display: block;
|
||||
}
|
||||
|
||||
.widget-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--btn-gap, 10px);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.widget-buttons--vertical { flex-direction: column; }
|
||||
.widget-buttons.align-center { justify-content: center; }
|
||||
.widget-buttons.align-left { justify-content: flex-start; }
|
||||
.widget-buttons.align-right { justify-content: flex-end; }
|
||||
.widget-buttons__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
background: var(--color-primary, #3b82f6);
|
||||
color: #fff !important;
|
||||
}
|
||||
.widget-buttons.is-radius .widget-buttons__btn { border-radius: 999px; }
|
||||
.widget-buttons.is-hollow .widget-buttons__btn {
|
||||
background: transparent;
|
||||
color: var(--color-primary, #3b82f6) !important;
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
.widget-buttons__btn--secondary { background: #64748b; }
|
||||
.widget-buttons__btn--outline {
|
||||
background: transparent;
|
||||
color: inherit !important;
|
||||
border: 1px solid var(--color-line, #ddd);
|
||||
}
|
||||
.widget-buttons__btn--text {
|
||||
background: transparent;
|
||||
color: var(--color-primary, #3b82f6) !important;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.widget-buttons__btn--pink { background: #ec4899; }
|
||||
.widget-buttons__btn--blue { background: #3b82f6; }
|
||||
.widget-buttons__btn--green { background: #22c55e; }
|
||||
|
||||
.aksrt-video-widget {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #111;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.aksrt-video-widget iframe,
|
||||
.aksrt-video-widget video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
.aksrt-video-widget.controller-hide video::-webkit-media-controls {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.widget-recent-comments__post {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.widget-recent-comments__post a {
|
||||
color: var(--muted-2-color, #999);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* Custom buttons group (zibll zib_widget_ui_buttons lite).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
@@ -9,106 +11,169 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class ButtonsWidget extends \WP_Widget {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct('aksrt_buttons', esc_html__('AKSRT 按钮组', 'aksrt'), [
|
||||
'description' => esc_html__('显示一组自定义按钮', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
public function __construct() {
|
||||
parent::__construct('aksrt_buttons', esc_html__('AKSRT 按钮组', 'aksrt'), [
|
||||
'description' => esc_html__('显示一组自定义按钮,支持外链跳转与样式。', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function widget($args, $instance): void {
|
||||
echo $args['before_widget'];
|
||||
$title = apply_filters('widget_title', $instance['title'] ?? '', $instance, $this->id_base);
|
||||
$buttons = isset($instance['buttons']) && is_array($instance['buttons']) ? $instance['buttons'] : [];
|
||||
$layout = sanitize_key($instance['layout'] ?? 'vertical');
|
||||
public function widget($args, $instance): void {
|
||||
$buttons = isset($instance['buttons']) && is_array($instance['buttons']) ? $instance['buttons'] : [];
|
||||
$layout = sanitize_key((string) ($instance['layout'] ?? 'horizontal'));
|
||||
if (!in_array($layout, ['vertical', 'horizontal'], true)) {
|
||||
$layout = 'horizontal';
|
||||
}
|
||||
$align = sanitize_key((string) ($instance['align'] ?? 'center'));
|
||||
if (!in_array($align, ['left', 'center', 'right'], true)) {
|
||||
$align = 'center';
|
||||
}
|
||||
$radius = !empty($instance['radius']);
|
||||
$hollow = !empty($instance['hollow']);
|
||||
$go_link = !empty($instance['go_link']);
|
||||
$gap = max(4, min(30, (int) ($instance['gap'] ?? 10)));
|
||||
|
||||
if ($title) echo $args['before_title'] . esc_html($title) . $args['after_title'];
|
||||
$items = [];
|
||||
foreach ($buttons as $btn) {
|
||||
if (!is_array($btn)) {
|
||||
continue;
|
||||
}
|
||||
$text = sanitize_text_field((string) ($btn['text'] ?? ''));
|
||||
$url = esc_url_raw(trim((string) ($btn['url'] ?? '')));
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
if ($url !== '' && $go_link && function_exists('aksrt_go_link')) {
|
||||
$url = aksrt_go_link($url, true);
|
||||
}
|
||||
$items[] = [
|
||||
'text' => $text,
|
||||
'url' => $url,
|
||||
'style' => sanitize_key((string) ($btn['style'] ?? 'primary')),
|
||||
'icon' => sanitize_text_field((string) ($btn['icon'] ?? $btn['icon_left'] ?? '')),
|
||||
'icon_right' => sanitize_text_field((string) ($btn['icon_right'] ?? '')),
|
||||
'new_window' => !empty($btn['new_window']) || !empty($btn['target']),
|
||||
];
|
||||
}
|
||||
if (!$items) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($buttons)) : ?>
|
||||
<div class="widget-buttons widget-buttons--<?php echo esc_attr($layout); ?>">
|
||||
<?php foreach ($buttons as $btn) :
|
||||
$btn_text = sanitize_text_field($btn['text'] ?? '');
|
||||
$btn_url = esc_url($btn['url'] ?? '');
|
||||
$btn_style = sanitize_key($btn['style'] ?? 'primary');
|
||||
$btn_icon = sanitize_text_field($btn['icon'] ?? '');
|
||||
$btn_target = !empty($btn['new_window']) ? '_blank' : '_self';
|
||||
if ($btn_text === '' || $btn_url === '') continue;
|
||||
?>
|
||||
<a href="<?php echo $btn_url; ?>" class="widget-buttons__btn widget-buttons__btn--<?php echo esc_attr($btn_style); ?>" target="<?php echo esc_attr($btn_target); ?>" rel="noopener">
|
||||
<?php if ($btn_icon) : ?><i class="<?php echo esc_attr($btn_icon); ?>" aria-hidden="true"></i> <?php endif; ?>
|
||||
<?php echo esc_html($btn_text); ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif;
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
|
||||
if ($title !== '') {
|
||||
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
echo $args['after_widget'];
|
||||
}
|
||||
$wrap = 'widget-buttons widget-buttons--' . $layout . ' align-' . $align;
|
||||
if ($radius) {
|
||||
$wrap .= ' is-radius';
|
||||
}
|
||||
if ($hollow) {
|
||||
$wrap .= ' is-hollow';
|
||||
}
|
||||
echo '<div class="' . esc_attr($wrap) . '" style="--btn-gap:' . esc_attr((string) $gap) . 'px">';
|
||||
foreach ($items as $btn) {
|
||||
$style = in_array($btn['style'], ['primary', 'secondary', 'outline', 'text', 'pink', 'blue', 'green', 'yellow', 'purple'], true)
|
||||
? $btn['style']
|
||||
: 'primary';
|
||||
$cls = 'widget-buttons__btn widget-buttons__btn--' . $style;
|
||||
$href = $btn['url'] !== '' ? $btn['url'] : 'javascript:void(0)';
|
||||
$tgt = $btn['new_window'] && $btn['url'] !== '' ? ' target="_blank" rel="noopener noreferrer"' : '';
|
||||
echo '<a href="' . esc_url($href) . '" class="' . esc_attr($cls) . '"' . $tgt . '>';
|
||||
if ($btn['icon'] !== '') {
|
||||
echo '<i class="' . esc_attr($btn['icon']) . '" aria-hidden="true"></i> ';
|
||||
}
|
||||
echo esc_html($btn['text']);
|
||||
if ($btn['icon_right'] !== '') {
|
||||
echo ' <i class="' . esc_attr($btn['icon_right']) . '" aria-hidden="true"></i>';
|
||||
}
|
||||
echo '</a>';
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
public function form($instance): void {
|
||||
$buttons = isset($instance['buttons']) && is_array($instance['buttons']) ? $instance['buttons'] : [];
|
||||
while (count($buttons) < 3) {
|
||||
$buttons[] = ['text' => '', 'url' => '', 'style' => 'primary', 'icon' => '', 'new_window' => false];
|
||||
}
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('title'); ?>"><?php esc_html_e('标题(可选):', 'aksrt'); ?></label>
|
||||
<input class="widefat" type="text" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" value="<?php echo esc_attr($instance['title'] ?? ''); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('layout'); ?>"><?php esc_html_e('布局:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo $this->get_field_id('layout'); ?>" name="<?php echo $this->get_field_name('layout'); ?>">
|
||||
<option value="vertical" <?php selected($instance['layout'] ?? '', 'vertical'); ?>><?php esc_html_e('垂直', 'aksrt'); ?></option>
|
||||
<option value="horizontal" <?php selected($instance['layout'] ?? '', 'horizontal'); ?>><?php esc_html_e('水平', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<hr>
|
||||
<?php foreach ($buttons as $i => $btn) : ?>
|
||||
<div style="border:1px solid #ddd;padding:6px;margin-bottom:6px;border-radius:4px;">
|
||||
<p style="margin:0 0 4px;">
|
||||
<input class="widefat" type="text" name="<?php echo $this->get_field_name("buttons[{$i}][text]"); ?>" value="<?php echo esc_attr($btn['text']); ?>" placeholder="<?php esc_attr_e('按钮文字', 'aksrt'); ?>">
|
||||
</p>
|
||||
<p style="margin:0 0 4px;">
|
||||
<input class="widefat" type="url" name="<?php echo $this->get_field_name("buttons[{$i}][url]"); ?>" value="<?php echo esc_attr($btn['url']); ?>" placeholder="<?php esc_attr_e('链接地址', 'aksrt'); ?>">
|
||||
</p>
|
||||
<p style="margin:0 0 4px;">
|
||||
<select class="widefat" name="<?php echo $this->get_field_name("buttons[{$i}][style]"); ?>">
|
||||
<option value="primary" <?php selected($btn['style'] ?? '', 'primary'); ?>><?php esc_html_e('主要', 'aksrt'); ?></option>
|
||||
<option value="secondary" <?php selected($btn['style'] ?? '', 'secondary'); ?>><?php esc_html_e('次要', 'aksrt'); ?></option>
|
||||
<option value="outline" <?php selected($btn['style'] ?? '', 'outline'); ?>><?php esc_html_e('线框', 'aksrt'); ?></option>
|
||||
<option value="text" <?php selected($btn['style'] ?? '', 'text'); ?>><?php esc_html_e('文字', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<p style="margin:0 0 4px;">
|
||||
<input class="widefat" type="text" name="<?php echo $this->get_field_name("buttons[{$i}][icon]"); ?>" value="<?php echo esc_attr($btn['icon'] ?? ''); ?>" placeholder="<?php esc_attr_e('图标(可选)', 'aksrt'); ?>">
|
||||
</p>
|
||||
<p style="margin:0;">
|
||||
<label><input type="checkbox" name="<?php echo $this->get_field_name("buttons[{$i}][new_window]"); ?>" value="1" <?php checked(!empty($btn['new_window'])); ?>> <?php esc_html_e('新窗口打开', 'aksrt'); ?></label>
|
||||
</p>
|
||||
</div>
|
||||
<?php endforeach;
|
||||
}
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
$buttons = [];
|
||||
if (isset($new_instance['buttons']) && is_array($new_instance['buttons'])) {
|
||||
$allowed_styles = ['primary', 'secondary', 'outline', 'text'];
|
||||
foreach ($new_instance['buttons'] as $btn) {
|
||||
if (!is_array($btn)) continue;
|
||||
$style = sanitize_key($btn['style'] ?? 'primary');
|
||||
if (!in_array($style, $allowed_styles, true)) $style = 'primary';
|
||||
$buttons[] = [
|
||||
'text' => sanitize_text_field($btn['text'] ?? ''),
|
||||
'url' => esc_url_raw($btn['url'] ?? ''),
|
||||
'style' => $style,
|
||||
'icon' => sanitize_text_field($btn['icon'] ?? ''),
|
||||
'new_window' => !empty($btn['new_window']),
|
||||
];
|
||||
}
|
||||
}
|
||||
return [
|
||||
'title' => sanitize_text_field($new_instance['title'] ?? ''),
|
||||
'layout' => in_array($new_instance['layout'] ?? '', ['vertical', 'horizontal'], true) ? $new_instance['layout'] : 'vertical',
|
||||
'buttons' => $buttons,
|
||||
];
|
||||
}
|
||||
public function form($instance): void {
|
||||
$buttons = isset($instance['buttons']) && is_array($instance['buttons']) ? $instance['buttons'] : [];
|
||||
while (count($buttons) < 4) {
|
||||
$buttons[] = ['text' => '', 'url' => '', 'style' => 'primary', 'icon' => '', 'icon_right' => '', 'new_window' => false];
|
||||
}
|
||||
?>
|
||||
<p>
|
||||
<label><?php esc_html_e('标题(可选)', 'aksrt'); ?>
|
||||
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) ($instance['title'] ?? '')); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><?php esc_html_e('布局', 'aksrt'); ?>
|
||||
<select name="<?php echo esc_attr($this->get_field_name('layout')); ?>">
|
||||
<option value="horizontal" <?php selected(($instance['layout'] ?? 'horizontal'), 'horizontal'); ?>><?php esc_html_e('水平', 'aksrt'); ?></option>
|
||||
<option value="vertical" <?php selected(($instance['layout'] ?? ''), 'vertical'); ?>><?php esc_html_e('垂直', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label><?php esc_html_e('对齐', 'aksrt'); ?>
|
||||
<select name="<?php echo esc_attr($this->get_field_name('align')); ?>">
|
||||
<option value="center" <?php selected(($instance['align'] ?? 'center'), 'center'); ?>><?php esc_html_e('居中', 'aksrt'); ?></option>
|
||||
<option value="left" <?php selected(($instance['align'] ?? ''), 'left'); ?>><?php esc_html_e('左', 'aksrt'); ?></option>
|
||||
<option value="right" <?php selected(($instance['align'] ?? ''), 'right'); ?>><?php esc_html_e('右', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('radius')); ?>" value="1" <?php checked(!empty($instance['radius'])); ?>> <?php esc_html_e('圆角胶囊', 'aksrt'); ?></label>
|
||||
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('hollow')); ?>" value="1" <?php checked(!empty($instance['hollow'])); ?>> <?php esc_html_e('镂空', 'aksrt'); ?></label>
|
||||
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('go_link')); ?>" value="1" <?php checked(!empty($instance['go_link'])); ?>> <?php esc_html_e('外链跳转页', 'aksrt'); ?></label>
|
||||
</p>
|
||||
<?php foreach ($buttons as $i => $btn) : ?>
|
||||
<div style="border:1px solid #ddd;padding:6px;margin-bottom:6px;border-radius:4px;">
|
||||
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name("buttons[{$i}][text]")); ?>" value="<?php echo esc_attr((string) ($btn['text'] ?? '')); ?>" placeholder="<?php esc_attr_e('按钮文字', 'aksrt'); ?>">
|
||||
<input class="widefat" type="url" name="<?php echo esc_attr($this->get_field_name("buttons[{$i}][url]")); ?>" value="<?php echo esc_attr((string) ($btn['url'] ?? '')); ?>" placeholder="<?php esc_attr_e('链接', 'aksrt'); ?>">
|
||||
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name("buttons[{$i}][icon]")); ?>" value="<?php echo esc_attr((string) ($btn['icon'] ?? '')); ?>" placeholder="<?php esc_attr_e('左图标 class', 'aksrt'); ?>">
|
||||
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name("buttons[{$i}][icon_right]")); ?>" value="<?php echo esc_attr((string) ($btn['icon_right'] ?? '')); ?>" placeholder="<?php esc_attr_e('右图标 class', 'aksrt'); ?>">
|
||||
<select class="widefat" name="<?php echo esc_attr($this->get_field_name("buttons[{$i}][style]")); ?>">
|
||||
<?php foreach (['primary' => '主要', 'secondary' => '次要', 'outline' => '线框', 'text' => '文字', 'pink' => '粉', 'blue' => '蓝', 'green' => '绿'] as $k => $lab) : ?>
|
||||
<option value="<?php echo esc_attr($k); ?>" <?php selected(($btn['style'] ?? 'primary'), $k); ?>><?php echo esc_html($lab); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name("buttons[{$i}][new_window]")); ?>" value="1" <?php checked(!empty($btn['new_window'])); ?>> <?php esc_html_e('新窗口', 'aksrt'); ?></label>
|
||||
</div>
|
||||
<?php endforeach;
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
$buttons = [];
|
||||
if (isset($new_instance['buttons']) && is_array($new_instance['buttons'])) {
|
||||
foreach ($new_instance['buttons'] as $btn) {
|
||||
if (!is_array($btn)) {
|
||||
continue;
|
||||
}
|
||||
$text = sanitize_text_field((string) ($btn['text'] ?? ''));
|
||||
if ($text === '') {
|
||||
continue;
|
||||
}
|
||||
$buttons[] = [
|
||||
'text' => $text,
|
||||
'url' => esc_url_raw(trim((string) ($btn['url'] ?? ''))),
|
||||
'style' => sanitize_key((string) ($btn['style'] ?? 'primary')),
|
||||
'icon' => sanitize_text_field((string) ($btn['icon'] ?? '')),
|
||||
'icon_right' => sanitize_text_field((string) ($btn['icon_right'] ?? '')),
|
||||
'new_window' => !empty($btn['new_window']),
|
||||
];
|
||||
if (count($buttons) >= 8) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
|
||||
'layout' => ($new_instance['layout'] ?? '') === 'vertical' ? 'vertical' : 'horizontal',
|
||||
'align' => in_array(($new_instance['align'] ?? ''), ['left', 'center', 'right'], true) ? $new_instance['align'] : 'center',
|
||||
'radius' => !empty($new_instance['radius']) ? 1 : 0,
|
||||
'hollow' => !empty($new_instance['hollow']) ? 1 : 0,
|
||||
'go_link' => !empty($new_instance['go_link']) ? 1 : 0,
|
||||
'gap' => max(4, min(30, (int) ($new_instance['gap'] ?? 10))),
|
||||
'buttons' => $buttons,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* Recent comments mini list (zibll widget_ui_new_comment lite).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
@@ -9,81 +11,115 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class RecentCommentsWidget extends \WP_Widget {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct('aksrt_recent_comments', esc_html__('AKSRT 最新评论', 'aksrt'), [
|
||||
'description' => esc_html__('显示最新评论列表', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
public function __construct() {
|
||||
parent::__construct('aksrt_recent_comments', esc_html__('AKSRT 最新评论', 'aksrt'), [
|
||||
'description' => esc_html__('显示最新评论列表', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function widget($args, $instance): void {
|
||||
echo $args['before_widget'];
|
||||
$title = apply_filters('widget_title', $instance['title'] ?? __('最新评论', 'aksrt'), $instance, $this->id_base);
|
||||
$count = max(1, min(20, (int) ($instance['count'] ?? 5)));
|
||||
$show_avatar = !empty($instance['show_avatar']);
|
||||
public function widget($args, $instance): void {
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
$title = apply_filters('widget_title', (string) ($instance['title'] ?? __('最新评论', 'aksrt')), $instance, $this->id_base);
|
||||
$count = max(1, min(20, (int) ($instance['count'] ?? $instance['limit'] ?? 5)));
|
||||
$show_avatar = !isset($instance['show_avatar']) || !empty($instance['show_avatar']);
|
||||
$outpost = max(0, (int) ($instance['outpost'] ?? 0));
|
||||
$outer = sanitize_text_field((string) ($instance['outer'] ?? ''));
|
||||
|
||||
if ($title) echo $args['before_title'] . esc_html($title) . $args['after_title'];
|
||||
if ($title !== '') {
|
||||
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
$comments = get_comments([
|
||||
'number' => $count,
|
||||
'status' => 'approve',
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
$query = [
|
||||
'number' => $count,
|
||||
'status' => 'approve',
|
||||
'post_status' => 'publish',
|
||||
];
|
||||
if ($outpost > 0) {
|
||||
$query['post__not_in'] = [$outpost];
|
||||
}
|
||||
if ($outer !== '') {
|
||||
// Exclude comments containing outer keywords (comma separated).
|
||||
// Implemented post-filter below.
|
||||
}
|
||||
$comments = get_comments($query);
|
||||
if ($outer !== '' && $comments) {
|
||||
$keys = array_filter(array_map('trim', preg_split('/[,,]/', $outer) ?: []));
|
||||
if ($keys) {
|
||||
$comments = array_values(array_filter($comments, static function ($c) use ($keys) {
|
||||
$body = (string) $c->comment_content;
|
||||
foreach ($keys as $k) {
|
||||
if ($k !== '' && str_contains($body, $k)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if ($comments) : ?>
|
||||
<div class="widget-recent-comments">
|
||||
<?php foreach ($comments as $comment) :
|
||||
$post = get_post($comment->comment_post_ID);
|
||||
if (!$post) continue;
|
||||
?>
|
||||
<div class="widget-recent-comments__item">
|
||||
<?php if ($show_avatar) : ?>
|
||||
<div class="widget-recent-comments__avatar">
|
||||
<?php echo get_avatar($comment, 32); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="widget-recent-comments__body">
|
||||
<div class="widget-recent-comments__meta">
|
||||
<strong class="widget-recent-comments__author"><?php echo esc_html($comment->comment_author); ?></strong>
|
||||
<span class="widget-recent-comments__time"><?php echo esc_html(human_time_diff(strtotime($comment->comment_date), current_time('timestamp'))); ?></span>
|
||||
</div>
|
||||
<p class="widget-recent-comments__text">
|
||||
<a href="<?php echo esc_url(get_comment_link($comment)); ?>"><?php echo esc_html(wp_trim_words($comment->comment_content, 15, '...')); ?></a>
|
||||
</p>
|
||||
<span class="widget-recent-comments__post">
|
||||
<?php esc_html_e('于', 'aksrt'); ?> <a href="<?php echo esc_url(get_permalink($post->ID)); ?>"><?php echo esc_html(get_the_title($post->ID)); ?></a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<?php echo function_exists('aksrt_get_widget_null') ? aksrt_get_widget_null(__('暂无评论', 'aksrt')) : '<p class="widget-empty">' . esc_html__('暂无评论', 'aksrt') . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php endif;
|
||||
if ($comments) {
|
||||
echo '<div class="widget-recent-comments comment-mini-lists">';
|
||||
foreach ($comments as $comment) {
|
||||
$post = get_post($comment->comment_post_ID);
|
||||
if (!$post) {
|
||||
continue;
|
||||
}
|
||||
echo '<div class="widget-recent-comments__item">';
|
||||
if ($show_avatar) {
|
||||
echo '<div class="widget-recent-comments__avatar">' . get_avatar($comment, 32) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
echo '<div class="widget-recent-comments__body">';
|
||||
echo '<div class="widget-recent-comments__meta">';
|
||||
echo '<strong class="widget-recent-comments__author">' . esc_html($comment->comment_author) . '</strong>';
|
||||
echo '<span class="widget-recent-comments__time">' . esc_html(human_time_diff(strtotime($comment->comment_date), current_time('timestamp'))) . '</span>';
|
||||
echo '</div>';
|
||||
echo '<p class="widget-recent-comments__text"><a href="' . esc_url(get_comment_link($comment)) . '">' . esc_html(wp_trim_words($comment->comment_content, 15, '...')) . '</a></p>';
|
||||
echo '<div class="widget-recent-comments__post muted-2-color em09"><a href="' . esc_url(get_permalink($post)) . '">' . esc_html(get_the_title($post)) . '</a></div>';
|
||||
echo '</div></div>';
|
||||
}
|
||||
echo '</div>';
|
||||
} else {
|
||||
echo '<p class="widget-empty">' . esc_html__('暂无评论', 'aksrt') . '</p>';
|
||||
}
|
||||
|
||||
echo $args['after_widget'];
|
||||
}
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
public function form($instance): void {
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('title'); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
|
||||
<input class="widefat" type="text" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" value="<?php echo esc_attr($instance['title'] ?? __('最新评论', 'aksrt')); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('count'); ?>"><?php esc_html_e('显示数量:', 'aksrt'); ?></label>
|
||||
<input class="tiny-text" type="number" min="1" max="20" id="<?php echo $this->get_field_id('count'); ?>" name="<?php echo $this->get_field_name('count'); ?>" value="<?php echo (int) ($instance['count'] ?? 5); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label><input type="checkbox" name="<?php echo $this->get_field_name('show_avatar'); ?>" value="1" <?php checked(!empty($instance['show_avatar'])); ?>> <?php esc_html_e('显示头像', 'aksrt'); ?></label>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
public function form($instance): void {
|
||||
?>
|
||||
<p>
|
||||
<label><?php esc_html_e('标题', 'aksrt'); ?>
|
||||
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) ($instance['title'] ?? __('最新评论', 'aksrt'))); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><?php esc_html_e('数量', 'aksrt'); ?>
|
||||
<input class="tiny-text" type="number" min="1" max="20" name="<?php echo esc_attr($this->get_field_name('count')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['count'] ?? 5))); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><?php esc_html_e('排除文章 ID', 'aksrt'); ?>
|
||||
<input class="tiny-text" type="number" min="0" name="<?php echo esc_attr($this->get_field_name('outpost')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['outpost'] ?? 0))); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><?php esc_html_e('排除关键词(逗号)', 'aksrt'); ?>
|
||||
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('outer')); ?>" value="<?php echo esc_attr((string) ($instance['outer'] ?? '')); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('show_avatar')); ?>" value="1" <?php checked(!isset($instance['show_avatar']) || !empty($instance['show_avatar'])); ?>> <?php esc_html_e('显示头像', 'aksrt'); ?></label>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
return [
|
||||
'title' => sanitize_text_field($new_instance['title'] ?? ''),
|
||||
'count' => max(1, min(20, (int) ($new_instance['count'] ?? 5))),
|
||||
'show_avatar' => !empty($new_instance['show_avatar']),
|
||||
];
|
||||
}
|
||||
public function update($new_instance, $old_instance): array {
|
||||
return [
|
||||
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
|
||||
'count' => max(1, min(20, (int) ($new_instance['count'] ?? 5))),
|
||||
'outpost' => max(0, (int) ($new_instance['outpost'] ?? 0)),
|
||||
'outer' => sanitize_text_field((string) ($new_instance['outer'] ?? '')),
|
||||
'show_avatar' => !empty($new_instance['show_avatar']) ? 1 : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* Video player widget (zibll dplayer lite via native video / oEmbed).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
@@ -9,124 +11,123 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class WidgetVideoPlayer extends \WP_Widget {
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'aksrt_video_player',
|
||||
__('视频播放器', 'aksrt'),
|
||||
['description' => __('在侧栏嵌入视频播放器。', 'aksrt')]
|
||||
);
|
||||
}
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'aksrt_video_player',
|
||||
__('视频播放器', 'aksrt'),
|
||||
['description' => __('侧栏/流体区嵌入视频播放器。', 'aksrt')]
|
||||
);
|
||||
}
|
||||
|
||||
public function widget($args, $instance): void {
|
||||
$title = !empty($instance['title']) ? $instance['title'] : '';
|
||||
$video_url = !empty($instance['video_url']) ? $instance['video_url'] : '';
|
||||
$poster = !empty($instance['poster']) ? $instance['poster'] : '';
|
||||
$autoplay = !empty($instance['autoplay']);
|
||||
$aspect_ratio = !empty($instance['aspect_ratio']) ? $instance['aspect_ratio'] : '16:9';
|
||||
public function widget($args, $instance): void {
|
||||
$title = (string) ($instance['title'] ?? '');
|
||||
$video_url = trim((string) ($instance['video_url'] ?? $instance['url'] ?? ''));
|
||||
$poster = trim((string) ($instance['poster'] ?? $instance['pic'] ?? ''));
|
||||
$autoplay = !empty($instance['autoplay']);
|
||||
$loop = !empty($instance['loop']);
|
||||
$muted = $autoplay || !empty($instance['muted']);
|
||||
$hide_controller = !empty($instance['hide_controller']);
|
||||
$scale = max(20, min(100, (int) ($instance['scale_height'] ?? 56)));
|
||||
$aspect = (string) ($instance['aspect_ratio'] ?? '');
|
||||
|
||||
if (empty($video_url)) return;
|
||||
if ($video_url === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析视频类型
|
||||
$embed_url = $this->get_embed_url($video_url);
|
||||
$is_embed = !empty($embed_url);
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
if ($title !== '') {
|
||||
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
echo $args['before_widget'];
|
||||
if ($aspect !== '' && str_contains($aspect, ':')) {
|
||||
$parts = explode(':', $aspect);
|
||||
if (count($parts) === 2 && (int) $parts[0] > 0) {
|
||||
$scale = (int) round(((int) $parts[1] / (int) $parts[0]) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
if ($title) {
|
||||
echo $args['before_title'] . esc_html($title) . $args['after_title'];
|
||||
}
|
||||
$embed = $this->get_embed_url($video_url);
|
||||
$cls = 'aksrt-video-widget' . ($hide_controller ? ' controller-hide' : '');
|
||||
echo '<div class="' . esc_attr($cls) . '" style="padding-bottom:' . esc_attr((string) $scale) . '%">';
|
||||
if ($embed !== '') {
|
||||
$src = $embed;
|
||||
if ($autoplay) {
|
||||
$src = add_query_arg(['autoplay' => '1', 'mute' => '1'], $src);
|
||||
}
|
||||
echo '<iframe src="' . esc_url($src) . '" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen loading="lazy"></iframe>';
|
||||
} else {
|
||||
$attrs = ' controls playsinline';
|
||||
if ($autoplay) {
|
||||
$attrs .= ' autoplay';
|
||||
}
|
||||
if ($loop) {
|
||||
$attrs .= ' loop';
|
||||
}
|
||||
if ($muted) {
|
||||
$attrs .= ' muted';
|
||||
}
|
||||
if ($poster !== '') {
|
||||
$attrs .= ' poster="' . esc_url($poster) . '"';
|
||||
}
|
||||
echo '<video' . $attrs . '><source src="' . esc_url($video_url) . '"></video>';
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
// 计算 padding-bottom
|
||||
$ratio_parts = explode(':', $aspect_ratio);
|
||||
if (count($ratio_parts) === 2 && (int) $ratio_parts[1] > 0) {
|
||||
$padding = ((int) $ratio_parts[1] / (int) $ratio_parts[0]) * 100;
|
||||
} else {
|
||||
$padding = 56.25; // 16:9
|
||||
}
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
echo '<div class="aksrt-video-widget" style="padding-bottom: ' . esc_attr(number_format($padding, 4)) . '%;">';
|
||||
private function get_embed_url(string $url): string {
|
||||
if (preg_match('#(?:youtube\.com/watch\?v=|youtu\.be/)([A-Za-z0-9_-]+)#', $url, $m)) {
|
||||
return 'https://www.youtube.com/embed/' . rawurlencode($m[1]);
|
||||
}
|
||||
if (preg_match('#bilibili\.com/video/(BV[\w]+)#', $url, $m)) {
|
||||
return 'https://player.bilibili.com/player.html?bvid=' . rawurlencode($m[1]) . '&high_quality=1';
|
||||
}
|
||||
if (preg_match('#v\.qq\.com/.+?/([A-Za-z0-9]+)\.html#', $url, $m)) {
|
||||
return 'https://v.qq.com/txp/iframe/player.html?vid=' . rawurlencode($m[1]);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($is_embed) {
|
||||
echo '<iframe src="' . esc_url($embed_url) . '" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen' . (function_exists('aksrt_lazy_loading') ? aksrt_lazy_loading('widget') : ' loading="lazy"') . '></iframe>';
|
||||
} else {
|
||||
echo '<video' . ($autoplay ? ' autoplay' : '') . ' controls playsinline';
|
||||
if ($poster) echo ' poster="' . esc_url($poster) . '"';
|
||||
echo '>';
|
||||
echo '<source src="' . esc_url($video_url) . '">';
|
||||
echo '</video>';
|
||||
}
|
||||
public function form($instance): void {
|
||||
?>
|
||||
<p>
|
||||
<label><?php esc_html_e('标题', 'aksrt'); ?>
|
||||
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) ($instance['title'] ?? '')); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><?php esc_html_e('视频 URL', 'aksrt'); ?>
|
||||
<input class="widefat" type="url" name="<?php echo esc_attr($this->get_field_name('video_url')); ?>" value="<?php echo esc_attr((string) ($instance['video_url'] ?? '')); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><?php esc_html_e('封面图 URL', 'aksrt'); ?>
|
||||
<input class="widefat" type="url" name="<?php echo esc_attr($this->get_field_name('poster')); ?>" value="<?php echo esc_attr((string) ($instance['poster'] ?? '')); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><?php esc_html_e('高度占比 %', 'aksrt'); ?>
|
||||
<input class="tiny-text" type="number" min="20" max="100" name="<?php echo esc_attr($this->get_field_name('scale_height')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['scale_height'] ?? 56))); ?>">
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('autoplay')); ?>" value="1" <?php checked(!empty($instance['autoplay'])); ?>> <?php esc_html_e('自动播放', 'aksrt'); ?></label><br>
|
||||
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('loop')); ?>" value="1" <?php checked(!empty($instance['loop'])); ?>> <?php esc_html_e('循环', 'aksrt'); ?></label><br>
|
||||
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('hide_controller')); ?>" value="1" <?php checked(!empty($instance['hide_controller'])); ?>> <?php esc_html_e('隐藏控制条样式', 'aksrt'); ?></label>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
|
||||
echo $args['after_widget'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 YouTube / Bilibili 链接为嵌入 URL
|
||||
*/
|
||||
private function get_embed_url(string $url): string {
|
||||
// YouTube
|
||||
if (preg_match('/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)/', $url, $m)) {
|
||||
return 'https://www.youtube.com/embed/' . $m[1];
|
||||
}
|
||||
|
||||
// Bilibili
|
||||
if (preg_match('/bilibili\.com\/video\/(BV[a-zA-Z0-9]+)/', $url, $m)) {
|
||||
return 'https://player.bilibili.com/player.html?bvid=' . $m[1] . '&high_quality=1';
|
||||
}
|
||||
|
||||
// 腾讯视频
|
||||
if (preg_match('/v\.qq\.com\/x\/page\/([a-zA-Z0-9]+)/', $url, $m)) {
|
||||
return 'https://v.qq.com/txp/iframe/player.html?vid=' . $m[1];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function form($instance): void {
|
||||
$title = !empty($instance['title']) ? $instance['title'] : '';
|
||||
$video_url = !empty($instance['video_url']) ? $instance['video_url'] : '';
|
||||
$poster = !empty($instance['poster']) ? $instance['poster'] : '';
|
||||
$autoplay = !empty($instance['autoplay']);
|
||||
$aspect_ratio = !empty($instance['aspect_ratio']) ? $instance['aspect_ratio'] : '16:9';
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题(可选):', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('video_url')); ?>"><?php esc_html_e('视频 URL:', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('video_url')); ?>" name="<?php echo esc_attr($this->get_field_name('video_url')); ?>" type="url" value="<?php echo esc_attr($video_url); ?>" placeholder="https://...">
|
||||
<small><?php esc_html_e('支持 MP4、YouTube、Bilibili、腾讯视频链接', 'aksrt'); ?></small>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('poster')); ?>"><?php esc_html_e('封面图 URL:', 'aksrt'); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('poster')); ?>" name="<?php echo esc_attr($this->get_field_name('poster')); ?>" type="url" value="<?php echo esc_attr($poster); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('aspect_ratio')); ?>"><?php esc_html_e('宽高比:', 'aksrt'); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('aspect_ratio')); ?>" name="<?php echo esc_attr($this->get_field_name('aspect_ratio')); ?>">
|
||||
<option value="16:9" <?php selected($aspect_ratio, '16:9'); ?>>16:9</option>
|
||||
<option value="4:3" <?php selected($aspect_ratio, '4:3'); ?>>4:3</option>
|
||||
<option value="1:1" <?php selected($aspect_ratio, '1:1'); ?>>1:1</option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" id="<?php echo esc_attr($this->get_field_id('autoplay')); ?>" name="<?php echo esc_attr($this->get_field_name('autoplay')); ?>" value="1" <?php checked($autoplay); ?>>
|
||||
<?php esc_html_e('自动播放(仅 MP4)', 'aksrt'); ?>
|
||||
</label>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance): array {
|
||||
$instance = [];
|
||||
$instance['title'] = sanitize_text_field($new_instance['title'] ?? '');
|
||||
$instance['video_url'] = esc_url_raw($new_instance['video_url'] ?? '');
|
||||
$instance['poster'] = esc_url_raw($new_instance['poster'] ?? '');
|
||||
$instance['autoplay'] = !empty($new_instance['autoplay']);
|
||||
$instance['aspect_ratio'] = in_array($new_instance['aspect_ratio'] ?? '', ['16:9', '4:3', '1:1'], true) ? $new_instance['aspect_ratio'] : '16:9';
|
||||
return $instance;
|
||||
}
|
||||
public function update($new_instance, $old_instance): array {
|
||||
return [
|
||||
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
|
||||
'video_url' => esc_url_raw(trim((string) ($new_instance['video_url'] ?? ''))),
|
||||
'poster' => esc_url_raw(trim((string) ($new_instance['poster'] ?? ''))),
|
||||
'scale_height' => max(20, min(100, (int) ($new_instance['scale_height'] ?? 56))),
|
||||
'autoplay' => !empty($new_instance['autoplay']) ? 1 : 0,
|
||||
'loop' => !empty($new_instance['loop']) ? 1 : 0,
|
||||
'hide_controller' => !empty($new_instance['hide_controller']) ? 1 : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user