Fix blur effect, implement skeleton loading, and add Duolingo JWT friend streak feature
This commit is contained in:
@@ -678,6 +678,16 @@ $(document).on("change" , ".search-filter" , function(e){
|
|||||||
part1OuterHeight = $leftbarPart1.outerHeight();
|
part1OuterHeight = $leftbarPart1.outerHeight();
|
||||||
changeLeftbarStickyStatus();
|
changeLeftbarStickyStatus();
|
||||||
}).observe(leftbarPart1, { attributes: true, childList: true, subtree: true });
|
}).observe(leftbarPart1, { attributes: true, childList: true, subtree: true });
|
||||||
|
// 监听 part2 和 part3 的尺寸变化,确保高度变动时重新计算布局
|
||||||
|
// 注意:这里必须使用 ResizeObserver 而不是 MutationObserver
|
||||||
|
// 因为 changeLeftbarStickyStatus 会修改 DOM 属性(classList, style),使用 MuationObserver 会导致无限循环死机
|
||||||
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
|
const ro = new ResizeObserver(function () {
|
||||||
|
changeLeftbarStickyStatus();
|
||||||
|
});
|
||||||
|
if (leftbarPart2) ro.observe(leftbarPart2);
|
||||||
|
if (leftbarPart3) ro.observe(leftbarPart3);
|
||||||
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
/*Headroom*/
|
/*Headroom*/
|
||||||
@@ -2185,9 +2195,9 @@ document.addEventListener('click', (e) => {
|
|||||||
if (document.getElementById("comment_emotion_btn") == null) {
|
if (document.getElementById("comment_emotion_btn") == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(e.target.id != "comment_emotion_btn" && e.target.id != "emotion_keyboard" && !document.getElementById("comment_emotion_btn").contains(e.target) && !document.getElementById("emotion_keyboard").contains(e.target)){
|
if (e.target.id != "comment_emotion_btn" && e.target.id != "emotion_keyboard" && !document.getElementById("comment_emotion_btn").contains(e.target) && !document.getElementById("emotion_keyboard").contains(e.target)) {
|
||||||
$("#comment_emotion_btn").removeClass("comment-emotion-keyboard-open");
|
$("#comment_emotion_btn").removeClass("comment-emotion-keyboard-open");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
/*查看评论编辑记录*/
|
/*查看评论编辑记录*/
|
||||||
function showCommentEditHistory(id) {
|
function showCommentEditHistory(id) {
|
||||||
@@ -3888,13 +3898,14 @@ $(document).on("click" , "#blog_categories .tag" , function(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 监听页面滚动,实时更新移动端目录高亮并自动滚动
|
// 监听页面滚动,实时更新移动端目录高亮并自动滚动
|
||||||
var mobileCatalogScrollTimer = null;
|
var mobileCatalogScrollTicking = false;
|
||||||
$(window).on("scroll.mobileCatalog", function () {
|
$(window).on("scroll.mobileCatalog", function () {
|
||||||
if (!window.mobileCatalogInitialized) return;
|
if (!window.mobileCatalogInitialized) return;
|
||||||
// 节流处理
|
// 使用 rAF 实现实时更新,无延迟
|
||||||
if (mobileCatalogScrollTimer) return;
|
if (mobileCatalogScrollTicking) return;
|
||||||
mobileCatalogScrollTimer = setTimeout(function() {
|
mobileCatalogScrollTicking = true;
|
||||||
mobileCatalogScrollTimer = null;
|
requestAnimationFrame(function () {
|
||||||
|
mobileCatalogScrollTicking = false;
|
||||||
// 更新高亮状态
|
// 更新高亮状态
|
||||||
updateMobileCatalogHighlight();
|
updateMobileCatalogHighlight();
|
||||||
// 只在侧边栏打开且目录展开时滚动
|
// 只在侧边栏打开且目录展开时滚动
|
||||||
@@ -3902,7 +3913,7 @@ $(document).on("click" , "#blog_categories .tag" , function(){
|
|||||||
$("#mobile_catalog_toggle").closest(".leftbar-mobile-collapse-section").hasClass("expanded")) {
|
$("#mobile_catalog_toggle").closest(".leftbar-mobile-collapse-section").hasClass("expanded")) {
|
||||||
scrollMobileCatalogToActive();
|
scrollMobileCatalogToActive();
|
||||||
}
|
}
|
||||||
}, 150);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 点击目录项后关闭侧边栏(已在 initMobileCatalog 中处理)
|
// 点击目录项后关闭侧边栏(已在 initMobileCatalog 中处理)
|
||||||
@@ -5037,7 +5048,7 @@ void 0;
|
|||||||
jQuery('#primary').removeClass('pjax-loading');
|
jQuery('#primary').removeClass('pjax-loading');
|
||||||
var bar = document.getElementById('page-loading-bar');
|
var bar = document.getElementById('page-loading-bar');
|
||||||
if (bar) { bar.style.width = '100%'; setTimeout(function () { bar.style.opacity = '0'; setTimeout(function () { bar.remove(); }, 300); }, 200); }
|
if (bar) { bar.style.width = '100%'; setTimeout(function () { bar.style.opacity = '0'; setTimeout(function () { bar.remove(); }, 300); }, 200); }
|
||||||
setTimeout(function() { initImageLoadAnimation(); initScrollAnimations(); initSmoothScroll(); }, 100);
|
setTimeout(function () { initArticleSkeletons(); initImageLoadAnimation(); initScrollAnimations(); initSmoothScroll(); }, 100);
|
||||||
var overlay = document.getElementById('article-loading-overlay');
|
var overlay = document.getElementById('article-loading-overlay');
|
||||||
if (overlay) {
|
if (overlay) {
|
||||||
overlay.classList.remove('is-visible');
|
overlay.classList.remove('is-visible');
|
||||||
@@ -5053,6 +5064,38 @@ void 0;
|
|||||||
// 主题切换过渡效果已在 setDarkmode() 函数中实现
|
// 主题切换过渡效果已在 setDarkmode() 函数中实现
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 7.1 骨架屏加载动画 (Skeleton Screen)
|
||||||
|
function initArticleSkeletons() {
|
||||||
|
if (typeof jQuery === 'undefined') return;
|
||||||
|
var $articles = jQuery('.article-list article.post:not(.skeleton-processed)');
|
||||||
|
if ($articles.length === 0) return;
|
||||||
|
|
||||||
|
$articles.each(function () {
|
||||||
|
var $this = jQuery(this);
|
||||||
|
$this.addClass('skeleton-processed skeleton-loading');
|
||||||
|
|
||||||
|
// 构造骨架屏 DOM
|
||||||
|
var skeletonHtml =
|
||||||
|
'<div class="argon-skeleton">' +
|
||||||
|
'<div class="argon-skeleton-item argon-skeleton-title"></div>' +
|
||||||
|
'<div class="argon-skeleton-item argon-skeleton-meta"></div>' +
|
||||||
|
'<div class="argon-skeleton-item argon-skeleton-line"></div>' +
|
||||||
|
'<div class="argon-skeleton-item argon-skeleton-line"></div>' +
|
||||||
|
'<div class="argon-skeleton-item argon-skeleton-line short"></div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
var $skeleton = jQuery(skeletonHtml);
|
||||||
|
$this.prepend($skeleton);
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
$this.removeClass('skeleton-loading');
|
||||||
|
setTimeout(function () {
|
||||||
|
$skeleton.remove();
|
||||||
|
}, 400); // 配合 CSS 的 0.4s transition
|
||||||
|
}, 800); // 骨架屏显示 800ms
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 8. 减少动画偏好检查
|
// 8. 减少动画偏好检查
|
||||||
function checkReducedMotion() {
|
function checkReducedMotion() {
|
||||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
@@ -5062,6 +5105,7 @@ void 0;
|
|||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
function init() {
|
function init() {
|
||||||
|
initArticleSkeletons();
|
||||||
checkReducedMotion();
|
checkReducedMotion();
|
||||||
initImageLoadAnimation();
|
initImageLoadAnimation();
|
||||||
initScrollAnimations();
|
initScrollAnimations();
|
||||||
|
|||||||
49
footer.php
49
footer.php
@@ -161,6 +161,55 @@
|
|||||||
|
|
||||||
<?php wp_footer(); ?>
|
<?php wp_footer(); ?>
|
||||||
|
|
||||||
|
<!-- 长文章 backdrop-filter 模糊层 JS -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
function initPostFullBlur() {
|
||||||
|
var card = document.querySelector('article.post.post-full.card');
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
// 创建模糊覆盖层
|
||||||
|
var overlay = document.createElement('div');
|
||||||
|
overlay.className = 'post-full-blur-overlay';
|
||||||
|
card.insertBefore(overlay, card.firstChild);
|
||||||
|
|
||||||
|
var ticking = false;
|
||||||
|
function updateOverlay() {
|
||||||
|
var cardRect = card.getBoundingClientRect();
|
||||||
|
// 覆盖层比视口高 800px(上下各多 400px),充分溢出窗口避免穿帮
|
||||||
|
// offset = 卡片顶部到视口顶部的距离 - 400px 上方余量
|
||||||
|
var offset = Math.max(0, -cardRect.top - 400);
|
||||||
|
// 限制偏移量不超过卡片高度减去覆盖层高度
|
||||||
|
var overlayHeight = window.innerHeight + 800;
|
||||||
|
var maxOffset = card.offsetHeight - overlayHeight;
|
||||||
|
if (maxOffset > 0) {
|
||||||
|
offset = Math.min(offset, maxOffset);
|
||||||
|
}
|
||||||
|
overlay.style.transform = 'translateY(' + offset + 'px)';
|
||||||
|
ticking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScroll() {
|
||||||
|
if (!ticking) {
|
||||||
|
ticking = true;
|
||||||
|
requestAnimationFrame(updateOverlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
window.addEventListener('resize', onScroll, { passive: true });
|
||||||
|
// 初始定位
|
||||||
|
updateOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initPostFullBlur);
|
||||||
|
} else {
|
||||||
|
initPostFullBlur();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5270,15 +5270,31 @@ function argon_get_duolingo_data() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$url = 'https://www.duolingo.com/2017-06-30/users?username=' . urlencode($username) . '&fields=streak,streakData%7BcurrentStreak,previousStreak%7D%7D';
|
$url = 'https://www.duolingo.com/2017-06-30/users?username=' . urlencode($username) . '&fields=id,streak,streakData%7BcurrentStreak,previousStreak%7D,quests,friendStreaks,friendships,following';
|
||||||
|
|
||||||
|
$headers = array(
|
||||||
|
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
|
||||||
|
);
|
||||||
|
$jwt = get_option('argon_duolingo_jwt', '');
|
||||||
|
if (!empty($jwt)) {
|
||||||
|
$headers['Authorization'] = 'Bearer ' . $jwt;
|
||||||
|
}
|
||||||
|
|
||||||
$response = wp_remote_get($url, array(
|
$response = wp_remote_get($url, array(
|
||||||
'timeout' => 10,
|
'timeout' => 15,
|
||||||
'headers' => array(
|
'headers' => $headers
|
||||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
||||||
)
|
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// 如果返回了诸如 401/403 错误(JWT 过期或无效),尝试移除 JWT 重新获取,确保基础连胜可见
|
||||||
|
if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) !== 200 && !empty($jwt)) {
|
||||||
|
unset($headers['Authorization']);
|
||||||
|
$response = wp_remote_get($url, array(
|
||||||
|
'timeout' => 15,
|
||||||
|
'headers' => $headers
|
||||||
|
));
|
||||||
|
$jwt = ''; // 标记为无效,后续不请求私有好友连胜数据
|
||||||
|
}
|
||||||
|
|
||||||
if (is_wp_error($response)) {
|
if (is_wp_error($response)) {
|
||||||
// 请求失败时返回旧缓存(如果有)
|
// 请求失败时返回旧缓存(如果有)
|
||||||
return $cached !== false ? $cached : false;
|
return $cached !== false ? $cached : false;
|
||||||
@@ -5305,10 +5321,48 @@ function argon_get_duolingo_data() {
|
|||||||
$end_date = isset($user['streakData']['currentStreak']['endDate']) ? $user['streakData']['currentStreak']['endDate'] : '';
|
$end_date = isset($user['streakData']['currentStreak']['endDate']) ? $user['streakData']['currentStreak']['endDate'] : '';
|
||||||
$is_today_done = ($end_date === $today);
|
$is_today_done = ($end_date === $today);
|
||||||
|
|
||||||
|
// 尝试解析好友连胜 (Friend Streak)
|
||||||
|
$friend_streak = 0;
|
||||||
|
|
||||||
|
// 只有在 JWT 有效时才继续拉取私密接口
|
||||||
|
if (!empty($jwt)) {
|
||||||
|
// 请求额外的好友连胜数据
|
||||||
|
$user_id = isset($user['id']) ? $user['id'] : 0;
|
||||||
|
if ($user_id) {
|
||||||
|
$friend_url = 'https://www.duolingo.com/2017-06-30/friends/users/' . $user_id . '?fields=friendStreaks';
|
||||||
|
$friend_resp = wp_remote_get($friend_url, array('timeout' => 15, 'headers' => $headers));
|
||||||
|
if (!is_wp_error($friend_resp)) {
|
||||||
|
$friend_body = wp_remote_retrieve_body($friend_resp);
|
||||||
|
$friend_data = json_decode($friend_body, true);
|
||||||
|
|
||||||
|
// 写出诊断日志以便开发者后续找字段
|
||||||
|
$debug_info = array('user_api' => $user, 'friend_api' => $friend_data);
|
||||||
|
file_put_contents(get_template_directory() . '/duolingo_debug.json', json_encode($debug_info, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||||
|
|
||||||
|
// 暂时尝试猜测常用名字
|
||||||
|
if (isset($user['friendStreaks'])) {
|
||||||
|
// 如果是在 user 对象里
|
||||||
|
foreach ((array)$user['friendStreaks'] as $fs) {
|
||||||
|
if (isset($fs['streakLength']) && $fs['streakLength'] > $friend_streak) $friend_streak = $fs['streakLength'];
|
||||||
|
if (isset($fs['length']) && $fs['length'] > $friend_streak) $friend_streak = $fs['length'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($friend_data['friendStreaks'])) {
|
||||||
|
// 如果是在 friends 对象里
|
||||||
|
foreach ((array)$friend_data['friendStreaks'] as $fs) {
|
||||||
|
if (isset($fs['streakLength']) && $fs['streakLength'] > $friend_streak) $friend_streak = $fs['streakLength'];
|
||||||
|
if (isset($fs['length']) && $fs['length'] > $friend_streak) $friend_streak = $fs['length'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$result = array(
|
$result = array(
|
||||||
'streak' => $streak,
|
'streak' => $streak,
|
||||||
'today' => $is_today_done,
|
'today' => $is_today_done,
|
||||||
'date' => $today
|
'date' => $today,
|
||||||
|
'friend_streak' => $friend_streak > 0 ? $friend_streak : ''
|
||||||
);
|
);
|
||||||
|
|
||||||
// 如果今日已完成,缓存到明天0点;否则缓存15分钟
|
// 如果今日已完成,缓存到明天0点;否则缓存15分钟
|
||||||
|
|||||||
16
settings.php
16
settings.php
@@ -1529,6 +1529,14 @@ function themeoptions_page(){
|
|||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th><label>Duolingo JWT Token</label></th>
|
||||||
|
<td>
|
||||||
|
<input name="argon_duolingo_jwt" type="text" class="regular-text" value="<?php echo get_option('argon_duolingo_jwt', ''); ?>" placeholder="<?php _e('手动填入抓包得到的 jwt', 'argon');?>">
|
||||||
|
<p class="description"><?php _e('在浏览器登录多邻国网页版后,按 F12 并在 Application -> Cookies 里找到 jwt 并填入,用于自动获取友情连胜数据', 'argon');?></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr><th class="subtitle"><h2 id="section-announcement"><?php _e('博客公告', 'argon');?></h2></th></tr>
|
<tr><th class="subtitle"><h2 id="section-announcement"><?php _e('博客公告', 'argon');?></h2></th></tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
@@ -6882,6 +6890,14 @@ function argon_update_themeoptions(){
|
|||||||
|
|
||||||
argon_update_option('argon_show_duolingo_streak');
|
argon_update_option('argon_show_duolingo_streak');
|
||||||
argon_update_option('argon_duolingo_username');
|
argon_update_option('argon_duolingo_username');
|
||||||
|
if (isset($_POST['argon_duolingo_jwt'])) {
|
||||||
|
argon_update_option('argon_duolingo_jwt');
|
||||||
|
}
|
||||||
|
delete_option('argon_duolingo_friend_streak');
|
||||||
|
|
||||||
|
// Clean up old transient and options
|
||||||
|
delete_option('argon_duolingo_email');
|
||||||
|
delete_transient('argon_duo_login_error');
|
||||||
|
|
||||||
argon_update_option('argon_banner_title');
|
argon_update_option('argon_banner_title');
|
||||||
|
|
||||||
|
|||||||
17
sidebar.php
17
sidebar.php
@@ -564,16 +564,17 @@ $author_desc = get_option('argon_sidebar_author_description');
|
|||||||
var headIndexInstance = $(document).data('headIndex');
|
var headIndexInstance = $(document).data('headIndex');
|
||||||
|
|
||||||
// 添加额外的滚动监听,确保目录跟随
|
// 添加额外的滚动监听,确保目录跟随
|
||||||
var scrollTimer = null;
|
var scrollTicking = false;
|
||||||
$(window).on('scroll.desktopCatalog', function() {
|
$(window).on('scroll.desktopCatalog', function() {
|
||||||
if (scrollTimer) {
|
if (!scrollTicking) {
|
||||||
clearTimeout(scrollTimer);
|
scrollTicking = true;
|
||||||
}
|
requestAnimationFrame(function() {
|
||||||
scrollTimer = setTimeout(function() {
|
|
||||||
if (headIndexInstance && typeof headIndexInstance.updateCurrent === 'function') {
|
if (headIndexInstance && typeof headIndexInstance.updateCurrent === 'function') {
|
||||||
headIndexInstance.updateCurrent();
|
headIndexInstance.updateCurrent();
|
||||||
}
|
}
|
||||||
}, 100);
|
scrollTicking = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// PJAX 后重新绑定
|
// PJAX 后重新绑定
|
||||||
@@ -663,8 +664,10 @@ $author_desc = get_option('argon_sidebar_author_description');
|
|||||||
$duo_data = argon_get_duolingo_data();
|
$duo_data = argon_get_duolingo_data();
|
||||||
if ($duo_data !== false) :
|
if ($duo_data !== false) :
|
||||||
$is_today_done = isset($duo_data['today']) && $duo_data['today'];
|
$is_today_done = isset($duo_data['today']) && $duo_data['today'];
|
||||||
|
$duo_friend_streak = isset($duo_data['friend_streak']) ? $duo_data['friend_streak'] : '';
|
||||||
|
$tooltip_attr = !empty($duo_friend_streak) ? ' data-toggle="tooltip" data-placement="bottom" title="' . esc_attr(sprintf(__('友情连胜: %s天', 'argon'), $duo_friend_streak)) . '"' : '';
|
||||||
?>
|
?>
|
||||||
<span class="duolingo-streak<?php echo $is_today_done ? '' : ' not-done'; ?>">
|
<span class="duolingo-streak<?php echo $is_today_done ? '' : ' not-done'; ?>"<?php echo $tooltip_attr; ?>>
|
||||||
<img src="<?php echo get_template_directory_uri(); ?>/assets/icons/duolingo-streak<?php echo $is_today_done ? '' : '-empty'; ?>.svg" class="duolingo-flame" alt="streak">
|
<img src="<?php echo get_template_directory_uri(); ?>/assets/icons/duolingo-streak<?php echo $is_today_done ? '' : '-empty'; ?>.svg" class="duolingo-flame" alt="streak">
|
||||||
<?php echo $duo_data['streak']; ?>
|
<?php echo $duo_data['streak']; ?>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user