feat: 添加文本标签支持并优化资源加载

- 在雪花效果中添加文本标签支持,可配置文本内容和权重
- 优化图像资源加载逻辑,支持异步解码
- 添加对 prefers-reduced-motion 媒体查询的支持
- 改进小屏幕设备上的雪花数量计算
- 重构后台管理界面,统一添加卡片布局
- 添加国际化支持,准备翻译文件
- 优化脚本依赖加载,按需引入形状渲染器
This commit is contained in:
tikkhun 2025-12-12 10:50:41 +08:00
parent 00471c205f
commit def279a87e
4 changed files with 385 additions and 111 deletions

View File

@ -15,16 +15,19 @@
var listContainer = null;
var emojiListContainer = null;
var emojiInput = document.getElementById('yooneSnowAddEmojiInput');
var emojiAddButton = document.getElementById('yooneSnowAddEmoji');
var emojiAddButton = null;
var emojiSuggestBox = document.getElementById('yooneSnowEmojiSuggest');
var shapeListContainer = document.getElementById('yooneSnowShapeList');
var shapeAddSelect = document.getElementById('yooneSnowAddShapeSelect');
var shapeAddButton = document.getElementById('yooneSnowAddShapeBtn');
var shapeAddButton = null;
var emojiSelect = document.getElementById('yooneSnowEmojiSelect');
var typeSelect = document.getElementById('yooneSnowAddTypeSelect');
var paneDefault = document.getElementById('yooneSnowAddDefaultPane');
var paneEmoji = document.getElementById('yooneSnowAddEmojiPane');
var paneMedia = document.getElementById('yooneSnowAddMediaPane');
var unifiedAddCard = document.getElementById('yooneSnowAddUnified');
var paneText = document.getElementById('yooneSnowAddTextPane');
var textInput = document.getElementById('yooneSnowAddTextInput');
// 统一列表容器为形状列表容器 旧容器不再使用
listContainer = shapeListContainer;
emojiListContainer = shapeListContainer;
@ -77,7 +80,7 @@
var baseSize = previewBaseSizeMap[shapeKey] || 3.2;
// 调用渲染器 使用画布中心作为位置
try { renderer(ctx, 16, 16, baseSize); } catch(e){}
// 对基于圖像的形狀進行補繪 當圖像加載完成時重試一次
// 对基于图像的形状进行补绘 当图像加载完成时重试一次
var assets = (window.YooneSnowSettings && window.YooneSnowSettings.assetsMap) ? window.YooneSnowSettings.assetsMap : {};
var url = assets[shapeKey];
if (url && typeof window.YooneSnowGetOrLoadImage === 'function'){
@ -197,7 +200,11 @@
wrapper.appendChild(weightInput);
wrapper.appendChild(input);
wrapper.appendChild(cancelBtn);
if (unifiedAddCard && unifiedAddCard.parentNode === shapeListContainer){
shapeListContainer.insertBefore(wrapper, unifiedAddCard);
} else {
shapeListContainer.appendChild(wrapper);
}
var previewEl = createShapePreviewElement(key);
if (previewEl) { previewHost.appendChild(previewEl); }
}
@ -216,7 +223,7 @@
}
});
// 綁定移除 emoji 按鈕事件 使用事件委託處理動態元素
// 绑定移除 emoji 按钮事件 使用事件委托处理动态元素
if (shapeListContainer){
shapeListContainer.addEventListener('click', function(event){
var target = event.target;
@ -282,14 +289,18 @@
wrapper.appendChild(weightInput);
wrapper.appendChild(input);
wrapper.appendChild(removeBtn);
if (unifiedAddCard && unifiedAddCard.parentNode === listContainer){
listContainer.insertBefore(wrapper, unifiedAddCard);
} else {
listContainer.appendChild(wrapper);
}
});
});
frame.open();
});
}
// Emoji 别名映射 用文本搜索到 emoji 字符
// Emoji 别名映射 用文本搜索到 emoji 字符
var emojiAliasMap = {
smile: '🙂',
happy: '😊',
@ -321,10 +332,10 @@
berry: '🍓'
};
// 填充下拉選單 以別名和字符組合顯
// 填充下拉菜单 以别名和字符组合显
function populateEmojiSelect(){
if (!emojiSelect) { return; }
// 清空除第一個提示項之外的選項
// 清空除第一个提示项之外的选项
while (emojiSelect.options.length > 1){ emojiSelect.remove(1); }
for (var key in emojiAliasMap){
var ch = emojiAliasMap[key];
@ -335,7 +346,7 @@
}
}
// 綁定下拉選擇事件 選擇後添加對應 emoji 並重置選
// 绑定下拉选择事件 选择后添加对应 emoji 并重置选
if (emojiSelect){
populateEmojiSelect();
emojiSelect.addEventListener('change', function(){
@ -347,7 +358,7 @@
});
}
// 顯示建議列表 根據查詢文本匹配別
// 显示建议列表 根据查询文本匹配别
function showEmojiSuggestions(query){
if (!emojiSuggestBox) { return; }
var q = String(query || '').toLowerCase().trim();
@ -371,7 +382,7 @@
}
}
// 將 emoji 字符添加到列表 並創建隱藏輸
// 将 emoji 字符添加到列表 并创建隐藏输
function addEmojiByChar(ch){
if (!shapeListContainer) { return; }
var key = String(ch);
@ -418,25 +429,55 @@
wrapper.appendChild(weightInput);
wrapper.appendChild(input);
wrapper.appendChild(removeBtn);
if (unifiedAddCard && unifiedAddCard.parentNode === shapeListContainer){
shapeListContainer.insertBefore(wrapper, unifiedAddCard);
} else {
shapeListContainer.appendChild(wrapper);
}
// 綁定 emoji 添加按鈕 點擊後優先按別名匹配 否則直接添加字符
if (emojiAddButton){
emojiAddButton.addEventListener('click', function(){
var q = emojiInput ? String(emojiInput.value || '') : '';
var trimmed = q.trim();
if (trimmed === '') { return; }
var lower = trimmed.toLowerCase();
if (emojiAliasMap[lower]){
addEmojiByChar(emojiAliasMap[lower]);
} else {
addEmojiByChar(trimmed);
}
});
}
// 監聽輸入變化 顯示建議列表 支持即時搜索
// 绑定 emoji 添加按钮 点击后优先按别名匹配 否则直接添加字符
// 监听输入变化 显示建议列表 支持即时搜索
function segmentGraphemes(text){
// 将输入参数转为字符串 防止出现非字符串类型
var segments = [];
// 初始化结果数组 用于存放分割后的字符簇
var s = String(text || '');
// 如果当前环境支持 Intl.Segmenter 优先使用字符簇级别的分段
if (typeof Intl !== 'undefined' && Intl.Segmenter){
try {
// 创建针对简体中文环境的分段器 按 grapheme 粒度分段
var sg = new Intl.Segmenter('zh-Hans', { granularity: 'grapheme' });
// 执行分段操作 并提取每个分段的文本内容
var it = sg.segment(s);
it.forEach(function(rec){ segments.push(rec.segment); });
// 分段成功后返回结果数组
return segments;
} catch(err){}
}
// 如果不支持 Intl.Segmenter 或发生异常 使用正则作为后备方案
try {
// 正则匹配常见的 emoji 字符簇 包含 ZWJ 组合和区域指示符
var re = /(?:\p{Extended_Pictographic}\uFE0F?(?:\u200D\p{Extended_Pictographic}\uFE0F?)*)|(?:\p{Regional_Indicator}{2})|(?:[#*0-9]\uFE0F?\u20E3)/gu;
var m = s.match(re);
// 如果匹配到至少一个字符簇 则直接返回匹配结果
if (m && m.length > 0){ return m; }
} catch(err){}
// 若未匹配到任何字符簇 且原始字符串非空 则返回包含原字符串的数组 否则返回空数组
return s ? [s] : [];
}
function isEmojiCluster(cluster){
var str = String(cluster || '');
if (str.trim() === ''){ return false; }
try { if (/\p{Extended_Pictographic}/u.test(str)){ return true; } } catch(err){}
try { if (/[\u{1F1E6}-\u{1F1FF}]{2}/u.test(str)){ return true; } } catch(err){}
if (/(?:[#*0-9]\uFE0F?\u20E3)/u.test(str)){ return true; }
if (/\u200D/.test(str)){ return true; }
if (/\uFE0F/.test(str)){ return true; }
return false;
}
if (emojiInput){
emojiInput.addEventListener('input', function(){
showEmojiSuggestions(emojiInput.value);
@ -448,9 +489,30 @@
if (q !== ''){
var lower = q.toLowerCase();
if (emojiAliasMap[lower]){ addEmojiByChar(emojiAliasMap[lower]); }
else { addEmojiByChar(q); }
else {
var clusters = segmentGraphemes(q);
for (var i = 0; i < clusters.length; i++){
if (isEmojiCluster(clusters[i])){ addEmojiByChar(clusters[i]); }
}
}
emojiInput.value = '';
emojiSuggestBox && (emojiSuggestBox.innerHTML = '');
}
}
});
emojiInput.addEventListener('paste', function(e){
var txt = '';
if (e && e.clipboardData){ txt = String(e.clipboardData.getData('text') || ''); }
if (txt.trim() === ''){ return; }
setTimeout(function(){
var q = String(emojiInput.value || txt).trim();
var clusters = segmentGraphemes(q);
for (var i = 0; i < clusters.length; i++){
if (isEmojiCluster(clusters[i])){ addEmojiByChar(clusters[i]); }
}
emojiInput.value = '';
emojiSuggestBox && (emojiSuggestBox.innerHTML = '');
}, 0);
});
}
@ -477,27 +539,97 @@
});
}
if (shapeAddButton){
shapeAddButton.addEventListener('click', function(){
var val = shapeAddSelect ? String(shapeAddSelect.value || '') : '';
if (val.trim() !== ''){
addShapeBox(val);
shapeAddSelect.value = '';
// 切换类型面板 显示对应控件 其他隐藏
function updateTypePane(){
var t = typeSelect ? String(typeSelect.value || '') : '';
if (!paneDefault || !paneEmoji || !paneMedia || !paneText) { return; }
paneDefault.style.display = (t === 'default') ? 'flex' : 'none';
paneEmoji.style.display = (t === 'emoji') ? 'flex' : 'none';
paneMedia.style.display = (t === 'media') ? 'flex' : 'none';
paneText.style.display = (t === 'text') ? 'flex' : 'none';
}
if (typeSelect){
typeSelect.value = 'default';
typeSelect.addEventListener('change', updateTypePane);
updateTypePane();
}
function addTextBox(textLabel){
if (!shapeListContainer) { return; }
var key = String(textLabel).trim();
if (key === '') { return; }
var exist = shapeListContainer.querySelector('.yoone-snow-text-item[data-text-label="' + key + '"]');
if (exist) { return; }
var wrapper = document.createElement('div');
wrapper.className = 'yoone-snow-text-item';
wrapper.setAttribute('data-text-label', key);
wrapper.style.border = '1px solid #ddd';
wrapper.style.padding = '8px';
wrapper.style.display = 'flex';
wrapper.style.flexDirection = 'column';
wrapper.style.alignItems = 'center';
wrapper.style.minWidth = '96px';
var preview = document.createElement('div');
preview.textContent = key;
preview.style.fontSize = '14px';
preview.style.lineHeight = '18px';
preview.style.width = '120px';
preview.style.minHeight = '32px';
preview.style.display = 'flex';
preview.style.alignItems = 'center';
preview.style.justifyContent = 'center';
preview.style.backgroundColor = '#e6e6e6';
preview.style.border = '1px solid #ddd';
preview.style.borderRadius = '4px';
preview.style.wordBreak = 'break-word';
preview.style.textAlign = 'center';
var weightInput = document.createElement('input');
weightInput.type = 'number';
weightInput.min = '0';
weightInput.name = 'yoone_snow_text_weights[' + key + ']';
weightInput.value = '1';
weightInput.style.width = '120px';
weightInput.style.marginTop = '6px';
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'yoone_snow_text_items[]';
input.value = key;
var removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'button yoone-snow-remove-text';
removeBtn.textContent = 'Cancel';
removeBtn.style.marginTop = '6px';
wrapper.appendChild(preview);
wrapper.appendChild(weightInput);
wrapper.appendChild(input);
wrapper.appendChild(removeBtn);
if (unifiedAddCard && unifiedAddCard.parentNode === shapeListContainer){
shapeListContainer.insertBefore(wrapper, unifiedAddCard);
} else {
shapeListContainer.appendChild(wrapper);
}
}
if (textInput){
textInput.addEventListener('keydown', function(e){
if (e && e.key === 'Enter'){
e.preventDefault();
var val = String(textInput.value || '').trim();
if (val !== ''){ addTextBox(val); textInput.value = ''; }
}
});
}
// 切換類型面板 顯示對應控件 其他隱藏
function updateTypePane(){
var t = typeSelect ? String(typeSelect.value || '') : '';
if (!paneDefault || !paneEmoji || !paneMedia) { return; }
paneDefault.style.display = (t === 'default') ? 'flex' : 'none';
paneEmoji.style.display = (t === 'emoji') ? 'flex' : 'none';
paneMedia.style.display = (t === 'media') ? 'flex' : 'none';
if (shapeListContainer){
shapeListContainer.addEventListener('click', function(event){
var target = event.target;
if (target && target.classList.contains('yoone-snow-remove-text')){
var item = target.closest('.yoone-snow-text-item');
if (item){ item.remove(); }
}
if (typeSelect){
typeSelect.addEventListener('change', updateTypePane);
updateTypePane();
});
}
}

View File

@ -22,8 +22,12 @@
const record = { img: img, ready: false };
window.YooneSnowImageCache[imageUrl] = record;
img.onload = function(){
// 加载成功 标记为可用
var decoder = img.decode && typeof img.decode === 'function' ? img.decode() : null;
if (decoder && typeof decoder.then === 'function'){
decoder.then(function(){ record.ready = true; }).catch(function(){ record.ready = true; });
} else {
record.ready = true;
}
};
img.onerror = function(){
// 加载失败 从缓存移除避免重复错误

View File

@ -4,6 +4,8 @@
const canvas = document.getElementById('effectiveAppsSnow');
// 条件判断 如果未找到画布元素则不执行
if (!canvas) return;
const prefersReducedMotion = (typeof window.matchMedia === 'function') && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) { canvas.style.display = 'none'; return; }
const context = canvas.getContext('2d');
let viewportWidth = window.innerWidth;
let viewportHeight = window.innerHeight;
@ -27,6 +29,9 @@
const emojiItems = (window.YooneSnowSettings && Array.isArray(window.YooneSnowSettings.emojiItems))
? window.YooneSnowSettings.emojiItems
: [];
const textItems = (window.YooneSnowSettings && Array.isArray(window.YooneSnowSettings.textItems))
? window.YooneSnowSettings.textItems
: [];
const defaultShapeWeights = { dot: 1, flake: 4, yuanbao: 1, coin: 1, santa_hat: 1, candy_cane: 1, christmas_sock: 1, christmas_tree: 1, reindeer: 1, christmas_berry: 1 };
const shapeWeightsRaw = (window.YooneSnowSettings && window.YooneSnowSettings.shapeWeights && typeof window.YooneSnowSettings.shapeWeights === 'object')
? window.YooneSnowSettings.shapeWeights
@ -37,6 +42,9 @@
const emojiWeightsRaw = (window.YooneSnowSettings && window.YooneSnowSettings.emojiWeights && typeof window.YooneSnowSettings.emojiWeights === 'object')
? window.YooneSnowSettings.emojiWeights
: {};
const textWeightsRaw = (window.YooneSnowSettings && window.YooneSnowSettings.textWeights && typeof window.YooneSnowSettings.textWeights === 'object')
? window.YooneSnowSettings.textWeights
: {};
const shapeWeights = {};
for (let key in defaultShapeWeights){
const val = typeof shapeWeightsRaw[key] !== 'undefined' ? parseInt(shapeWeightsRaw[key], 10) : defaultShapeWeights[key];
@ -87,7 +95,8 @@
const maxCountRaw = (window.YooneSnowSettings && typeof window.YooneSnowSettings.maxCount !== 'undefined')
? parseInt(window.YooneSnowSettings.maxCount, 10)
: 0;
const computedAutoCount = Math.floor(Math.min(400, Math.max(120, (viewportWidth * viewportHeight) / 12000)));
const isSmallScreen = Math.min(viewportWidth, viewportHeight) <= 768;
const computedAutoCount = Math.floor(Math.min(isSmallScreen ? 240 : 400, Math.max(isSmallScreen ? 80 : 120, (viewportWidth * viewportHeight) / (isSmallScreen ? 24000 : 12000))));
const snowflakesTargetCount = Math.max(1, (isNaN(maxCountRaw) || maxCountRaw <= 0) ? computedAutoCount : maxCountRaw);
const snowflakes = [];
// 定义连续生成控制参数 使用时间积累的方式平滑新增
@ -117,6 +126,15 @@
if (ew > 0){ items.push({ kind: 'emoji', text: ch, weight: ew }); }
}
}
if (textItems.length > 0){
for (let tIndex = 0; tIndex < textItems.length; tIndex++){
const tx = String(textItems[tIndex] || '').trim();
if (tx === ''){ continue; }
const twRaw = typeof textWeightsRaw[tx] !== 'undefined' ? parseInt(textWeightsRaw[tx], 10) : 1;
const tw = isNaN(twRaw) ? 1 : Math.max(0, twRaw);
if (tw > 0){ items.push({ kind: 'text', text: tx, weight: tw }); }
}
}
for (let mIndex = 0; mIndex < mediaItems.length; mIndex++){
const mediaUrl = mediaItems[mIndex];
const mediaWeight = typeof mediaWeightsRaw[mediaUrl] !== 'undefined' ? parseInt(mediaWeightsRaw[mediaUrl], 10) : 1;
@ -145,7 +163,11 @@
if (items[i].kind === 'media'){
return { type: 'media_image', url: items[i].url, text: null };
} else {
if (items[i].kind === 'emoji'){
return { type: 'emoji_text', url: null, text: items[i].text };
} else {
return { type: 'text_label', url: null, text: items[i].text };
}
}
}
}
@ -275,6 +297,17 @@
context.restore();
continue;
}
if (flake.shapeType === 'text_label' && flake.emojiText){
context.save();
const fontSize = Math.max(12, flake.radius * 5.5);
context.font = String(Math.floor(fontSize)) + 'px system-ui, -apple-system, Segoe UI, Roboto, Noto Sans';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillStyle = 'rgba(255,255,255,0.9)';
context.fillText(String(flake.emojiText), flake.positionX, flake.positionY);
context.restore();
continue;
}
// 否则执行注册表中的形状渲染函数
const registry = window.YooneSnowShapeRenderers || {};
const renderer = registry[flake.shapeType] || registry['dot'];

View File

@ -16,55 +16,74 @@ function yoone_snow_enqueue_assets() {
if (!yoone_snow_is_enabled()) { return; }
$style_handle = 'yoone-snow-style';
$style_src = plugins_url('css/snow.css', __FILE__);
wp_register_style($style_handle, $style_src, array(), '1.1.0', 'all');
$style_ver = @filemtime(plugin_dir_path(__FILE__) . 'css/snow.css');
wp_register_style($style_handle, $style_src, array(), $style_ver ? (string)$style_ver : '1.1.0', 'all');
wp_enqueue_style($style_handle);
// 注册形状渲染脚本 保证主脚本之前加载
$shape_index_handle = 'yoone-snow-shapes-index';
wp_register_script($shape_index_handle, plugins_url('js/shapes/index.js', __FILE__), array(), '1.1.0', true);
$ver_shapes_index = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/index.js');
wp_register_script($shape_index_handle, plugins_url('js/shapes/index.js', __FILE__), array(), $ver_shapes_index ? (string)$ver_shapes_index : '1.1.0', true);
$shape_utils_handle = 'yoone-snow-shapes-utils';
wp_register_script($shape_utils_handle, plugins_url('js/shapes/utils.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$ver_shapes_utils = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/utils.js');
wp_register_script($shape_utils_handle, plugins_url('js/shapes/utils.js', __FILE__), array($shape_index_handle), $ver_shapes_utils ? (string)$ver_shapes_utils : '1.1.0', true);
$shape_dot_handle = 'yoone-snow-shapes-dot';
wp_register_script($shape_dot_handle, plugins_url('js/shapes/dot.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$ver_shapes_dot = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/dot.js');
wp_register_script($shape_dot_handle, plugins_url('js/shapes/dot.js', __FILE__), array($shape_index_handle), $ver_shapes_dot ? (string)$ver_shapes_dot : '1.1.0', true);
$shape_flake_handle = 'yoone-snow-shapes-flake';
wp_register_script($shape_flake_handle, plugins_url('js/shapes/flake.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$ver_shapes_flake = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/flake.js');
wp_register_script($shape_flake_handle, plugins_url('js/shapes/flake.js', __FILE__), array($shape_index_handle), $ver_shapes_flake ? (string)$ver_shapes_flake : '1.1.0', true);
$shape_yuanbao_handle = 'yoone-snow-shapes-yuanbao';
wp_register_script($shape_yuanbao_handle, plugins_url('js/shapes/yuanbao.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$ver_shapes_yuanbao = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/yuanbao.js');
wp_register_script($shape_yuanbao_handle, plugins_url('js/shapes/yuanbao.js', __FILE__), array($shape_index_handle), $ver_shapes_yuanbao ? (string)$ver_shapes_yuanbao : '1.1.0', true);
$shape_coin_handle = 'yoone-snow-shapes-coin';
wp_register_script($shape_coin_handle, plugins_url('js/shapes/coin.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$ver_shapes_coin = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/coin.js');
wp_register_script($shape_coin_handle, plugins_url('js/shapes/coin.js', __FILE__), array($shape_index_handle), $ver_shapes_coin ? (string)$ver_shapes_coin : '1.1.0', true);
$shape_santa_handle = 'yoone-snow-shapes-santa-hat';
wp_register_script($shape_santa_handle, plugins_url('js/shapes/santa_hat.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_santa = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/santa_hat.js');
wp_register_script($shape_santa_handle, plugins_url('js/shapes/santa_hat.js', __FILE__), array($shape_utils_handle), $ver_shapes_santa ? (string)$ver_shapes_santa : '1.1.0', true);
$shape_cane_handle = 'yoone-snow-shapes-candy-cane';
wp_register_script($shape_cane_handle, plugins_url('js/shapes/candy_cane.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_cane = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/candy_cane.js');
wp_register_script($shape_cane_handle, plugins_url('js/shapes/candy_cane.js', __FILE__), array($shape_utils_handle), $ver_shapes_cane ? (string)$ver_shapes_cane : '1.1.0', true);
$shape_sock_handle = 'yoone-snow-shapes-christmas-sock';
wp_register_script($shape_sock_handle, plugins_url('js/shapes/christmas_sock.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_sock = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_sock.js');
wp_register_script($shape_sock_handle, plugins_url('js/shapes/christmas_sock.js', __FILE__), array($shape_utils_handle), $ver_shapes_sock ? (string)$ver_shapes_sock : '1.1.0', true);
$shape_tree_handle = 'yoone-snow-shapes-christmas-tree';
wp_register_script($shape_tree_handle, plugins_url('js/shapes/christmas_tree.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_tree = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_tree.js');
wp_register_script($shape_tree_handle, plugins_url('js/shapes/christmas_tree.js', __FILE__), array($shape_utils_handle), $ver_shapes_tree ? (string)$ver_shapes_tree : '1.1.0', true);
$shape_reindeer_handle = 'yoone-snow-shapes-reindeer';
wp_register_script($shape_reindeer_handle, plugins_url('js/shapes/reindeer.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_reindeer = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/reindeer.js');
wp_register_script($shape_reindeer_handle, plugins_url('js/shapes/reindeer.js', __FILE__), array($shape_utils_handle), $ver_shapes_reindeer ? (string)$ver_shapes_reindeer : '1.1.0', true);
$shape_berry_handle = 'yoone-snow-shapes-christmas-berry';
wp_register_script($shape_berry_handle, plugins_url('js/shapes/christmas_berry.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_berry = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_berry.js');
wp_register_script($shape_berry_handle, plugins_url('js/shapes/christmas_berry.js', __FILE__), array($shape_utils_handle), $ver_shapes_berry ? (string)$ver_shapes_berry : '1.1.0', true);
// 注册并加载主脚本 设置依赖确保顺序正确
$script_handle = 'yoone-snow-script';
$script_src = plugins_url('js/snow-canvas.js', __FILE__);
$script_ver = @filemtime(plugin_dir_path(__FILE__) . 'js/snow-canvas.js');
$deps = array($shape_index_handle);
$needs_utils = false;
foreach ($mixed_items_sanitized as $key) {
if ($key === 'dot') { $deps[] = $shape_dot_handle; }
if ($key === 'flake') { $deps[] = $shape_flake_handle; }
if ($key === 'yuanbao') { $deps[] = $shape_yuanbao_handle; }
if ($key === 'coin') { $deps[] = $shape_coin_handle; }
if ($key === 'santa_hat') { $deps[] = $shape_santa_handle; $needs_utils = true; }
if ($key === 'candy_cane') { $deps[] = $shape_cane_handle; $needs_utils = true; }
if ($key === 'christmas_sock') { $deps[] = $shape_sock_handle; $needs_utils = true; }
if ($key === 'christmas_tree') { $deps[] = $shape_tree_handle; $needs_utils = true; }
if ($key === 'reindeer') { $deps[] = $shape_reindeer_handle; $needs_utils = true; }
if ($key === 'christmas_berry') { $deps[] = $shape_berry_handle; $needs_utils = true; }
}
if (!empty($media_urls)) { $needs_utils = true; }
if ($needs_utils) { $deps[] = $shape_utils_handle; }
$deps = array_values(array_unique($deps));
wp_register_script(
$script_handle,
$script_src,
array(
$shape_index_handle,
$shape_dot_handle,
$shape_flake_handle,
$shape_yuanbao_handle,
$shape_coin_handle,
$shape_santa_handle,
$shape_cane_handle,
$shape_sock_handle,
$shape_tree_handle,
$shape_reindeer_handle,
$shape_berry_handle
),
'1.1.0',
$deps,
$script_ver ? (string)$script_ver : '1.1.0',
true
);
wp_enqueue_script($script_handle);
@ -168,6 +187,30 @@ function yoone_snow_enqueue_assets() {
}
return $clean;
})(),
'textItems' => (function(){
$items = get_option('yoone_snow_text_items', array());
if (!is_array($items)) { $items = array(); }
$clean = array();
foreach ($items as $it) {
$s = trim((string)$it);
if ($s !== '') { $clean[] = $s; }
}
return $clean;
})(),
'textWeights' => (function(){
$map = get_option('yoone_snow_text_weights', array());
if (!is_array($map)) { $map = array(); }
$clean = array();
foreach ($map as $k => $v) {
$key = trim((string)$k);
$num = intval($v);
if ($key !== '') {
if ($num < 0) { $num = 0; }
$clean[$key] = $num;
}
}
return $clean;
})(),
));
}
@ -181,30 +224,42 @@ function yoone_snow_admin_enqueue($hook) {
// 在后台也注册并加载 shapes 渲染脚本 以供预览复用
// 形状索引与工具脚本 提供全局渲染器与图像加载能力
$shape_index_handle = 'yoone-snow-shapes-index';
wp_register_script($shape_index_handle, plugins_url('js/shapes/index.js', __FILE__), array(), '1.1.0', true);
$ver_shapes_index = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/index.js');
wp_register_script($shape_index_handle, plugins_url('js/shapes/index.js', __FILE__), array(), $ver_shapes_index ? (string)$ver_shapes_index : '1.1.0', true);
$shape_utils_handle = 'yoone-snow-shapes-utils';
wp_register_script($shape_utils_handle, plugins_url('js/shapes/utils.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$ver_shapes_utils = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/utils.js');
wp_register_script($shape_utils_handle, plugins_url('js/shapes/utils.js', __FILE__), array($shape_index_handle), $ver_shapes_utils ? (string)$ver_shapes_utils : '1.1.0', true);
// 各具体形状脚本 注册为依赖以保证顺序
$shape_dot_handle = 'yoone-snow-shapes-dot';
wp_register_script($shape_dot_handle, plugins_url('js/shapes/dot.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$ver_shapes_dot = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/dot.js');
wp_register_script($shape_dot_handle, plugins_url('js/shapes/dot.js', __FILE__), array($shape_index_handle), $ver_shapes_dot ? (string)$ver_shapes_dot : '1.1.0', true);
$shape_flake_handle = 'yoone-snow-shapes-flake';
wp_register_script($shape_flake_handle, plugins_url('js/shapes/flake.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$ver_shapes_flake = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/flake.js');
wp_register_script($shape_flake_handle, plugins_url('js/shapes/flake.js', __FILE__), array($shape_index_handle), $ver_shapes_flake ? (string)$ver_shapes_flake : '1.1.0', true);
$shape_yuanbao_handle = 'yoone-snow-shapes-yuanbao';
wp_register_script($shape_yuanbao_handle, plugins_url('js/shapes/yuanbao.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$ver_shapes_yuanbao = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/yuanbao.js');
wp_register_script($shape_yuanbao_handle, plugins_url('js/shapes/yuanbao.js', __FILE__), array($shape_index_handle), $ver_shapes_yuanbao ? (string)$ver_shapes_yuanbao : '1.1.0', true);
$shape_coin_handle = 'yoone-snow-shapes-coin';
wp_register_script($shape_coin_handle, plugins_url('js/shapes/coin.js', __FILE__), array($shape_index_handle), '1.1.0', true);
$ver_shapes_coin = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/coin.js');
wp_register_script($shape_coin_handle, plugins_url('js/shapes/coin.js', __FILE__), array($shape_index_handle), $ver_shapes_coin ? (string)$ver_shapes_coin : '1.1.0', true);
$shape_santa_handle = 'yoone-snow-shapes-santa-hat';
wp_register_script($shape_santa_handle, plugins_url('js/shapes/santa_hat.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_santa = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/santa_hat.js');
wp_register_script($shape_santa_handle, plugins_url('js/shapes/santa_hat.js', __FILE__), array($shape_utils_handle), $ver_shapes_santa ? (string)$ver_shapes_santa : '1.1.0', true);
$shape_cane_handle = 'yoone-snow-shapes-candy-cane';
wp_register_script($shape_cane_handle, plugins_url('js/shapes/candy_cane.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_cane = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/candy_cane.js');
wp_register_script($shape_cane_handle, plugins_url('js/shapes/candy_cane.js', __FILE__), array($shape_utils_handle), $ver_shapes_cane ? (string)$ver_shapes_cane : '1.1.0', true);
$shape_sock_handle = 'yoone-snow-shapes-christmas-sock';
wp_register_script($shape_sock_handle, plugins_url('js/shapes/christmas_sock.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_sock = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_sock.js');
wp_register_script($shape_sock_handle, plugins_url('js/shapes/christmas_sock.js', __FILE__), array($shape_utils_handle), $ver_shapes_sock ? (string)$ver_shapes_sock : '1.1.0', true);
$shape_tree_handle = 'yoone-snow-shapes-christmas-tree';
wp_register_script($shape_tree_handle, plugins_url('js/shapes/christmas_tree.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_tree = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_tree.js');
wp_register_script($shape_tree_handle, plugins_url('js/shapes/christmas_tree.js', __FILE__), array($shape_utils_handle), $ver_shapes_tree ? (string)$ver_shapes_tree : '1.1.0', true);
$shape_reindeer_handle = 'yoone-snow-shapes-reindeer';
wp_register_script($shape_reindeer_handle, plugins_url('js/shapes/reindeer.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_reindeer = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/reindeer.js');
wp_register_script($shape_reindeer_handle, plugins_url('js/shapes/reindeer.js', __FILE__), array($shape_utils_handle), $ver_shapes_reindeer ? (string)$ver_shapes_reindeer : '1.1.0', true);
$shape_berry_handle = 'yoone-snow-shapes-christmas-berry';
wp_register_script($shape_berry_handle, plugins_url('js/shapes/christmas_berry.js', __FILE__), array($shape_utils_handle), '1.1.0', true);
$ver_shapes_berry = @filemtime(plugin_dir_path(__FILE__) . 'js/shapes/christmas_berry.js');
wp_register_script($shape_berry_handle, plugins_url('js/shapes/christmas_berry.js', __FILE__), array($shape_utils_handle), $ver_shapes_berry ? (string)$ver_shapes_berry : '1.1.0', true);
// 在后台本页入队 shapes 相关脚本 以便预览复用前端实现
wp_enqueue_script($shape_index_handle);
@ -252,7 +307,7 @@ function yoone_snow_admin_enqueue($hook) {
$shape_reindeer_handle,
$shape_berry_handle
),
'1.1.0',
(@filemtime(plugin_dir_path(__FILE__) . 'js/admin-media.js') ? (string)@filemtime(plugin_dir_path(__FILE__) . 'js/admin-media.js') : '1.1.0'),
true
);
// 传递资源映射用于后台形状预览 保持与前端一致
@ -304,10 +359,9 @@ function yoone_snow_register_settings() {
// 添加设置分区 标题为 Snow Settings
add_settings_section(
'yoone_snow_section',
'Snow Settings',
esc_html__('Snow Settings', 'yoone-snow'),
function() {
// 输出分区描述 使用英文标点保证兼容
echo '<p>Configure snow appearance</p>';
echo '<p>' . esc_html__('Configure snow appearance', 'yoone-snow') . '</p>';
},
'yoone_snow'
);
@ -341,6 +395,8 @@ function yoone_snow_register_settings() {
if (!is_array($current_media)) { $current_media = array(); }
$current_emojis = get_option('yoone_snow_emoji_items', array());
if (!is_array($current_emojis)) { $current_emojis = array(); }
$current_texts = get_option('yoone_snow_text_items', array());
if (!is_array($current_texts)) { $current_texts = array(); }
// 读取当前权重值 用于在卡片中预填
$shape_weights_current = get_option('yoone_snow_shape_weights', array());
if (!is_array($shape_weights_current)) { $shape_weights_current = array(); }
@ -348,6 +404,8 @@ function yoone_snow_register_settings() {
if (!is_array($media_weights_current)) { $media_weights_current = array(); }
$emoji_weights_current = get_option('yoone_snow_emoji_weights', array());
if (!is_array($emoji_weights_current)) { $emoji_weights_current = array(); }
$text_weights_current = get_option('yoone_snow_text_weights', array());
if (!is_array($text_weights_current)) { $text_weights_current = array(); }
echo '<div id="yooneSnowShapeList" style="display:flex;flex-wrap:wrap;gap:12px;">';
// 渲染默认形状卡片
@ -388,42 +446,49 @@ function yoone_snow_register_settings() {
echo '<button type="button" class="button yoone-snow-remove-media" style="margin-top:6px;">Remove</button>';
echo '</div>';
}
// 文本卡片
foreach ($current_texts as $text_item) {
$label = trim((string)$text_item);
if ($label === '') { continue; }
echo '<div class="yoone-snow-text-item" data-text-label="' . esc_attr($label) . '" style="border:1px solid #ddd;padding:8px;display:flex;flex-direction:column;align-items:center;min-width:96px;">';
echo '<div style="font-size:14px;line-height:18px;margin-bottom:6px;width:120px;min-height:32px;display:flex;align-items:center;justify-content:center;background-color:#e6e6e6;border:1px solid #ddd;border-radius:4px;word-break:break-word;text-align:center;">' . esc_html($label) . '</div>';
$textWeightVal = isset($text_weights_current[$label]) ? intval($text_weights_current[$label]) : 1;
echo '<input type="number" min="0" name="yoone_snow_text_weights[' . esc_attr($label) . ']" value="' . esc_attr($textWeightVal) . '" style="width:120px;margin-top:6px;" />';
echo '<input type="hidden" name="yoone_snow_text_items[]" value="' . esc_attr($label) . '" />';
echo '<button type="button" class="button yoone-snow-remove-text" style="margin-top:6px;">Cancel</button>';
echo '</div>';
// 统一添加控件 包含类型下拉与三类子控件
echo '<div id="yooneSnowAddUnified" style="margin-top:8px;display:flex;flex-direction:column;gap:8px;">';
}
echo '<div id="yooneSnowAddUnified" class="yoone-snow-shape-item" style="border:1px solid #ddd;padding:8px;display:flex;flex-direction:column;align-items:center;gap:8px;min-width:96px;">';
echo '<div style="display:flex;gap:8px;align-items:center;">';
echo '<label>Type <select id="yooneSnowAddTypeSelect" style="min-width:180px;">';
echo '<option value="">Select type</option>';
echo '<option value="default">Default</option>';
echo '<option value="default" selected>Default</option>';
echo '<option value="emoji">Emoji</option>';
echo '<option value="media">Media</option>';
echo '<option value="text">Text</option>';
echo '</select></label>';
echo '</div>';
// 默认形状子面板
echo '<div id="yooneSnowAddDefaultPane" style="display:none;gap:8px;align-items:center;">';
echo '<div id="yooneSnowAddDefaultPane" style="display:flex;gap:8px;align-items:center;position:relative;">';
echo '<select id="yooneSnowAddShapeSelect" style="min-width:240px;"><option value="">Select shape</option>';
foreach ($options as $key => $label) {
echo '<option value="' . esc_attr($key) . '">' . esc_html($label) . '</option>';
}
echo '</select>';
echo '<button type="button" class="button" id="yooneSnowAddShapeBtn">Add Shape</button>';
echo '</div>';
// Emoji 子面板
echo '<div id="yooneSnowAddEmojiPane" style="display:none;gap:8px;align-items:center;">';
echo '<select id="yooneSnowEmojiSelect" style="min-width:240px;"><option value="">Select emoji</option></select>';
echo '<input type="text" id="yooneSnowAddEmojiInput" placeholder="Type emoji or alias" style="width:240px;" />';
echo '<button type="button" class="button" id="yooneSnowAddEmoji">Add Emoji</button>';
echo '<div id="yooneSnowEmojiSuggest" style="margin-top:6px;display:flex;flex-wrap:wrap;gap:6px;"></div>';
echo '</div>';
// 媒体子面板
echo '<div id="yooneSnowAddMediaPane" style="display:none;gap:8px;align-items:center;">';
echo '<button type="button" class="button button-primary" id="yooneSnowAddMediaUnified">Add Images</button>';
echo '</div>';
echo '<div id="yooneSnowAddTextPane" style="display:none;gap:8px;align-items:center;">';
echo '<input type="text" id="yooneSnowAddTextInput" placeholder="Type text" style="width:240px;" />';
echo '</div>';
echo '<p class="description">Add shapes by type all in one list</p>';
echo '</div>';
// 权重输入已集成到各类型卡片之中 无需单独权重区域
echo '</div>';
},
'yoone_snow',
'yoone_snow_section'
@ -555,6 +620,41 @@ function yoone_snow_register_settings() {
'default' => array(),
));
register_setting('yoone_snow_options', 'yoone_snow_text_items', array(
'type' => 'array',
'sanitize_callback' => function($value) {
if (is_string($value)) {
$value = array_filter(array_map('trim', explode(',', $value)));
}
if (!is_array($value)) { $value = array(); }
$out = array();
foreach ($value as $item) {
$s = trim((string)$item);
if ($s !== '') { $out[] = $s; }
}
return array_values(array_unique($out));
},
'default' => array(),
));
register_setting('yoone_snow_options', 'yoone_snow_text_weights', array(
'type' => 'array',
'sanitize_callback' => function($value) {
if (!is_array($value)) { $value = array(); }
$clean = array();
foreach ($value as $ch => $num) {
$key = trim((string)$ch);
$weight = intval($num);
if ($key !== '') {
if ($weight < 0) { $weight = 0; }
$clean[$key] = $weight;
}
}
return $clean;
},
'default' => array(),
));
// 注册首页显示时长设置 项为整数秒 0 表示无限
register_setting('yoone_snow_options', 'yoone_snow_home_duration', array(
'type' => 'integer',
@ -710,14 +810,14 @@ function yoone_snow_register_settings() {
// 添加设置页面到后台菜单 条目在设置菜单下
function yoone_snow_add_settings_page() {
add_options_page(
'Yoone Snow',
'Yoone Snow',
esc_html__('Yoone Snow', 'yoone-snow'),
esc_html__('Yoone Snow', 'yoone-snow'),
'manage_options',
'yoone_snow',
function() {
// 渲染设置页面 表单提交到 options.php
echo '<div class="wrap">';
echo '<h1>Yoone Snow</h1>';
echo '<h1>' . esc_html__('Yoone Snow', 'yoone-snow') . '</h1>';
echo '<form method="post" action="options.php">';
settings_fields('yoone_snow_options');
do_settings_sections('yoone_snow');
@ -737,7 +837,7 @@ add_action('admin_enqueue_scripts', 'yoone_snow_admin_enqueue');
function yoone_snow_plugin_action_links($links) {
// 构造设置页面链接 使用 admin_url 保证后台路径正确
$settingsUrl = admin_url('options-general.php?page=yoone_snow');
$settingsLink = '<a href="' . esc_url($settingsUrl) . '">Settings</a>';
$settingsLink = '<a href="' . esc_url($settingsUrl) . '">' . esc_html__('Settings', 'yoone-snow') . '</a>';
// 将设置链接插入到最前面 便于用户点击
array_unshift($links, $settingsLink);
return $links;
@ -745,3 +845,8 @@ function yoone_snow_plugin_action_links($links) {
// 绑定到当前插件的 action links 钩子 使用 plugin_basename 计算插件标识
add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'yoone_snow_plugin_action_links');
function yoone_snow_load_textdomain() {
load_plugin_textdomain('yoone-snow', false, dirname(plugin_basename(__FILE__)) . '/languages');
}
add_action('plugins_loaded', 'yoone_snow_load_textdomain');