@windcoder/astro-theme-orchid 0.3.0 → 0.4.1
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.
- package/README.md +2 -0
- package/package.json +1 -1
- package/scripts/generate-lqips.mjs +239 -20
- package/src/components/AlbumCard.astro +4 -1
- package/src/components/BaseLayout.astro +209 -64
- package/src/components/DisplaySettings.astro +383 -0
- package/src/components/Header.astro +56 -2
- package/src/components/MusicPlayer.astro +45 -5
- package/src/components/MusicWidget.astro +265 -0
- package/src/components/PhotoCard.astro +13 -1
- package/src/components/PostCard.astro +9 -2
- package/src/components/SakuraEffect.astro +126 -28
- package/src/components/Sidebar.astro +28 -7
- package/src/i18n/en.ts +36 -0
- package/src/i18n/ja.ts +213 -0
- package/src/i18n/ko.ts +212 -0
- package/src/i18n/resolve.ts +10 -1
- package/src/i18n/ru.ts +213 -0
- package/src/i18n/zh-CN.ts +37 -0
- package/src/i18n/zh-TW.ts +208 -0
- package/src/index.ts +24 -0
- package/src/plugins/rehype.ts +66 -2
- package/src/routes/friends.astro +10 -2
- package/src/routes/page/[page].astro +3 -0
- package/src/routes/posts/[...slug].astro +7 -2
- package/src/routes/search.astro +24 -0
- package/src/routes/sponsor.astro +17 -4
- package/src/schema.ts +364 -71
- package/src/scripts/app.ts +817 -37
- package/src/styles/global.css +865 -36
- package/src/utils/gallery.ts +3 -1
- package/src/utils/image.ts +90 -0
- package/src/utils/lqip.ts +21 -12
- package/src/utils/thumb.ts +31 -0
- package/src/vite.ts +95 -8
package/src/utils/gallery.ts
CHANGED
|
@@ -7,6 +7,8 @@ import path from "node:path";
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const IMAGE_RE = /\.(jpe?g|png|webp|avif|gif)$/i;
|
|
10
|
+
/* §4.106:.thumb.webp 是 generate-lqips.mjs 的缩略图产物,不是照片本体 */
|
|
11
|
+
const THUMB_RE = /\.thumb\.webp$/i;
|
|
10
12
|
const COVER_RE = /^cover\./i;
|
|
11
13
|
|
|
12
14
|
export interface GalleryAlbumMeta {
|
|
@@ -31,7 +33,7 @@ export function scanAlbumPhotos(albumId: string): string[] {
|
|
|
31
33
|
|
|
32
34
|
const files = fs
|
|
33
35
|
.readdirSync(dir)
|
|
34
|
-
.filter((f) => IMAGE_RE.test(f))
|
|
36
|
+
.filter((f) => IMAGE_RE.test(f) && !THUMB_RE.test(f))
|
|
35
37
|
.sort();
|
|
36
38
|
const coverIdx = files.findIndex((f) => COVER_RE.test(f));
|
|
37
39
|
if (coverIdx > 0) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 站点图片路径三格式解析(§4.97,对齐参考站 ImageWrapper 能力):
|
|
3
|
+
* 1. `/` 开头 → 站点 public 静态资源,原样输出(不经构建管线优化);
|
|
4
|
+
* 2. `http(s)://` / `//` / `data:` → 外链或内联,原样输出;
|
|
5
|
+
* 3. 其余相对路径(如 "assets/images/avatar.png")→ 相对站点项目 src/ 目录
|
|
6
|
+
* 构建期解析,且经 sharp image service 重编码(默认 webp + 压缩,
|
|
7
|
+
* §4.106 升级),返回优化产物 URL(唯一被优化的格式)。
|
|
8
|
+
*
|
|
9
|
+
* 解析机制:import.meta.glob 匹配站点根下 src 全目录的媒体扩展名,`/` 前缀
|
|
10
|
+
* 相对 Vite 项目根,主题包组件运行在站点构建管线中,因此命中站点项目的 src/
|
|
11
|
+
* (§4.108:不写死 assets/ 前缀,src 下任意目录的图片/视频均可解析;
|
|
12
|
+
* ⚠️ 本段注释不可出现 glob 的双星斜杠字面量——含块注释终止符子串会提前闭合);
|
|
13
|
+
* 非 eager 按需 await,仅实际用到的媒体进入构建产物。
|
|
14
|
+
* svg(矢量)与 gif(动图)不做 sharp 编码——转码会破坏矢量/动画,原样输出。
|
|
15
|
+
* ico 不参与 glob(favicon 走 public / 远程,无优化需求)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { getImage, type ImageMetadata } from "astro:assets";
|
|
19
|
+
|
|
20
|
+
/** glob 命中的资源元数据:图片为 ImageMetadata,视频等为产物 URL 字符串 */
|
|
21
|
+
type AssetMeta = ImageMetadata | string;
|
|
22
|
+
|
|
23
|
+
/** 三格式判定:true = 相对 src/ 的本地媒体资源(需构建期解析) */
|
|
24
|
+
export function isLocalImagePath(src: string): boolean {
|
|
25
|
+
if (!src) return false;
|
|
26
|
+
return !(
|
|
27
|
+
src.startsWith("/") ||
|
|
28
|
+
src.startsWith("http://") ||
|
|
29
|
+
src.startsWith("https://") ||
|
|
30
|
+
src.startsWith("//") ||
|
|
31
|
+
src.startsWith("data:")
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 相对路径 → glob key:不写死 assets/ 前缀(§4.108),相对路径不加 `/` 即
|
|
37
|
+
* 相对站点 src/ 目录——"assets/images/avatar.png" → /src/assets/images/avatar.png,
|
|
38
|
+
* "styles/x.svg" → /src/styles/x.svg,src 下任意目录均可解析。
|
|
39
|
+
*/
|
|
40
|
+
export function toAssetKey(src: string): string {
|
|
41
|
+
return `/src/${src.replace(/^\.?\//, "")}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type AssetModules = Record<string, () => Promise<AssetMeta>>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 解析单张站点静态媒体(图片/视频)为可直接引用的 URL。
|
|
48
|
+
* public / 外链原样返回;相对路径查 glob 表,命中则 await 产物元数据;
|
|
49
|
+
* 未命中(文件不存在)时 console.warn 并原样返回,页面不断构建。
|
|
50
|
+
*
|
|
51
|
+
* §4.106:位图(png/jpg/webp/avif)额外经 getImage() 走 sharp image service
|
|
52
|
+
* 构建期重编码(默认转 webp + 质量压缩,Astro 同图同参有编码缓存);
|
|
53
|
+
* svg/gif 保持原始产物 URL 不转码。失败时降级原样返回,页面不断构建。
|
|
54
|
+
* §4.108:视频(mp4/webm/mov)glob 导出为产物 URL 字符串,直接返回。
|
|
55
|
+
*
|
|
56
|
+
* 注意:import.meta.glob 第一参数必须是字符串字面量(vite 编译期静态分析,
|
|
57
|
+
* 变量/常量在 dev 下直接报 Invalid glob import syntax),模式不能提取为常量。
|
|
58
|
+
*/
|
|
59
|
+
export async function resolveSiteImage(src: string): Promise<string> {
|
|
60
|
+
if (!isLocalImagePath(src)) return src;
|
|
61
|
+
const modules = import.meta.glob(
|
|
62
|
+
"/src/**/*.{png,jpg,jpeg,webp,avif,gif,svg,mp4,webm,mov}",
|
|
63
|
+
{ import: "default" },
|
|
64
|
+
) as unknown as AssetModules;
|
|
65
|
+
const loader = modules[toAssetKey(src)];
|
|
66
|
+
if (!loader) {
|
|
67
|
+
console.warn(
|
|
68
|
+
`[orchid-theme] 媒体文件未找到: ${toAssetKey(src)}(相对路径不带 / 时相对站点 src/ 目录解析)`,
|
|
69
|
+
);
|
|
70
|
+
return src;
|
|
71
|
+
}
|
|
72
|
+
const meta = await loader();
|
|
73
|
+
if (typeof meta === "string") return meta;
|
|
74
|
+
if (meta.format === "svg" || meta.format === "gif") return meta.src;
|
|
75
|
+
try {
|
|
76
|
+
const encoded = await getImage({ src: meta });
|
|
77
|
+
return encoded.src;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.warn(
|
|
80
|
+
`[orchid-theme] 图片优化失败,回退原图: ${toAssetKey(src)}`,
|
|
81
|
+
err,
|
|
82
|
+
);
|
|
83
|
+
return meta.src;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 数组批量解析(banner/壁纸多图、歌单封面等),顺序保持 */
|
|
88
|
+
export async function resolveSiteImages(srcs: string[]): Promise<string[]> {
|
|
89
|
+
return Promise.all(srcs.map((src) => resolveSiteImage(src)));
|
|
90
|
+
}
|
package/src/utils/lqip.ts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LQIP 占位渐变(M3b-b9,P1-14
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* LQIP 占位渐变(M3b-b9,P1-14;§4.107 扩展外链):
|
|
3
|
+
* 构建期从 virtual:theme/lqips 查表返回内联渐变样式。两类键——
|
|
4
|
+
* `public:<posix 路径>`(本地 public 图)与 `url:<完整外链 URL>`(§4.107 构建期
|
|
5
|
+
* 抓取采样的外链图)。查不到(未收录外链 / data URL / 未生成 lqips.json)时
|
|
6
|
+
* 兜底为主题色柔和渐变(color-mix 跟随明暗主题),不再是灰色纯色。
|
|
5
7
|
*/
|
|
6
8
|
import lqips from "virtual:theme/lqips";
|
|
7
9
|
|
|
8
|
-
/**
|
|
10
|
+
/** 18 字符 compact hex → 三段 135° 渐变背景样式 */
|
|
11
|
+
function hexToStyle(hex: string): string {
|
|
12
|
+
return `background:linear-gradient(135deg, #${hex.slice(0, 6)} 0%, #${hex.slice(6, 12)} 50%, #${hex.slice(12, 18)} 100%)`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 图片 src(本地 / 开头 public 路径,或完整外链 URL)→ LQIP 渐变背景。
|
|
17
|
+
* 键规则与 scripts/generate-lqips.mjs 一致:外链 URL 须与渲染期逐字符一致
|
|
18
|
+
* (randomCoverUrl / urls.txt / frontmatter image 或首图原样)。
|
|
19
|
+
*/
|
|
9
20
|
export function getLqipStyle(src: string): string {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
return "background:var(--muted)";
|
|
21
|
+
const key = src.startsWith("/")
|
|
22
|
+
? `public:${src.replace(/^\//, "")}`
|
|
23
|
+
: `url:${src}`;
|
|
24
|
+
const hex = lqips[key];
|
|
25
|
+
if (hex && /^[0-9a-f]{18}$/i.test(hex)) return hexToStyle(hex);
|
|
26
|
+
return `background:linear-gradient(135deg, color-mix(in srgb, var(--accent) 20%, var(--surface)) 0%, color-mix(in srgb, var(--accent) 9%, var(--surface)) 100%)`;
|
|
18
27
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 缩略图查表(§4.106):构建期从 virtual:theme/thumbs 查表,把列表封面
|
|
3
|
+
* public 原图路径换成 generate-lqips.mjs 预生成的 480 宽 webp 缩略图 URL,
|
|
4
|
+
* 并携带原图宽高(灯箱打开原图时用,详见 app.ts itemData filter)。
|
|
5
|
+
* 查不到(外链 / data URL / 未生成 thumbs.json)时全部回退原图,自然降级。
|
|
6
|
+
*/
|
|
7
|
+
import thumbs from "virtual:theme/thumbs";
|
|
8
|
+
|
|
9
|
+
export interface ThumbInfo {
|
|
10
|
+
/** 原图 URL(即传入 src,灯箱大图通道) */
|
|
11
|
+
full: string;
|
|
12
|
+
/** 列表展示 URL:命中缩略图时为 `<原图去 .thumb 前缀目录>/<名>.thumb.webp`,否则同 full */
|
|
13
|
+
src: string;
|
|
14
|
+
/** 原图宽高(未命中/无数据时缺省) */
|
|
15
|
+
width?: number;
|
|
16
|
+
height?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** public 路径 → 缩略图信息;非 public 路径或查表未命中时 src === full */
|
|
20
|
+
export function getThumbInfo(src: string): ThumbInfo {
|
|
21
|
+
const hit = thumbs[`public:${src.replace(/^\//, "")}`];
|
|
22
|
+
if (hit?.thumb && hit.width > 0 && hit.height > 0) {
|
|
23
|
+
return {
|
|
24
|
+
full: src,
|
|
25
|
+
src: `/${hit.thumb}`,
|
|
26
|
+
width: hit.width,
|
|
27
|
+
height: hit.height,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return { full: src, src };
|
|
31
|
+
}
|
package/src/vite.ts
CHANGED
|
@@ -68,17 +68,21 @@ export function themeConfigPlugin(config: ResolvedThemeConfig) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* LQIP
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
71
|
+
* LQIP + 缩略图虚拟模块(M3b-b9,P1-14;§4.106 扩展双产物):读取站点根
|
|
72
|
+
* lqips.json(键 `public:<路径>` → 18 字符 compact hex)与 thumbs.json
|
|
73
|
+
* (键 `public:<路径>` → { thumb, width, height }),分别以
|
|
74
|
+
* `virtual:theme/lqips`(utils/lqip.ts 构建期内联占位渐变)与
|
|
75
|
+
* `virtual:theme/thumbs`(utils/thumb.ts 列表封面换缩略图 URL)暴露。
|
|
76
|
+
* 文件缺失(未跑生成脚本)时导出空对象,消费端自然降级(纯色/原图),不阻塞构建。
|
|
75
77
|
* 注意:本插件必须注册在 overridesPlugin 之前——后者的 resolveId 会兜住
|
|
76
78
|
* 所有 virtual:theme/* 并返回一个不存在的文件路径。
|
|
77
79
|
*/
|
|
78
80
|
export function themeLqipsPlugin() {
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
+
type ThumbMeta = { thumb: string; width: number; height: number };
|
|
82
|
+
const lqipsId = "virtual:theme/lqips";
|
|
83
|
+
const thumbsId = "virtual:theme/thumbs";
|
|
81
84
|
let lqips: Record<string, string> = {};
|
|
85
|
+
let thumbs: Record<string, ThumbMeta> = {};
|
|
82
86
|
return {
|
|
83
87
|
name: "@windcoder/astro-theme-orchid:lqips",
|
|
84
88
|
enforce: "pre" as const,
|
|
@@ -94,12 +98,95 @@ export function themeLqipsPlugin() {
|
|
|
94
98
|
} catch {
|
|
95
99
|
lqips = {};
|
|
96
100
|
}
|
|
101
|
+
try {
|
|
102
|
+
thumbs = JSON.parse(
|
|
103
|
+
fs.readFileSync(path.join(rootDir, "thumbs.json"), "utf8"),
|
|
104
|
+
);
|
|
105
|
+
} catch {
|
|
106
|
+
thumbs = {};
|
|
107
|
+
}
|
|
97
108
|
},
|
|
98
109
|
resolveId(id: string) {
|
|
99
|
-
|
|
110
|
+
if (id === lqipsId) return "\0" + lqipsId;
|
|
111
|
+
if (id === thumbsId) return "\0" + thumbsId;
|
|
112
|
+
return null;
|
|
100
113
|
},
|
|
101
114
|
load(id: string) {
|
|
102
|
-
if (id ===
|
|
115
|
+
if (id === "\0" + lqipsId)
|
|
116
|
+
return `export default ${JSON.stringify(lqips)};`;
|
|
117
|
+
if (id === "\0" + thumbsId)
|
|
118
|
+
return `export default ${JSON.stringify(thumbs)};`;
|
|
119
|
+
return undefined;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Pagefind dev 中间件(§4.104):dev 下为搜索提供构建期索引。
|
|
126
|
+
* Vite 禁止以模块 import 引用 public 内的 JS("should not be imported
|
|
127
|
+
* from source code"),把索引同步进 public 行不通;故与社区标准方案
|
|
128
|
+
* astro-pagefind 同款——前置中间件把构建产物 dist/pagefind 直接静态服务。
|
|
129
|
+
* dist/pagefind 不存在(未跑过 build)时放行,app.ts 的 loadPagefind
|
|
130
|
+
* 捕获失败后降级为「索引不可用」提示。仅 dev 生效(configureServer 只在
|
|
131
|
+
* dev server 调用);build/preview 的索引由 pagefind --site dist 原地产出。
|
|
132
|
+
*/
|
|
133
|
+
const PAGEFIND_MIME: Record<string, string> = {
|
|
134
|
+
".js": "text/javascript",
|
|
135
|
+
".mjs": "text/javascript",
|
|
136
|
+
".css": "text/css",
|
|
137
|
+
".json": "application/json",
|
|
138
|
+
".wasm": "application/wasm",
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export function pagefindDevPlugin() {
|
|
142
|
+
let distPagefind = "";
|
|
143
|
+
return {
|
|
144
|
+
name: "@windcoder/astro-theme-orchid:pagefind-dev",
|
|
145
|
+
enforce: "pre" as const,
|
|
146
|
+
configResolved(resolvedConfig: {
|
|
147
|
+
root: URL | string;
|
|
148
|
+
build: { outDir: string };
|
|
149
|
+
}) {
|
|
150
|
+
const rootDir =
|
|
151
|
+
typeof resolvedConfig.root === "string"
|
|
152
|
+
? resolvedConfig.root
|
|
153
|
+
: fileURLToPath(resolvedConfig.root);
|
|
154
|
+
distPagefind = path.resolve(
|
|
155
|
+
rootDir,
|
|
156
|
+
resolvedConfig.build.outDir,
|
|
157
|
+
"pagefind",
|
|
158
|
+
);
|
|
159
|
+
},
|
|
160
|
+
configureServer(server: {
|
|
161
|
+
middlewares: {
|
|
162
|
+
use: (
|
|
163
|
+
fn: (
|
|
164
|
+
req: { url?: string },
|
|
165
|
+
res: {
|
|
166
|
+
setHeader: (name: string, value: string) => void;
|
|
167
|
+
end: (data: Buffer) => void;
|
|
168
|
+
},
|
|
169
|
+
next: () => void,
|
|
170
|
+
) => void,
|
|
171
|
+
) => void;
|
|
172
|
+
};
|
|
173
|
+
}) {
|
|
174
|
+
server.middlewares.use((req, res, next) => {
|
|
175
|
+
const url = (req.url || "").split("?")[0];
|
|
176
|
+
if (!url.startsWith("/pagefind/")) return next();
|
|
177
|
+
// 剥掉 "/pagefind/" 前缀后再拼接,避免 pagefind 二次重复;
|
|
178
|
+
// resolve + 前缀校验防目录穿越;读不到(未 build)时放行走 404
|
|
179
|
+
const file = path.resolve(distPagefind, url.slice("/pagefind/".length));
|
|
180
|
+
if (!file.startsWith(distPagefind + path.sep)) return next();
|
|
181
|
+
fs.readFile(file, (err, buf) => {
|
|
182
|
+
if (err) return next();
|
|
183
|
+
res.setHeader(
|
|
184
|
+
"Content-Type",
|
|
185
|
+
PAGEFIND_MIME[path.extname(file)] ?? "application/octet-stream",
|
|
186
|
+
);
|
|
187
|
+
res.end(buf);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
103
190
|
},
|
|
104
191
|
};
|
|
105
192
|
}
|