PageSourceSearch

https://github.nb-channel.top/js/theme.js?v=9

js nb-channel.top collected 2026-10-06 13:15:42 UTC 8,086 bytes, 186 lines download raw bytes

1/* ============================================================
2   NB频道 · 主题引擎
3   在 <head> 里同步加载(不加 defer),保证渲染前就定好属性,
4   不会出现「先看到默认配色、闪一下才变色」。
5
6   主题分两类:
7     · 常驻(always)—— 随时都能选
8     · 节日(window)—— 只在指定日期窗口里出现在选项里
9
10   节日里春节、中秋是农历,每年公历日期都不同,
11   所以用 FESTIVAL 表把最近几年的正日子写死,前后各放几天。
12   用法:<script src="../js/theme.js"></script>
13   ============================================================ */
14(function () {
15    'use strict';
16
17    var KEY = 'nb_theme';        // 存用户选的主题 id
18    var ATTR = 'theme';          // 加在 <html> 上的属性名
19
20    /* ---------- 节日正日子(公历)----------
21       农历节日每年换算出来的公历日期不一样,这里列最近几年。
22       表里没有的年份 → 该节日不出现(宁可不显示,也不要显示错)。 */
23    var FESTIVAL = {
24    };
25
26    /* 每个节日提前 / 延后几天出现 */
27    var SPAN = {};
28
29    /* ---------- 主题清单 ----------
30       window: 节日主题的正日子来源(FESTIVAL 的键名)
31       没有 window 的就是常驻主题 */
32    var THEMES = [
33        { id: 'default',   name: '默认',      icon: '\uD83C\uDFA8',
34          desc: 'NB频道原本的蓝白配色' },
35        { id: 'tech',      name: '科技',      icon: '\uD83D\uDE80',
36          desc: '深空蓝 + 霓虹光效,配理科站的气质' },
37        { id: 'cyber',     name: '赛博朋克',  icon: '\uD83C\uDF03',
38          desc: '霓虹紫粉 + 雨夜,故障艺术标题' },
39        { id: 'ink',       name: '墨韵',      icon: '\uD83D\uDD8C\uFE0F',
40          desc: '宣纸米白 + 焦墨,安静、省电' },
41        { id: 'pixel',     name: '像素',      icon: '\uD83D\uDC7E',
42          desc: '8-bit 有限色板,回到红白机年代' },
43        { id: 'eyecare',   name: '护眼',      icon: '\uD83C\uDF19',
44          desc: '低对比暖灰,去光效,夜里看不累' },
45        { id: 'national',  name: '国庆',      icon: '\uD83C\uDDE8\uD83C\uDDF3',
46          desc: '红金配色,限时 10 月 1 日 ~ 7 日',
47          fixed: [10, 1, 7] },
48    ];
49
50    var BY_ID = {};
51    THEMES.forEach(function (t) { BY_ID[t.id] = t; });
52
53    /* ---------- 日期判定 ---------- */
54    function sameDay(d, m, day) {
55        return (d.getMonth() + 1) === m && d.getDate() === day;
56    }
57    /* 以 (m, day) 为中心,前后各 span 天,看 now 在不在里头 */
58    function inSpan(now, m, day, span) {
59        var center = new Date(now.getFullYear(), m - 1, day);
60        center.setHours(0, 0, 0, 0);
61        var t = new Date(now.getFullYear(), now.getMonth(), now.getDate());
62        var diff = Math.round((t - center) / 86400000);
63        return Math.abs(diff) <= span;
64    }
65
66    /* 这个主题此刻可不可选 */
67    function available(id, now) {
68        var t = BY_ID[id];
69        if (!t) return false;
70        var d = now || new Date();
71
72        if (t.fixed) {                                   // 国庆:固定公历区间
73            var m = d.getMonth() + 1, day = d.getDate();
74            var a = t.fixed[0] * 100 + t.fixed[1];
75            var b = t.fixed[0] * 100 + t.fixed[2];
76            var cur = m * 100 + day;
77            return cur >= a && cur <= b;
78        }
79        if (t.festival) {                                // 春节 / 中秋:查表
80            var table = FESTIVAL[t.festival] || {};
81            var md = table[d.getFullYear()];
82            if (!md) return false;                       // 这一年没登记
83            return inSpan(d, md[0], md[1], SPAN[t.festival] || 3);
84        }
85        return true;                                     // 常驻
86    }
87
88    /* ---------- 读取用户选择 ---------- */
89    function readChoice() {
90        var v = null;
91        /* URL 参数优先:?theme=cyber / ?theme=ink / ?theme=default …
92           用途一是方便调试,二是可以让别人分享「某个主题下的这一页」。 */
93        try {
94            var m = /[?&]theme=([a-z]+)/i.exec(location.search);
95            /* 调试用:?theme=xxx&preview=1
96               可以无视节日的日期窗口直接预览,方便平时调样式。
97               普通访客不带这个参数,看到的仍然是正常的窗口限制。 */
98            var preview = /[?&]preview=1/i.test(location.search);
99            if (m) {
100                var q = m[1].toLowerCase();
101                if (BY_ID[q] && (preview || available(q))) return q;
102                if (q === 'default') return 'default';
103            }
104        } catch (e) {}
105        try { v = localStorage.getItem(KEY); } catch (e) { v = null; }
106        if (!v || v === 'default') return 'default';
107        if (!BY_ID[v]) return 'default';
108        /* 节日主题过期了 → 回到默认 */
109        if (!available(v)) return 'default';
110        return v;
111    }
112
113    /* ---------- 应用到 <html> ---------- */
114    function apply(choice) {
115        var el = document.documentElement;
116        /* 用户没选过 → 一律用「默认」。
117           节日主题只是「那几天在选项里出现」,不会自动套上。 */
118        var real = 'default';
119        if (choice && choice !== 'default' && BY_ID[choice] && available(choice)) {
120            real = choice;
121        }
122        /* 先记下当前是不是深色模式,再摘掉 dark 类。
123           摘掉是因为它的选择器权重(html.dark body)会压住主题底色;
124           但深色这个状态本身要留下来 —— 浅色主题(墨韵、护眼)靠它
125           切到自己的深色变体(themes.css 里的 html[theme=x][data-dark])。 */
126        var wasDark = el.classList.contains('dark') ||
127            (window.matchMedia &&
128             window.matchMedia('(prefers-color-scheme: dark)').matches);
129
130        if (real === 'default') {
131            el.removeAttribute(ATTR);
132        } else {
133            el.setAttribute(ATTR, real);
134            el.classList.remove('dark');
135        }
136        if (wasDark) el.setAttribute('data-dark', '1');
137        else el.removeAttribute('data-dark');
138        el.setAttribute('data-theme-resolved', real);
139    }
140
141    /* 系统深色偏好变化时也跟着更新 data-dark,
142       这样用户改系统设置不用刷新页面 */
143    try {
144        var mq = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
145        if (mq && mq.addEventListener) {
146            mq.addEventListener('change', function () { apply(readChoice()); });
147        }
148    } catch (e) {}
149
150    /* ---------- 对外接口 ---------- */
151    var API = {
152        KEY: KEY,
153        get: readChoice,
154        resolved: readChoice,
155        set: function (choice) {
156            var real = (choice && BY_ID[choice] && available(choice)) ? choice : 'default';
157            try { localStorage.setItem(KEY, real); } catch (e) {}
158            apply(real);
159            try {
160                window.dispatchEvent(new CustomEvent('nb-theme-change', {
161                    detail: { theme: real }
162                }));
163            } catch (e) {}
164        },
165        /* 兼容旧调用 */
166        nationalAvailable: function (now) { return available('national', now); },
167        available: available,
168        /* 可选主题列表(只列此刻能选的) */
169        list: function (now) {
170            return THEMES.filter(function (t) { return available(t.id, now); })
171                         .map(function (t) {
172                             return { id: t.id, name: t.name, icon: t.icon, desc: t.desc };
173                         });
174        },
175        /* 全部主题(含此刻不可选的),调试用 */
176        all: function () {
177            return THEMES.map(function (t) {
178                return { id: t.id, name: t.name, icon: t.icon, desc: t.desc,
179                         on: available(t.id) };
180            });
181        }
182    };
183
184    apply(readChoice());
185    window.NBTheme = API;
186})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.