hexo-theme-solitude 3.0.21 → 4.0.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 (142) hide show
  1. package/.github/FUNDING.yml +3 -3
  2. package/README.md +11 -1
  3. package/README_en-US.md +11 -1
  4. package/_config.yml +16 -7
  5. package/languages/default.yml +56 -36
  6. package/languages/en.yml +90 -69
  7. package/languages/es.yml +94 -73
  8. package/languages/zh-CN.yml +56 -34
  9. package/languages/zh-TW.yml +80 -57
  10. package/layout/archive.pug +60 -14
  11. package/layout/category.pug +13 -11
  12. package/layout/includes/body/gadsense.pug +2 -2
  13. package/layout/includes/console.pug +71 -72
  14. package/layout/includes/footer.pug +24 -9
  15. package/layout/includes/head/config.pug +83 -39
  16. package/layout/includes/head/mode.pug +7 -7
  17. package/layout/includes/head/page_config.pug +20 -11
  18. package/layout/includes/inject/body.pug +14 -36
  19. package/layout/includes/keyboard.pug +3 -3
  20. package/layout/includes/loading.pug +1 -37
  21. package/layout/includes/mixins/articleSort.pug +3 -3
  22. package/layout/includes/mixins/common.pug +8 -4
  23. package/layout/includes/mixins/pace.pug +1 -1
  24. package/layout/includes/mixins/pagination.pug +3 -3
  25. package/layout/includes/nav.pug +4 -3
  26. package/layout/includes/page/about.pug +9 -8
  27. package/layout/includes/page/brevity.pug +2 -2
  28. package/layout/includes/page/links.pug +23 -11
  29. package/layout/includes/page/music.pug +0 -5
  30. package/layout/includes/rightmenu.pug +20 -20
  31. package/layout/includes/sidebar.pug +2 -2
  32. package/layout/includes/widgets/aside/asideInfoCard.pug +2 -2
  33. package/layout/includes/widgets/aside/asideTag.pug +2 -2
  34. package/layout/includes/widgets/home/bbTimeList.pug +3 -3
  35. package/layout/includes/widgets/home/categoryBar.pug +30 -15
  36. package/layout/includes/widgets/home/hometop.pug +113 -8
  37. package/layout/includes/widgets/home/postList.pug +1 -2
  38. package/layout/includes/widgets/home/topGroup.pug +3 -7
  39. package/layout/includes/widgets/nav/group.pug +4 -2
  40. package/layout/includes/widgets/nav/menu.pug +6 -4
  41. package/layout/includes/widgets/nav/right.pug +7 -6
  42. package/layout/includes/widgets/page/about/award.pug +67 -38
  43. package/layout/includes/widgets/page/about/hobbies.pug +12 -3
  44. package/layout/includes/widgets/page/banner.pug +1 -1
  45. package/layout/includes/widgets/page/kit/content.pug +2 -2
  46. package/layout/includes/widgets/page/links/banner.pug +30 -19
  47. package/layout/includes/widgets/page/message/artalk.pug +3 -3
  48. package/layout/includes/widgets/page/message/content.pug +3 -3
  49. package/layout/includes/widgets/page/message/twikoo.pug +3 -3
  50. package/layout/includes/widgets/page/message/valine.pug +4 -4
  51. package/layout/includes/widgets/page/message/waline.pug +3 -3
  52. package/layout/includes/widgets/page/recentcomment/artalk.pug +5 -9
  53. package/layout/includes/widgets/page/recentcomment/twikoo.pug +5 -9
  54. package/layout/includes/widgets/page/recentcomment/valine.pug +6 -10
  55. package/layout/includes/widgets/page/recentcomment/waline.pug +5 -9
  56. package/layout/includes/widgets/post/award.pug +1 -28
  57. package/layout/includes/widgets/post/copyright.pug +10 -10
  58. package/layout/includes/widgets/post/postInfo.pug +10 -11
  59. package/layout/includes/widgets/post/postMeta.pug +3 -2
  60. package/layout/includes/widgets/rightside/hide.pug +5 -5
  61. package/layout/includes/widgets/rightside/show.pug +7 -7
  62. package/layout/includes/widgets/third-party/comments/artalk.pug +9 -9
  63. package/layout/includes/widgets/third-party/comments/giscus.pug +2 -2
  64. package/layout/includes/widgets/third-party/comments/twikoo.pug +5 -5
  65. package/layout/includes/widgets/third-party/comments/valine.pug +6 -6
  66. package/layout/includes/widgets/third-party/comments/waline.pug +6 -6
  67. package/layout/includes/widgets/third-party/hot/twikoo.pug +1 -1
  68. package/layout/includes/widgets/third-party/music.pug +9 -5
  69. package/layout/includes/widgets/third-party/news-comment/artalk.pug +6 -6
  70. package/layout/includes/widgets/third-party/news-comment/twikoo.pug +7 -7
  71. package/layout/includes/widgets/third-party/news-comment/valine.pug +8 -8
  72. package/layout/includes/widgets/third-party/news-comment/waline.pug +7 -7
  73. package/layout/includes/widgets/third-party/pjax.pug +16 -10
  74. package/layout/includes/widgets/third-party/search/algolia-search.pug +2 -2
  75. package/layout/includes/widgets/third-party/search/docsearch.pug +3 -3
  76. package/layout/includes/widgets/third-party/search/index.pug +1 -3
  77. package/layout/includes/widgets/third-party/search/local-search.pug +9 -9
  78. package/layout/index.pug +8 -6
  79. package/package.json +8 -7
  80. package/plugins.yml +22 -21
  81. package/scripts/event/cdn.js +11 -6
  82. package/scripts/event/friend_links.js +30 -0
  83. package/scripts/event/merge_config.js +15 -7
  84. package/scripts/filter/comment.js +2 -2
  85. package/scripts/filter/default.js +1 -1
  86. package/scripts/helper/inject_head_js.js +79 -33
  87. package/scripts/helper/related_post.js +16 -21
  88. package/scripts/tags/chart.js +1 -1
  89. package/scripts/tags/typeit.js +1 -1
  90. package/source/css/_components/capsule.styl +259 -168
  91. package/source/css/_components/rightside.styl +98 -242
  92. package/source/css/_global/animation.styl +1 -10
  93. package/source/css/_global/index.styl +27 -1
  94. package/source/css/_layout/aside.styl +7 -1
  95. package/source/css/_layout/console.styl +145 -165
  96. package/source/css/_layout/header.styl +238 -47
  97. package/source/css/_layout/pagination.styl +176 -132
  98. package/source/css/_layout/rightmenu.styl +11 -2
  99. package/source/css/_layout/sidebar.styl +39 -17
  100. package/source/css/_mode/index.styl +7 -1
  101. package/source/css/_page/_about/about.styl +18 -2
  102. package/source/css/_page/_about/buff.styl +1 -1
  103. package/source/css/_page/_about/contentinfo.styl +2 -1
  104. package/source/css/_page/_about/game.styl +57 -1
  105. package/source/css/_page/_about/like.styl +7 -2
  106. package/source/css/_page/_about/maxim.styl +1 -1
  107. package/source/css/_page/_about/reward.styl +226 -108
  108. package/source/css/_page/_about/skills.styl +5 -3
  109. package/source/css/_page/_home/category-bar.styl +162 -88
  110. package/source/css/_page/_home/home-top.styl +309 -263
  111. package/source/css/_page/_home/home.styl +34 -7
  112. package/source/css/_page/archive.styl +412 -0
  113. package/source/css/_page/category.styl +27 -1
  114. package/source/css/_page/index.styl +2 -0
  115. package/source/css/_page/links.styl +190 -0
  116. package/source/css/_page/other.styl +37 -0
  117. package/source/css/_post/index.styl +2 -1
  118. package/source/css/_post/meta.styl +245 -217
  119. package/source/css/_search/local-search.styl +158 -82
  120. package/source/css/var.styl +2 -2
  121. package/source/js/archive-page.js +191 -0
  122. package/source/js/core/actions.js +48 -0
  123. package/source/js/core/api.js +32 -0
  124. package/source/js/core/config.js +13 -0
  125. package/source/js/core/lifecycle.js +52 -0
  126. package/source/js/core/preloader.js +34 -0
  127. package/source/js/core/resources.js +65 -0
  128. package/source/js/covercolor/api.js +13 -103
  129. package/source/js/covercolor/ave.js +14 -77
  130. package/source/js/covercolor/local.js +22 -116
  131. package/source/js/covercolor/shared.js +102 -0
  132. package/source/js/friend_links.js +502 -0
  133. package/source/js/main.js +793 -177
  134. package/source/js/music.js +24 -14
  135. package/source/js/post_ai.js +13 -2
  136. package/source/js/right_menu.js +44 -24
  137. package/source/js/search/algolia.js +29 -21
  138. package/source/js/search/local.js +231 -266
  139. package/source/js/tw_cn.js +37 -12
  140. package/source/js/utils.js +37 -25
  141. package/layout/includes/widgets/randomlink.pug +0 -43
  142. package/scripts/filter/randomPosts.js +0 -16
package/source/js/main.js CHANGED
@@ -1,3 +1,39 @@
1
+ import { Solitude } from "./core/api.js";
2
+ import { initActionDelegation } from "./core/actions.js";
3
+ import { lifecycle } from "./core/lifecycle.js";
4
+ import { initPreloader } from "./core/preloader.js";
5
+
6
+ let ai = null;
7
+ let coverColor = () => {};
8
+ let initializeMusicPlayer = () => {};
9
+
10
+ const loadFeatureModules = async () => {
11
+ const features = Solitude.config.feature_modules || {};
12
+ const requests = [];
13
+
14
+ if (features.search === "local") requests.push(import("./search/local.js"));
15
+ if (features.search === "algolia") requests.push(import("./search/algolia.js"));
16
+ if (features.friend_links) requests.push(import("./friend_links.js"));
17
+ if (features.right_menu) requests.push(import("./right_menu.js"));
18
+ if (features.translate) requests.push(import("./tw_cn.js"));
19
+ if (features.post_ai && Solitude.page.is_post) {
20
+ requests.push(import("./post_ai.js").then((module) => { ai = module.default; }));
21
+ }
22
+ if (features.music) {
23
+ requests.push(import("./music.js").then((module) => {
24
+ initializeMusicPlayer = module.initializeMusicPlayer;
25
+ }));
26
+ }
27
+ if (features.covercolor) {
28
+ requests.push(import(`./covercolor/${features.covercolor}.js`).then((module) => {
29
+ coverColor = module.coverColor;
30
+ Solitude.coverColor = coverColor;
31
+ }));
32
+ }
33
+
34
+ await Promise.all(requests);
35
+ };
36
+
1
37
  const sidebarFn = () => {
2
38
  const $toggleMenu = document.getElementById("toggle-menu");
3
39
  const $mobileSidebarMenus = document.getElementById("sidebar-menus");
@@ -6,7 +42,7 @@ const sidebarFn = () => {
6
42
 
7
43
  const toggleMobileSidebar = (isOpen) => {
8
44
  $body.style.overflow = isOpen ? "hidden" : "";
9
- utils[isOpen ? "fadeIn" : "fadeOut"]($menuMask, 0.5);
45
+ Solitude[isOpen ? "fadeIn" : "fadeOut"]($menuMask, 0.5);
10
46
  $mobileSidebarMenus.classList.toggle("open", isOpen);
11
47
  };
12
48
 
@@ -16,31 +52,41 @@ const sidebarFn = () => {
16
52
  }
17
53
  };
18
54
 
19
- $toggleMenu.addEventListener("click", () => toggleMobileSidebar(true));
20
- $menuMask.addEventListener("click", closeMobileSidebar);
55
+ if (!$toggleMenu || !$mobileSidebarMenus || !$menuMask) return;
21
56
 
22
- window.addEventListener("resize", () => {
23
- if (
24
- utils.isHidden($toggleMenu) &&
25
- $mobileSidebarMenus.classList.contains("open")
26
- ) {
27
- closeMobileSidebar();
28
- }
29
- sco.refreshWaterFall();
57
+ lifecycle.listen($toggleMenu, "click", () => toggleMobileSidebar(true));
58
+ lifecycle.listen($menuMask, "click", closeMobileSidebar);
59
+
60
+ let resizeFrame;
61
+ lifecycle.listen(window, "resize", () => {
62
+ if (resizeFrame) return;
63
+ resizeFrame = requestAnimationFrame(() => {
64
+ resizeFrame = null;
65
+ if (
66
+ Solitude.isHidden($toggleMenu) &&
67
+ $mobileSidebarMenus.classList.contains("open")
68
+ ) {
69
+ closeMobileSidebar();
70
+ }
71
+ });
30
72
  });
73
+ lifecycle.add(() => cancelAnimationFrame(resizeFrame));
31
74
  };
32
75
 
33
76
  const scrollFn = () => {
34
77
  const $rightside = document.getElementById("rightside");
35
78
  const $header = document.getElementById("page-header");
36
- let initTop = 0;
79
+ let initTop = window.scrollY || document.documentElement.scrollTop;
37
80
 
38
81
  const updateHeaderAndRightside = (isDown, currentTop) => {
39
- if (currentTop > 0) {
82
+ const isAtTop = currentTop <= 0;
83
+ $header.classList.toggle("nav-at-top", isAtTop);
84
+
85
+ if (!isAtTop) {
40
86
  $header.classList.toggle("nav-visible", !isDown);
41
87
  $header.classList.add("nav-fixed");
42
88
  if ($rightside) {
43
- $rightside.style.opacity = "0.8";
89
+ $rightside.style.opacity = "1";
44
90
  $rightside.style.transform = "translateX(-58px)";
45
91
  }
46
92
  } else {
@@ -52,23 +98,33 @@ const scrollFn = () => {
52
98
  }
53
99
  };
54
100
 
55
- const throttledScroll = utils.throttle(() => {
101
+ const handleScroll = () => {
56
102
  initThemeColor();
57
103
  const currentTop = window.scrollY || document.documentElement.scrollTop;
58
104
  const isDown = currentTop > initTop;
59
105
  initTop = currentTop;
60
106
  updateHeaderAndRightside(isDown, currentTop);
61
- }, 200);
107
+ };
62
108
 
63
- window.addEventListener("scroll", (e) => {
64
- throttledScroll(e);
65
- if (window.scrollY === 0) {
66
- $header.classList.remove("nav-fixed", "nav-visible");
67
- if ($rightside) {
68
- $rightside.style.cssText = "opacity: ''; transform: ''";
69
- }
109
+ let ticking = false;
110
+ const onScroll = () => {
111
+ const currentTop = window.scrollY || document.documentElement.scrollTop;
112
+ if (currentTop <= 0) {
113
+ initTop = 0;
114
+ updateHeaderAndRightside(false, 0);
115
+ return;
70
116
  }
71
- });
117
+ if (!ticking) {
118
+ window.requestAnimationFrame(() => {
119
+ handleScroll();
120
+ ticking = false;
121
+ });
122
+ ticking = true;
123
+ }
124
+ };
125
+
126
+ lifecycle.listen(window, "scroll", onScroll, { passive: true });
127
+ updateHeaderAndRightside(false, initTop);
72
128
  };
73
129
 
74
130
  const percent = () => {
@@ -84,23 +140,24 @@ const percent = () => {
84
140
  body.clientHeight,
85
141
  docEl.clientHeight
86
142
  ) - docEl.clientHeight;
87
- const scrolledPercent = Math.round((scrollPos / totalScrollableHeight) * 100);
143
+ const scrolledPercent = totalScrollableHeight > 0
144
+ ? Math.round((scrollPos / totalScrollableHeight) * 100)
145
+ : 0;
88
146
  const navToTop = document.querySelector("#nav-totop");
89
147
  const rsToTop = document.querySelector(".rs_show .top i");
90
148
  const percentDisplay = document.querySelector("#percent");
91
- const isNearEnd =
92
- window.scrollY + docEl.clientHeight >=
93
- (
94
- document.getElementById("post-comment") ||
95
- document.getElementById("footer")
96
- ).offsetTop;
149
+ const endTarget =
150
+ document.getElementById("post-comment") || document.getElementById("footer");
151
+ const isNearEnd = endTarget
152
+ ? window.scrollY + docEl.clientHeight >= endTarget.offsetTop
153
+ : false;
97
154
 
98
155
  navToTop?.classList.toggle("long", isNearEnd || scrolledPercent > 90);
99
156
  rsToTop?.classList.toggle("show", isNearEnd || scrolledPercent > 90);
100
- percentDisplay.textContent =
157
+ if (percentDisplay) percentDisplay.textContent =
101
158
  isNearEnd || scrolledPercent > 90
102
159
  ? navToTop
103
- ? GLOBAL_CONFIG.lang.backtop
160
+ ? Solitude.config.lang.backtop
104
161
  : ""
105
162
  : scrolledPercent;
106
163
 
@@ -114,7 +171,290 @@ const percent = () => {
114
171
  const showTodayCard = () => {
115
172
  const el = document.getElementById("todayCard");
116
173
  const topGroup = document.querySelector(".topGroup");
117
- topGroup?.addEventListener("mouseleave", () => el?.classList.remove("hide"));
174
+ lifecycle.listen(topGroup, "mouseleave", () => el?.classList.remove("hide"));
175
+ };
176
+
177
+ const initHomeCenter = () => {
178
+ const container = document.getElementById("home_center");
179
+ if (!container || container.dataset.initialized === "true") return;
180
+ container.dataset.initialized = "true";
181
+
182
+ const banners = [...container.querySelectorAll(".home-center-banner-item")];
183
+ const items = [...container.querySelectorAll(".home-center-item")];
184
+ const indicators = [
185
+ ...container.querySelectorAll(".home-center-indicator"),
186
+ ];
187
+ const banner = container.querySelector(".home-center-banner");
188
+ const titleLink = container.querySelector(".home-center-title-link");
189
+ const titleTag = container.querySelector(".home-center-title-tag span");
190
+ const categoryBar = document.getElementById("category-bar");
191
+ let activeIndex = 0;
192
+ let scrollFrame;
193
+
194
+ const getCachedColor = (src) => {
195
+ try {
196
+ const cache = JSON.parse(localStorage.getItem("Solitude")) || {};
197
+ const item = cache.postcolor?.[src];
198
+ if (item && (!item.expiration || item.expiration > Date.now())) {
199
+ return item.value;
200
+ }
201
+ } catch (error) {
202
+ return null;
203
+ }
204
+ return null;
205
+ };
206
+
207
+ const cacheColor = (src, color) => {
208
+ try {
209
+ const cache = JSON.parse(localStorage.getItem("Solitude")) || {};
210
+ cache.postcolor = cache.postcolor || {};
211
+ cache.postcolor[src] = {
212
+ value: color,
213
+ expiration: Date.now() + 43200000,
214
+ };
215
+ localStorage.setItem("Solitude", JSON.stringify(cache));
216
+ } catch (error) {
217
+ // Color caching is optional; rendering must continue without storage.
218
+ }
219
+ };
220
+
221
+ const rgbToThemeHex = ([r, g, b]) =>
222
+ `#${[r, g, b]
223
+ .map((value) =>
224
+ Math.floor(value * 0.8)
225
+ .toString(16)
226
+ .padStart(2, "0")
227
+ )
228
+ .join("")}`;
229
+
230
+ const getAverageColor = (image) => {
231
+ const canvas = document.createElement("canvas");
232
+ canvas.width = 32;
233
+ canvas.height = 32;
234
+ const context = canvas.getContext("2d", { willReadFrequently: true });
235
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
236
+ const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
237
+ const color = [0, 0, 0];
238
+ let count = 0;
239
+ for (let index = 0; index < pixels.length; index += 4) {
240
+ if (pixels[index + 3] < 128) continue;
241
+ color[0] += pixels[index];
242
+ color[1] += pixels[index + 1];
243
+ color[2] += pixels[index + 2];
244
+ count++;
245
+ }
246
+ return count ? color.map((value) => Math.round(value / count)) : null;
247
+ };
248
+
249
+ const normalizeHomeCenterColor = (value) => {
250
+ const match = value?.match(/^#([0-9a-f]{6})$/i);
251
+ if (!match) return value;
252
+ const number = parseInt(match[1], 16);
253
+ const rgb = [number >> 16, (number >> 8) & 0xff, number & 0xff];
254
+ const brightness = Math.round(
255
+ (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000
256
+ );
257
+ if (brightness >= 125) return value;
258
+ return `#${rgb
259
+ .map((channel) => Math.min(channel + 50, 255).toString(16).padStart(2, "0"))
260
+ .join("")}`;
261
+ };
262
+
263
+ const applyItemColor = (index, color) => {
264
+ if (!color || !banners[index]) return;
265
+ const colorOp = `color-mix(in srgb, ${color} 14%, transparent)`;
266
+ const colorDeep = `color-mix(in srgb, ${color} 87%, transparent)`;
267
+ banners[index].style.setProperty("--home-center-theme", color);
268
+ banners[index].style.setProperty("--home-center-theme-op", colorOp);
269
+ banners[index].style.setProperty("--home-center-theme-op-deep", colorDeep);
270
+ items[index]?.style.setProperty("--item-theme", color);
271
+ items[index]?.style.setProperty("--item-theme-op", colorOp);
272
+ items[index]?.style.setProperty("--item-theme-op-deep", colorDeep);
273
+ if (activeIndex === index) select(index);
274
+ };
275
+
276
+ const extractItemColor = (index, sourceImage) => {
277
+ if (banners[index].dataset.color) return;
278
+ const src = sourceImage.currentSrc || sourceImage.src;
279
+ if (!src) return;
280
+ const cachedColor = getCachedColor(src);
281
+ if (cachedColor) {
282
+ applyItemColor(index, normalizeHomeCenterColor(cachedColor));
283
+ return;
284
+ }
285
+
286
+ const image = new Image();
287
+ image.crossOrigin = "Anonymous";
288
+ image.onload = () => {
289
+ if (!container.isConnected) return;
290
+ try {
291
+ const dominantColor = window.ColorThief?.getColorSync(image);
292
+ const rgb = dominantColor ? dominantColor.array() : getAverageColor(image);
293
+ if (!rgb) return;
294
+ const color = rgbToThemeHex(rgb);
295
+ cacheColor(src, color);
296
+ applyItemColor(index, normalizeHomeCenterColor(color));
297
+ } catch (error) {
298
+ // Canvas access can fail for image hosts without CORS support.
299
+ }
300
+ };
301
+ image.onerror = () => {};
302
+ image.src = src;
303
+ };
304
+
305
+ const navigate = (link, event) => {
306
+ if (!link) return;
307
+ if (event?.metaKey || event?.ctrlKey) {
308
+ window.open(link, "_blank");
309
+ } else if (Solitude.pjax?.loadUrl) {
310
+ Solitude.navigate(link);
311
+ } else {
312
+ window.location.href = link;
313
+ }
314
+ };
315
+
316
+ const select = (index) => {
317
+ if (!banners[index]) return;
318
+ activeIndex = index;
319
+ banners.forEach((banner, bannerIndex) => {
320
+ const isActive = bannerIndex === index;
321
+ banner.classList.toggle("active", isActive);
322
+ banner.setAttribute("aria-hidden", String(!isActive));
323
+ banner.tabIndex = isActive ? 0 : -1;
324
+ });
325
+ items.forEach((item, itemIndex) => {
326
+ const isActive = itemIndex === index;
327
+ item.classList.toggle("active", isActive);
328
+ item.setAttribute("aria-current", String(isActive));
329
+ });
330
+ indicators.forEach((indicator, indicatorIndex) =>
331
+ indicator.classList.toggle("active", indicatorIndex === index)
332
+ );
333
+ const selected = banners[index];
334
+ const selectedStyle = getComputedStyle(selected);
335
+ const color = selectedStyle.getPropertyValue("--home-center-theme").trim();
336
+ const colorOp = selectedStyle
337
+ .getPropertyValue("--home-center-theme-op")
338
+ .trim();
339
+ const colorDeep = selectedStyle
340
+ .getPropertyValue("--home-center-theme-op-deep")
341
+ .trim();
342
+ titleLink.textContent = selected.dataset.title;
343
+ titleLink.href = selected.dataset.link;
344
+ titleTag.textContent = selected.dataset.label;
345
+ container.style.setProperty("--current-theme", color);
346
+ container.style.setProperty("--current-theme-op", colorOp);
347
+ container.style.setProperty("--current-theme-op-deep", colorDeep);
348
+ categoryBar?.style.setProperty("--current-banner-theme", color);
349
+ };
350
+
351
+ items.forEach((item, index) => {
352
+ item.addEventListener("mouseenter", () => select(index));
353
+ item.addEventListener("focusin", () => select(index));
354
+ });
355
+ banners.forEach((item, index) => {
356
+ let pointerType = "";
357
+ item.addEventListener(
358
+ "pointerdown",
359
+ (event) => (pointerType = event.pointerType),
360
+ true
361
+ );
362
+ item.addEventListener("click", (event) => {
363
+ if (pointerType === "touch" && window.innerWidth <= 768) {
364
+ event.preventDefault();
365
+ select(index);
366
+ } else {
367
+ navigate(item.dataset.link, event);
368
+ }
369
+ pointerType = "";
370
+ });
371
+ item.addEventListener("keydown", (event) => {
372
+ if (event.key === "Enter" || event.key === " ") {
373
+ event.preventDefault();
374
+ navigate(item.dataset.link, event);
375
+ }
376
+ });
377
+ });
378
+ indicators.forEach((indicator, index) => {
379
+ indicator.addEventListener("click", (event) => {
380
+ event.preventDefault();
381
+ select(index);
382
+ banner.scrollTo({ left: banner.clientWidth * index, behavior: "smooth" });
383
+ });
384
+ });
385
+ banner.addEventListener("scroll", () => {
386
+ cancelAnimationFrame(scrollFrame);
387
+ scrollFrame = requestAnimationFrame(() => {
388
+ if (window.innerWidth > 768 || !banner.clientWidth) return;
389
+ select(Math.round(banner.scrollLeft / banner.clientWidth));
390
+ });
391
+ });
392
+ select(0);
393
+ banners.forEach((item, index) => {
394
+ if (item.dataset.color) return;
395
+ const image = item.querySelector(".home-center-cover-img");
396
+ if (!image) return;
397
+ if (image.complete && image.naturalWidth) {
398
+ extractItemColor(index, image);
399
+ } else {
400
+ image.addEventListener("load", () => extractItemColor(index, image), {
401
+ once: true,
402
+ });
403
+ }
404
+ });
405
+ };
406
+
407
+ const initTooltip = () => {
408
+ const tooltip =
409
+ document.querySelector(".custom-tooltip") ||
410
+ document.body.appendChild(
411
+ Object.assign(document.createElement("div"), {
412
+ className: "custom-tooltip",
413
+ })
414
+ );
415
+
416
+ tooltip.style.opacity = "0";
417
+ tooltip.style.backdropFilter = "none";
418
+ if (!window.matchMedia("(hover: hover)").matches) return;
419
+
420
+ const rootFontSize = parseFloat(
421
+ getComputedStyle(document.documentElement).fontSize
422
+ );
423
+
424
+ document.querySelectorAll("[heotip]").forEach((element) => {
425
+ if (element.dataset.tooltipInitialized === "true") return;
426
+ element.dataset.tooltipInitialized = "true";
427
+
428
+ element.addEventListener("mouseenter", () => {
429
+ tooltip.textContent = element.getAttribute("heotip");
430
+ tooltip.style.left = "0";
431
+ tooltip.style.top = "0";
432
+ tooltip.style.backdropFilter = "blur(10px)";
433
+ tooltip.style.opacity = "1";
434
+
435
+ const targetRect = element.getBoundingClientRect();
436
+ const tooltipRect = tooltip.getBoundingClientRect();
437
+ const gap = 10;
438
+ const maxLeft = window.innerWidth - tooltipRect.width - rootFontSize;
439
+ const centeredLeft =
440
+ targetRect.left + (targetRect.width - tooltipRect.width) / 2;
441
+ const left = Math.max(rootFontSize, Math.min(centeredLeft, maxLeft));
442
+ const preferredTop =
443
+ targetRect.top >= tooltipRect.height + gap
444
+ ? targetRect.top - tooltipRect.height - gap
445
+ : targetRect.bottom + gap;
446
+ const maxTop = window.innerHeight - tooltipRect.height - rootFontSize;
447
+ const top = Math.max(rootFontSize, Math.min(preferredTop, maxTop));
448
+
449
+ tooltip.style.left = `${left}px`;
450
+ tooltip.style.top = `${top}px`;
451
+ });
452
+
453
+ element.addEventListener("mouseleave", () => {
454
+ tooltip.style.backdropFilter = "none";
455
+ tooltip.style.opacity = "0";
456
+ });
457
+ });
118
458
  };
119
459
 
120
460
  const initObserver = () => {
@@ -126,7 +466,7 @@ const initObserver = () => {
126
466
  const observer = new IntersectionObserver((entries) => {
127
467
  entries.forEach((entry) => {
128
468
  paginationElement.classList.toggle("show-window", entry.isIntersecting);
129
- if (GLOBAL_CONFIG.comment.commentBarrage) {
469
+ if (Solitude.config.comment?.commentBarrage && commentBarrageElement) {
130
470
  commentBarrageElement.style.bottom = entry.isIntersecting
131
471
  ? "-200px"
132
472
  : "0px";
@@ -134,12 +474,13 @@ const initObserver = () => {
134
474
  });
135
475
  });
136
476
  observer.observe(commentElement);
477
+ lifecycle.add(() => observer.disconnect());
137
478
  }
138
479
  };
139
480
 
140
481
  const addCopyright = () => {
141
- if (!GLOBAL_CONFIG.copyright) return;
142
- const { limit, author, link, source, info } = GLOBAL_CONFIG.copyright;
482
+ if (!Solitude.config.copyright) return;
483
+ const { limit, author, link, source, info } = Solitude.config.copyright;
143
484
 
144
485
  document.body.addEventListener("copy", (e) => {
145
486
  e.preventDefault();
@@ -153,7 +494,7 @@ const addCopyright = () => {
153
494
  };
154
495
 
155
496
  const asideStatus = () => {
156
- const status = utils.saveToLocal.get("aside-status");
497
+ const status = Solitude.saveToLocal.get("aside-status");
157
498
  document.documentElement.classList.toggle("hide-aside", status === "hide");
158
499
  };
159
500
 
@@ -162,7 +503,7 @@ function initThemeColor() {
162
503
  const themeColor =
163
504
  currentTop > 0
164
505
  ? "--efu-card-bg"
165
- : PAGE_CONFIG.is_post
506
+ : Solitude.page.is_post
166
507
  ? "--efu-main"
167
508
  : "--efu-background";
168
509
  applyThemeColor(
@@ -170,6 +511,8 @@ function initThemeColor() {
170
511
  );
171
512
  }
172
513
 
514
+ Solitude.initThemeColor = initThemeColor;
515
+
173
516
  function applyThemeColor(color) {
174
517
  const themeColorMeta = document.querySelector('meta[name="theme-color"]');
175
518
  const appleMobileWebAppMeta = document.querySelector(
@@ -185,12 +528,64 @@ function applyThemeColor(color) {
185
528
  const handleThemeChange = (mode) => {
186
529
  const themeChange = window.globalFn?.themeChange || {};
187
530
  Object.values(themeChange).forEach((fn) => fn(mode));
531
+ lifecycle.emit("themeChange", { mode });
188
532
  };
189
533
 
190
- const sco = {
534
+ const actions = {
191
535
  lastWittyWord: "",
192
536
  wasPageHidden: false,
193
537
  musicPlaying: false,
538
+ consoleNavState: null,
539
+ randomPost() {
540
+ const posts = Solitude.config.random_posts || [];
541
+ if (!posts.length) return;
542
+ Solitude.navigate(`${Solitude.config.root}${posts[Solitude.randomNum(posts.length)]}`);
543
+ },
544
+ noop() {},
545
+ navigateTo(url) {
546
+ Solitude.navigate(url);
547
+ },
548
+ openExternal(url) {
549
+ if (url) window.open(url, "_blank", "noopener");
550
+ },
551
+ toggleTargetClass(target, event, element) {
552
+ const selector = target || element?.dataset.solitudeTarget;
553
+ const className = element?.dataset.solitudeClass || "show";
554
+ document.querySelector(selector)?.classList.toggle(className);
555
+ },
556
+ setTargetClass(target, event, element) {
557
+ const selector = target || element?.dataset.solitudeTarget;
558
+ const className = element?.dataset.solitudeClass || "show";
559
+ const enabled = element?.dataset.solitudeEnabled !== "false";
560
+ document
561
+ .querySelectorAll(selector)
562
+ .forEach((item) => item.classList.toggle(className, enabled));
563
+ },
564
+ showReward() {
565
+ document
566
+ .querySelectorAll(".reward-main")
567
+ .forEach((item) => { item.style.display = "flex"; });
568
+ const quitBox = document.getElementById("quit-box");
569
+ if (quitBox) quitBox.style.display = "flex";
570
+ },
571
+ hideReward() {
572
+ document
573
+ .querySelectorAll(".reward-main")
574
+ .forEach((item) => { item.style.display = "none"; });
575
+ const quitBox = document.getElementById("quit-box");
576
+ if (quitBox) quitBox.style.display = "none";
577
+ },
578
+ runConfiguredAction(command) {
579
+ const source = String(command || "").trim().replace(/;$/, "");
580
+ const call = source.match(/^(?:Solitude\.)?([A-Za-z_$][\w$]*)\(\)$/);
581
+ if (call && typeof Solitude[call[1]] === "function") {
582
+ Solitude[call[1]]();
583
+ } else {
584
+ const navigation = source.match(/^(?:pjax\.loadUrl|Solitude\.navigate)\((['"])(.*?)\1\)$/);
585
+ if (navigation) Solitude.navigate(navigation[2]);
586
+ }
587
+ Solitude.hideRightMenu?.();
588
+ },
194
589
  scrollTo(elementId) {
195
590
  const targetElement = document.getElementById(elementId);
196
591
  if (targetElement) {
@@ -199,47 +594,73 @@ const sco = {
199
594
  window.scroll({ top: targetPosition, behavior: "smooth" });
200
595
  }
201
596
  },
202
- musicBind() {
203
- const $music = document.querySelector("#nav-music meting-js");
204
- if ($music && $music.aplayer) {
205
- this.isMusicBind = true;
206
- $music.onclick = () => this.musicPlaying && this.musicToggle(true);
207
- $music.aplayer.on('loadeddata', () =>{
208
- coverColor(true);
209
- })
210
- }
211
- },
212
- musicToggle(isMeting = true) {
213
- if (!this.isMusicBind) this.musicBind();
214
-
215
- const $music = document.querySelector("#nav-music");
216
- const $meting = document.querySelector("#nav-music meting-js");
597
+ syncMusicState(isPlaying) {
598
+ const $music = document.getElementById("nav-music");
217
599
  const $console = document.getElementById("consoleMusic");
218
-
219
- this.musicPlaying = !this.musicPlaying;
220
-
221
- $music.classList.toggle("playing", this.musicPlaying);
222
- $music.classList.toggle("stretch", this.musicPlaying);
600
+ this.musicPlaying = Boolean(isPlaying);
601
+
602
+ $music?.classList.toggle("playing", this.musicPlaying);
603
+ $music?.classList.toggle("stretch", this.musicPlaying);
223
604
  $console?.classList.toggle("on", this.musicPlaying);
224
-
225
- if (typeof rm !== "undefined" && rm?.menuItems.music[0]) {
605
+
606
+ if (Solitude.rightMenu?.menuItems?.music?.[0]) {
226
607
  const $rmText = document.querySelector("#menu-music-toggle span");
227
608
  const $rmIcon = document.querySelector("#menu-music-toggle i");
228
- $rmText.textContent = this.musicPlaying
229
- ? GLOBAL_CONFIG.right_menu.music.stop
230
- : GLOBAL_CONFIG.right_menu.music.start;
231
- $rmIcon.className = `solitude fas ${this.musicPlaying ? 'fa-pause' : 'fa-play'}`;
609
+ if ($rmText) {
610
+ const label = this.musicPlaying
611
+ ? Solitude.config.right_menu.music.stop
612
+ : Solitude.config.right_menu.music.start;
613
+ Solitude.rightMenu.setLabel($rmText, label);
614
+ }
615
+ if ($rmIcon) {
616
+ $rmIcon.className = `solitude fas ${this.musicPlaying ? "fa-pause" : "fa-play"}`;
617
+ }
618
+ }
619
+ },
620
+ musicBind() {
621
+ const $meting = document.querySelector("#nav-music meting-js");
622
+ const aplayer = $meting?.aplayer;
623
+ if (!aplayer) {
624
+ this.isMusicBind = false;
625
+ return null;
626
+ }
627
+
628
+ this.isMusicBind = true;
629
+ if (!aplayer.solitudeCapsuleBound) {
630
+ aplayer.on("play", () => this.syncMusicState(true));
631
+ aplayer.on("pause", () => this.syncMusicState(false));
632
+ aplayer.on("ended", () => this.syncMusicState(false));
633
+ aplayer.on("loadeddata", () => {
634
+ if (typeof coverColor === "function") coverColor(true);
635
+ this.syncMusicState(Boolean(aplayer.audio && !aplayer.audio.paused));
636
+ });
637
+ aplayer.solitudeCapsuleBound = true;
232
638
  }
233
639
 
234
- if (isMeting && $meting) {
235
- this.musicPlaying ? $meting.aplayer.play() : $meting.aplayer.pause();
640
+ this.syncMusicState(Boolean(aplayer.audio && !aplayer.audio.paused));
641
+ return aplayer;
642
+ },
643
+ handleMusicClick(event) {
644
+ if (event.target?.closest?.(".music-control-btn")) return;
645
+ if (!this.musicPlaying) this.musicToggle();
646
+ },
647
+ musicToggle(isMeting = true) {
648
+ const aplayer = this.musicBind();
649
+ if (!aplayer) return;
650
+
651
+ const shouldPlay = Boolean(aplayer.audio?.paused);
652
+ if (!isMeting) {
653
+ this.syncMusicState(shouldPlay);
654
+ return;
236
655
  }
656
+
657
+ shouldPlay ? aplayer.play() : aplayer.pause();
237
658
  },
238
659
  musicSkipBack() {
239
- document.querySelector("meting-js")?.aplayer?.skipBack();
660
+ document.querySelector("#nav-music meting-js")?.aplayer?.skipBack();
240
661
  },
241
662
  musicSkipForward() {
242
- document.querySelector("meting-js")?.aplayer?.skipForward();
663
+ document.querySelector("#nav-music meting-js")?.aplayer?.skipForward();
243
664
  },
244
665
  switchCommentBarrage() {
245
666
  const commentBarrageElement = document.querySelector(".comment-barrage");
@@ -252,14 +673,16 @@ const sco = {
252
673
  window.getComputedStyle(commentBarrageElement).display === "flex";
253
674
  commentBarrageElement.style.display = isDisplayed ? "none" : "flex";
254
675
  consoleCommentBarrage?.classList.toggle("on", !isDisplayed);
255
- utils.saveToLocal.set("commentBarrageSwitch", !isDisplayed, 0.2);
256
- rm?.menuItems.barrage && rm.barrage(isDisplayed);
676
+ Solitude.saveToLocal.set("commentBarrageSwitch", !isDisplayed, 0.2);
677
+ if (Solitude.rightMenu?.menuItems.barrage) {
678
+ Solitude.rightMenu.barrage(isDisplayed);
679
+ }
257
680
  },
258
681
  switchHideAside() {
259
682
  const htmlClassList = document.documentElement.classList;
260
683
  const consoleHideAside = document.querySelector("#consoleHideAside");
261
684
  const isHideAside = htmlClassList.contains("hide-aside");
262
- utils.saveToLocal.set("aside-status", isHideAside ? "show" : "hide", 1);
685
+ Solitude.saveToLocal.set("aside-status", isHideAside ? "show" : "hide", 1);
263
686
  htmlClassList.toggle("hide-aside");
264
687
  consoleHideAside.classList.toggle("on", !isHideAside);
265
688
  },
@@ -281,7 +704,7 @@ const sco = {
281
704
  );
282
705
  },
283
706
  changeWittyWord() {
284
- const greetings = GLOBAL_CONFIG.aside.witty_words || [];
707
+ const greetings = Solitude.config.aside.witty_words || [];
285
708
  if (greetings.length === 0) {
286
709
  document.getElementById("sayhi").textContent = "Solitude";
287
710
  this.lastWittyWord = null;
@@ -304,40 +727,115 @@ const sco = {
304
727
  document.documentElement.getAttribute("data-theme") === "dark";
305
728
  const newMode = isDarkMode ? "light" : "dark";
306
729
  document.documentElement.setAttribute("data-theme", newMode);
307
- utils.saveToLocal.set("theme", newMode, 0.02);
308
- utils.snackbarShow(GLOBAL_CONFIG.lang.theme[newMode], false, 2000);
309
- if (typeof rm === "object") rm.mode(!isDarkMode) && rm.hideRightMenu();
730
+ Solitude.saveToLocal.set("theme", newMode, 0.02);
731
+ Solitude.snackbarShow(Solitude.config.lang.theme[newMode], false, 2000);
732
+ if (Solitude.rightMenu) {
733
+ Solitude.rightMenu.mode(!isDarkMode);
734
+ Solitude.rightMenu.hideRightMenu();
735
+ }
310
736
  handleThemeChange(newMode);
311
737
  },
312
738
  hideTodayCard: () =>
313
739
  document.getElementById("todayCard").classList.add("hide"),
314
- toTop: () => utils.scrollToDest(0),
315
- showConsole: () =>
316
- document.getElementById("console")?.classList.toggle("show", true),
317
- hideConsole: () =>
318
- document.getElementById("console")?.classList.remove("show"),
740
+ toTop: () => Solitude.scrollToDest(0),
741
+ showConsole() {
742
+ const consoleElement = document.getElementById("console");
743
+ if (!consoleElement || consoleElement.classList.contains("show")) return;
744
+
745
+ const header = document.getElementById("page-header");
746
+ if (header) {
747
+ this.consoleNavState = {
748
+ fixed: header.classList.contains("nav-fixed"),
749
+ visible: header.classList.contains("nav-visible"),
750
+ };
751
+ header.classList.add("nav-fixed");
752
+ header.classList.remove("nav-visible");
753
+ header.classList.add("console-open");
754
+ }
755
+
756
+ consoleElement.classList.add("show");
757
+ document
758
+ .querySelector("#nav-console .console_switchbutton")
759
+ ?.classList.add("console-open");
760
+ },
761
+ hideConsole() {
762
+ const consoleElement = document.getElementById("console");
763
+ if (!consoleElement?.classList.contains("show")) return;
764
+
765
+ consoleElement.classList.remove("show");
766
+ document
767
+ .querySelector("#nav-console .console_switchbutton")
768
+ ?.classList.remove("console-open");
769
+
770
+ const header = document.getElementById("page-header");
771
+ if (header && this.consoleNavState) {
772
+ header.classList.remove("console-open");
773
+ header.classList.toggle("nav-fixed", this.consoleNavState.fixed);
774
+ header.classList.toggle("nav-visible", this.consoleNavState.visible);
775
+ }
776
+ this.consoleNavState = null;
777
+ },
778
+ toggleConsole() {
779
+ const consoleElement = document.getElementById("console");
780
+ if (consoleElement?.classList.contains("show")) {
781
+ this.hideConsole();
782
+ } else {
783
+ this.showConsole();
784
+ }
785
+ },
786
+ onConsoleCardGroupClick(event) {
787
+ if (event.target.closest?.(".console-card")) return;
788
+ this.hideConsole();
789
+ },
790
+ onNavBlankClickCloseConsole(event) {
791
+ if (!document.getElementById("console")?.classList.contains("show")) return;
792
+ if (
793
+ event.target.closest?.(
794
+ "a, button, .back-home-button, .menus_item, #page-name"
795
+ )
796
+ ) {
797
+ return;
798
+ }
799
+ this.hideConsole();
800
+ },
319
801
  refreshWaterFall() {
802
+ const allElements = [...document.querySelectorAll(".waterfall")];
803
+ const elements = allElements.filter(
804
+ (element) => element.dataset.solitudeWaterfall !== "true"
805
+ );
806
+ if (!elements.length) return;
807
+ elements.forEach((element) => {
808
+ element.dataset.solitudeWaterfall = "true";
809
+ });
810
+
811
+ const timers = new Set();
320
812
  const observer = new IntersectionObserver((entries) => {
321
813
  entries.forEach((entry) => {
322
814
  if (entry.isIntersecting) {
323
- setTimeout(() => {
815
+ observer.unobserve(entry.target);
816
+ const timer = setTimeout(() => {
817
+ timers.delete(timer);
818
+ if (!entry.target.isConnected) return;
324
819
  waterfall(entry.target).then(() => {
325
- entry.target.classList.add("show");
820
+ if (entry.target.isConnected) entry.target.classList.add("show");
326
821
  });
327
822
  }, 300);
823
+ timers.add(timer);
328
824
  }
329
825
  });
330
826
  });
331
- document
332
- .querySelectorAll(".waterfall")
333
- .forEach((el) => observer.observe(el));
827
+ elements.forEach((element) => observer.observe(element));
828
+ lifecycle.add(() => {
829
+ observer.disconnect();
830
+ timers.forEach(clearTimeout);
831
+ });
334
832
  },
335
833
  addRuntime() {
336
834
  const el = document.getElementById("runtimeshow");
337
- if (el && GLOBAL_CONFIG.runtime) {
835
+ if (el && Solitude.config.runtime) {
338
836
  el.innerText =
339
- utils.timeDiff(new Date(GLOBAL_CONFIG.runtime), new Date()) +
340
- GLOBAL_CONFIG.lang.day;
837
+ Solitude.timeDiff(new Date(Solitude.config.runtime), new Date()) +
838
+ Solitude.config.lang.day;
341
839
  }
342
840
  },
343
841
  toTalk(txt) {
@@ -354,15 +852,15 @@ const sco = {
354
852
  new Event("input", { bubble: true, cancelable: true })
355
853
  );
356
854
  el.value = "> " + txt.replace(/\n/g, "\n> ") + "\n\n";
357
- utils.scrollToDest(
358
- utils.getEleTop(document.getElementById("post-comment")),
855
+ Solitude.scrollToDest(
856
+ Solitude.getEleTop(document.getElementById("post-comment")),
359
857
  300
360
858
  );
361
859
  el.focus();
362
860
  el.setSelectionRange(-1, -1);
363
861
  }
364
862
  });
365
- utils.snackbarShow(GLOBAL_CONFIG.lang.totalk, false, 2000);
863
+ Solitude.snackbarShow(Solitude.config.lang.totalk, false, 2000);
366
864
  },
367
865
  initbbtalk() {
368
866
  const bberTalkElement = document.querySelector("#bber-talk");
@@ -385,7 +883,7 @@ const sco = {
385
883
  if (captionText) {
386
884
  image.insertAdjacentHTML(
387
885
  "afterend",
388
- `<div class="img-alt is-center">${utils.escapeHtml(
886
+ `<div class="img-alt is-center">${Solitude.escapeHtml(
389
887
  captionText
390
888
  )}</div>`
391
889
  );
@@ -393,15 +891,15 @@ const sco = {
393
891
  });
394
892
  },
395
893
  scrollToComment: () =>
396
- utils.scrollToDest(
397
- utils.getEleTop(document.getElementById("post-comment")),
894
+ Solitude.scrollToDest(
895
+ Solitude.getEleTop(document.getElementById("post-comment")),
398
896
  300
399
897
  ),
400
898
  setTimeState() {
401
899
  const el = document.getElementById("sayhi");
402
900
  if (el) {
403
901
  const hours = new Date().getHours();
404
- const lang = GLOBAL_CONFIG.aside.state;
902
+ const lang = Solitude.config.aside.state;
405
903
 
406
904
  const localData = getLocalData([
407
905
  "twikoo",
@@ -415,7 +913,11 @@ const sco = {
415
913
  for (let key of keys) {
416
914
  const data = localStorage.getItem(key);
417
915
  if (data) {
418
- return JSON.parse(data);
916
+ try {
917
+ return JSON.parse(data);
918
+ } catch (error) {
919
+ localStorage.removeItem(key);
920
+ }
419
921
  }
420
922
  }
421
923
  return null;
@@ -423,8 +925,8 @@ const sco = {
423
925
  const nick = localData ? localData.nick || localData.display_name : null;
424
926
 
425
927
  const prefix = this.wasPageHidden
426
- ? GLOBAL_CONFIG.aside.witty_comment.back + nick
427
- : GLOBAL_CONFIG.aside.witty_comment.prefix + nick;
928
+ ? Solitude.config.aside.witty_comment.back + nick
929
+ : Solitude.config.aside.witty_comment.prefix + nick;
428
930
 
429
931
  const greetings = [
430
932
  { start: 0, end: 5, text: nick ? prefix : lang.goodnight },
@@ -456,7 +958,7 @@ const sco = {
456
958
  categoriesBarActive() {
457
959
  const categoryBar = document.querySelector("#category-bar");
458
960
  const currentPath = decodeURIComponent(window.location.pathname);
459
- const isHomePage = currentPath === GLOBAL_CONFIG.root;
961
+ const isHomePage = currentPath === Solitude.config.root;
460
962
  if (categoryBar) {
461
963
  const categoryItems = categoryBar.querySelectorAll(".category-bar-item");
462
964
  categoryItems.forEach((item) => item.classList.remove("select"));
@@ -482,14 +984,19 @@ const sco = {
482
984
  behavior: "smooth",
483
985
  });
484
986
  };
485
- scrollBar.addEventListener("scroll", () => {
486
- clearTimeout(this.timeoutId);
487
- this.timeoutId = setTimeout(() => {
488
- nextElement.style.transform = isScrollBarAtEnd()
489
- ? "rotate(180deg)"
490
- : "";
491
- }, 150);
492
- });
987
+ if (scrollBar.dataset.solitudeScrollBound !== "true") {
988
+ scrollBar.dataset.solitudeScrollBound = "true";
989
+ lifecycle.listen(scrollBar, "scroll", () => {
990
+ clearTimeout(this.timeoutId);
991
+ this.timeoutId = setTimeout(() => {
992
+ if (nextElement) {
993
+ nextElement.style.transform = isScrollBarAtEnd()
994
+ ? "rotate(180deg)"
995
+ : "";
996
+ }
997
+ }, 150);
998
+ }, { passive: true });
999
+ }
493
1000
  scroll();
494
1001
  }
495
1002
  },
@@ -505,18 +1012,20 @@ const sco = {
505
1012
  if (!pageText) return;
506
1013
  const pageButton = document.getElementById("toPageButton");
507
1014
  const pageNumbers = document.querySelectorAll(".page-number");
508
- const lastPageNumber = +pageNumbers[pageNumbers.length - 1].textContent;
509
- if (!pageText || lastPageNumber === 1) {
510
- toGroup.style.display = "none";
1015
+ const lastPageNumber = +(pageNumbers[pageNumbers.length - 1]?.textContent || 1);
1016
+ if (lastPageNumber === 1) {
1017
+ if (toGroup) toGroup.style.display = "none";
511
1018
  return;
512
1019
  }
513
- pageText.addEventListener("keydown", (event) => {
1020
+ lifecycle.listen(pageText, "keydown", (event) => {
514
1021
  if (event.key === "Enter") {
515
- sco.toPage();
516
- pjax.loadUrl(pageButton.href);
1022
+ Solitude.toPage();
1023
+ Solitude.navigate(pageButton.href);
517
1024
  }
518
1025
  });
519
- pageText.addEventListener("input", () => {
1026
+ lifecycle.listen(pageText, "input", () => {
1027
+ pageText.value = pageText.value.replace(/[^0-9]/g, "");
1028
+ if (pageText.value === "0") pageText.value = "";
520
1029
  pageButton.classList.toggle(
521
1030
  "haveValue",
522
1031
  pageText.value !== "" && pageText.value !== "0"
@@ -583,13 +1092,13 @@ const sco = {
583
1092
  owoBig.style.left = `${itemRect.left - owoBig.offsetWidth / 4}px`;
584
1093
  owoBig.style.top = `${itemRect.top}px`;
585
1094
  };
586
- document.addEventListener("mouseover", showOwoBig);
587
- document.addEventListener("mouseout", hideOwoBig);
1095
+ lifecycle.listen(document, "mouseover", showOwoBig);
1096
+ lifecycle.listen(document, "mouseout", hideOwoBig);
588
1097
  },
589
1098
  changeTimeFormat(selector) {
590
1099
  selector.forEach((item) => {
591
1100
  const timeVal = item.getAttribute("datetime");
592
- item.textContent = utils.diffDate(timeVal, true);
1101
+ item.textContent = Solitude.diffDate(timeVal, true);
593
1102
  item.style.display = "inline";
594
1103
  });
595
1104
  },
@@ -605,7 +1114,7 @@ const sco = {
605
1114
  loadTwoComment();
606
1115
  }
607
1116
  };
608
- utils.addEventListenerPjax(switchBtn, "click", handleSwitchBtn);
1117
+ Solitude.addEventListenerPjax(switchBtn, "click", handleSwitchBtn);
609
1118
  },
610
1119
  homeTypeit() {
611
1120
  if (typeof home_subtitle === "undefined") return;
@@ -619,11 +1128,15 @@ const sco = {
619
1128
  ty.type(item).pause(500).delete(item);
620
1129
  });
621
1130
  ty.go();
1131
+ lifecycle.add(() => ty.destroy?.());
622
1132
  },
623
1133
  };
624
1134
 
1135
+ Object.assign(Solitude, actions);
1136
+ Solitude.toggleTheme = () => Solitude.switchDarkMode();
1137
+
625
1138
  const addHighlight = () => {
626
- const highlight = GLOBAL_CONFIG.highlight;
1139
+ const highlight = Solitude.config.highlight;
627
1140
  if (!highlight) return;
628
1141
  const { copy, expand, limit, syntax } = highlight;
629
1142
  const $isPrismjs = syntax === "prismjs";
@@ -644,7 +1157,7 @@ const addHighlight = () => {
644
1157
  ? `<i class="solitude fas fa-angles-down"></i>`
645
1158
  : "<i></i>";
646
1159
 
647
- const alertInfo = (ele, text) => utils.snackbarShow(text, false, 2000);
1160
+ const alertInfo = (ele, text) => Solitude.snackbarShow(text, false, 2000);
648
1161
 
649
1162
  const copyFn = (e) => {
650
1163
  const $buttonParent = e.parentNode;
@@ -658,7 +1171,7 @@ const addHighlight = () => {
658
1171
  selection.removeAllRanges();
659
1172
  selection.addRange(range);
660
1173
  document.execCommand("copy");
661
- alertInfo(e.lastChild, GLOBAL_CONFIG.lang.copy.success);
1174
+ alertInfo(e.lastChild, Solitude.config.lang.copy.success);
662
1175
  selection.removeAllRanges();
663
1176
  $buttonParent.classList.remove("copy-true");
664
1177
  };
@@ -686,14 +1199,14 @@ const addHighlight = () => {
686
1199
  const hlTools = document.createElement("div");
687
1200
  hlTools.className = `highlight-tools ${expandClass}`;
688
1201
  hlTools.innerHTML = expandEle + lang + caption + copyEle;
689
- utils.addEventListenerPjax(hlTools, "click", ToolsFn);
1202
+ Solitude.addEventListenerPjax(hlTools, "click", ToolsFn);
690
1203
  fragment.appendChild(hlTools);
691
1204
  }
692
1205
  if (limit && item.offsetHeight > limit + 30) {
693
1206
  const ele = document.createElement("div");
694
1207
  ele.className = "code-expand-btn";
695
1208
  ele.innerHTML = limitEle;
696
- utils.addEventListenerPjax(ele, "click", shrinkEle);
1209
+ Solitude.addEventListenerPjax(ele, "click", shrinkEle);
697
1210
  fragment.appendChild(ele);
698
1211
  }
699
1212
  if (service === "hl") {
@@ -706,17 +1219,17 @@ const addHighlight = () => {
706
1219
  if ($isPrismjs) {
707
1220
  $syntaxHighlight.forEach((item) => {
708
1221
  const langName = item.getAttribute("data-language") || "Code";
709
- const highlightLangEle = `<div class="code-lang">${utils.escapeHtml(
1222
+ const highlightLangEle = `<div class="code-lang">${Solitude.escapeHtml(
710
1223
  langName
711
1224
  )}</div>`;
712
- utils.wrap(item, "figure", { class: "highlight" });
1225
+ Solitude.wrap(item, "figure", { class: "highlight" });
713
1226
  createEle(highlightLangEle, item);
714
1227
  });
715
1228
  } else {
716
1229
  $syntaxHighlight.forEach((item) => {
717
1230
  let langName = item.getAttribute("class").split(" ")[1];
718
1231
  if (langName === "plain" || langName === undefined) langName = "Code";
719
- const highlightLangEle = `<div class="code-lang">${utils.escapeHtml(
1232
+ const highlightLangEle = `<div class="code-lang">${Solitude.escapeHtml(
720
1233
  langName
721
1234
  )}</div>`;
722
1235
  createEle(highlightLangEle, item, "hl");
@@ -735,8 +1248,8 @@ class toc {
735
1248
  el.forEach((e) => {
736
1249
  e.addEventListener("click", (event) => {
737
1250
  event.preventDefault();
738
- utils.scrollToDest(
739
- utils.getEleTop(
1251
+ Solitude.scrollToDest(
1252
+ Solitude.getEleTop(
740
1253
  document.getElementById(
741
1254
  decodeURI(
742
1255
  (event.target.className === "toc-text"
@@ -774,7 +1287,7 @@ class toc {
774
1287
  if (top === 0) return false;
775
1288
  let currentIndex = "";
776
1289
  list.forEach((ele, index) => {
777
- if (top > utils.getEleTop(ele) - 80) {
1290
+ if (top > Solitude.getEleTop(ele) - 80) {
778
1291
  currentIndex = index;
779
1292
  }
780
1293
  });
@@ -794,11 +1307,11 @@ class toc {
794
1307
  }
795
1308
  };
796
1309
 
797
- window.tocScrollFn = utils.throttle(() => {
1310
+ const tocScrollFn = Solitude.throttle(() => {
798
1311
  const currentTop = window.scrollY || document.documentElement.scrollTop;
799
1312
  findHeadPosition(currentTop);
800
1313
  }, 100);
801
- window.addEventListener("scroll", tocScrollFn);
1314
+ lifecycle.listen(window, "scroll", tocScrollFn, { passive: true });
802
1315
  }
803
1316
  }
804
1317
 
@@ -815,7 +1328,7 @@ class tabs {
815
1328
  const $tabItem = this.parentNode;
816
1329
  if (!$tabItem.classList.contains("active")) {
817
1330
  const $tabContent = $tabItem.parentNode.nextElementSibling;
818
- const $siblings = utils.siblings($tabItem, ".active")[0];
1331
+ const $siblings = Solitude.siblings($tabItem, ".active")[0];
819
1332
  $siblings && $siblings.classList.remove("active");
820
1333
  $tabItem.classList.add("active");
821
1334
  const tabId = this.getAttribute("data-href").replace("#", "");
@@ -828,22 +1341,25 @@ class tabs {
828
1341
  }
829
1342
 
830
1343
  static lureAddListener() {
831
- if (!GLOBAL_CONFIG.lure) return;
1344
+ if (!Solitude.config.lure) return;
832
1345
  const title = document.title;
833
- document.addEventListener("visibilitychange", () => {
834
- const { lure } = GLOBAL_CONFIG;
1346
+ let restoreTimer;
1347
+ lifecycle.listen(document, "visibilitychange", () => {
1348
+ const { lure } = Solitude.config;
835
1349
  document.title =
836
1350
  document.visibilityState === "hidden" ? lure.jump : lure.back;
837
1351
  if (document.visibilityState === "visible") {
838
- setTimeout(() => {
1352
+ clearTimeout(restoreTimer);
1353
+ restoreTimer = setTimeout(() => {
839
1354
  document.title = title;
840
1355
  }, 2000);
841
1356
  }
842
1357
  });
1358
+ lifecycle.add(() => clearTimeout(restoreTimer));
843
1359
  }
844
1360
 
845
1361
  static expireAddListener() {
846
- const { expire } = GLOBAL_CONFIG;
1362
+ const { expire } = Solitude.config;
847
1363
  if (!expire) return;
848
1364
  const list = document.querySelectorAll(".post-meta-date time");
849
1365
  const post_date = list.length
@@ -873,7 +1389,7 @@ class tabs {
873
1389
  }
874
1390
 
875
1391
  const scrollFnToDo = () => {
876
- const { toc } = PAGE_CONFIG;
1392
+ const { toc } = Solitude.page;
877
1393
 
878
1394
  if (toc) {
879
1395
  const $cardTocLayout = document.getElementById("card-toc");
@@ -883,8 +1399,8 @@ const scrollFnToDo = () => {
883
1399
  if (!target) return;
884
1400
 
885
1401
  e.preventDefault();
886
- utils.scrollToDest(
887
- utils.getEleTop(
1402
+ Solitude.scrollToDest(
1403
+ Solitude.getEleTop(
888
1404
  document.getElementById(
889
1405
  decodeURI(target.getAttribute("href")).replace("#", "")
890
1406
  )
@@ -895,7 +1411,7 @@ const scrollFnToDo = () => {
895
1411
  $cardTocLayout.classList.remove("open");
896
1412
  }
897
1413
  };
898
- utils.addEventListenerPjax($cardToc, "click", tocItemClickFn);
1414
+ Solitude.addEventListenerPjax($cardToc, "click", tocItemClickFn);
899
1415
  }
900
1416
  };
901
1417
 
@@ -903,44 +1419,128 @@ const forPostFn = () => {
903
1419
  scrollFnToDo();
904
1420
  };
905
1421
 
906
- window.refreshFn = () => {
907
- const { is_home, is_page, page, is_post, ai_text } = PAGE_CONFIG;
1422
+ const initPostCoverTilt = () => {
1423
+ const cover = document.querySelector(".post-cover-aside");
1424
+ const canTilt = window.matchMedia(
1425
+ "(hover: hover) and (pointer: fine)"
1426
+ ).matches;
1427
+ const reduceMotion = window.matchMedia(
1428
+ "(prefers-reduced-motion: reduce)"
1429
+ ).matches;
1430
+ if (!cover || !canTilt || reduceMotion) return;
1431
+
1432
+ const updateTilt = (event) => {
1433
+ const rect = cover.getBoundingClientRect();
1434
+ const x = (event.clientX - rect.left) / rect.width;
1435
+ const y = (event.clientY - rect.top) / rect.height;
1436
+ const rotateX = (0.5 - y) * 10;
1437
+ const rotateY = (x - 0.5) * 10;
1438
+
1439
+ cover.style.setProperty("--post-cover-glow-x", `${(x * 100).toFixed(2)}%`);
1440
+ cover.style.setProperty("--post-cover-glow-y", `${(y * 100).toFixed(2)}%`);
1441
+ cover.style.setProperty("--post-cover-rotate-x", `${rotateX.toFixed(2)}deg`);
1442
+ cover.style.setProperty("--post-cover-rotate-y", `${rotateY.toFixed(2)}deg`);
1443
+ cover.style.setProperty("--post-cover-img-x", `${(-rotateY).toFixed(2)}px`);
1444
+ cover.style.setProperty("--post-cover-img-y", `${rotateX.toFixed(2)}px`);
1445
+ };
1446
+
1447
+ const resetTilt = () => {
1448
+ cover.style.setProperty("--post-cover-rotate-x", "0deg");
1449
+ cover.style.setProperty("--post-cover-rotate-y", "0deg");
1450
+ cover.style.setProperty("--post-cover-img-x", "0px");
1451
+ cover.style.setProperty("--post-cover-img-y", "0px");
1452
+ };
1453
+
1454
+ lifecycle.listen(cover, "pointerenter", updateTilt);
1455
+ lifecycle.listen(cover, "pointermove", updateTilt);
1456
+ lifecycle.listen(cover, "pointerleave", resetTilt);
1457
+ lifecycle.listen(cover, "pointercancel", resetTilt);
1458
+ };
1459
+
1460
+ const initAboutCardGlow = () => {
1461
+ const aboutPage = document.getElementById("about-page");
1462
+ const canHover = window.matchMedia(
1463
+ "(hover: hover) and (pointer: fine)"
1464
+ ).matches;
1465
+ if (!aboutPage || !canHover) return;
1466
+
1467
+ aboutPage.querySelectorAll(".author-content-item").forEach((card) => {
1468
+ if (card.dataset.aboutGlowBound === "true") return;
1469
+
1470
+ const glow = document.createElement("div");
1471
+ glow.className = "about-pointer-glow";
1472
+ glow.setAttribute("aria-hidden", "true");
1473
+ card.prepend(glow);
1474
+ card.classList.add("about-glow-host");
1475
+ card.dataset.aboutGlowBound = "true";
1476
+
1477
+ const updateGlowPosition = (event) => {
1478
+ const rect = card.getBoundingClientRect();
1479
+ const x = ((event.clientX - rect.left) / rect.width) * 100;
1480
+ const y = ((event.clientY - rect.top) / rect.height) * 100;
1481
+ card.style.setProperty("--about-glow-x", `${x}%`);
1482
+ card.style.setProperty("--about-glow-y", `${y}%`);
1483
+ };
1484
+
1485
+ card.addEventListener("pointerenter", (event) => {
1486
+ updateGlowPosition(event);
1487
+ card.classList.add("is-about-glow-active");
1488
+ });
1489
+ card.addEventListener("pointermove", updateGlowPosition);
1490
+ card.addEventListener("pointerleave", () => {
1491
+ card.classList.remove("is-about-glow-active");
1492
+ });
1493
+ card.addEventListener("pointercancel", () => {
1494
+ card.classList.remove("is-about-glow-active");
1495
+ });
1496
+ });
1497
+ };
1498
+
1499
+ Solitude.refresh = async () => {
1500
+ lifecycle.disposePage();
1501
+ await loadFeatureModules();
1502
+ const { is_home, is_page, page, is_post, ai_text } = Solitude.page;
908
1503
  const { runtime, lazyload, lightbox, randomlink, covercolor, lure, expire } =
909
- GLOBAL_CONFIG;
1504
+ Solitude.config;
910
1505
  const timeSelector = ".datetime, .webinfo-item time, .post-meta-date time";
911
1506
  document.body.setAttribute("data-type", page);
912
- sco.changeTimeFormat(document.querySelectorAll(timeSelector));
913
- runtime && sco.addRuntime();
1507
+ Solitude.changeTimeFormat(document.querySelectorAll(timeSelector));
1508
+ runtime && Solitude.addRuntime();
914
1509
  [
915
1510
  scrollFn,
916
1511
  sidebarFn,
917
- sco.addPhotoFigcaption,
918
- sco.setTimeState,
919
- sco.tagPageActive,
920
- sco.categoriesBarActive,
921
- sco.listenToPageInputPress,
922
- sco.musicBind,
923
- sco.addNavBackgroundInit,
924
- sco.refreshWaterFall,
1512
+ initTooltip,
1513
+ () => Solitude.addPhotoFigcaption(),
1514
+ () => Solitude.setTimeState(),
1515
+ () => Solitude.tagPageActive(),
1516
+ () => Solitude.categoriesBarActive(),
1517
+ () => Solitude.listenToPageInputPress(),
1518
+ () => Solitude.musicBind(),
1519
+ () => Solitude.addNavBackgroundInit(),
1520
+ () => Solitude.refreshWaterFall(),
925
1521
  ].forEach((fn) => fn());
926
- lazyload.enable && utils.lazyloadImg();
1522
+ lazyload.enable && Solitude.lazyloadImg();
927
1523
  lightbox &&
928
- utils.lightbox(
1524
+ Solitude.lightbox(
929
1525
  document.querySelectorAll(
930
1526
  ".article-container img:not(.flink-avatar,.gallery-group img, .no-lightbox)"
931
1527
  )
932
1528
  );
933
- randomlink && randomLinksList();
1529
+ randomlink && Solitude.randomLinksList?.();
1530
+ Solitude.config.friend_links.async && Solitude.friendLinks?.init();
934
1531
  if (is_post) {
935
- if (ai_text) {
1532
+ initPostCoverTilt();
1533
+ if (ai_text && ai) {
936
1534
  ai.init();
1535
+ lifecycle.add(() => ai.cancel());
937
1536
  }
938
1537
  }
939
- sco.switchComments();
1538
+ Solitude.switchComments();
940
1539
  initObserver();
941
1540
  if (is_home) {
942
1541
  showTodayCard();
943
- sco.homeTypeit();
1542
+ initHomeCenter();
1543
+ Solitude.homeTypeit();
944
1544
  }
945
1545
  typeof updatePostsBasedOnComments === "function" &&
946
1546
  updatePostsBasedOnComments();
@@ -952,25 +1552,41 @@ window.refreshFn = () => {
952
1552
  tabs.expireAddListener();
953
1553
  }
954
1554
  if (covercolor.enable) coverColor();
955
- if (PAGE_CONFIG.toc) toc.init();
1555
+ if (Solitude.page.toc) toc.init();
956
1556
  if (lure) tabs.lureAddListener();
957
- page === "music" && initializeMusicPlayer();
1557
+ if (page === "music") {
1558
+ initializeMusicPlayer();
1559
+ lifecycle.add(() => Solitude.musicPlayer?.destroy?.());
1560
+ }
1561
+ if (page === "archive") {
1562
+ const { archivePageController, initArchivePage } = await import("./archive-page.js");
1563
+ initArchivePage();
1564
+ lifecycle.add(() => archivePageController.destroy());
1565
+ }
1566
+ initAboutCardGlow();
958
1567
  forPostFn();
959
1568
  };
960
1569
 
961
- document.addEventListener("DOMContentLoaded", () => {
962
- [
963
- addCopyright,
964
- window.refreshFn,
965
- asideStatus,
966
- () => (window.onscroll = percent),
967
- sco.initConsoleState,
968
- ].forEach((fn) => fn());
969
- });
1570
+ const initializeApp = async () => {
1571
+ initActionDelegation(Solitude);
1572
+ initPreloader(Solitude);
1573
+ addCopyright();
1574
+ await Solitude.refresh();
1575
+ asideStatus();
1576
+ window.onscroll = percent;
1577
+ Solitude.initConsoleState();
1578
+ lifecycle.emit("ready", { config: Solitude.config, page: Solitude.page });
1579
+ };
1580
+
1581
+ if (document.readyState === "loading") {
1582
+ document.addEventListener("DOMContentLoaded", initializeApp, { once: true });
1583
+ } else {
1584
+ initializeApp();
1585
+ }
970
1586
 
971
1587
  document.addEventListener("visibilitychange", () => {
972
1588
  if (document.hidden) {
973
- sco.wasPageHidden = true;
1589
+ Solitude.wasPageHidden = true;
974
1590
  }
975
1591
  });
976
1592
 
@@ -980,13 +1596,13 @@ window.onkeydown = (e) => {
980
1596
  code === "F12" ||
981
1597
  (ctrlKey && shiftKey && (code === "KeyI" || code === "KeyC"))
982
1598
  ) {
983
- utils.snackbarShow(GLOBAL_CONFIG.lang.f12, false, 3000);
1599
+ Solitude.snackbarShow(Solitude.config.lang.f12, false, 3000);
984
1600
  }
985
1601
  if (code === "Escape") {
986
- sco.hideConsole();
1602
+ Solitude.hideConsole();
987
1603
  }
988
1604
  };
989
1605
 
990
1606
  document.addEventListener("copy", () => {
991
- utils.snackbarShow(GLOBAL_CONFIG.lang.copy.success, false, 3000);
1607
+ Solitude.snackbarShow(Solitude.config.lang.copy.success, false, 3000);
992
1608
  });