hexo-theme-stellar 1.32.4 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/_config.yml +22 -7
  2. package/_data/icons.yml +8 -1
  3. package/languages/en.yml +1 -1
  4. package/languages/zh-CN.yml +1 -1
  5. package/languages/zh-TW.yml +1 -1
  6. package/layout/_partial/comments/artalk/script.ejs +7 -6
  7. package/layout/_partial/head.ejs +7 -1
  8. package/layout/_partial/main/article/contributors.ejs +1 -1
  9. package/layout/_partial/main/article/read_next.ejs +2 -2
  10. package/layout/_partial/main/navbar/article_banner.ejs +1 -1
  11. package/layout/_partial/main/navbar/breadcrumb/blog.ejs +3 -3
  12. package/layout/_partial/main/navbar/breadcrumb/note.ejs +2 -2
  13. package/layout/_partial/main/navbar/breadcrumb/page.ejs +1 -1
  14. package/layout/_partial/main/navbar/breadcrumb/wiki.ejs +2 -2
  15. package/layout/_partial/main/navbar/dateinfo.ejs +1 -1
  16. package/layout/_partial/main/navbar/ghinfo.ejs +1 -1
  17. package/layout/_partial/main/navbar/nav_tabs_blog.ejs +12 -12
  18. package/layout/_partial/main/navbar/nav_tabs_wiki.ejs +2 -2
  19. package/layout/_partial/main/notebook/note_tags.ejs +1 -1
  20. package/layout/_partial/scripts/lazyload.ejs +1 -1
  21. package/layout/_partial/scripts/utils.ejs +69 -39
  22. package/layout/_partial/sidebar/index_leftbar.ejs +1 -1
  23. package/layout/_partial/sidebar/menu.ejs +1 -1
  24. package/layout/_partial/widgets/components/link.ejs +1 -1
  25. package/layout/_partial/widgets/ghissues.ejs +7 -1
  26. package/layout/_partial/widgets/ghrepo.ejs +2 -2
  27. package/layout/_partial/widgets/ghuser.ejs +1 -1
  28. package/layout/_partial/widgets/recent.ejs +1 -1
  29. package/layout/_partial/widgets/related.ejs +2 -2
  30. package/layout/_partial/widgets/tagtree.ejs +1 -1
  31. package/layout/_partial/widgets/timeline.ejs +7 -1
  32. package/layout/_partial/widgets/tree.ejs +1 -1
  33. package/layout/archive.ejs +1 -1
  34. package/layout/categories.ejs +1 -1
  35. package/layout/index.ejs +1 -1
  36. package/layout/index_topic.ejs +1 -1
  37. package/layout/index_wiki.ejs +2 -2
  38. package/layout/notebooks.ejs +1 -1
  39. package/layout/notes.ejs +1 -1
  40. package/layout/tags.ejs +1 -1
  41. package/package.json +1 -1
  42. package/scripts/events/index.js +1 -1
  43. package/scripts/events/lib/config.js +7 -1
  44. package/scripts/filters/pretty_urls.js +25 -0
  45. package/scripts/helpers/json_ld.js +5 -4
  46. package/scripts/helpers/pretty_url.js +22 -0
  47. package/scripts/tags/index.js +2 -0
  48. package/scripts/tags/lib/albums.js +1 -1
  49. package/scripts/tags/lib/chat.js +1 -1
  50. package/scripts/tags/lib/friends.js +1 -1
  51. package/scripts/tags/lib/link.js +1 -1
  52. package/scripts/tags/lib/posters.js +1 -1
  53. package/scripts/tags/lib/rating.js +32 -0
  54. package/scripts/tags/lib/sites.js +1 -1
  55. package/scripts/tags/lib/timeline.js +2 -1
  56. package/scripts/tags/lib/vote.js +28 -0
  57. package/source/css/_common/button.styl +1 -0
  58. package/source/css/_components/pages/article-story.styl +2 -0
  59. package/source/css/_components/tag-plugins/image.styl +1 -0
  60. package/source/css/_components/tag-plugins/rating.styl +39 -0
  61. package/source/css/_components/tag-plugins/timeline.styl +5 -5
  62. package/source/css/_components/tag-plugins/vote.styl +49 -0
  63. package/source/css/_components/widgets/timeline.styl +1 -1
  64. package/source/css/_defines/theme_base.styl +1 -0
  65. package/source/css/_plugins/comments/artalk.styl +51 -11
  66. package/source/css/_plugins/lazyload.styl +3 -0
  67. package/source/js/main.js +12 -5
  68. package/source/js/search/local-search.js +3 -1
  69. package/source/js/services/artalk_latest_comment.js +1 -1
  70. package/source/js/services/contributors.js +1 -1
  71. package/source/js/services/fcircle.js +1 -1
  72. package/source/js/services/friends.js +1 -1
  73. package/source/js/services/friends_and_posts.js +1 -1
  74. package/source/js/services/ghinfo.js +1 -1
  75. package/source/js/services/giscus_latest_comment.js +1 -1
  76. package/source/js/services/memos.js +1 -1
  77. package/source/js/services/rating.js +142 -0
  78. package/source/js/services/siteinfo.js +1 -1
  79. package/source/js/services/sites.js +1 -1
  80. package/source/js/services/timeline.js +1 -1
  81. package/source/js/services/twikoo_latest_comment.js +1 -1
  82. package/source/js/services/vote.js +113 -0
  83. package/source/js/services/waline_latest_comment.js +1 -1
  84. package/source/js/services/weibo.js +1 -1
@@ -1,6 +1,9 @@
1
1
  $lazyTransitionType = hexo-config('plugins.lazyload.transition')
2
2
  $loadingImageSize = 2rem
3
3
 
4
+ img:not([src])
5
+ visibility: hidden
6
+
4
7
  .lazy-box
5
8
  position: relative
6
9
  overflow: hidden
package/source/js/main.js CHANGED
@@ -229,20 +229,27 @@ const init = {
229
229
  document.body.appendChild(notice);
230
230
  }
231
231
  if (!canonical.originalHost) return;
232
+ const currentURL = new URL(window.location.href);
233
+ const currentHost = currentURL.hostname.replace(/^www\./, '');
234
+ if (currentHost == 'localhost') return;
235
+ const encodedCurrentHost = window.btoa(currentHost);
236
+ const isCurrentHostValid = canonical.encoded === encodedCurrentHost;
232
237
  const canonicalTag = document.querySelector('link[rel="canonical"]');
233
238
  if (!canonicalTag) {
239
+ if (isCurrentHostValid) {
240
+ return;
241
+ }
242
+ if (canonical.officialHosts?.includes(currentHost)) {
243
+ showTip(true);
244
+ return;
245
+ }
234
246
  showTip(false);
235
247
  return;
236
248
  }
237
249
  const canonicalURL = new URL(canonicalTag.href);
238
- const currentURL = new URL(window.location.href);
239
250
  const canonicalHost = canonicalURL.hostname.replace(/^www\./, '');
240
- const currentHost = currentURL.hostname.replace(/^www\./, '');
241
- if (currentHost == 'localhost') return;
242
251
  const encodedCanonicalHost = window.btoa(canonicalHost);
243
- const encodedCurrentHost = window.btoa(currentHost);
244
252
  const isCanonicalHostValid = canonical.encoded === encodedCanonicalHost;
245
- const isCurrentHostValid = canonical.encoded === encodedCurrentHost;
246
253
  if (isCanonicalHostValid && isCurrentHostValid) {
247
254
  return;
248
255
  }
@@ -48,7 +48,9 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
48
48
  var dataContent = data.content;
49
49
  var dataContentLowerCase = dataContent.toLowerCase();
50
50
  var dataUrl = data.path.startsWith('//') ? data.path.slice(1) : data.path;
51
-
51
+ dataUrl = dataUrl.replace(/\/?index\.html$/, '/'); // index.html → /
52
+ dataUrl = dataUrl.replace(/\.html$/, '/'); // xxx.html → xxx/
53
+
52
54
  var indexTitle = -1;
53
55
  var indexContent = -1;
54
56
  var firstOccur = -1;
@@ -5,7 +5,7 @@ utils.jq(() => {
5
5
  const el = els[i];
6
6
  const limit = parseInt(el.getAttribute('limit')) || 10;
7
7
 
8
- const api = el.getAttribute('api') + '&limit=' + limit;
8
+ const api = el.dataset.api + '&limit=' + limit;
9
9
  if (api == null) {
10
10
  continue;
11
11
  }
@@ -29,7 +29,7 @@ utils.jq(() => {
29
29
  const els = document.getElementsByClassName('ds-contributors');
30
30
  for (var i = 0; i < els.length; i++) {
31
31
  const el = els[i];
32
- const api = el.getAttribute('api');
32
+ const api = el.dataset.api;
33
33
  if (api == null) {
34
34
  continue;
35
35
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-fcircle');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-friends');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-friends_and_posts');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-ghinfo');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-giscus');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -2,7 +2,7 @@ utils.jq(() => {
2
2
  const els = Array.from(document.getElementsByClassName('ds-memos'));
3
3
 
4
4
  els.forEach(el => {
5
- const api = el.getAttribute('api');
5
+ const api = el.dataset.api;
6
6
  if (!api) return;
7
7
 
8
8
  const default_avatar = el.getAttribute('avatar') || def.avatar;
@@ -0,0 +1,142 @@
1
+ function getRatingKey(id) {
2
+ return `rating-${id}`;
3
+ }
4
+
5
+ function hasRated(id) {
6
+ return !!localStorage.getItem(getRatingKey(id));
7
+ }
8
+
9
+ function getRatedValue(id) {
10
+ return parseInt(localStorage.getItem(getRatingKey(id)) || '0');
11
+ }
12
+
13
+ function storeRating(id, value) {
14
+ localStorage.setItem(getRatingKey(id), value);
15
+ }
16
+
17
+ function clearHover(el) {
18
+ el.querySelectorAll('.star').forEach(s => s.classList.remove('hover'));
19
+ }
20
+
21
+ function updatePreview(el, avg) {
22
+ const rounded = Math.floor(avg);
23
+ el.querySelectorAll('.star').forEach(s => {
24
+ const v = parseInt(s.dataset.value);
25
+ s.classList.toggle('preview', v <= rounded);
26
+ });
27
+ }
28
+
29
+ function setupHoverEffect(el) {
30
+ const stars = el.querySelectorAll('.star');
31
+ if (!stars.length) return;
32
+
33
+ stars.forEach(star => {
34
+ const value = parseInt(star.dataset.value);
35
+
36
+ star.addEventListener('mouseenter', () => {
37
+ stars.forEach(s => {
38
+ s.classList.remove('preview');
39
+ const v = parseInt(s.dataset.value);
40
+ s.classList.toggle('hover', v <= value);
41
+ });
42
+ });
43
+
44
+ star.addEventListener('mouseleave', () => {
45
+ clearHover(el);
46
+ // 恢复平均分预览
47
+ const avg = parseFloat(el.querySelector('.avg')?.textContent.replace(/[()]/g, '') || '0');
48
+ updatePreview(el, avg);
49
+ });
50
+ });
51
+ }
52
+
53
+ function calculateAverage(rating = {}) {
54
+ const validScores = Object.entries(rating).filter(([k]) => !isNaN(Number(k)));
55
+ const total = validScores.reduce((sum, [k, c]) => sum + Number(k) * c, 0);
56
+ const votes = validScores.reduce((sum, [, c]) => sum + c, 0);
57
+ return votes > 0 ? (total / votes).toFixed(1) : '0.0';
58
+ }
59
+
60
+ async function loadRating(el) {
61
+ const id = el.dataset.id;
62
+ const api = el.dataset.api;
63
+ if (!id || !api) return;
64
+
65
+ try {
66
+ const res = await fetch(`${api}/info?id=${encodeURIComponent(id)}`);
67
+ const data = await res.json();
68
+ const rating = data.rating || {};
69
+ const avg = calculateAverage(rating);
70
+
71
+ // 计算评分人数
72
+ const validScores = Object.entries(rating).filter(([k]) => !isNaN(Number(k)));
73
+ const totalVotes = validScores.reduce((sum, [, c]) => sum + c, 0);
74
+
75
+ // 设置平均分
76
+ let avgEl = el.querySelector('.avg');
77
+ if (!avgEl) {
78
+ avgEl = document.createElement('span');
79
+ avgEl.className = 'avg';
80
+ el.appendChild(avgEl);
81
+ }
82
+ avgEl.textContent = `(${avg})`;
83
+
84
+ // 设置评分人数
85
+ let countEl = el.querySelector('.count');
86
+ if (!countEl) {
87
+ countEl = document.createElement('span');
88
+ countEl.className = 'count';
89
+ el.appendChild(countEl);
90
+ }
91
+ countEl.textContent = `${totalVotes}`;
92
+
93
+ updatePreview(el, avg);
94
+ } catch (e) {
95
+ console.warn(`[rating] 加载失败: id=${id}`, e);
96
+ }
97
+ }
98
+
99
+ async function submitRating(el, value) {
100
+ const id = el.dataset.id;
101
+ const api = el.dataset.api;
102
+ if (!id || !api || hasRated(id)) return;
103
+
104
+ storeRating(id, value);
105
+ el.classList.add('rated');
106
+
107
+ try {
108
+ await fetch(`${api}/update?id=${encodeURIComponent(id)}&value=${value}`, {
109
+ method: 'POST'
110
+ });
111
+ loadRating(el);
112
+ } catch (e) {
113
+ console.warn(`[rating] 提交失败: id=${id}`, e);
114
+ }
115
+ }
116
+
117
+ function initRatings() {
118
+ document.querySelectorAll('.ds-rating').forEach(el => {
119
+ const { id, api } = el.dataset;
120
+ if (!id || !api) return;
121
+
122
+ loadRating(el);
123
+ setupHoverEffect(el);
124
+
125
+ if (hasRated(id)) {
126
+ el.classList.add('rated');
127
+ }
128
+
129
+ el.querySelectorAll('.star').forEach(star => {
130
+ const value = star.dataset.value;
131
+ star.addEventListener('click', () => {
132
+ if (!hasRated(id)) submitRating(el, value);
133
+ });
134
+ });
135
+ });
136
+ }
137
+
138
+ if (document.readyState === 'loading') {
139
+ window.addEventListener('DOMContentLoaded', initRatings);
140
+ } else {
141
+ initRatings();
142
+ }
@@ -6,7 +6,7 @@ function setCardLink(nodes) {
6
6
  // If it is not a tag element then it is not processed
7
7
  if (el.nodeType !== 1) return;
8
8
  el.removeAttribute('cardlink');
9
- const api = el.getAttribute('api');
9
+ const api = el.dataset.api;
10
10
  if (api == null) return;
11
11
  fetch(api).then(function(response) {
12
12
  if (response.ok) {
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-sites');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }
@@ -13,7 +13,7 @@ utils.jq(() => {
13
13
  const timelines = document.getElementsByClassName('ds-timeline');
14
14
  for (var i = 0; i < timelines.length; i++) {
15
15
  const el = timelines[i];
16
- const api = el.getAttribute('api');
16
+ const api = el.dataset.api;
17
17
  if (api == null) {
18
18
  continue;
19
19
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const el = document.querySelector('.ds-twikoo');
4
4
  utils.onLoading(el); // 加载动画
5
5
 
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  const limit = parseInt(el.getAttribute('limit')) || 10;
8
8
  const reply = el.getAttribute('hide') !== 'reply';
9
9
  if (!api) return;
@@ -0,0 +1,113 @@
1
+ function getVoteKey(id) {
2
+ return `vote-${id}`;
3
+ }
4
+
5
+ function hasVoted(id) {
6
+ return !!localStorage.getItem(getVoteKey(id));
7
+ }
8
+
9
+ function getVotedValue(id) {
10
+ return localStorage.getItem(getVoteKey(id)); // 'up' 或 'down'
11
+ }
12
+
13
+ function storeVote(id, value) {
14
+ localStorage.setItem(getVoteKey(id), value);
15
+ }
16
+
17
+ function removeVote(id) {
18
+ localStorage.removeItem(getVoteKey(id));
19
+ }
20
+
21
+ function markVoted(el, value) {
22
+ el.classList.add('voted');
23
+ if (value === 'up') {
24
+ el.querySelector('.vote-up')?.classList.add('active');
25
+ } else if (value === 'down') {
26
+ el.querySelector('.vote-down')?.classList.add('active');
27
+ }
28
+ }
29
+
30
+ function revertVote(el, value) {
31
+ const id = el.dataset.id;
32
+
33
+ // 回退数值
34
+ if (value === 'up') {
35
+ const upEl = el.querySelector('.up');
36
+ if (upEl) upEl.textContent = Math.max(0, parseInt(upEl.textContent || '1') - 1);
37
+ } else if (value === 'down') {
38
+ const downEl = el.querySelector('.down');
39
+ if (downEl) downEl.textContent = Math.max(0, parseInt(downEl.textContent || '1') - 1);
40
+ }
41
+
42
+ // 回退样式和状态
43
+ el.classList.remove('voted', 'active');
44
+ el.querySelector('.vote-up')?.classList.remove('active');
45
+ el.querySelector('.vote-down')?.classList.remove('active');
46
+ removeVote(id);
47
+ }
48
+
49
+ async function loadVote(el) {
50
+ const id = el.dataset.id;
51
+ const api = el.dataset.api;
52
+ if (!id || !api) return;
53
+
54
+ try {
55
+ const res = await fetch(`${api}/info?id=${encodeURIComponent(id)}`);
56
+ const data = await res.json();
57
+
58
+ el.querySelector('.up').textContent = data.votes?.up ?? 0;
59
+ el.querySelector('.down').textContent = data.votes?.down ?? 0;
60
+ } catch (e) {
61
+ console.warn(`[vote] 加载失败: id=${id}`, e);
62
+ }
63
+ }
64
+
65
+ function submitVote(el, value) {
66
+ const id = el.dataset.id;
67
+ const api = el.dataset.api;
68
+ if (!id || !api || hasVoted(id)) return;
69
+
70
+ const upEl = el.querySelector('.up');
71
+ const downEl = el.querySelector('.down');
72
+
73
+ // 乐观更新
74
+ if (value === 'up' && upEl) upEl.textContent = parseInt(upEl.textContent || '0') + 1;
75
+ if (value === 'down' && downEl) downEl.textContent = parseInt(downEl.textContent || '0') + 1;
76
+
77
+ storeVote(id, value);
78
+ markVoted(el, value);
79
+
80
+ // 后台同步
81
+ fetch(`${api}/update?id=${encodeURIComponent(id)}&value=${encodeURIComponent(value)}`, {
82
+ method: 'POST'
83
+ }).catch(e => {
84
+ console.warn(`[vote] 后台同步失败,撤销投票: id=${id}`, e);
85
+ revertVote(el, value);
86
+ });
87
+ }
88
+
89
+ function initVotes() {
90
+ document.querySelectorAll('.ds-vote').forEach(el => {
91
+ const { id, api } = el.dataset;
92
+ if (!id || !api) return;
93
+
94
+ loadVote(el);
95
+
96
+ const votedValue = getVotedValue(id);
97
+ if (votedValue) markVoted(el, votedValue);
98
+
99
+ el.querySelector('.vote-up')?.addEventListener('click', () => {
100
+ if (!el.classList.contains('active')) submitVote(el, 'up');
101
+ });
102
+
103
+ el.querySelector('.vote-down')?.addEventListener('click', () => {
104
+ if (!el.classList.contains('active')) submitVote(el, 'down');
105
+ });
106
+ });
107
+ }
108
+
109
+ if (document.readyState === 'loading') {
110
+ window.addEventListener('DOMContentLoaded', initVotes);
111
+ } else {
112
+ initVotes();
113
+ }
@@ -4,7 +4,7 @@ utils.jq(() => {
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
6
  const limit = parseInt(el.getAttribute('limit')) || 10;
7
- const apiBase = el.getAttribute('api');
7
+ const apiBase = el.dataset.api;
8
8
  if (apiBase == null) {
9
9
  continue;
10
10
  }
@@ -3,7 +3,7 @@ utils.jq(() => {
3
3
  const els = document.getElementsByClassName('ds-weibo');
4
4
  for (var i = 0; i < els.length; i++) {
5
5
  const el = els[i];
6
- const api = el.getAttribute('api');
6
+ const api = el.dataset.api;
7
7
  if (api == null) {
8
8
  continue;
9
9
  }