fix: 按 Halo 应用市场审核意见整改外部资源与隐私(v1.2.5 修订)
- 默认零第三方自动请求:移除 21 个模板的 preconnect/dns-prefetch、PrismJS+autoloader、qrcode-generator 及内置一言/IP 定位 API - 一言与代码高亮配色改为自托管;欢迎弹窗自定义 IP 接口改为 opt-in;微信分享改为复制链接;LightGallery 按 GPLv3 使用 - README 新增「外部资源与隐私」「验证与回归测试」章节,修正无效链接并补充字体 OFL 来源;同步 docs 与修正虚构的目录树 - friends 等 12 处 #strings.substring 加空值保护;logo 由两份 1.5MB 压缩去重为单份 146KB - 新增 pnpm audit:external 与打包前置合规审计;制品补齐 34 个 docs 文档与预览图
This commit is contained in:
1 parent
c0ad7331d4
commit
dda86294b0
36 files changed
+829
-235
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
暖如灯火,静如留白。暖屿以柔和的色调和舒缓的节奏回归内容本身,为记录与阅读留出一方安静从容的角落。
|
||||
|
||||
> 本项目由 AI 开发完成,人工仅进行部分功能测试及问题找寻
|
||||
> 本项目使用 AI 辅助开发。开发者已按 Halo 主题规范完成代码审查、构建产物核验、外部资源审计与主要模板的回归检查清单(见[验证与回归测试](#验证与回归测试));主题不加载远程脚本、不动态下载代码、不进行未披露的数据收集。
|
||||
|
||||
## 环境要求
|
||||
|
||||
@@ -13,14 +13,59 @@
|
||||
|
||||
1. **响应式设计** — 适配桌面端与移动端,支持暗色模式,移动端使用动态视口单位适配地址栏
|
||||
2. **插件兼容性** — 适配 11 款常用 Halo 插件,安装即用;提供页面布局契约,插件前台页面自动复用主题外观
|
||||
3. **内置灯箱** — 主题内置 LightGallery 灯箱,支持 EXIF 信息展示;安装 lightgallery.js 插件后自动禁用
|
||||
3. **内置灯箱** — 主题内置 LightGallery 灯箱(GPLv3,随主题自托管打包),支持 EXIF 信息展示;安装 lightgallery.js 插件后自动禁用
|
||||
4. **可配置性** — 17 个设置分组、122 个配置项,几乎所有模块可独立开关
|
||||
5. **性能优化** — 服务端字数统计(API 扩展包)、图片懒加载与响应式缩略图、首屏整屏吸附(仅首屏边界接管,内容区保留原生滚动)、Hero 离屏停绘、CSS/HTML 压缩;详见[开发指南 - 性能与滚动顺滑](docs/customize/development.md)
|
||||
6. **页面布局契约** — 提供 `templates/layout.html`,插件页面不再回退到 Halo 内置布局
|
||||
5. **性能优化** — 服务端字数统计(API 扩展包)、图片懒加载与响应式缩略图、首屏整屏吸附(仅首屏边界接管,内容区保留原生滚动)、Hero 离屏停绘、CSS/HTML 压缩;详见[开发指南 - 性能与滚动顺滑](https://nxxy335.top/WarmIsland/customize/development.html)
|
||||
6. **零默认外部请求** — 所有脚本、样式、字体、图标、句子库、主题图标均随主题自托管,默认不向任何第三方发起自动请求;详见[外部资源与隐私](#外部资源与隐私)
|
||||
7. **页面布局契约** — 提供 `templates/layout.html`,插件页面不再回退到 Halo 内置布局
|
||||
|
||||
## 预览
|
||||
|
||||

|
||||

|
||||
|
||||
> 预览图同时随安装包分发(包根目录 `WarmIsland Preview.webp`),离线查看制品时也可打开。
|
||||
|
||||
## 外部资源与隐私
|
||||
|
||||
本章节对应 [Halo 应用市场审核指南](https://docs.halo.run/developer-guide/app-store/app-review-guidelines) 1.4 / 4.4.4 / 5.1,逐项说明服务名称、用途、数据类型、默认状态与关闭方法。
|
||||
|
||||
### 默认行为
|
||||
|
||||
**默认配置下,主题不会自动向任何第三方服务发起请求。** 页面加载的 CSS、JS、字体(LXGW WenKai 本地分片)、图标、主题 Logo、一言句子库全部来自站点自身的 `/themes/warm-island/assets/`,不经过 CDN,也不使用 `preconnect` / `dns-prefetch`。已移除的远程依赖见下方[自托管与许可](#自托管与许可)。
|
||||
|
||||
### 逐项说明
|
||||
|
||||
| 服务 / 资源 | 用途 | 涉及的数据类型 | 默认状态 | 关闭 / 替换方法 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 主题自身静态资源 `/themes/warm-island/assets/*` | 样式、脚本、字体、图标、Logo、内置句子库 | 无(同域请求,不含用户数据) | 始终启用,渲染必需 | 不可关闭;均为随包自托管文件 |
|
||||
| 自定义一言 API `hero_hitokoto_api` | Hero 首屏随机句子 | 无请求体;发起 GET 时第三方可见访客 IP / User-Agent | **关闭**:留空,使用 `assets/data/hitokoto.json` 内置本地句子库 | 主题设置 → Hero 首屏 → 清空「自定义一言 API」 |
|
||||
| 自定义 IP 定位主 / 备选 API `welcome_popup_api` / `welcome_popup_fallback_api` | 欢迎弹窗显示「欢迎来自 XX 的朋友」 | **访客 IP** 与第三方返回的粗略地理位置 | **关闭**:两项均留空,弹窗只显示本地欢迎语;且欢迎弹窗开关本身默认关闭 | 主题设置 → 欢迎弹窗 → 关闭开关,或清空两个 API 字段 |
|
||||
| 分享按钮:QQ / 微博 / X / Facebook / Telegram | 分享文章到对应平台 | 文章 URL 与标题(写在目标站 URL 参数中) | 默认开启 `qq,wechat,weibo`;**仅在访客点击后**跳转,页面加载时不请求 | 主题设置 → 文章 → 关闭「显示分享按钮」,或在「分享渠道」取消勾选 |
|
||||
| 微信分享弹层 | 生成并复制文章链接 | 文章 URL(浏览器本地字符串,不发送) | 与分享按钮一致 | 同上;已移除二维码远程库,改为「复制链接」 |
|
||||
| 页脚备案 / Halo / 应用市场外链 | 备案信息、致谢、插件安装提示 | 无(访客主动点击才跳转) | 由站点配置决定,默认仅显示 Halo 致谢 | 主题设置 → 页脚 → 关闭对应显示项 |
|
||||
| 插件前台页面(豆瓣、Steam、朋友圈、微信读书等) | 展示插件提供的数据 | 由对应插件决定,主题不发起请求 | 安装并启用对应插件后才出现 | 卸载 / 停用对应插件;主题自动降级隐藏 |
|
||||
| 内置 LightGallery 灯箱 | 图片放大预览、EXIF 展示 | 无(本地打包,不联网) | 默认启用 | 安装 lightgallery.js 插件后主题自动让位 |
|
||||
|
||||
> 自定义 API 属于**站点管理员主动配置**的可选能力:一旦填写,浏览器会按设置向该第三方发起请求。请自行评估服务商的隐私政策,并在站点隐私说明中补充披露;主题不会代替站点做这项声明。
|
||||
|
||||
### 隐私说明(5.1)
|
||||
|
||||
| 项目 | 说明 |
|
||||
| --- | --- |
|
||||
| 收集的数据类型 | 默认配置下**无**。主题不读取、不上传用户信息、站点内容、访问日志或订单数据 |
|
||||
| 数据用途 | 仅用于页面渲染;自定义 API 场景下由管理员自行决定 |
|
||||
| 保存位置与期限 | 主题不写入数据库、不上传数据、不生成服务端日志。前端仅在访客本机使用 `sessionStorage`(欢迎弹窗「同会话仅显示一次」)与 `localStorage`(点赞状态),可随时清除浏览器站点数据 |
|
||||
| 是否传输到第三方 | 默认**否**。仅当管理员显式填写「自定义一言 API」或「自定义 IP 定位 API」时才会向该服务发起请求,其中 IP 定位会暴露访客 IP |
|
||||
| 关闭 / 撤销方式 | 见上表;清空相应设置项或关闭对应开关即可立即停止,无需重装主题 |
|
||||
| 遥测 / 统计 / 崩溃上报 / 广告 / AI 分析 / 跨站追踪 | **均不存在**。主题不含任何埋点、指纹或第三方统计脚本 |
|
||||
|
||||
### 自托管与许可
|
||||
|
||||
- **已移除的远程依赖**:`cdn.jsdelivr.net` 预连接与预解析(原出现在全部模板)、PrismJS 1.29.0 核心与 `autoloader`(会按代码语言从 CDN 动态拉取并执行组件)、`qrcode-generator`、默认的一言与 IP 定位接口。
|
||||
- **代码高亮**:主题不再自带 Prism.js 运行时。语法高亮请安装 Halo 应用市场的代码高亮插件,或启用「API 扩展包」的服务端代码高亮;主题只提供两套自托管的 `.token` 配色(`assets/vendor/prism-warm.css` / `prism-cold.css`),由「文章 → 代码高亮主题」切换。
|
||||
- **字体**:[霞鹜文楷 LXGW WenKai](https://github.com/lxgw/LxgwWenKai) 以 [SIL OFL 1.1](https://openfontlicense.org/) 发布,由 `cn-font-split` 切分为本地 woff2 分片随主题分发;字体来源与版权声明保留在 `public/assets/fonts/LXGWWenKai/result.css` 中,无远程字体请求。
|
||||
- **灯箱**:LightGallery 2.9.0 以 GPLv3 发布,本主题同样采用 GPL-3.0,按开源许可条款使用(非商业授权);已去除占位 `licenseKey`,控制台不再出现 `license key is not valid for production use`。
|
||||
- **图标**:来源与协议见[图标与字体来源](#图标与字体来源)。
|
||||
|
||||
## 已适配插件
|
||||
|
||||
@@ -29,7 +74,7 @@
|
||||
| [瞬间](https://halo.run/store/apps/app-SnwWD) | `PluginMoments` | `/moments` | 瞬间动态页面,支持点赞、评论、标签筛选、顶部封面 |
|
||||
| [图库](https://halo.run/store/apps/app-BmQJW) | `PluginPhotos` | `/photos` | 图片展示页面,支持分组、灯箱预览 |
|
||||
| [友情链接](https://halo.run/store/apps/app-hfbQg) | `PluginLinks` | `/links` | 友链展示页面,支持随机排列、随机传送门、评论区开关 |
|
||||
| [朋友圈](https://github.com/halo-sigs/plugin-friends) | `plugin-friends` | `/friends` | RSS 订阅朋友圈页面 |
|
||||
| [朋友圈](https://www.halo.run/store/apps/app-yISsV) | `plugin-friends` | `/friends` | RSS 订阅朋友圈页面 |
|
||||
| [装备管理](https://github.com/chengzhongxue/plugin-equipment) | `equipment` | `/equipments` | 装备展示页面,按分组展示,支持封面图 |
|
||||
| [豆瓣](https://github.com/chengzhongxue/plugin-douban) | `plugin-douban` | `/douban` | 豆瓣记录页面,支持类型/状态/标签筛选,默认音乐;需插件 **v1.2.6+**(SQLite 数据结构) |
|
||||
| [Steam 信息展示](https://github.com/Tim0x0/halo-plugin-steam) | `PluginSteam` | `/steam` | Steam 游戏库页面,含用户资料、统计数据、最近游玩、游戏库分页 |
|
||||
@@ -54,16 +99,16 @@
|
||||
|
||||
### 首页
|
||||
|
||||
- Hero 首屏:明暗双背景图、大标题/副标题独立配色、一言(支持自动刷新与淡入淡出过渡)、下滑箭头平滑滚动
|
||||
- 欢迎弹窗:基于访客 IP 地理定位的欢迎提示,支持 5 种位置、自定义 API 与会话去重开关
|
||||
- Hero 首屏:明暗双背景图、大标题/副标题独立配色、一言(默认使用内置本地句子库,可选自定义 API 与自动刷新)、下滑箭头平滑滚动
|
||||
- 欢迎弹窗:默认显示纯本地静态欢迎语,支持 5 种位置、会话去重开关;仅在显式配置自定义 IP 定位 API 时才显示访客所在地(见[外部资源与隐私](#外部资源与隐私))
|
||||
- 滚动吸附:首屏与内容区之间滚轮/触摸自动吸附,进入内容区后恢复原生自由滚动
|
||||
- 文章卡片:可配置显示字数(依赖 API 扩展包)、阅读量、分类、标签、摘要行数
|
||||
|
||||
### 文章
|
||||
|
||||
- 目录(TOC):桌面端侧栏 + 移动端抽屉,滚动高亮、展开模式可选
|
||||
- 上下篇导航:悬停反馈与方向箭头动画
|
||||
- 相关推荐、分享按钮、赞赏、版权声明可独立开关
|
||||
- 代码高亮:由代码高亮插件 / API 扩展包输出,主题提供 Warm / Cold 两套本地配色;主题不加载任何远程高亮脚本
|
||||
- 上下篇导航、相关推荐、分享按钮、赞赏、版权声明可独立开关;微信分享改为复制文章链接,无需二维码库
|
||||
- 字数与阅读时间:优先使用 API 扩展包的服务端统计,未安装时前端计算兜底
|
||||
|
||||
### 全局
|
||||
@@ -76,7 +121,7 @@
|
||||
|
||||
## 灯箱集成
|
||||
|
||||
主题内置 LightGallery 灯箱,为图片提供放大预览功能,并支持 EXIF 信息展示。
|
||||
主题内置 LightGallery 灯箱,为图片提供放大预览功能,并支持 EXIF 信息展示。该库以 GPLv3 发布,本主题以 GPL-3.0 开源,按开源许可条款自托管使用;控制台不会出现商业授权提示。
|
||||
|
||||
### lightgallery.js 插件兼容
|
||||
|
||||
@@ -98,7 +143,7 @@
|
||||
3. 安装完成后点击「启用」
|
||||
|
||||
> **升级后首页 404「资源不存在 / Template index was not found」**:说明上传的 ZIP 里没有 `templates/`
|
||||
> 模板文件(从源码仓库打包时漏了构建步骤)。请换用完整安装包重新上传,详见 [FAQ 安装相关](docs/faq.md)。
|
||||
> 模板文件(从源码仓库打包时漏了构建步骤)。请换用完整安装包重新上传,详见 [FAQ 安装相关](https://nxxy335.top/WarmIsland/faq.html)。
|
||||
>
|
||||
> **升级后如果整站没了样式**:Halo 页面缓存或前置反向代理里可能还留着上一版本的 HTML,
|
||||
> 它引用的 CSS 文件名带内容哈希,升级后已被删除,请求会直接 500。
|
||||
@@ -107,7 +152,7 @@
|
||||
>
|
||||
> **升级主题或修改配色后,部分页面不生效**:如果安装了「页面静态缓存」插件,
|
||||
> 它只随文章变更自动刷新,主题层的变更需要手动点一次「刷新页面缓存」,
|
||||
> 详见 [FAQ:升级主题或修改配色后部分页面不生效](docs/faq.md#升级主题或修改配色后部分页面的新样式配色不生效)。
|
||||
> 详见 [FAQ:升级主题或修改配色后部分页面不生效](https://nxxy335.top/WarmIsland/faq.html)。
|
||||
|
||||
或从源码构建:
|
||||
|
||||
@@ -115,11 +160,11 @@
|
||||
git clone https://git.nxxy335.top/nxxy335/halo-theme-WarmIsland.git
|
||||
cd halo-theme-WarmIsland
|
||||
pnpm install
|
||||
pnpm release # build → snapshot → package
|
||||
pnpm release # build → snapshot → package(含外部资源审计)
|
||||
```
|
||||
|
||||
`pnpm build` 只生成 `templates/`(模板 + 静态资源),不会产出 ZIP;
|
||||
安装包由 `pnpm package` 生成到 `dist/warm-island-<version>.zip`。
|
||||
安装包由 `pnpm package` 生成到 `dist/warm-island-<version>.zip`。安装包同时包含 `docs/*.md` 与预览图,便于离线查阅上架说明中引用的文档。
|
||||
|
||||
## 开发
|
||||
|
||||
@@ -136,6 +181,9 @@ pnpm build
|
||||
# 把本次构建的哈希资源快照进 compat/assets(发版前必做)
|
||||
pnpm snapshot
|
||||
|
||||
# 外部资源 / 隐私 / README 链接审计(打包时也会自动执行)
|
||||
pnpm audit:external
|
||||
|
||||
# 打包 dist/warm-island-<version>.zip
|
||||
pnpm package
|
||||
|
||||
@@ -145,7 +193,8 @@ pnpm smoke
|
||||
|
||||
发版流程:`pnpm release`(等价于 `pnpm build` → `pnpm snapshot` → `pnpm package`),
|
||||
并把 `compat/assets` 的新增文件一起提交。`pnpm package` 会校验模板、资源目录与版本号一致性,
|
||||
产物缺失时直接报错,避免打出不含 `templates/` 的空壳安装包。
|
||||
并执行外部资源审计(禁止第三方预连接、远程脚本、动态加载、无效 README 链接、重复大图),
|
||||
任一不通过都会直接报错,避免打出不合规或不含 `templates/` 的空壳安装包。
|
||||
|
||||
### 目录结构
|
||||
|
||||
@@ -155,9 +204,13 @@ pnpm smoke
|
||||
│ ├── layouts/ # Layout.astro(含页面布局契约渲染模式)
|
||||
│ ├── pages/ # 页面模板(构建后输出到 templates/)
|
||||
│ └── styles/ # SCSS 样式(正文样式在 _content.scss,全局生效)
|
||||
├── compat/
|
||||
│ └── assets/ # 历史版本哈希静态资源,构建时合并进 templates/assets
|
||||
├── scripts/ # 构建后置、资源快照、打包、冒烟测试
|
||||
├── public/
|
||||
│ ├── assets/data/ # 内置本地一言句子库(静态 JSON)
|
||||
│ ├── assets/fonts/ # LXGW WenKai 本地字体分片
|
||||
│ ├── assets/vendor/ # 自托管的代码高亮配色 CSS
|
||||
│ └── assets/logo.png # 主题图标(唯一一份,256×256 压缩版)
|
||||
├── compat/assets/ # 历史版本哈希静态资源,构建时合并进 templates/assets
|
||||
├── scripts/ # 构建后置、资源快照、打包、外部资源审计、冒烟测试
|
||||
├── templates/ # 构建产物,Halo 实际加载的模板
|
||||
├── settings.yaml # 后台设置项定义
|
||||
└── theme.yaml # 主题清单与自定义页面模板注册
|
||||
@@ -167,14 +220,34 @@ pnpm smoke
|
||||
|
||||
- **静态资源文件名带内容哈希**:升级后旧文件被删除,被缓存的 HTML 仍会请求旧文件名并触发 500,
|
||||
所以每个版本的哈希文件都要留一份(见 `compat/assets` 与 `pnpm snapshot`)
|
||||
- **不得引入远程脚本 / 样式 / 字体 / 预连接**:所有第三方资源必须自托管并标注许可,`pnpm package` 会拦截回归
|
||||
- **Thymeleaf 字符串字面量必须加单引号**:`th:text="'文本'"`,裸中文遇到逗号/空格/emoji 会导致页面 500
|
||||
- **`th:attr` 只用简单表达式**(取属性、Elvis 默认值、三元、拼接、等值比较),表达式对象方法调用会被受限求值器拒绝
|
||||
- **凡是与后台开关绑定的模板表达式都要给默认值**:`?: '默认值'`,配置未落库时属性会被整体移除,功能会静默失效
|
||||
- **截取首字符前先判空**:`#strings.substring('', 0, 1)` 会抛字符串越界异常,统一写成 `!#strings.isEmpty(x) ? #strings.substring(x, 0, 1) : '·'`
|
||||
- **正文样式必须写在全局样式表**(`src/styles/_content.scss`):Astro scoped 样式对 Thymeleaf 运行时注入的内容无效
|
||||
- **内联脚本统一加 `th:inline="none"`**:避免 Thymeleaf 解析脚本中的 `[[` / `[(` / `${}`
|
||||
- **新插入的动画元素要强制回流**(读一次 `offsetWidth`):否则初态与终态在同一帧被合并,transition 不触发
|
||||
- 修改主题外观无需单独维护 `layout.astro`:页面布局契约复用 `Layout.astro`,构建时自动生成
|
||||
|
||||
## 验证与回归测试
|
||||
|
||||
自动化覆盖(每次发版执行):
|
||||
|
||||
- `pnpm build` 生成全部 21 个模板,构建后置脚本校验所有静态资源引用真实存在
|
||||
- `pnpm audit:external` / `pnpm package` 审计第三方远程加载、预连接、动态加载、README 制品链接、Logo 去重
|
||||
- `pnpm smoke` 校验点击特效脚本;安装包内容校验模板数量、`theme.yaml` 版本与元数据一致性
|
||||
|
||||
上架前建议在真实 Halo ≥ 2.27 实例上做一次人工回归(主题声明为 AI 辅助开发,此项由站点管理员完成并留存结果):
|
||||
|
||||
| 页面 | 检查要点 |
|
||||
| --- | --- |
|
||||
| 首页 | Hero、内置一言、滚动吸附、文章卡片、欢迎弹窗(默认本地文案) |
|
||||
| 文章 | 正文 / 代码块配色、目录、分享按钮与微信复制链接、灯箱、点赞 |
|
||||
| 分类 / 标签 / 归档 | 列表、分页、空状态、暗色模式 |
|
||||
| 友链 / 留言板 / 订阅 | 空字段兜底(作者为空不报错)、表单与分页 |
|
||||
| 全局 | 导航栏、页脚、移动端抽屉、深浅色切换、控制台无报错 / 无外部请求 |
|
||||
|
||||
## 技术栈
|
||||
|
||||
- [Halo](https://halo.run) 2.27+
|
||||
@@ -186,13 +259,15 @@ pnpm smoke
|
||||
|
||||
[GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
## 图标来源
|
||||
## 图标与字体来源
|
||||
|
||||
本主题使用的图标来自以下开源项目:
|
||||
本主题使用的图标与字体来自以下开源项目,均随主题自托管分发、不使用远程 CDN:
|
||||
|
||||
| 来源 | 协议 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| [霞鹜文楷 LXGW WenKai](https://github.com/lxgw/LxgwWenKai) | [SIL OFL 1.1](https://openfontlicense.org/) | 全站中文字体(本地 woff2 分片,含保留字体名与版权声明) |
|
||||
| [Font Awesome](https://fontawesome.com/) Free v7.2.0 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) / [SIL OFL 1.1](https://scripts.sil.org/OFL) | QQ 分享图标 |
|
||||
| [Simple Icons](https://simpleicons.org/) | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | 微博、X、Facebook、Telegram 分享图标 |
|
||||
| [Feather Icons](https://feathericons.com/) | [MIT](https://opensource.org/licenses/MIT) | 导航、界面交互图标 |
|
||||
| [Halo](https://halo.run/) | [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.en.html) | 主题内置图标 |
|
||||
| [LightGallery](https://www.lightgalleryjs.com/) 2.9.0 | GPLv3 | 图片灯箱(npm 依赖随构建打包,非远程加载) |
|
||||
@@ -0,0 +1,62 @@
|
||||
var I=function(){return I=Object.assign||function(t){for(var e,i=1,s=arguments.length;i<s;i++){e=arguments[i];for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r])}return t},I.apply(this,arguments)};function N(){for(var o=0,t=0,e=arguments.length;t<e;t++)o+=arguments[t].length;for(var i=Array(o),s=0,t=0;t<e;t++)for(var r=arguments[t],n=0,l=r.length;n<l;n++,s++)i[s]=r[n];return i}var p={afterAppendSlide:"lgAfterAppendSlide",init:"lgInit",hasVideo:"lgHasVideo",containerResize:"lgContainerResize",updateSlides:"lgUpdateSlides",afterAppendSubHtml:"lgAfterAppendSubHtml",beforeOpen:"lgBeforeOpen",afterOpen:"lgAfterOpen",slideItemLoad:"lgSlideItemLoad",beforeSlide:"lgBeforeSlide",afterSlide:"lgAfterSlide",posterClick:"lgPosterClick",dragStart:"lgDragStart",dragMove:"lgDragMove",dragEnd:"lgDragEnd",beforeNextSlide:"lgBeforeNextSlide",beforePrevSlide:"lgBeforePrevSlide",beforeClose:"lgBeforeClose",afterClose:"lgAfterClose"},q={mode:"lg-slide",easing:"ease",speed:400,licenseKey:"0000-0000-000-0000",height:"100%",width:"100%",addClass:"",startClass:"lg-start-zoom",backdropDuration:300,container:"",startAnimationDuration:400,zoomFromOrigin:!0,hideBarsDelay:0,showBarsAfter:1e4,slideDelay:0,supportLegacyBrowser:!0,allowMediaOverlap:!1,videoMaxSize:"1280-720",loadYouTubePoster:!0,defaultCaptionHeight:0,ariaLabelledby:"",ariaDescribedby:"",resetScrollPosition:!0,hideScrollbar:!1,closable:!0,swipeToClose:!0,closeOnTap:!0,showCloseIcon:!0,showMaximizeIcon:!1,loop:!0,escKey:!0,keyPress:!0,trapFocus:!0,controls:!0,slideEndAnimation:!0,hideControlOnEnd:!1,mousewheel:!1,getCaptionFromTitleOrAlt:!0,appendSubHtmlTo:".lg-sub-html",subHtmlSelectorRelative:!1,preload:2,numberOfSlideItemsInDom:10,selector:"",selectWithin:"",nextHtml:"",prevHtml:"",index:0,iframeWidth:"100%",iframeHeight:"100%",iframeMaxWidth:"100%",iframeMaxHeight:"100%",download:!0,counter:!0,appendCounterTo:".lg-toolbar",swipeThreshold:50,enableSwipe:!0,enableDrag:!0,dynamic:!1,dynamicEl:[],extraProps:[],exThumbImage:"",isMobile:void 0,mobileSettings:{controls:!1,showCloseIcon:!1,download:!1},plugins:[],strings:{closeGallery:"Close gallery",toggleMaximize:"Toggle maximize",previousSlide:"Previous slide",nextSlide:"Next slide",download:"Download",playVideo:"Play video",mediaLoadingFailed:"Oops... Failed to load content..."}};function V(){(function(){if(typeof window.CustomEvent=="function")return!1;function o(t,e){e=e||{bubbles:!1,cancelable:!1,detail:null};var i=document.createEvent("CustomEvent");return i.initCustomEvent(t,e.bubbles,e.cancelable,e.detail),i}window.CustomEvent=o})(),(function(){Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector)})()}var D=(function(){function o(t){return this.cssVenderPrefixes=["TransitionDuration","TransitionTimingFunction","Transform","Transition"],this.selector=this._getSelector(t),this.firstElement=this._getFirstEl(),this}return o.generateUUID=function(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(t){var e=Math.random()*16|0,i=t=="x"?e:e&3|8;return i.toString(16)})},o.prototype._getSelector=function(t,e){if(e===void 0&&(e=document),typeof t!="string")return t;e=e||document;var i=t.substring(0,1);return i==="#"?e.querySelector(t):e.querySelectorAll(t)},o.prototype._each=function(t){return this.selector?(this.selector.length!==void 0?[].forEach.call(this.selector,t):t(this.selector,0),this):this},o.prototype._setCssVendorPrefix=function(t,e,i){var s=e.replace(/-([a-z])/gi,function(r,n){return n.toUpperCase()});this.cssVenderPrefixes.indexOf(s)!==-1?(t.style[s.charAt(0).toLowerCase()+s.slice(1)]=i,t.style["webkit"+s]=i,t.style["moz"+s]=i,t.style["ms"+s]=i,t.style["o"+s]=i):t.style[s]=i},o.prototype._getFirstEl=function(){return this.selector&&this.selector.length!==void 0?this.selector[0]:this.selector},o.prototype.isEventMatched=function(t,e){var i=e.split(".");return t.split(".").filter(function(s){return s}).every(function(s){return i.indexOf(s)!==-1})},o.prototype.attr=function(t,e){return e===void 0?this.firstElement?this.firstElement.getAttribute(t):"":(this._each(function(i){i.setAttribute(t,e)}),this)},o.prototype.find=function(t){return c(this._getSelector(t,this.selector))},o.prototype.first=function(){return this.selector&&this.selector.length!==void 0?c(this.selector[0]):c(this.selector)},o.prototype.eq=function(t){return c(this.selector[t])},o.prototype.parent=function(){return c(this.selector.parentElement)},o.prototype.get=function(){return this._getFirstEl()},o.prototype.removeAttr=function(t){var e=t.split(" ");return this._each(function(i){e.forEach(function(s){return i.removeAttribute(s)})}),this},o.prototype.wrap=function(t){if(!this.firstElement)return this;var e=document.createElement("div");return e.className=t,this.firstElement.parentNode.insertBefore(e,this.firstElement),this.firstElement.parentNode.removeChild(this.firstElement),e.appendChild(this.firstElement),this},o.prototype.addClasLine truncated
|
||||
<iframe class="lg-object" frameborder="0" `+n+' src="'+s+`" allowfullscreen="true"></iframe>
|
||||
</div>`},getImgMarkup:function(o,t,e,i,s,r){var n=i?'srcset="'+i+'"':"",l=s?'sizes="'+s+'"':"",g="<img "+e+" "+n+" "+l+' class="lg-object lg-image" data-index="'+o+'" src="'+t+'" />',h="";if(r){var d=typeof r=="string"?JSON.parse(r):r;h=d.map(function(u){var a="";return Object.keys(u).forEach(function(m){a+=" "+m+'="'+u[m]+'"'}),"<source "+a+"></source>"})}return""+h+g},getResponsiveSrc:function(o){for(var t=[],e=[],i="",s=0;s<o.length;s++){var r=o[s].split(" ");r[0]===""&&r.splice(0,1),e.push(r[0]),t.push(r[1])}for(var n=window.innerWidth,l=0;l<t.length;l++)if(parseInt(t[l],10)>n){i=e[l];break}return i},isImageLoaded:function(o){return!(!o||!o.complete||o.naturalWidth===0)},getVideoPosterMarkup:function(o,t,e,i,s){var r="";s&&s.youtube?r="lg-has-youtube":s&&s.vimeo?r="lg-has-vimeo":r="lg-has-html5";var n=t;return typeof t!="string"&&(n=t.outerHTML),'<div class="lg-video-cont '+r+'" style="'+e+`">
|
||||
<div class="lg-video-play-button">
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
focusable="false"
|
||||
aria-labelledby="`+i+`"
|
||||
role="img"
|
||||
class="lg-video-play-icon"
|
||||
>
|
||||
<title>`+i+`</title>
|
||||
<polygon class="lg-video-play-icon-inner" points="1,0 20,10 1,20"></polygon>
|
||||
</svg>
|
||||
<svg class="lg-video-play-icon-bg" viewBox="0 0 50 50" focusable="false">
|
||||
<circle cx="50%" cy="50%" r="20"></circle></svg>
|
||||
<svg class="lg-video-play-icon-circle" viewBox="0 0 50 50" focusable="false">
|
||||
<circle cx="50%" cy="50%" r="20"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
`+n+`
|
||||
<img class="lg-object lg-video-poster" src="`+o+`" />
|
||||
</div>`},getFocusableElements:function(o){var t=o.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'),e=[].filter.call(t,function(i){var s=window.getComputedStyle(i);return s.display!=="none"&&s.visibility!=="hidden"});return e},getDynamicOptions:function(o,t,e,i){var s=[],r=N(j,t);return[].forEach.call(o,function(n){for(var l={},g=0;g<n.attributes.length;g++){var h=n.attributes[g];if(h.specified){var d=K(h.name),u="";r.indexOf(d)>-1&&(u=d),u&&(l[u]=h.value)}}var a=c(n),m=a.find("img").first().attr("alt"),f=a.attr("title"),y=i?a.attr(i):a.find("img").first().attr("src");l.thumb=y,e&&!l.subHtml&&(l.subHtml=f||m||""),l.alt=m||f||"",s.push(l)}),s},isMobile:function(){return/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)},isVideo:function(o,t,e){if(!o){if(t)return{html5:!0};console.error("lightGallery :- data-src is not provided on slide item "+(e+1)+". Please make sure the selector property is properly configured. More info - https://www.lightgalleryjs.com/demos/html-markup/");return}var i=o.match(/\/\/(?:www\.)?youtu(?:\.be|be\.com|be-nocookie\.com)\/(?:watch\?v=|embed\/)?([a-z0-9\-\_\%]+)([\&|?][\S]*)*/i),s=o.match(/\/\/(?:www\.)?(?:player\.)?vimeo.com\/(?:video\/)?([0-9a-z\-_]+)(.*)?/i),r=o.match(/https?:\/\/(.+)?(wistia\.com|wi\.st)\/(medias|embed)\/([0-9a-z\-_]+)(.*)/);if(i)return{youtube:i};if(s)return{vimeo:s};if(r)return{wistia:r}}},$=0,U=(function(){function o(t,e){if(this.lgOpened=!1,this.index=0,this.plugins=[],this.lGalleryOn=!1,this.lgBusy=!1,this.currentItemsInDom=[],this.prevScrollTop=0,this.bodyPaddingRight=0,this.isDummyImageRemoved=!1,this.dragOrSwipeEnabled=!1,this.mediaContainerPosition={top:0,bottom:0},!t)return this;if($++,this.lgId=$,this.el=t,this.LGel=c(t),this.generateSettings(e),this.buildModules(),this.settings.dynamic&&this.settings.dynamicEl!==void 0&&!Array.isArray(this.settings.dynamicEl))throw"When using dynamic mode, you must also define dynamicEl as an Array.";return this.galleryItems=this.getItems(),this.normalizeSettings(),this.init(),this.validateLicense(),this}return o.prototype.generateSettings=function(t){if(this.settings=I(I({},q),t),this.settings.isMobile&&typeof this.settings.isMobile=="function"?this.settings.isMobile():b.isMobile()){var e=I(I({},this.settings.mobileSettings),this.settings.mobileSettings);this.settings=I(I({},this.settings),e)}},o.prototype.normalizeSettings=function(){if(this.settings.slideEndAnimation&&(this.settings.hideControlOnEnd=!1),this.settings.closable||(this.settings.swipeToClose=!1),this.zoomFromOrigin=this.settings.zoomFromOrigin,this.settings.dynamic&&(this.zoomFromOrigin=!1),this.settings.container){var t=this.settings.container;if(typeof t=="function")this.settings.container=t();else if(typeof t=="string"){var e=document.querySelector(t);this.settings.container=e??document.body}}else this.settings.container=document.body;this.settings.preload=Math.min(this.settings.preload,this.galleryItems.length)},o.prototype.init=function(){var t=this;this.addSlideVideoInfo(this.galleryItems),this.buildStructure(),this.LGel.trigger(p.init,{instance:this}),this.settings.keyPress&&this.keyPress(),setTimeout(function(){t.enableDrag(),t.enableSwipe(),t.triggerPosterClick()},50),this.arrow(),this.settings.mousewheel&&this.mousewheel(),this.settings.dynamic||this.openGalleryOnItemClick()},o.prototype.openGalleryOnItemClick=function(){for(var t=this,e=function(r){var n=i.items[r],l=c(n),g=D.generateUUID();l.attr("data-lg-id",g).on("click.lgcustom-item-"+g,function(h){h.preventDefault();var d=t.settings.index||r;t.openGallery(d,n)})},i=this,s=0;s<this.items.length;s++)e(s)},o.prototype.buildModules=function(){var t=this;this.settings.plugins.forEach(function(e){t.plugins.push(new e(t,c))})},o.prototype.validateLicense=function(){this.settings.licenseKey?this.settings.licenseKey==="0000-0000-000-0000"&&console.warn("lightGallery: "+this.settings.licenseKey+" license key is not valid for production use"):console.error("Please provide a valid license key")},o.prototype.getSlideItem=function(t){return c(this.getSlideItemId(t))},o.prototype.getSlideItemId=function(t){return"#lg-item-"+this.lgId+"-"+t},o.prototype.getIdName=function(t){return t+"-"+this.lgId},o.prototype.getElementById=function(t){return c("#"+this.getIdName(t))},o.prototype.manageSingleSlideClassName=function(){this.galleryItems.length<2?this.outer.addClass("lg-single-item"):this.outer.removeClass("lg-single-item")},o.prototype.buildStructure=function(){var t=this,e=this.$container&&this.$container.get();if(!e){var i="",s="";this.settings.controls&&(i='<button type="button" id="'+this.getIdName("lg-prev")+'" aria-label="'+this.settings.strings.previousSlide+'" class="lg-prev lg-icon"> '+this.settings.prevHtml+` </button>
|
||||
<button type="button" id="`+this.getIdName("lg-next")+'" aria-label="'+this.settings.strings.nextSlide+'" class="lg-next lg-icon"> '+this.settings.nextHtml+" </button>"),this.settings.appendSubHtmlTo!==".lg-item"&&(s='<div class="lg-sub-html" role="status" aria-live="polite"></div>');var r="";this.settings.allowMediaOverlap&&(r+="lg-media-overlap ");var n=this.settings.ariaLabelledby?'aria-labelledby="'+this.settings.ariaLabelledby+'"':"",l=this.settings.ariaDescribedby?'aria-describedby="'+this.settings.ariaDescribedby+'"':"",g="lg-container "+this.settings.addClass+" "+(document.body!==this.settings.container?"lg-inline":""),h=this.settings.closable&&this.settings.showCloseIcon?'<button type="button" aria-label="'+this.settings.strings.closeGallery+'" id="'+this.getIdName("lg-close")+'" class="lg-close lg-icon"></button>':"",d=this.settings.showMaximizeIcon?'<button type="button" aria-label="'+this.settings.strings.toggleMaximize+'" id="'+this.getIdName("lg-maximize")+'" class="lg-maximize lg-icon"></button>':"",u=`
|
||||
<div class="`+g+'" id="'+this.getIdName("lg-container")+'" tabindex="-1" aria-modal="true" '+n+" "+l+` role="dialog"
|
||||
>
|
||||
<div id="`+this.getIdName("lg-backdrop")+`" class="lg-backdrop"></div>
|
||||
|
||||
<div id="`+this.getIdName("lg-outer")+'" class="lg-outer lg-use-css3 lg-css3 lg-hide-items '+r+` ">
|
||||
|
||||
<div id="`+this.getIdName("lg-content")+`" class="lg-content">
|
||||
<div id="`+this.getIdName("lg-inner")+`" class="lg-inner">
|
||||
</div>
|
||||
`+i+`
|
||||
</div>
|
||||
<div id="`+this.getIdName("lg-toolbar")+`" class="lg-toolbar lg-group">
|
||||
`+d+`
|
||||
`+h+`
|
||||
</div>
|
||||
`+(this.settings.appendSubHtmlTo===".lg-outer"?s:"")+`
|
||||
<div id="`+this.getIdName("lg-components")+`" class="lg-components">
|
||||
`+(this.settings.appendSubHtmlTo===".lg-sub-html"?s:"")+`
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;c(this.settings.container).append(u),document.body!==this.settings.container&&c(this.settings.container).css("position","relative"),this.outer=this.getElementById("lg-outer"),this.$lgComponents=this.getElementById("lg-components"),this.$backdrop=this.getElementById("lg-backdrop"),this.$container=this.getElementById("lg-container"),this.$inner=this.getElementById("lg-inner"),this.$content=this.getElementById("lg-content"),this.$toolbar=this.getElementById("lg-toolbar"),this.$backdrop.css("transition-duration",this.settings.backdropDuration+"ms");var a=this.settings.mode+" ";this.manageSingleSlideClassName(),this.settings.enableDrag&&(a+="lg-grab "),this.outer.addClass(a),this.$inner.css("transition-timing-function",this.settings.easing),this.$inner.css("transition-duration",this.settings.speed+"ms"),this.settings.download&&this.$toolbar.append('<a id="'+this.getIdName("lg-download")+'" target="_blank" rel="noopener" aria-label="'+this.settings.strings.download+'" download class="lg-download lg-icon"></a>'),this.counter(),c(window).on("resize.lg.global"+this.lgId+" orientationchange.lg.global"+this.lgId,function(){t.refreshOnResize()}),this.hideBars(),this.manageCloseGallery(),this.toggleMaximize(),this.initModules()}},o.prototype.refreshOnResize=function(){if(this.lgOpened){var t=this.galleryItems[this.index],e=t.__slideVideoInfo;this.mediaContainerPosition=this.getMediaContainerPosition();var i=this.mediaContainerPosition,s=i.top,r=i.bottom;if(this.currentImageSize=b.getSize(this.items[this.index],this.outer,s+r,e&&this.settings.videoMaxSize),e&&this.resizeVideoSlide(this.index,this.currentImageSize),this.zoomFromOrigin&&!this.isDummyImageRemoved){var n=this.getDummyImgStyles(this.currentImageSize);this.outer.find(".lg-current .lg-dummy-img").first().attr("style",n)}this.LGel.trigger(p.containerResize)}},o.prototype.resizeVideoSlide=function(t,e){var i=this.getVideoContStyle(e),s=this.getSlideItem(t);s.find(".lg-video-cont").attr("style",i)},o.prototype.updateSlides=function(t,e){if(this.index>t.length-1&&(this.index=t.length-1),t.length===1&&(this.index=0),!t.length){this.closeGallery();return}var i=this.galleryItems[e].src;this.galleryItems=t,this.updateControls(),this.$inner.empty(),this.currentItemsInDom=[];var s=0;this.galleryItems.some(function(r,n){return r.src===i?(s=n,!0):!1}),this.currentItemsInDom=this.organizeSlideItems(s,-1),this.loadContent(s,!0),this.getSlideItem(s).addClass("lg-current"),this.index=s,this.updateCurrentCounter(s),this.LGel.trigger(p.updateSlides)},o.prototype.getItems=function(){if(this.items=[],this.settings.dynamic)return this.settings.dynamicEl||[];if(this.settings.selector==="this")this.items.push(this.el);else if(this.settings.selector)if(typeof this.settings.selector=="string")if(this.settings.selectWithin){var t=c(this.settings.selectWithin);this.items=t.find(this.settings.selector).get()}else this.items=this.el.querySelectorAll(this.settings.selector);else this.items=this.settings.selector;else this.items=this.el.children;return b.getDynamicOptions(this.items,this.settings.extraProps,this.settings.getCaptionFromTitleOrAlt,this.settings.exThumbImage)},o.prototype.shouldHideScrollbar=function(){return this.settings.hideScrollbar&&document.body===this.settings.container},o.prototype.hideScrollbar=function(){if(this.shouldHideScrollbar()){this.bodyPaddingRight=parseFloat(c("body").style().paddingRight);var t=document.documentElement.getBoundingClientRect(),e=window.innerWidth-t.width;c(document.body).css("padding-right",e+this.bodyPaddingRight+"px"),c(document.body).addClass("lg-overlay-open")}},o.prototype.resetScrollBar=function(){this.shouldHideScrollbar()&&(c(document.body).css("padding-right",this.bodyPaddingRight+"px"),c(document.body).removeClass("lg-overlay-open"))},o.prototype.openGallery=function(t,e){var i=this;if(t===void 0&&(t=this.settings.index),!this.lgOpened){this.lgOpened=!0,this.outer.removeClass("lg-hide-items"),this.hideScrollbar(),this.$container.addClass("lg-show");var s=this.getItemsToBeInsertedToDom(t,t);this.currentItemsInDom=s;var r="";s.forEach(function(a){r=r+('<div id="'+a+'" class="lg-item"></div>')}),this.$inner.append(r),this.addHtml(t);var n="";this.mediaContainerPosition=this.getMediaContainerPosition();var l=this.mediaContainerPosition,g=l.top,h=l.bottom;this.settings.allowMediaOverlap||this.setMediaContainerPosition(g,h);var d=this.galleryItems[t].__slideVideoInfo;this.zoomFromOrigin&&e&&(this.currentImageSize=b.getSize(e,this.outer,g+h,d&&this.settings.videoMaxSize),n=b.getTransform(e,this.outer,g,h,this.currentImageSize)),(!this.zoomFromOrigin||!n)&&(this.outer.addClass(this.settings.startClass),this.getSlideItem(t).removeClass("lg-complete"));var u=this.settings.zoomFromOrigin?100:this.settings.backdropDuration;setTimeout(function(){i.outer.addClass("lg-components-open")},u),this.index=t,this.LGel.trigger(p.beforeOpen),this.getSlideItem(t).addClass("lg-current"),this.lGalleryOn=!1,this.prevScrollTop=c(window).scrollTop(),
Line truncated
|
||||
<span id="`+this.getIdName("lg-counter-current")+'" class="lg-counter-current">'+(this.index+1)+` </span> /
|
||||
<span id="`+this.getIdName("lg-counter-all")+'" class="lg-counter-all">'+this.galleryItems.length+" </span></div>";this.outer.find(this.settings.appendCounterTo).append(t)}},o.prototype.addHtml=function(t){var e,i;if(this.galleryItems[t].subHtmlUrl?i=this.galleryItems[t].subHtmlUrl:e=this.galleryItems[t].subHtml,!i)if(e){var s=e.substring(0,1);if(s==="."||s==="#")try{this.settings.subHtmlSelectorRelative&&!this.settings.dynamic?e=c(this.items).eq(t).find(e).first().html():e=c(e).first().html()}catch{console.warn('Error processing subHtml selector "'+e+'"'),e=""}}else e="";if(this.settings.appendSubHtmlTo!==".lg-item")i?b.fetchCaptionFromUrl(i,this.outer.find(".lg-sub-html"),"replace"):this.outer.find(".lg-sub-html").html(e);else{var r=c(this.getSlideItemId(t));i?b.fetchCaptionFromUrl(i,r,"append"):r.append('<div class="lg-sub-html">'+e+"</div>")}typeof e<"u"&&e!==null&&(e===""?this.outer.find(this.settings.appendSubHtmlTo).addClass("lg-empty-html"):this.outer.find(this.settings.appendSubHtmlTo).removeClass("lg-empty-html")),this.LGel.trigger(p.afterAppendSubHtml,{index:t})},o.prototype.preload=function(t){for(var e=1;e<=this.settings.preload&&!(e>=this.galleryItems.length-t);e++)this.loadContent(t+e,!1);for(var i=1;i<=this.settings.preload&&!(t-i<0);i++)this.loadContent(t-i,!1)},o.prototype.getDummyImgStyles=function(t){return t?"width:"+t.width+`px;
|
||||
margin-left: -`+t.width/2+`px;
|
||||
margin-top: -`+t.height/2+`px;
|
||||
height:`+t.height+"px":""},o.prototype.getVideoContStyle=function(t){return t?"width:"+t.width+`px;
|
||||
height:`+t.height+"px":""},o.prototype.getDummyImageContent=function(t,e,i){var s;if(this.settings.dynamic||(s=c(this.items).eq(e)),s){var r=void 0;if(this.settings.exThumbImage?r=s.attr(this.settings.exThumbImage):r=s.find("img").first().attr("src"),!r)return"";var n=this.getDummyImgStyles(this.currentImageSize),l=document.createElement("img");return l.alt=i||"",l.src=r,l.className="lg-dummy-img",l.style.cssText=n,t.addClass("lg-first-slide"),this.outer.addClass("lg-first-slide-loading"),l}return""},o.prototype.setImgMarkup=function(t,e,i){var s=this.galleryItems[i],r=s.alt,n=s.srcset,l=s.sizes,g=s.sources,h="",d=r?'alt="'+r+'"':"";this.isFirstSlideWithZoomAnimation()?h=this.getDummyImageContent(e,i,d):h=b.getImgMarkup(i,t,d,n,l,g);var u=document.createElement("picture");u.className="lg-img-wrap",c(u).append(h),e.prepend(u)},o.prototype.onSlideObjectLoad=function(t,e,i,s){var r=t.find(".lg-object").first();b.isImageLoaded(r.get())||e?i():(r.on("load.lg error.lg",function(){i&&i()}),r.on("error.lg",function(){s&&s()}))},o.prototype.onLgObjectLoad=function(t,e,i,s,r,n){var l=this;this.onSlideObjectLoad(t,n,function(){l.triggerSlideItemLoad(t,e,i,s,r)},function(){t.addClass("lg-complete lg-complete_"),t.html('<span class="lg-error-msg">'+l.settings.strings.mediaLoadingFailed+"</span>")})},o.prototype.triggerSlideItemLoad=function(t,e,i,s,r){var n=this,l=this.galleryItems[e],g=r&&this.getSlideType(l)==="video"&&!l.poster?s:0;setTimeout(function(){t.addClass("lg-complete lg-complete_"),n.LGel.trigger(p.slideItemLoad,{index:e,delay:i||0,isFirstSlide:r})},g)},o.prototype.isFirstSlideWithZoomAnimation=function(){return!!(!this.lGalleryOn&&this.zoomFromOrigin&&this.currentImageSize)},o.prototype.addSlideVideoInfo=function(t){var e=this;t.forEach(function(i,s){i.__slideVideoInfo=b.isVideo(i.src,!!i.video,s),i.__slideVideoInfo&&e.settings.loadYouTubePoster&&!i.poster&&i.__slideVideoInfo.youtube&&(i.poster="//img.youtube.com/vi/"+i.__slideVideoInfo.youtube[1]+"/maxresdefault.jpg")})},o.prototype.loadContent=function(t,e){var i=this,s=this.galleryItems[t],r=c(this.getSlideItemId(t)),n=s.poster,l=s.srcset,g=s.sizes,h=s.sources,d=s.src,u=s.video,a=u&&typeof u=="string"?JSON.parse(u):u;if(s.responsive){var m=s.responsive.split(",");d=b.getResponsiveSrc(m)||d}var f=s.__slideVideoInfo,y="",w=!!s.iframe,v=!this.lGalleryOn,C=0;if(v&&(this.zoomFromOrigin&&this.currentImageSize?C=this.settings.startAnimationDuration+10:C=this.settings.backdropDuration+10),!r.hasClass("lg-loaded")){if(f){var P=this.mediaContainerPosition,F=P.top,B=P.bottom,H=b.getSize(this.items[t],this.outer,F+B,f&&this.settings.videoMaxSize);y=this.getVideoContStyle(H)}if(w){var T=b.getIframeMarkup(this.settings.iframeWidth,this.settings.iframeHeight,this.settings.iframeMaxWidth,this.settings.iframeMaxHeight,d,s.iframeTitle);r.prepend(T)}else if(n){var G="",W=v&&this.zoomFromOrigin&&this.currentImageSize;W&&(G=this.getDummyImageContent(r,t,""));var T=b.getVideoPosterMarkup(n,G||"",y,this.settings.strings.playVideo,f);r.prepend(T)}else if(f){var T='<div class="lg-video-cont " style="'+y+'"></div>';r.prepend(T)}else if(this.setImgMarkup(d,r,t),l||h){var Y=r.find(".lg-object");this.initPictureFill(Y)}(n||f)&&this.LGel.trigger(p.hasVideo,{index:t,src:d,html5Video:a,hasPoster:!!n}),this.LGel.trigger(p.afterAppendSlide,{index:t}),this.lGalleryOn&&this.settings.appendSubHtmlTo===".lg-item"&&this.addHtml(t)}var x=0;C&&!c(document.body).hasClass("lg-from-hash")&&(x=C),this.isFirstSlideWithZoomAnimation()&&(setTimeout(function(){r.removeClass("lg-start-end-progress lg-start-progress").removeAttr("style")},this.settings.startAnimationDuration+100),r.hasClass("lg-loaded")||setTimeout(function(){if(i.getSlideType(s)==="image"){var k=s.alt,R=k?'alt="'+k+'"':"";if(r.find(".lg-img-wrap").append(b.getImgMarkup(t,d,R,l,g,s.sources)),l||h){var Z=r.find(".lg-object");i.initPictureFill(Z)}}(i.getSlideType(s)==="image"||i.getSlideType(s)==="video"&&n)&&(i.onLgObjectLoad(r,t,C,x,!0,!1),i.onSlideObjectLoad(r,!!(f&&f.html5&&!n),function(){i.loadContentOnFirstSlideLoad(t,r,x)},function(){i.loadContentOnFirstSlideLoad(t,r,x)}))},this.settings.startAnimationDuration+100)),r.addClass("lg-loaded"),(!this.isFirstSlideWithZoomAnimation()||this.getSlideType(s)==="video"&&!n)&&this.onLgObjectLoad(r,t,C,x,v,!!(f&&f.html5&&!n)),(!this.zoomFromOrigin||!this.currentImageSize)&&r.hasClass("lg-complete_")&&!this.lGalleryOn&&setTimeout(function(){r.addClass("lg-complete")},this.settings.backdropDuration),this.lGalleryOn=!0,e===!0&&(r.hasClass("lg-complete_")?this.preload(t):r.find(".lg-object").first().on("load.lg error.lg",function(){i.preload(t)}))},o.prototype.loadContentOnFirstSlideLoad=function(t,e,i){var s=this;setTimeout(function(){e.find(".lg-dummy-img").remove(),e.removeClass("lg-first-slide"),s.outer.removeClass("lg-first-slide-loading"),s.isDummyImageRemoved=!0,s.preload(t)},i+300)},o.prototype.getItemsToBeInsertedToDom=function(t,e,i){var s=this;i===void 0&&(i=0);var r=[]
Line truncated
|
||||
<div class="lg-thumb lg-group">
|
||||
</div>
|
||||
</div>`;this.core.outer.addClass("lg-has-thumb"),this.settings.appendThumbnailsTo===".lg-components"?this.core.$lgComponents.append(e):this.core.outer.append(e),this.$thumbOuter=this.core.outer.find(".lg-thumb-outer").first(),this.$lgThumb=this.core.outer.find(".lg-thumb").first(),this.settings.animateThumb&&this.core.outer.find(".lg-thumb").css("transition-duration",this.core.settings.speed+"ms").css("width",this.thumbTotalWidth+"px").css("position","relative"),this.setThumbItemHtml(this.core.galleryItems)},o.prototype.enableThumbDrag=function(){var t=this,e={cords:{startX:0,endX:0},isMoved:!1,newTranslateX:0,startTime:new Date,endTime:new Date,touchMoveTime:0},i=!1;this.$thumbOuter.addClass("lg-grab"),this.core.outer.find(".lg-thumb").first().on("mousedown.lg.thumb",function(s){t.thumbTotalWidth>t.thumbOuterWidth&&(s.preventDefault(),e.cords.startX=s.pageX,e.startTime=new Date,t.thumbClickable=!1,i=!0,t.core.outer.get().scrollLeft+=1,t.core.outer.get().scrollLeft-=1,t.$thumbOuter.removeClass("lg-grab").addClass("lg-grabbing"))}),this.$LG(window).on("mousemove.lg.thumb.global"+this.core.lgId,function(s){t.core.lgOpened&&i&&(e.cords.endX=s.pageX,e=t.onThumbTouchMove(e))}),this.$LG(window).on("mouseup.lg.thumb.global"+this.core.lgId,function(){t.core.lgOpened&&(e.isMoved?e=t.onThumbTouchEnd(e):t.thumbClickable=!0,i&&(i=!1,t.$thumbOuter.removeClass("lg-grabbing").addClass("lg-grab")))})},o.prototype.enableThumbSwipe=function(){var t=this,e={cords:{startX:0,endX:0},isMoved:!1,newTranslateX:0,startTime:new Date,endTime:new Date,touchMoveTime:0};this.$lgThumb.on("touchstart.lg",function(i){t.thumbTotalWidth>t.thumbOuterWidth&&(i.preventDefault(),e.cords.startX=i.targetTouches[0].pageX,t.thumbClickable=!1,e.startTime=new Date)}),this.$lgThumb.on("touchmove.lg",function(i){t.thumbTotalWidth>t.thumbOuterWidth&&(i.preventDefault(),e.cords.endX=i.targetTouches[0].pageX,e=t.onThumbTouchMove(e))}),this.$lgThumb.on("touchend.lg",function(){e.isMoved?e=t.onThumbTouchEnd(e):t.thumbClickable=!0})},o.prototype.rebuildThumbnails=function(){var t=this;this.$thumbOuter.addClass("lg-rebuilding-thumbnails"),setTimeout(function(){t.thumbTotalWidth=t.core.galleryItems.length*(t.settings.thumbWidth+t.settings.thumbMargin),t.$lgThumb.css("width",t.thumbTotalWidth+"px"),t.$lgThumb.empty(),t.setThumbItemHtml(t.core.galleryItems),t.animateThumb(t.core.index)},50),setTimeout(function(){t.$thumbOuter.removeClass("lg-rebuilding-thumbnails")},200)},o.prototype.setTranslate=function(t){this.$lgThumb.css("transform","translate3d(-"+t+"px, 0px, 0px)")},o.prototype.getPossibleTransformX=function(t){return t>this.thumbTotalWidth-this.thumbOuterWidth&&(t=this.thumbTotalWidth-this.thumbOuterWidth),t<0&&(t=0),t},o.prototype.animateThumb=function(t){if(this.$lgThumb.css("transition-duration",this.core.settings.speed+"ms"),this.settings.animateThumb){var e=0;switch(this.settings.currentPagerPosition){case"left":e=0;break;case"middle":e=this.thumbOuterWidth/2-this.settings.thumbWidth/2;break;case"right":e=this.thumbOuterWidth-this.settings.thumbWidth}this.translateX=(this.settings.thumbWidth+this.settings.thumbMargin)*t-1-e,this.translateX>this.thumbTotalWidth-this.thumbOuterWidth&&(this.translateX=this.thumbTotalWidth-this.thumbOuterWidth),this.translateX<0&&(this.translateX=0),this.setTranslate(this.translateX)}},o.prototype.onThumbTouchMove=function(t){return t.newTranslateX=this.translateX,t.isMoved=!0,t.touchMoveTime=new Date().valueOf(),t.newTranslateX-=t.cords.endX-t.cords.startX,t.newTranslateX=this.getPossibleTransformX(t.newTranslateX),this.setTranslate(t.newTranslateX),this.$thumbOuter.addClass("lg-dragging"),t},o.prototype.onThumbTouchEnd=function(t){t.isMoved=!1,t.endTime=new Date,this.$thumbOuter.removeClass("lg-dragging");var e=t.endTime.valueOf()-t.startTime.valueOf(),i=t.cords.endX-t.cords.startX,s=Math.abs(i)/e;return s>.15&&t.endTime.valueOf()-t.touchMoveTime<30?(s+=1,s>2&&(s+=1),s=s+s*(Math.abs(i)/this.thumbOuterWidth),this.$lgThumb.css("transition-duration",Math.min(s-1,2)+"settings"),i=i*s,this.translateX=this.getPossibleTransformX(this.translateX-i),this.setTranslate(this.translateX)):this.translateX=t.newTranslateX,Math.abs(t.cords.endX-t.cords.startX)<this.settings.thumbnailSwipeThreshold&&(this.thumbClickable=!0),t},o.prototype.getThumbHtml=function(t,e,i){var s=this.core.galleryItems[e].__slideVideoInfo||{},r;s.youtube&&this.settings.loadYouTubeThumbnail?r="//img.youtube.com/vi/"+s.youtube[1]+"/"+this.settings.youTubeThumbSize+".jpg":r=t;var n=document.createElement("div");n.setAttribute("data-lg-item-id",e+""),n.className="lg-thumb-item "+(e===this.core.index?"active":""),n.style.cssText="width: "+this.settings.thumbWidth+"px; height: "+this.settings.thumbHeight+"; margin-right: "+this.settings.thumbMargin+"px;";var l=document.createElement("img");return l.alt=i||"",l.setAttribute("data-lg-item-id",e+""),l.src=r,n.appendChild(l),n},o.prototype.setThumbItemHtml=function(t){for(var e=0;e<t.length;e++){var i=this.getLine truncated
|
||||
webkitfullscreenchange.lg.global`+this.core.lgId+`
|
||||
mozfullscreenchange.lg.global`+this.core.lgId+`
|
||||
MSFullscreenChange.lg.global`+this.core.lgId,function(){t.core.lgOpened&&t.core.outer.toggleClass("lg-fullscreen-on")}),this.core.outer.find(".lg-fullscreen").first().on("click.lg",function(){t.isFullScreen()?t.exitFullscreen():t.requestFullscreen()})},o.prototype.closeGallery=function(){this.isFullScreen()&&this.exitFullscreen()},o.prototype.destroy=function(){this.$LG(document).off("fullscreenchange.lg.global"+this.core.lgId+`
|
||||
webkitfullscreenchange.lg.global`+this.core.lgId+`
|
||||
mozfullscreenchange.lg.global`+this.core.lgId+`
|
||||
MSFullscreenChange.lg.global`+this.core.lgId)},o})(),X=new WeakSet;function rt(o){return!!X.has(o)}function nt(o){X.add(o)}function lt(o,t){if(!o)return t||"";var e=o.split("|");if(e.length<8)return t||"";var i=e[0],s=e[1],r=e[2],n=e[3],l=e[4],g=e[5],h=e[6],d=e[7],u=i||s||r||n||l||g||h;if(!u)return t||"";var a='<div class="wi-lg-caption">'+(t||"")+"</div>";if(a+='<div class="wi-lg-exif">',(i||s)&&(a+='<span class="wi-lg-exif__item wi-lg-exif__camera">'+(i?i+" ":"")+s+"</span>"),r&&(a+='<span class="wi-lg-exif__item wi-lg-exif__lens">'+r+"</span>"),a+='<div class="wi-lg-exif__params">',n&&(a+='<span class="wi-lg-exif__item wi-lg-exif__aperture">f/'+n+"</span>"),l&&(a+='<span class="wi-lg-exif__item wi-lg-exif__shutter">'+l+"s</span>"),g&&(a+='<span class="wi-lg-exif__item wi-lg-exif__iso">ISO '+g+"</span>"),h){var m=h+"mm";d&&d!==h&&(m+=" (eq. "+d+"mm)"),a+='<span class="wi-lg-exif__item wi-lg-exif__focal">'+m+"</span>"}return a+="</div></div>",a}function at(o){var t=o.querySelectorAll(".wi-lightgallery-item[data-exif]");t.forEach(function(e){var i=e.getAttribute("data-exif");if(i){var s=e.getAttribute("data-sub-html")||"",r=lt(i,s);r&&r!==s&&e.setAttribute("data-sub-html",r)}})}function gt(){return{selector:".wi-lightgallery-item",plugins:[tt,it,ot],speed:280,mode:"lg-fade",licenseKey:"GPLv3",download:!1,counter:!0,preload:2,thumbWidth:80,thumbHeight:"60px",thumbMargin:4,zoomFromOrigin:!0,actualSize:!1,showZoomInOutIcons:!0,toggleThumb:!0,allowMediaOverlap:!0,mobileSettings:{controls:!0,showCloseIcon:!0,download:!1}}}function O(o){if(!rt(o)){var t=o.querySelectorAll(".wi-lightgallery-item");t.length!==0&&(at(o),J(o,gt()),nt(o))}}function ht(){return window.innerWidth<768}function E(){ut(),dt(),ct(),document.addEventListener("click",function(o){var t=o.target.closest(".lg-toggle-thumb");if(t){o.stopPropagation();var e=t.closest(".lg-outer");e&&e.classList.toggle("wi-thumb-shown")}},!0),document.addEventListener("click",function(o){var t=o.target.closest(".wi-lightgallery-item");t&&setTimeout(function(){var e=document.querySelector(".lg-outer");e&&!e.classList.contains("wi-thumb-shown")&&!ht()&&e.classList.add("wi-thumb-shown")},350)})}function ut(){var o=[".wi-post__body",".wi-page__body"];o.forEach(function(t){var e=document.querySelectorAll(t);e.forEach(function(i){mt(i),O(i)})})}function dt(){var o=document.querySelector(".wi-photos-page__all-groups");if(o){var t=o.querySelectorAll(".wi-photos-page__link");t.forEach(function(r){r.classList.add("wi-lightgallery-item")}),O(o);return}var e=document.querySelector(".wi-photos-page__grid:not(.wi-photos-page__all-groups .wi-photos-page__grid)");if(e){var i=e.querySelectorAll(".wi-photos-page__link");i.forEach(function(r){r.classList.add("wi-lightgallery-item")}),O(e);return}var s=document.querySelectorAll(".wi-photos-page__grid");s.forEach(function(r){ft(r),O(r)})}function ct(){var o=[".wi-moments-page__media",".wi-moments-page__card-media",".wi-moments-page__masonry-media"];o.forEach(function(t){var e=document.querySelectorAll(t);e.forEach(function(i){pt(i),O(i)})})}function mt(o){var t=o.querySelectorAll("img");t.forEach(function(e){var i=e.getAttribute("src");if(i){var s=e.getAttribute("width"),r=e.getAttribute("height");if(!(s&&parseInt(s)<50||r&&parseInt(r)<50)){var n=(e.getAttribute("class")||"").toLowerCase();if(!/emoji|icon|avatar|logo|badge|smiley/.test(n)&&e.getAttribute("role")!=="presentation"&&e.getAttribute("aria-hidden")!=="true"){var l=e.closest("a");if(l){if(l.getAttribute("target")==="_blank")return;if(l.classList.add("wi-lightgallery-item"),!l.getAttribute("data-src")){var g=l.getAttribute("href")||i;l.setAttribute("data-src",g)}var h=e.getAttribute("alt")||"";h&&!l.getAttribute("data-sub-html")&&l.setAttribute("data-sub-html",h)}else{var d=document.createElement("a");d.setAttribute("href",i),d.setAttribute("data-src",i),d.classList.add("wi-lightgallery-item");var u=e.getAttribute("alt")||"";u&&d.setAttribute("data-sub-html",u),e.parentNode.insertBefore(d,e),d.appendChild(e)}}}}})}function ft(o){var t=o.querySelectorAll(".wi-photos-page__link");t.forEach(function(e){e.classList.add("wi-lightgallery-item")})}function pt(o){var t=o.querySelectorAll("img");t.forEach(function(e){var i=e.getAttribute("src");i&&(e.classList.add("wi-lightgallery-item"),e.getAttribute("data-src")||e.setAttribute("data-src",i))})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){"requestIdleCallback"in window?requestIdleCallback(E,{timeout:2e3}):setTimeout(E,200)}):"requestIdleCallback"in window?requestIdleCallback(E,{timeout:2e3}):setTimeout(E,200);
|
||||
@@ -0,0 +1 @@
|
||||
.wi-reading-progress[data-astro-cid-ftxg5n3y]{position:fixed;top:0;left:0;width:0;height:3px;background:var(--accent);z-index:101;transition:width .1s linear}.wi-post[data-astro-cid-ftxg5n3y]{display:grid;gap:1rem;padding-block:1rem;overflow-x:hidden}.wi-post__header[data-astro-cid-ftxg5n3y]{display:grid;gap:var(--space-lg);padding:var(--space-lg) 0 var(--space-md);border-bottom:1px solid var(--rule);text-align:center}.wi-post__cover-wrap[data-astro-cid-ftxg5n3y]{border-radius:12px;overflow:hidden}.wi-post__cover[data-astro-cid-ftxg5n3y]{width:100%;max-height:480px;object-fit:cover;border-radius:12px}.wi-post__title[data-astro-cid-ftxg5n3y]{font-family:var(--font-sans);font-size:clamp(1.8rem,4vw,2.8rem);font-weight:700;line-height:var(--leading-tight);letter-spacing:-.02em;color:var(--ink);margin:0}.wi-page-accent[data-astro-cid-ftxg5n3y]{display:block;width:48px;height:3px;margin:var(--space-sm) auto 0;border-radius:9999px;background:var(--accent)}.wi-post__meta[data-astro-cid-ftxg5n3y]{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:.4rem;color:var(--ink-3);font-size:var(--text-sm);letter-spacing:var(--tracking-wide)}.wi-post__meta[data-astro-cid-ftxg5n3y] a[data-astro-cid-ftxg5n3y]{color:var(--ink-2);text-decoration:none;transition:color var(--duration-fast) var(--ease-out-quart)}.wi-post__meta[data-astro-cid-ftxg5n3y] a[data-astro-cid-ftxg5n3y]:hover{color:var(--accent)}.wi-post__meta-sep[data-astro-cid-ftxg5n3y]{color:var(--ink-3)}.wi-post__author[data-astro-cid-ftxg5n3y]{display:inline-flex;align-items:center;gap:.35rem}.wi-post__author-avatar[data-astro-cid-ftxg5n3y]{width:20px;height:20px;border-radius:50%;object-fit:cover}.wi-post__meta-categories[data-astro-cid-ftxg5n3y] a[data-astro-cid-ftxg5n3y]+a[data-astro-cid-ftxg5n3y]:before{content:", "}.wi-post__meta-tags[data-astro-cid-ftxg5n3y] a[data-astro-cid-ftxg5n3y]+a[data-astro-cid-ftxg5n3y]:before{content:", "}.wi-post__stats[data-astro-cid-ftxg5n3y]{display:inline-flex;align-items:center;gap:.4rem}.wi-post__stat-item[data-astro-cid-ftxg5n3y]+.wi-post__stat-item[data-astro-cid-ftxg5n3y]:before{content:"·";margin-right:.4rem;color:var(--ink-3)}.wi-post__body[data-astro-cid-ftxg5n3y]{width:100%;font-size:clamp(1.05rem,1rem + .25vw,1.15rem);line-height:1.85;color:#3d3530;overflow-wrap:break-word;word-wrap:break-word}html.dark .wi-post__body[data-astro-cid-ftxg5n3y]{color:#d9d2c9}.wi-toc[data-astro-cid-ftxg5n3y]{display:none}@media(min-width:1280px){.wi-toc[data-astro-cid-ftxg5n3y]{display:block;position:fixed;top:80px;width:220px;max-height:calc(100vh - 100px);overflow-y:auto;z-index:10}}.wi-toc__header[data-astro-cid-ftxg5n3y]{display:flex;align-items:center;padding-bottom:var(--space-sm);margin-bottom:var(--space-sm);border-bottom:1px solid var(--rule)}.wi-toc__title[data-astro-cid-ftxg5n3y]{font-family:var(--font-sans);font-size:var(--text-sm);font-weight:600;color:var(--ink);letter-spacing:var(--tracking-wide)}.wi-toc__nav[data-astro-cid-ftxg5n3y]{display:flex;flex-direction:column;gap:0;overflow:hidden}.wi-toc-fab[data-astro-cid-ftxg5n3y]{display:none;position:fixed;bottom:32px;right:24px;width:44px;height:44px;border-radius:50%;border:1px solid var(--rule);background:var(--bg-raised);color:var(--ink-2);cursor:pointer;z-index:20;align-items:center;justify-content:center;box-shadow:var(--shadow-md);transition:background .2s ease,color .2s ease,border-color .2s ease,opacity .3s ease}.wi-toc-fab[data-astro-cid-ftxg5n3y]:hover{color:var(--accent);border-color:var(--accent)}@media(max-width:1279px){.wi-toc-fab[data-astro-cid-ftxg5n3y]{display:inline-flex;bottom:24px}}@media(max-width:640px){.wi-toc-fab[data-astro-cid-ftxg5n3y]{right:16px;bottom:24px;width:40px;height:40px}.wi-post__share-btn[data-astro-cid-ftxg5n3y]{width:44px;height:44px}.wi-like-btn[data-astro-cid-ftxg5n3y]{padding:.75rem 1.75rem}}.wi-toc-drawer[data-astro-cid-ftxg5n3y]{display:none}.wi-toc-drawer--open[data-astro-cid-ftxg5n3y]{display:block}.wi-toc-drawer__overlay[data-astro-cid-ftxg5n3y]{position:fixed;inset:0;background:#0006;z-index:50}.wi-toc-drawer__panel[data-astro-cid-ftxg5n3y]{position:fixed;bottom:0;left:0;right:0;max-height:60vh;background:var(--bg);border-top:1px solid var(--rule);border-radius:16px 16px 0 0;padding:1.25rem;z-index:51;overflow-y:auto;transform:translateY(0);transition:transform .3s cubic-bezier(.16,1,.3,1)}.wi-toc-drawer__header[data-astro-cid-ftxg5n3y]{display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:1px solid var(--rule)}.wi-toc-drawer__title[data-astro-cid-ftxg5n3y]{font-family:var(--font-sans);font-size:var(--text-sm);font-weight:600;color:var(--ink)}.wi-toc-drawer__close[data-astro-cid-ftxg5n3y]{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;background:none;color:var(--ink-3);cursor:pointer}.wi-toc-drawer__close[data-astro-cid-ftxg5n3y]:hover{color:var(--accent)}.wi-toc-drawer__nav[data-astro-cid-ftxg5n3y]{disLine truncated
|
||||
@@ -2,6 +2,24 @@
|
||||
|
||||
## v1.2.5
|
||||
|
||||
### 上架审核整改(v1.2.5 修订)
|
||||
|
||||
- 🔒 **默认零第三方自动请求**:移除全部 21 个模板对 `cdn.jsdelivr.net` 的 `preconnect` / `dns-prefetch`
|
||||
- 🔒 移除文章页 PrismJS 1.29.0、`autoloader`(会按代码语言从 CDN 动态拉取并执行组件)与
|
||||
`qrcode-generator`;代码高亮改由应用市场代码高亮插件 / API 扩展包提供,主题只保留自托管的
|
||||
Warm / Cold token 配色(微信分享改为「复制链接」)
|
||||
- 🔒 一言默认改用内置本地句子库 `assets/data/hitokoto.json`,不再默认请求 `v1.hitokoto.cn`
|
||||
- 🔒 欢迎弹窗默认显示本地静态欢迎语,移除内置的 IP 定位主 / 备选第三方接口;IP 定位改为
|
||||
**管理员显式配置**的可选能力(开启时会向该服务发送访客 IP,需自行披露)
|
||||
- 📄 README 新增「外部资源与隐私」章节(服务名称 / 用途 / 数据类型 / 默认状态 / 关闭方法 /
|
||||
遥测声明)与「验证与回归测试」清单;补充 LXGW WenKai 的 OFL 来源与 LightGallery GPLv3 说明
|
||||
- 🔗 修正上架说明中的无效链接:文档与预览图改为有效绝对地址并随包分发;朋友圈插件链接改为应用市场地址
|
||||
- 🐛 全部 `#strings.substring(..., 0, 1)` 增加空值保护,修复 `friends.html` 作者为空时的字符串越界
|
||||
- 🖼️ `logo.png` 由两份 1024×1024(各约 1.5MB)压缩去重为单份 256×256(约 146KB)
|
||||
- ✅ LightGallery 去掉占位 `licenseKey`,按 GPLv3 开源许可使用,控制台不再出现授权提示
|
||||
- 🧪 新增 `pnpm audit:external` 与打包前置校验:拦截第三方远程加载、预连接、动态加载、
|
||||
README 死链、重复大图;`docs/*.md` 与预览图一并打进安装包
|
||||
|
||||
### 兼容性与元数据
|
||||
|
||||
- ⬆️ Halo 版本要求提升至 **2.27.0+**(页面布局契约、微信读书页面依赖 2.27 的能力)
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
### 代码高亮主题(article_code_theme)
|
||||
|
||||
WarmIsland 使用 Prism.js 实现代码语法高亮,提供两种主题风格:
|
||||
主题不再加载 Prism.js 运行时与 autoloader,**语法高亮由 Halo 应用市场的代码高亮插件或「API 扩展包」服务端高亮输出**。本项只切换主题自托管的两套 `.token` 配色(`assets/vendor/prism-warm.css` / `prism-cold.css`),不产生任何远程请求:
|
||||
|
||||
| 值 | 说明 |
|
||||
| --- | --- |
|
||||
@@ -100,7 +100,7 @@ article_code_theme: warm
|
||||
```
|
||||
|
||||
::: tip
|
||||
代码高亮支持多种编程语言,包括 JavaScript、Python、Java、Go、Rust、YAML、JSON、Bash 等。语言检测基于代码块的语言标记自动完成。
|
||||
支持的语言范围取决于所安装的高亮插件(常见的 JavaScript、Python、Java、Go、Rust、YAML、JSON、Bash 等均可覆盖)。语言识别基于代码块的语言标记,建议始终显式标注。
|
||||
:::
|
||||
|
||||
### 目录展开模式(article_toc_expand)
|
||||
@@ -136,7 +136,7 @@ article_related_title: "猜你喜欢"
|
||||
| 平台 | 说明 |
|
||||
| --- | --- |
|
||||
| QQ | 点击后弹出 QQ 分享窗口,自动携带文章标题和链接 |
|
||||
| 微信 | 扫码分享到微信(生成文章完整 URL 的二维码) |
|
||||
| 微信 | 弹出文章链接并提供「复制链接」按钮,由访客在微信内分享(不加载二维码库) |
|
||||
| 微博 | 分享到新浪微博 |
|
||||
| X (Twitter) | 分享到 X |
|
||||
| Facebook | 分享到 Facebook |
|
||||
@@ -157,7 +157,7 @@ article_related_title: "猜你喜欢"
|
||||
| Telegram | ❌ |
|
||||
|
||||
::: info 图标来源
|
||||
分享按钮图标来自 [Font Awesome](https://fontawesome.com/)(Free v7.2.0,CC BY 4.0 / SIL OFL 1.1 协议)和 [Simple Icons](https://simpleicons.org/)(CC0 协议)。微信图标点击后会弹出二维码弹窗,用户可使用微信扫码分享。
|
||||
分享按钮图标来自 [Font Awesome](https://fontawesome.com/)(Free v7.2.0,CC BY 4.0 / SIL OFL 1.1 协议)和 [Simple Icons](https://simpleicons.org/)(CC0 协议)。微信图标点击后会弹出「复制链接」弹层,用户复制后可在微信内分享;二维码由远程库生成的旧实现已移除,主题不再加载任何第三方脚本。
|
||||
:::
|
||||
|
||||
#### 分享区域标题(article_share_title)
|
||||
@@ -174,7 +174,7 @@ article_share_channels:
|
||||
```
|
||||
|
||||
::: tip
|
||||
分享功能会自动携带文章标题和完整链接,无需手动配置分享内容。微信分享通过二维码实现,二维码内容为文章的完整 URL(包含域名),用户扫码后可直接打开文章。QQ 分享通过弹窗方式打开 QQ 分享页面。移动端会优先显示适合移动端的分享方式。
|
||||
分享功能会自动携带文章标题和完整链接,无需手动配置分享内容。微信分享通过「复制链接」实现:弹层显示文章完整 URL(含域名),点击按钮写入剪贴板,全程在浏览器本地完成、不发送外部请求。QQ 分享通过弹窗方式打开 QQ 分享页面。除访客主动点击外,页面加载时不会访问任何第三方平台。
|
||||
:::
|
||||
|
||||
## 阅读增强功能
|
||||
|
||||
+13
-9
@@ -10,8 +10,8 @@ Hero 首屏是 WarmIsland 主题的标志性区域,占据首页首屏的全部
|
||||
| `hero_title` | 文本 | — | 主标题(留空则使用站点标题) |
|
||||
| `hero_subtitle` | 文本 | — | 副标题(留空则使用站点副标题) |
|
||||
| `hero_description_mode` | 下拉 | `hitokoto` | 描述文案来源:`none` / `hitokoto` / `custom` |
|
||||
| `hero_hitokoto_api` | URL | `https://v1.hitokoto.cn/` | 一言 API 地址 |
|
||||
| `hero_hitokoto_categories` | 多选 | `d, i, k` | 一言句子类型 |
|
||||
| `hero_hitokoto_api` | URL | 留空 | 自定义一言 API;留空使用主题内置本地句子库(零外部请求) |
|
||||
| `hero_hitokoto_categories` | 多选 | `d, i, k` | 句子类型筛选,仅对自定义 API 生效 |
|
||||
| `hero_custom_description` | 多行文本 | — | 自定义描述文案 |
|
||||
| `hero_hitokoto_auto_refresh` | 开关 | `false` | 一言自动刷新 |
|
||||
| `hero_hitokoto_refresh_interval` | 下拉 | `10000` | 一言刷新间隔(秒) |
|
||||
@@ -49,24 +49,28 @@ hero_subtitle: "记录温暖与灵感"
|
||||
| 值 | 说明 |
|
||||
| --- | --- |
|
||||
| `none` | 不显示描述文案 |
|
||||
| `hitokoto` | 从一言 API 实时获取随机句子(默认) |
|
||||
| `hitokoto` | 从主题内置本地句子库随机取一条(默认,零外部请求) |
|
||||
| `custom` | 显示自定义的描述文案 |
|
||||
|
||||
### 一言模式(hitokoto)
|
||||
|
||||
选择 `hitokoto` 模式后,Hero 区域会从一言 API 获取随机句子,每次刷新页面都会展示不同的内容。
|
||||
选择 `hitokoto` 模式后,Hero 区域**默认读取主题内置的本地句子库** `assets/data/hitokoto.json`
|
||||
(随主题自托管的静态 JSON),每次刷新随机展示一条,不依赖任何外部服务,也不会因为第三方接口
|
||||
不可用而出现空白。可直接编辑该 JSON 替换成自己喜欢的句子。
|
||||
|
||||
#### 一言 API 地址(hero_hitokoto_api)
|
||||
#### 自定义一言 API(hero_hitokoto_api)
|
||||
|
||||
默认使用官方 API `https://v1.hitokoto.cn/`。如果你部署了一言的私有实例,可以修改为自建 API 地址。
|
||||
默认**留空**(零外部请求)。如果需要使用在线一言或自建实例,可在此填写 API 地址;
|
||||
填写后浏览器才会向该地址发起 GET 请求,第三方可见访客 IP / User-Agent。
|
||||
请自行评估服务商隐私政策,并在站点隐私说明中披露,详见 README「外部资源与隐私」。
|
||||
|
||||
::: tip
|
||||
自建一言 API 可以提升加载速度并避免第三方服务不稳定的问题。部署方式请参考 [hitokoto 官方文档](https://developer.hitokoto.cn/)。
|
||||
::: warning
|
||||
这是站点管理员主动开启的可选能力。留空即可随时关闭,无需重装主题。
|
||||
:::
|
||||
|
||||
#### 一言句子类型(hero_hitokoto_categories)
|
||||
|
||||
通过勾选来筛选获取的句子类型,支持多选:
|
||||
仅在填写了自定义 API 时生效,用于筛选该接口返回的句子类型,支持多选:
|
||||
|
||||
| 值 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -54,18 +54,36 @@ pnpm build
|
||||
### 打包主题
|
||||
|
||||
```bash
|
||||
pnpm release # 推荐:build → snapshot → package
|
||||
# 或分步:pnpm build && pnpm snapshot && pnpm package
|
||||
pnpm release # 推荐:build → snapshot → package(含合规审计)
|
||||
# 或分步:pnpm build && pnpm snapshot && pnpm audit:external && pnpm package
|
||||
```
|
||||
|
||||
生成 `dist/warm-island-<version>.zip`,可直接在 Halo 后台上传安装。
|
||||
ZIP 根目录直接放 `theme.yaml` / `settings.yaml` / `templates/`,不要额外套一层目录。
|
||||
ZIP 根目录直接放 `theme.yaml` / `settings.yaml` / `templates/`,不要额外套一层目录;
|
||||
`docs/*.md` 与预览图也会一并打包,保证上架说明里的引用在制品中真实存在。
|
||||
|
||||
> **`templates/` 被 `.gitignore` 忽略**,克隆仓库后默认不存在。若跳过 `pnpm build`
|
||||
> 直接 `pnpm package`,过去会产出不含任何模板的空壳包,上传后 Halo 报
|
||||
> `Template index was not found.` 并让首页 404。现在打包脚本会先校验
|
||||
> `index.html` 等模板、资源目录与版本号,不满足即报错退出。
|
||||
|
||||
### 上架合规审计
|
||||
|
||||
`scripts/audit-external.mjs`(`pnpm audit:external`)在打包前自动执行,对应应用市场审核指南
|
||||
1.4 / 4.4.4 / 4.6 / 5.1,检查:
|
||||
|
||||
- 模板中不含第三方 `preconnect` / `dns-prefetch` / `preload`
|
||||
- 不含 `src` / `href` 指向 http(s) 的远程脚本、样式、图片、媒体
|
||||
- 不含硬编码的外部 `fetch()` / 动态 `import()` / CSS `url(https://...)`,
|
||||
也不含 Prism autoloader 之类的动态加载痕迹与被移除的服务域名
|
||||
- README 中的相对链接都能在安装包清单里找到对应文件
|
||||
- `templates/logo.png` 不存在(只允许一份压缩后的 `templates/assets/logo.png`)
|
||||
|
||||
任一不通过,`pnpm package` 直接 `exit 1`,避免打出不合规制品。
|
||||
`<a href="https://...">` 这类需要访客主动点击的外链是允许的,但必须在 README
|
||||
「外部资源与隐私」章节逐项披露。新增第三方资源时,请先自托管到 `public/assets/`
|
||||
并在 README 的许可表中登记来源与协议。
|
||||
|
||||
## 性能与滚动顺滑
|
||||
|
||||
首页滚动曾经卡顿,根因不是 DOM 数量,而是「每帧高成本绘制 + 劫持滚动输入」:
|
||||
@@ -98,60 +116,36 @@ ZIP 根目录直接放 `theme.yaml` / `settings.yaml` / `templates/`,不要额
|
||||
|
||||
## 项目结构
|
||||
|
||||
以下按磁盘实际文件列出(`src/pages/` 下每个 `.astro` 对应一个输出模板,共 21 个):
|
||||
|
||||
```
|
||||
WarmIsland/
|
||||
├── src/ # 源代码
|
||||
│ ├── components/ # Astro 组件
|
||||
│ │ ├── HeroSection.astro # Hero 首屏
|
||||
│ │ ├── PostCard.astro # 文章卡片
|
||||
│ │ ├── Pagination.astro # 分页组件
|
||||
│ │ ├── TableOfContents.astro # 目录组件
|
||||
│ │ ├── ReadingProgress.astro # 阅读进度条
|
||||
│ │ ├── LikeButton.astro # 点赞按钮
|
||||
│ │ ├── CommentSection.astro # 评论区
|
||||
│ │ ├── Footer.astro # 页脚
|
||||
│ │ └── ...
|
||||
│ ├── layouts/ # 布局组件
|
||||
│ │ ├── BaseLayout.astro # 基础布局
|
||||
│ │ └── PostLayout.astro # 文章布局
|
||||
│ ├── lib/ # 工具库
|
||||
│ │ ├── pages.ts # 页面路由定义
|
||||
│ │ └── utils.ts # 工具函数
|
||||
│ ├── pages/ # 页面入口
|
||||
│ │ ├── index.astro # 首页
|
||||
│ │ ├── post.astro # 文章详情
|
||||
│ │ ├── archives.astro # 归档
|
||||
│ │ ├── categories.astro # 分类
|
||||
│ │ ├── tags.astro # 标签
|
||||
│ │ ├── moments.astro # 瞬间
|
||||
│ │ ├── photos.astro # 图库
|
||||
│ │ ├── links.astro # 友情链接
|
||||
│ │ ├── friends.astro # 朋友圈
|
||||
│ │ ├── page.astro # 自定义页面
|
||||
│ │ ├── page_messageboard.astro # 留言板
|
||||
│ │ ├── douban.astro # 豆瓣
|
||||
│ │ ├── equipments.astro # 装备
|
||||
│ │ ├── footprints.astro # 足迹
|
||||
│ │ └── steam.astro # Steam
|
||||
│ └── styles/ # 样式文件
|
||||
│ ├── main.scss # 主样式入口
|
||||
│ ├── _variables.scss # SCSS 变量
|
||||
│ ├── _colors.scss # 颜色定义
|
||||
│ ├── _typography.scss # 排版
|
||||
│ ├── _spacing.scss # 间距
|
||||
│ ├── _animations.scss # 动画
|
||||
│ ├── _mixins.scss # 混入
|
||||
│ └── components/ # 组件样式
|
||||
├── templates/ # Thymeleaf 模板(构建输出)
|
||||
├── public/ # 静态资源
|
||||
│ └── assets/ # 编译后的 CSS/JS
|
||||
├── docs/ # 文档
|
||||
├── theme.yaml # 主题元信息
|
||||
├── settings.yaml # 主题设置定义
|
||||
├── astro.config.mjs # Astro 配置
|
||||
├── package.json # 项目依赖
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
└── vite.config.ts # Vite 配置
|
||||
├── src/
|
||||
│ ├── components/ # 18 个组件:Navbar / Footer / HeroSection / WelcomePopup /
|
||||
│ │ # LightGallery / ClickEffect / ScrollReveal / ThemeSwitcher 等
|
||||
│ ├── layouts/ # Layout.astro(含页面布局契约渲染模式)
|
||||
│ ├── pages/ # index / post / page / archives / categories / category /
|
||||
│ │ # tags / tag / links / friends / moments / moment / photos /
|
||||
│ │ # douban / equipments / important-dates / steam / weread /
|
||||
│ │ # page_messageboard / page_subscriptions / layout
|
||||
│ └── styles/ # main.scss + 12 个 _*.scss 片段
|
||||
├── public/
|
||||
│ ├── assets/
|
||||
│ │ ├── data/ # hitokoto.json 内置本地句子库
|
||||
│ │ ├── fonts/ # LXGW WenKai 本地 woff2 分片与 result.css
|
||||
│ │ ├── vendor/ # 自托管 prism-warm.css / prism-cold.css
|
||||
│ │ └── logo.png # 主题图标(唯一一份,256×256 压缩版)
|
||||
│ ├── error/ # 错误页静态资源
|
||||
│ └── favicon.ico
|
||||
├── compat/assets/ # 历史版本哈希静态资源,构建时合并进 templates/assets
|
||||
├── scripts/ # postbuild / snapshot-assets / package / audit-external / smoke-clickfx
|
||||
├── docs/ # VitePress 文档站(*.md 随安装包分发)
|
||||
├── templates/ # 构建产物(被 .gitignore 忽略)
|
||||
├── dist/ # ZIP 安装包(被 .gitignore 忽略)
|
||||
├── theme.yaml # 主题清单与自定义页面模板注册
|
||||
├── settings.yaml # 后台设置项定义
|
||||
├── astro.config.mjs / package.json / pnpm-lock.yaml / tsconfig.json
|
||||
└── WarmIsland Preview.webp
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
@@ -170,7 +164,8 @@ WarmIsland/
|
||||
|------|------|
|
||||
| [SCSS](https://sass-lang.com/) | 样式预处理,CSS 变量驱动的配色系统 |
|
||||
| 原生 JavaScript | 滚动、菜单、主题切换、无限滚动等交互,无前端框架运行时 |
|
||||
| [LightGallery](https://www.lightgalleryjs.com/) | 图片灯箱(可选) |
|
||||
| [LightGallery](https://www.lightgalleryjs.com/) 2.9.0(GPLv3) | 图片灯箱,默认内置;安装 lightgallery.js 插件后自动让位 |
|
||||
| [LXGW WenKai](https://github.com/lxgw/LxgwWenKai)(OFL 1.1) | 本地字体分片,随主题自托管,无远程字体请求 |
|
||||
| Halo 插件生态 | 评论、代码高亮、服务端字数统计等由 Halo 与可选插件提供 |
|
||||
|
||||
### 构建工具
|
||||
|
||||
+22
-7
@@ -120,11 +120,24 @@ v1.2.1 起安装包会保留各版本的哈希资源(v1.1.7 之后全部覆盖
|
||||
|
||||
安装并启用插件后,页面会自动显示内容。未安装插件时页面会显示友好的提示信息。
|
||||
|
||||
### 一言 API 加载失败?
|
||||
### 主题会向第三方服务发请求吗?
|
||||
|
||||
1. 检查网络是否能访问 `https://v1.hitokoto.cn/`
|
||||
2. 如果在国内服务器,可能存在网络延迟
|
||||
3. 可以在主题设置中更换一言 API 地址为自建镜像
|
||||
**默认不会。** 所有脚本、样式、字体、图标、主题 Logo 与一言句子库都随主题自托管,
|
||||
页面不预连接任何 CDN,也不加载远程脚本。只有两类例外:
|
||||
|
||||
1. 访客**主动点击**分享按钮 / 备案链接 / 应用市场链接时才跳转到对应站点;
|
||||
2. 站点管理员在主题设置里**显式填写**了「自定义一言 API」或「自定义 IP 定位 API」——
|
||||
后者会把访客 IP 发送给该服务,请在站点隐私说明中披露。
|
||||
|
||||
完整逐项说明见 README 的「外部资源与隐私」章节。发版前可运行 `pnpm audit:external` 复核。
|
||||
|
||||
### 一言不显示,或想换成自己的句子?
|
||||
|
||||
1. 主题默认读取内置的 `assets/data/hitokoto.json` 本地句子库,不依赖外部服务;
|
||||
直接编辑该文件即可替换文案
|
||||
2. 想继续使用在线一言,可在主题设置 → Hero 首屏 → 填写「自定义一言 API」;
|
||||
此时才会产生外部请求,且「句子类型」筛选仅对自定义 API 生效
|
||||
3. 若选择「自定义」模式,请在「自定义描述文案」中填写内容
|
||||
|
||||
### 文章目录不显示?
|
||||
|
||||
@@ -135,9 +148,11 @@ v1.2.1 起安装包会保留各版本的哈希资源(v1.1.7 之后全部覆盖
|
||||
|
||||
### 代码高亮不生效?
|
||||
|
||||
1. 确认代码块使用了正确的语言标记(如 \`\`\`python)
|
||||
2. Prism.js 支持自动语言检测,但部分语言需要手动标记
|
||||
3. 检查浏览器控制台是否有 Prism.js 加载错误
|
||||
主题**不再自带 Prism.js 运行时**(含 autoloader),因此不会自动给代码块加语法着色:
|
||||
|
||||
1. 安装 Halo 应用市场的代码高亮插件,或启用「API 扩展包」的服务端代码高亮
|
||||
2. 主题设置 → 文章 → 「代码高亮主题」仅切换本地自托管的 Warm / Cold `.token` 配色
|
||||
3. 确认代码块使用了正确的语言标记(如 \`\`\`python),以便插件识别语言
|
||||
|
||||
## 样式相关
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ WarmIsland(暖屿)是一款为 [Halo](https://halo.run) 打造的博客主
|
||||
进入 **Hero 首屏** 设置:
|
||||
|
||||
- 设置主标题和副标题
|
||||
- 选择描述文案来源(一言/自定义/无)
|
||||
- 选择描述文案来源(一言 / 自定义 / 无);「一言」默认读取主题内置的本地句子库,不请求外部服务
|
||||
- 可选上传背景图片
|
||||
|
||||
### 第五步:页脚信息
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
- **文章目录** — 桌面端侧边悬浮 + 移动端底部抽屉,支持展开模式选项
|
||||
- **阅读进度条** — 实时显示阅读进度
|
||||
- **代码高亮** — Prism.js 支持,Warm / Cold 双主题
|
||||
- **代码高亮** — 由代码高亮插件 / API 扩展包输出,主题提供 Warm / Cold 本地配色,不加载远程脚本
|
||||
- **图片灯箱** — 点击放大,支持左右切换
|
||||
- **图片懒加载** — blur-up 渐显效果,优化加载体验
|
||||
- **点赞功能** — 一键点赞,状态持久化
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
### ⚙️ 灵活配置
|
||||
|
||||
13 个配置分组,覆盖站点的方方面面:
|
||||
17 个配置分组、120 余个配置项,覆盖站点的方方面面:
|
||||
|
||||
- 基础信息、样式、导航栏、Hero 首屏
|
||||
- 首页、文章、瞬间、图库、友情链接
|
||||
@@ -81,13 +81,12 @@
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Astro** — 静态站点生成器,编译模板
|
||||
- **vite-plugin-halo-theme** — Halo 主题 Vite 插件
|
||||
- **Astro 6** — 构建输出 Thymeleaf 模板
|
||||
- **Thymeleaf** — Halo 使用的服务端模板引擎
|
||||
- **Vue 3** — 部分交互组件(搜索、主题切换、光感跟随)
|
||||
- **SCSS** — CSS 预处理器
|
||||
- **Prism.js** — 代码语法高亮
|
||||
- **LightGallery** — 图片灯箱
|
||||
- **原生 JavaScript + SCSS** — 交互与样式(无 Vue / 无远程运行时)
|
||||
- **LightGallery 2.9.0(GPLv3)** — 图片灯箱,随构建自托管打包
|
||||
- **LXGW WenKai(OFL 1.1)** — 本地字体分片,无远程字体请求
|
||||
- **代码高亮** — 交由 Halo 应用市场插件提供,主题只自托管配色 CSS
|
||||
|
||||
## 适用场景
|
||||
|
||||
|
||||
+8
-3
@@ -34,10 +34,15 @@
|
||||
|
||||
## 代码高亮
|
||||
|
||||
使用 Prism.js 实现代码块语法高亮,支持自动语言检测。可在主题设置中选择两种主题:
|
||||
主题**不再自带 Prism.js 运行时**(含按语言从 CDN 动态加载组件的 autoloader)。语法高亮由
|
||||
Halo 应用市场的代码高亮插件或「API 扩展包」服务端高亮输出,主题只提供两套自托管的
|
||||
`.token` 配色,在「主题设置 → 文章 → 代码高亮主题」中切换:
|
||||
|
||||
- **Warm**:浅色代码高亮,与暖色调设计一致
|
||||
- **Cold**:深色代码高亮(Tomorrow Night),适合代码密集型文章
|
||||
- **Warm**:浅色代码高亮,与暖色调设计一致(默认)
|
||||
- **Cold**:深色代码高亮(Tomorrow 风格),适合代码密集型文章
|
||||
|
||||
未安装高亮插件时,代码块仍以等宽字体正常显示,只是没有语法着色;主题不会因此报错,
|
||||
也不会发起任何外部请求。
|
||||
|
||||
## 图片灯箱
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"build": "astro build && node scripts/postbuild.mjs",
|
||||
"snapshot": "node scripts/snapshot-assets.mjs",
|
||||
"package": "node scripts/package.mjs",
|
||||
"audit:external": "node scripts/audit-external.mjs",
|
||||
"release": "pnpm run build && pnpm run snapshot && pnpm run package",
|
||||
"smoke": "node scripts/smoke-clickfx.mjs",
|
||||
"astro": "astro",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"note": "WarmIsland 内置本地一言句子库,纯静态数据,零外部请求。站点可自行编辑本文件替换文案。",
|
||||
"sentences": [
|
||||
{ "text": "暖如灯火,静如留白。", "from": "暖屿" },
|
||||
{ "text": "慢慢来,比较快。", "from": "暖屿" },
|
||||
{ "text": "把日子过成自己喜欢的样子。", "from": "暖屿" },
|
||||
{ "text": "风会记得每一朵花的香。", "from": "暖屿" },
|
||||
{ "text": "愿你在忙碌里,仍留一扇看云的窗。", "from": "暖屿" },
|
||||
{ "text": "写下此刻,便是对时间最轻柔的挽留。", "from": "暖屿" },
|
||||
{ "text": "山不争高,水不争先。", "from": "暖屿" },
|
||||
{ "text": "有些答案,要走很远才遇见。", "from": "暖屿" },
|
||||
{ "text": "夜色温柔,适合读一封长信。", "from": "暖屿" },
|
||||
{ "text": "把灯留着,等一个晚归的人。", "from": "暖屿" },
|
||||
{ "text": "心有所栖,便是归处。", "from": "暖屿" },
|
||||
{ "text": "日常里藏着最不动声色的欢喜。", "from": "暖屿" },
|
||||
{ "text": "不必催促春天,花自会开。", "from": "暖屿" },
|
||||
{ "text": "走过的路,都会变成身上的光。", "from": "暖屿" },
|
||||
{ "text": "一杯热茶,足以安顿一个傍晚。", "from": "暖屿" },
|
||||
{ "text": "愿你所念之人,也在念着你。", "from": "暖屿" },
|
||||
{ "text": "安静不是空白,是留给自己呼吸。", "from": "暖屿" },
|
||||
{ "text": "把复杂留给世界,把简单留给生活。", "from": "暖屿" },
|
||||
{ "text": "长路漫漫,慢慢走也算抵达。", "from": "暖屿" },
|
||||
{ "text": "今天也值得被认真记下。", "from": "暖屿" },
|
||||
{ "text": "微光虽小,足以照亮一隅。", "from": "暖屿" },
|
||||
{ "text": "在喧嚣之外,给自己留一座小岛。", "from": "暖屿" }
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
/* WarmIsland 自托管代码配色(Cold / 深色,参考 Prism Tomorrow 配色)
|
||||
- 仅定义 .token.* 语法着色,供 Halo 代码高亮插件或「API 扩展包」服务端高亮输出使用。
|
||||
- 主题本身不再加载 Prism.js / autoloader / 任何远程脚本。
|
||||
- 本文件随主题分发,不含 @import、url()、远程字体或外部请求。
|
||||
*/
|
||||
pre[class*="language-"],
|
||||
code[class*="language-"] {
|
||||
color: #cccccc;
|
||||
background: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"] {
|
||||
background: #2d2d2d;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #f2777a;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #99cc99;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.token.variable {
|
||||
color: #f99157;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #cc99cc;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #6699cc;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: #ffcc66;
|
||||
}
|
||||
|
||||
.token.bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
/* WarmIsland 自托管代码配色(Warm / 浅色)
|
||||
- 仅定义 .token.* 语法着色,供 Halo 代码高亮插件或「API 扩展包」服务端高亮输出使用。
|
||||
- 主题本身不再加载 Prism.js / autoloader / 任何远程脚本。
|
||||
- 本文件随主题分发,不含 @import、url()、远程字体或外部请求。
|
||||
*/
|
||||
pre[class*="language-"],
|
||||
code[class*="language-"] {
|
||||
color: #4b4038;
|
||||
background: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"] {
|
||||
background: #f7f2ec;
|
||||
border: 1px solid rgba(90, 70, 55, 0.1);
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #8c8279;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #7a6f66;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #b04a3c;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #567c4e;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.token.variable {
|
||||
color: #8a6b3f;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #a0527f;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #3f6e9e;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: #bd7423;
|
||||
}
|
||||
|
||||
.token.bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 外部资源与隐私审计(发版前自动检查,Node 内置模块,无第三方依赖)
|
||||
*
|
||||
* 对应 Halo 应用市场审核指南:
|
||||
* - 1.4 远程执行与动态加载:禁止远程加载核心代码 / 动态拉取并执行组件
|
||||
* - 4.4.4 性能与外部资源:不得预连接外部 CDN、加载远程脚本样式、打包无关大资源
|
||||
* - 4.6 / 2.2:不得出现占位内容、无效链接
|
||||
*
|
||||
* 检查项:
|
||||
* 1. 模板中不得有指向第三方的 <link rel=preconnect|dns-prefetch|preload>
|
||||
* 2. 模板中不得有 src/href 直接指向 http(s) 的 script/link/img/iframe/source/video/audio
|
||||
* 3. 模板与静态资源中不得硬编码 fetch("https://...") / import("https://...") / url(https://...)
|
||||
* 4. 不得出现历史上被审核点名的第三方服务域名,或 autoloader 动态加载痕迹
|
||||
* 5. README 中的相对链接必须在安装包清单中真实存在
|
||||
*
|
||||
* 说明:<a href="https://..."> 这类需要访客主动点击的分享 / 备案 / 应用市场外链是允许的,
|
||||
* 不属于「自动外部请求」,但已在 README「外部资源与隐私」章节逐项披露。
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
/** 曾经造成未披露外部请求的服务域名,出现即视为回归 */
|
||||
const BLOCKED_HOSTS = [
|
||||
"cdn.jsdelivr.net",
|
||||
"unpkg.com",
|
||||
"cdnjs.cloudflare.com",
|
||||
"fonts.googleapis.com",
|
||||
"fonts.gstatic.com",
|
||||
"v1.hitokoto.cn",
|
||||
"myip.ipip.net",
|
||||
"r.inews.qq.com",
|
||||
];
|
||||
|
||||
const REMOTE_LOAD_RE =
|
||||
/<(?:script|link|img|iframe|source|video|audio)\b[^>]*\b(?:src|href)\s*=\s*["']https?:\/\/[^"']+["']/gi;
|
||||
const PRECONNECT_RE =
|
||||
/<link\b[^>]*\brel\s*=\s*["'](?:preconnect|dns-prefetch|preload)["'][^>]*\bhref\s*=\s*["']https?:\/\/[^"']+["']/gi;
|
||||
const REMOTE_FETCH_RE = /\b(?:fetch|importScripts)\s*\(\s*["']https?:\/\/[^"']+["']/gi;
|
||||
const REMOTE_IMPORT_RE = /\bimport\s*\(\s*["']https?:\/\/[^"']+["']/gi;
|
||||
const REMOTE_CSS_URL_RE = /url\(\s*["']?https?:\/\/[^)"']+/gi;
|
||||
const AUTOLOADER_RE = /\b(?:prism-autoloader|languages_path|Prism\.plugins\.autoloader)\b/gi;
|
||||
|
||||
function listFiles(dir, exts, out = []) {
|
||||
if (!fs.existsSync(dir)) return out;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const abs = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
/* 字体目录只有本地 woff2 与版权注释,不参与审计 */
|
||||
if (entry.name === "fonts") continue;
|
||||
listFiles(abs, exts, out);
|
||||
} else if (exts.some((ext) => entry.name.endsWith(ext))) {
|
||||
out.push(abs);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function auditTemplates(templatesDir = path.join(root, "templates")) {
|
||||
const problems = [];
|
||||
const files = listFiles(templatesDir, [".html", ".js", ".css"]);
|
||||
const htmlFiles = files.filter((f) => f.endsWith(".html"));
|
||||
/* 对外报告的模板数只算 templates/ 根目录下的 Halo 页面模板;
|
||||
templates/error/ 下的错误页 HTML 同样参与审计,但不计入模板数。 */
|
||||
const htmlCount = htmlFiles.filter((f) => path.dirname(f) === templatesDir).length;
|
||||
|
||||
if (htmlFiles.length === 0) {
|
||||
problems.push("未找到任何 templates/*.html,构建产物不完整");
|
||||
return { fileCount: 0, htmlCount: 0, problems };
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const rel = path.relative(root, file).replace(/\\/g, "/");
|
||||
const text = fs.readFileSync(file, "utf8");
|
||||
const isHtml = file.endsWith(".html");
|
||||
|
||||
const rules = isHtml
|
||||
? [
|
||||
["远程预连接 / 预加载", PRECONNECT_RE],
|
||||
["远程脚本 / 样式 / 媒体", REMOTE_LOAD_RE],
|
||||
["硬编码外部 fetch", REMOTE_FETCH_RE],
|
||||
["远程动态 import", REMOTE_IMPORT_RE],
|
||||
["autoloader 动态加载", AUTOLOADER_RE],
|
||||
]
|
||||
: [
|
||||
["硬编码外部 fetch", REMOTE_FETCH_RE],
|
||||
["远程动态 import", REMOTE_IMPORT_RE],
|
||||
["CSS 远程 url()", REMOTE_CSS_URL_RE],
|
||||
];
|
||||
|
||||
for (const [label, re] of rules) {
|
||||
re.lastIndex = 0;
|
||||
const hit = re.exec(text);
|
||||
if (hit) {
|
||||
problems.push(`${rel}: ${label} → ${hit[0].slice(0, 110)}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const host of BLOCKED_HOSTS) {
|
||||
if (isHtml && text.includes(host)) {
|
||||
problems.push(`${rel}: 出现已移除的第三方服务域名 ${host}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { fileCount: files.length, htmlCount, problems };
|
||||
}
|
||||
|
||||
export function auditReadmeLinks(entryNames, readmePath = path.join(root, "README.md")) {
|
||||
const problems = [];
|
||||
if (!fs.existsSync(readmePath)) return ["README.md 不存在"];
|
||||
const readme = fs.readFileSync(readmePath, "utf8");
|
||||
const linkRe = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
||||
let match;
|
||||
let checked = 0;
|
||||
while ((match = linkRe.exec(readme)) !== null) {
|
||||
const href = match[1];
|
||||
if (/^(?:https?:|mailto:|#)/.test(href)) continue;
|
||||
let target = href.split("#")[0];
|
||||
if (!target) continue;
|
||||
try {
|
||||
target = decodeURIComponent(target);
|
||||
} catch (e) {
|
||||
/* 保留原值 */
|
||||
}
|
||||
target = target.replace(/^\.\//, "");
|
||||
checked++;
|
||||
if (!entryNames.has(target)) {
|
||||
problems.push(`README.md 相对链接在安装包中不存在:${href}(期望 ${target})`);
|
||||
}
|
||||
}
|
||||
return { problems, checked };
|
||||
}
|
||||
|
||||
function report(label, problems) {
|
||||
if (problems.length === 0) {
|
||||
console.log(`[audit] PASS ${label}`);
|
||||
return 0;
|
||||
}
|
||||
for (const p of problems) console.error(`[audit] FAIL ${label}: ${p}`);
|
||||
return problems.length;
|
||||
}
|
||||
|
||||
const isCli = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
||||
if (isCli) {
|
||||
const templates = auditTemplates();
|
||||
let failures = 0;
|
||||
failures += report(
|
||||
`模板外部资源(${templates.htmlCount} 个 HTML / ${templates.fileCount} 个文件)`,
|
||||
templates.problems
|
||||
);
|
||||
/* 独立运行时按当前磁盘上的打包清单口径检查 README 链接 */
|
||||
const distZip = path.join(root, "dist");
|
||||
const zip = fs.existsSync(distZip)
|
||||
? fs.readdirSync(distZip).filter((f) => f.endsWith(".zip") && f.includes("warm-island"))
|
||||
: [];
|
||||
if (zip.length) {
|
||||
/* 简单口径:zip 存在时仅提示需通过 pnpm package 做完整链接校验 */
|
||||
console.log(`[audit] 提示:README 链接校验在 pnpm package 内执行(发现 ${zip.length} 个安装包)`);
|
||||
} else {
|
||||
console.log("[audit] 提示:README 链接校验在 pnpm package 内执行(当前无安装包)");
|
||||
}
|
||||
console.log(`[audit] 模板审计共 ${failures} 项问题`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
}
|
||||
+48
-3
@@ -10,6 +10,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import zlib from "node:zlib";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { auditTemplates, auditReadmeLinks } from "./audit-external.mjs";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
@@ -131,11 +132,17 @@ const INCLUDE_ROOT = [
|
||||
"settings.yaml",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
/* 上架说明引用的预览图必须真实存在于制品中(审核指南 4.6 / 2.2) */
|
||||
"WarmIsland Preview.webp",
|
||||
"pnpm-lock.yaml",
|
||||
"pnpm-workspace.yaml",
|
||||
];
|
||||
const INCLUDE_DIRS = ["templates"];
|
||||
const EXCLUDE_DIRS = new Set([".git", "node_modules", "src", "dist", "compat", ".astro"]);
|
||||
/* README 引用的 docs/*.md 也要随包分发,避免制品内出现死链 */
|
||||
const INCLUDE_DOCS = ["docs"];
|
||||
const EXCLUDE_DIRS = new Set([
|
||||
".git", "node_modules", "src", "dist", "compat", ".astro", ".vitepress",
|
||||
]);
|
||||
|
||||
/* ---------------- 构建产物前置校验 ----------------
|
||||
* templates/ 被 .gitignore 忽略,克隆仓库或清理工作区后默认不存在。
|
||||
@@ -173,14 +180,16 @@ for (const name of INCLUDE_ROOT) {
|
||||
}
|
||||
}
|
||||
|
||||
function walk(dir, prefix) {
|
||||
function walk(dir, prefix, opts = {}) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const abs = path.join(dir, entry.name);
|
||||
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||
if (entry.isDirectory()) {
|
||||
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
||||
walk(abs, rel);
|
||||
walk(abs, rel, opts);
|
||||
} else {
|
||||
/* docs 只收 Markdown 源文件,不收 VitePress 构建产物 / 依赖 README */
|
||||
if (opts.mdOnly && !entry.name.endsWith(".md")) continue;
|
||||
entries.push({ name: rel, data: fs.readFileSync(abs), date: fs.statSync(abs).mtime });
|
||||
}
|
||||
}
|
||||
@@ -190,6 +199,10 @@ for (const dir of INCLUDE_DIRS) {
|
||||
const full = path.join(root, dir);
|
||||
if (fs.existsSync(full)) walk(full, dir);
|
||||
}
|
||||
for (const dir of INCLUDE_DOCS) {
|
||||
const full = path.join(root, dir);
|
||||
if (fs.existsSync(full)) walk(full, dir, { mdOnly: true });
|
||||
}
|
||||
|
||||
if (!entries.some((entry) => entry.name === "templates/index.html")) {
|
||||
fail("打包清单中缺少 templates/index.html");
|
||||
@@ -201,6 +214,38 @@ if (pkgVersion !== version) {
|
||||
fail(`版本号不一致:package.json=${pkgVersion},theme.yaml=${version}`);
|
||||
}
|
||||
|
||||
/* ---------------- 上架合规前置校验 ----------------
|
||||
* 1. 主题不得出现未披露的第三方自动请求 / 远程动态加载(审核指南 1.4 / 4.4.4)
|
||||
* 2. README 的相对链接必须在制品内真实存在(审核指南 4.6 / 2.2)
|
||||
* 3. logo 只保留一份压缩版,避免打包两份 1.5MB 大图
|
||||
*/
|
||||
const templateAudit = auditTemplates(templatesDir);
|
||||
for (const problem of templateAudit.problems) {
|
||||
console.error(`[package] ${problem}`);
|
||||
}
|
||||
if (templateAudit.problems.length) {
|
||||
fail("外部资源审计未通过:模板仍存在第三方远程加载 / 预连接");
|
||||
}
|
||||
|
||||
const linkAudit = auditReadmeLinks(new Set(entries.map((entry) => entry.name)));
|
||||
for (const problem of linkAudit.problems) {
|
||||
console.error(`[package] ${problem}`);
|
||||
}
|
||||
if (linkAudit.problems.length) {
|
||||
fail("README 存在指向制品中不存在文件的相对链接");
|
||||
}
|
||||
|
||||
if (fs.existsSync(path.join(templatesDir, "logo.png"))) {
|
||||
fail("templates/logo.png 仍存在:应只保留压缩后的 templates/assets/logo.png");
|
||||
}
|
||||
const logoFile = path.join(templatesDir, "assets", "logo.png");
|
||||
if (fs.existsSync(logoFile) && fs.statSync(logoFile).size > 300 * 1024) {
|
||||
fail("templates/assets/logo.png 超过 300KB,请先压缩再打包");
|
||||
}
|
||||
console.log(
|
||||
`[package] 合规审计通过(${templateAudit.htmlCount} 个模板、${linkAudit.checked} 个相对链接、logo 单份压缩)`
|
||||
);
|
||||
|
||||
const outFile = path.join(root, "dist", `warm-island-${version}.zip`);
|
||||
buildZip(entries, outFile);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 构建后置处理:
|
||||
* 1. 复制 logo.png 到 assets(保留历史行为)
|
||||
* 1. 校验主题 logo 存在且只保留一份(public/assets/logo.png → templates/assets/logo.png)
|
||||
* 2. 合并 compat/assets 下的历史版本静态资源,避免站点/代理缓存中的旧 HTML
|
||||
* 仍然引用上一版本的哈希文件名时出现 500(FileNotFoundException)
|
||||
* 3. 校验所有 templates/*.html 引用的静态资源真实存在,缺失即构建失败
|
||||
@@ -16,11 +16,13 @@ const templatesDir = path.join(root, "templates");
|
||||
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
|
||||
// 1. logo
|
||||
const logoSrc = path.join(templatesDir, "logo.png");
|
||||
if (fs.existsSync(logoSrc)) {
|
||||
fs.copyFileSync(logoSrc, path.join(assetsDir, "logo.png"));
|
||||
// 1. logo:旧实现把 templates/logo.png 再复制一份到 assets,安装包里会有两份 1.5MB 大图。
|
||||
// 现在源文件统一放在 public/assets/logo.png(256x256 压缩版),这里只做存在性校验。
|
||||
if (!fs.existsSync(path.join(assetsDir, "logo.png"))) {
|
||||
console.error("[postbuild] 未找到 templates/assets/logo.png(public/assets/logo.png 应随构建复制)");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("[postbuild] logo.png 已去重,仅保留 templates/assets/logo.png");
|
||||
|
||||
// 2. 历史资源合并(已存在的同名文件以当前构建为准,不覆盖)
|
||||
if (fs.existsSync(compatDir)) {
|
||||
|
||||
+11
-9
@@ -36,7 +36,7 @@ spec:
|
||||
id: welcome_popup_enabled
|
||||
label: 启用欢迎弹窗
|
||||
value: false
|
||||
help: 首页显示"欢迎来自 XX·XX·XX 的朋友"的浮动提示弹窗,基于访客 IP 地理定位
|
||||
help: 首页显示浮动欢迎提示;默认只显示本地静态欢迎语,不发起任何外部请求
|
||||
- $formkit: select
|
||||
name: welcome_popup_position
|
||||
id: welcome_popup_position
|
||||
@@ -67,16 +67,16 @@ spec:
|
||||
- $formkit: url
|
||||
name: welcome_popup_api
|
||||
id: welcome_popup_api
|
||||
label: IP 定位主 API
|
||||
value: "https://myip.ipip.net/json"
|
||||
help: 返回 JSON 的 IP 定位主接口,默认 IPIP;留空使用默认值。支持 IPIP/腾讯/ipapi.co 等常见格式
|
||||
label: 自定义 IP 定位主 API(可选)
|
||||
value: ""
|
||||
help: 留空则只显示本地欢迎语。填写后会把访客 IP 发送给该第三方服务换取地理位置,需自行在站点隐私说明中披露,详见 README「外部资源与隐私」
|
||||
if: "$get(welcome_popup_enabled).value === true"
|
||||
- $formkit: url
|
||||
name: welcome_popup_fallback_api
|
||||
id: welcome_popup_fallback_api
|
||||
label: IP 定位备选 API
|
||||
value: "https://r.inews.qq.com/api/ip2city?otype=json"
|
||||
help: 主接口失败时尝试的备选接口,默认腾讯 ip2city;留空则不使用备选
|
||||
label: 自定义 IP 定位备选 API(可选)
|
||||
value: ""
|
||||
help: 留空不使用备选;填写后同样会向第三方发送访客 IP
|
||||
if: "$get(welcome_popup_enabled).value === true"
|
||||
- $formkit: switch
|
||||
name: welcome_popup_show_once
|
||||
@@ -278,8 +278,9 @@ spec:
|
||||
value: hitokoto
|
||||
- $formkit: url
|
||||
name: hero_hitokoto_api
|
||||
label: 一言 API 地址
|
||||
value: https://v1.hitokoto.cn/
|
||||
label: 自定义一言 API(可选)
|
||||
value: ""
|
||||
help: 留空使用主题内置的本地句子库(零外部请求);填写后才会向该地址发起请求,需自行在站点隐私说明中披露,详见 README「外部资源与隐私」
|
||||
- $formkit: checkbox
|
||||
name: hero_hitokoto_categories
|
||||
label: 一言句子类型
|
||||
@@ -501,6 +502,7 @@ spec:
|
||||
- label: Cold
|
||||
value: cold
|
||||
value: warm
|
||||
help: 仅切换主题自托管的本地代码配色样式;语法高亮本身由 Halo 应用市场的代码高亮插件或「API 扩展包」服务端高亮提供,主题不再加载任何远程高亮脚本
|
||||
- $formkit: switch
|
||||
name: article_extra_spacing
|
||||
label: 中英文混排自动加空格
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
th:unless="${link.spec.logo}"
|
||||
class="wi-friends__avatar wi-friends__avatar--placeholder"
|
||||
>
|
||||
<span th:text="${#strings.substring(link.spec.displayName, 0, 1)}"></span>
|
||||
<span th:text="${!#strings.isEmpty(link.spec.displayName) ? #strings.substring(link.spec.displayName, 0, 1) : '·'}"></span>
|
||||
</div>
|
||||
<div class="wi-friends__info">
|
||||
<span class="wi-friends__name" th:text="${link.spec.displayName}"></span>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
class="hero__description"
|
||||
th:if="${theme.config?.hero?.hero_description_mode == 'hitokoto'}"
|
||||
id="wi-hitokoto"
|
||||
th:attr="data-api=${theme.config?.hero?.hero_hitokoto_api ?: 'https://v1.hitokoto.cn/'},data-categories=${theme.config?.hero?.hero_hitokoto_categories},data-auto-refresh=${theme.config?.hero?.hero_hitokoto_auto_refresh == true ? 'true' : 'false'},data-refresh-interval=${theme.config?.hero?.hero_hitokoto_refresh_interval ?: 10000}"
|
||||
th:attr="data-api=${theme.config?.hero?.hero_hitokoto_api ?: ''},data-categories=${theme.config?.hero?.hero_hitokoto_categories},data-auto-refresh=${theme.config?.hero?.hero_hitokoto_auto_refresh == true ? 'true' : 'false'},data-refresh-interval=${theme.config?.hero?.hero_hitokoto_refresh_interval ?: 10000}"
|
||||
></p>
|
||||
<p
|
||||
class="hero__description"
|
||||
@@ -125,7 +125,10 @@
|
||||
var el = document.getElementById("wi-hitokoto");
|
||||
if (!el) return;
|
||||
|
||||
var api = el.getAttribute("data-api") || "https://v1.hitokoto.cn/";
|
||||
/* 默认读取主题内置的本地句子库(同域静态 JSON,零第三方请求)。
|
||||
仅当站点管理员在主题设置里显式填写自定义 API 时,才会产生外部请求。 */
|
||||
var api = el.getAttribute("data-api");
|
||||
var localUrl = "/themes/warm-island/assets/data/hitokoto.json";
|
||||
var categories = el.getAttribute("data-categories");
|
||||
var autoRefresh = el.getAttribute("data-auto-refresh") === "true";
|
||||
var refreshInterval = parseInt(el.getAttribute("data-refresh-interval"), 10) || 10000;
|
||||
@@ -151,15 +154,24 @@
|
||||
el.textContent = "\u2026";
|
||||
}
|
||||
|
||||
return fetch(buildUrl())
|
||||
return fetch(api ? buildUrl() : localUrl)
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
return res.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
var text = data.hitokoto || "";
|
||||
var from = data.from || data.from_who || "";
|
||||
if (from) {
|
||||
var text, from;
|
||||
if (!api && data && Array.isArray(data.sentences) && data.sentences.length) {
|
||||
/* 内置词库:随机取一条,避免每次刷新都是同一句 */
|
||||
var item = data.sentences[Math.floor(Math.random() * data.sentences.length)];
|
||||
text = item.text || "";
|
||||
from = item.from || "";
|
||||
} else {
|
||||
/* 站点显式配置的自定义一言 API */
|
||||
text = data.hitokoto || "";
|
||||
from = data.from || data.from_who || "";
|
||||
}
|
||||
if (from && text) {
|
||||
text += " \u2014\u2014 " + from;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,12 @@
|
||||
plugins: [lgZoom, lgThumbnail, lgFullscreen],
|
||||
speed: 280,
|
||||
mode: 'lg-fade',
|
||||
licenseKey: '0000-0000-0000-0000',
|
||||
/* lightGallery 2.9.0 以 GPLv3 发布,本主题同样采用 GPL-3.0,
|
||||
按其开源许可条款使用(不申请商业授权)。
|
||||
官方示例里的 '0000-0000-000-0000' 会触发
|
||||
"license key is not valid for production use" 警告,
|
||||
这里去掉占位字符串、显式声明许可依据,控制台不再出现该提示。 */
|
||||
licenseKey: 'GPLv3',
|
||||
download: false,
|
||||
counter: true,
|
||||
preload: 2,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
th:unless="${comment.spec.owner.avatar}"
|
||||
class="wi-message-wall__avatar wi-message-wall__avatar--placeholder"
|
||||
>
|
||||
<span th:text="${#strings.substring(comment.spec.owner.displayName, 0, 1)}"></span>
|
||||
<span th:text="${!#strings.isEmpty(comment.spec.owner?.displayName) ? #strings.substring(comment.spec.owner?.displayName, 0, 1) : '·'}"></span>
|
||||
</div>
|
||||
<div class="wi-message-wall__meta">
|
||||
<span class="wi-message-wall__name" th:text="${comment.spec.owner.displayName}"></span>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="wi-welcome-popup"
|
||||
id="wi-welcome-popup"
|
||||
th:if="${theme.config?.welcome_popup?.welcome_popup_enabled == true}"
|
||||
th:attr="data-position=${theme.config?.welcome_popup?.welcome_popup_position ?: 'top-center'},data-duration=${theme.config?.welcome_popup?.welcome_popup_duration ?: 5},data-api=${theme.config?.welcome_popup?.welcome_popup_api},data-fallback-api=${theme.config?.welcome_popup?.welcome_popup_fallback_api},data-show-once=${theme.config?.welcome_popup?.welcome_popup_show_once != false ? 'true' : 'false'}"
|
||||
th:attr="data-position=${theme.config?.welcome_popup?.welcome_popup_position ?: 'top-center'},data-duration=${theme.config?.welcome_popup?.welcome_popup_duration ?: 5},data-api=${theme.config?.welcome_popup?.welcome_popup_api ?: ''},data-fallback-api=${theme.config?.welcome_popup?.welcome_popup_fallback_api ?: ''},data-show-once=${theme.config?.welcome_popup?.welcome_popup_show_once != false ? 'true' : 'false'}"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
@@ -16,7 +16,7 @@
|
||||
<circle cx="12" cy="10" r="3"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="wi-welcome-popup__content" id="wi-welcome-popup-text">加载中…</div>
|
||||
<div class="wi-welcome-popup__content" id="wi-welcome-popup-text">欢迎来访,愿你在这里度过一段安静的时光</div>
|
||||
<button class="wi-welcome-popup__close" type="button" aria-label="关闭">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18"></path>
|
||||
@@ -46,8 +46,12 @@
|
||||
var closeBtn = popup.querySelector(".wi-welcome-popup__close");
|
||||
var progress = document.getElementById("wi-welcome-popup-progress");
|
||||
var duration = parseInt(popup.getAttribute("data-duration"), 10) || 5;
|
||||
var api = popup.getAttribute("data-api") || "https://myip.ipip.net/json";
|
||||
var fallbackApi = popup.getAttribute("data-fallback-api") || "https://r.inews.qq.com/api/ip2city?otype=json";
|
||||
/* 默认零外部请求:主题内置静态欢迎语,不向任何第三方发送访客 IP。
|
||||
仅当站点管理员在主题设置里显式填写自定义 IP 定位 API 时才会 fetch;
|
||||
一旦填写,浏览器会向该服务发送访客 IP,需自行在站点隐私说明中披露
|
||||
(详见 README「外部资源与隐私」)。 */
|
||||
var api = popup.getAttribute("data-api") || "";
|
||||
var fallbackApi = popup.getAttribute("data-fallback-api") || "";
|
||||
|
||||
var hideTimer = null;
|
||||
var progressRaf = null;
|
||||
@@ -91,6 +95,14 @@
|
||||
hideTimer = setTimeout(hidePopup, duration * 1000);
|
||||
}
|
||||
|
||||
function renderWelcome() {
|
||||
if (textEl) {
|
||||
textEl.textContent = "欢迎来访,愿你在这里度过一段安静的时光";
|
||||
}
|
||||
popup.classList.add("wi-welcome-popup--visible");
|
||||
startAutoHide();
|
||||
}
|
||||
|
||||
function renderMessage(location) {
|
||||
var loc = (location || "").trim();
|
||||
if (!loc) loc = "未知星球";
|
||||
@@ -102,26 +114,24 @@
|
||||
}
|
||||
|
||||
function fetchLocation() {
|
||||
if (textEl) {
|
||||
textEl.textContent = "加载中…";
|
||||
}
|
||||
popup.classList.add("wi-welcome-popup--visible");
|
||||
|
||||
/* 按顺序尝试主 API -> 备选 API */
|
||||
/* 按顺序尝试主 API -> 备选 API;两者都为空则完全不发外部请求 */
|
||||
var endpoints = [];
|
||||
if (api) endpoints.push(api);
|
||||
if (fallbackApi && fallbackApi !== api) endpoints.push(fallbackApi);
|
||||
|
||||
if (endpoints.length === 0) {
|
||||
renderMessage("");
|
||||
renderWelcome();
|
||||
return;
|
||||
}
|
||||
|
||||
if (textEl) textEl.textContent = "正在获取位置…";
|
||||
var idx = 0;
|
||||
|
||||
function tryNext() {
|
||||
if (idx >= endpoints.length) {
|
||||
renderMessage("");
|
||||
renderWelcome();
|
||||
return;
|
||||
}
|
||||
var url = endpoints[idx++];
|
||||
@@ -167,15 +177,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* 解析多种 IP API 的返回格式:
|
||||
- IPIP: {"ret":"ok","data":{"location":["中国","河北","张家口","","联通"]}}
|
||||
- 腾讯 ip2city: {"ret":0,"province":"河北","city":"张家口",...}
|
||||
- ipapi.co / ip-api.com: {"country":"China","regionName":"Hebei","city":"Zhangjiakou"}
|
||||
- 太平洋: {"pro":"广东省","city":"广州市",...} */
|
||||
/* 解析常见 IP 定位接口的返回格式(仅在管理员显式配置自定义 API 时才会用到):
|
||||
- { data: { location: [...] } }
|
||||
- { province, city, ... } / { regionName, city, ... }
|
||||
- { pro, city, ... } */
|
||||
function parseLocation(data) {
|
||||
if (!data) return "";
|
||||
|
||||
/* IPIP: data.data.location 数组 */
|
||||
/* data.location 数组格式 */
|
||||
if (data.data && Array.isArray(data.data.location)) {
|
||||
var loc = data.data.location
|
||||
.filter(function (s) {
|
||||
|
||||
@@ -86,8 +86,8 @@ const fragmentAttr = layoutFragment ? "html (head, content)" : undefined;
|
||||
th:href="${theme.config?.basic?.favicon ?: '/favicon.ico'}"
|
||||
/>
|
||||
<link rel="alternate" type="application/rss+xml" th:title="${site.title}" th:href="@{/feed.xml}" />
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
|
||||
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
|
||||
<!-- 主题不预连接任何第三方 CDN:所有脚本、样式、字体均随主题自托管。
|
||||
分享链接等外链只在访客主动点击时打开,不产生自动外部请求。 -->
|
||||
<link rel="stylesheet" th:href="@{/assets/fonts/LXGWWenKai/result.css}" media="print" onload="this.media='all'" />
|
||||
<noscript><link rel="stylesheet" th:href="@{/assets/fonts/LXGWWenKai/result.css}" /></noscript>
|
||||
<style is:inline th:if="${theme.config?.style?.accent_color}" th:utext="${':root { --accent: ' + theme.config?.style?.accent_color + '; --accent-hover: ' + theme.config?.style?.accent_color + '; }'}">/*accent*/</style>
|
||||
|
||||
@@ -141,7 +141,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
/>
|
||||
</div>
|
||||
<div class="wi-douban__poster wi-douban__poster--placeholder" th:if="${#strings.isEmpty(item.poster)}">
|
||||
<span th:text="${#strings.substring(item.name ?: '', 0, 1)}"></span>
|
||||
<span th:text="${!#strings.isEmpty(item.name) ? #strings.substring(item.name, 0, 1) : '·'}"></span>
|
||||
</div>
|
||||
<div class="wi-douban__body">
|
||||
<h3 class="wi-douban__name" th:text="${item.name}"></h3>
|
||||
|
||||
@@ -45,7 +45,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
class="wi-equipments__cover wi-equipments__cover--placeholder"
|
||||
th:if="${#strings.isEmpty(equip.spec?.cover)}"
|
||||
>
|
||||
<span th:text="${#strings.substring(equip.spec?.displayName ?: '', 0, 1)}"></span>
|
||||
<span th:text="${!#strings.isEmpty(equip.spec?.displayName) ? #strings.substring(equip.spec?.displayName, 0, 1) : '·'}"></span>
|
||||
</div>
|
||||
<div class="wi-equipments__body">
|
||||
<h3
|
||||
|
||||
@@ -41,7 +41,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
class="wi-friends-page__avatar wi-friends-page__avatar--placeholder"
|
||||
>
|
||||
<span
|
||||
th:text="${#strings.substring(spec.author ?: '', 0, 1)}"
|
||||
th:text="${!#strings.isEmpty(spec.author) ? #strings.substring(spec.author, 0, 1) : '·'}"
|
||||
></span>
|
||||
</div>
|
||||
<div class="wi-friends-page__author-info">
|
||||
|
||||
@@ -341,7 +341,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<span class="wi-id__avatar">
|
||||
<span
|
||||
class="wi-id__avatar-fallback"
|
||||
th:text="${#strings.substring(p.displayName, 0, 1)}"
|
||||
th:text="${!#strings.isEmpty(p.displayName) ? #strings.substring(p.displayName, 0, 1) : '·'}"
|
||||
>老</span
|
||||
>
|
||||
<img
|
||||
@@ -407,7 +407,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<span class="wi-id__avatar">
|
||||
<span
|
||||
class="wi-id__avatar-fallback"
|
||||
th:text="${#strings.substring(p.displayName, 0, 1)}"
|
||||
th:text="${!#strings.isEmpty(p.displayName) ? #strings.substring(p.displayName, 0, 1) : '·'}"
|
||||
>老</span
|
||||
>
|
||||
<img
|
||||
@@ -466,7 +466,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
<span class="wi-id__avatar">
|
||||
<span
|
||||
class="wi-id__avatar-fallback"
|
||||
th:text="${#strings.substring(p.displayName, 0, 1)}"
|
||||
th:text="${!#strings.isEmpty(p.displayName) ? #strings.substring(p.displayName, 0, 1) : '·'}"
|
||||
>张</span
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -70,7 +70,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
class="wi-links__avatar wi-links__avatar--placeholder"
|
||||
>
|
||||
<span
|
||||
th:text="${#strings.substring(link.spec.displayName, 0, 1)}"
|
||||
th:text="${!#strings.isEmpty(link.spec.displayName) ? #strings.substring(link.spec.displayName, 0, 1) : '·'}"
|
||||
></span>
|
||||
</div>
|
||||
<!-- 可达性检测徽章(plugin-links 后台「友链检测」产生):绿点=可访问,红点=异常,未检测不显示 -->
|
||||
|
||||
@@ -43,7 +43,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
class="wi-moment-detail__avatar wi-moment-detail__avatar--fallback"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span th:text="${#strings.substring(site.title, 0, 1)}"></span>
|
||||
<span th:text="${!#strings.isEmpty(site.title) ? #strings.substring(site.title, 0, 1) : '·'}"></span>
|
||||
</div>
|
||||
|
||||
<div class="wi-moment-detail__main">
|
||||
|
||||
@@ -45,7 +45,7 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
class="wi-moments-page__avatar wi-moments-page__avatar--fallback"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span th:text="${#strings.substring(site.title, 0, 1)}"></span>
|
||||
<span th:text="${!#strings.isEmpty(site.title) ? #strings.substring(site.title, 0, 1) : '·'}"></span>
|
||||
</div>
|
||||
|
||||
<div class="wi-moments-page__main">
|
||||
|
||||
@@ -18,7 +18,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
订阅
|
||||
</h1> <p class="wi-subs__subtitle" th:text="${theme.config?.subscriptions?.subscriptions_subtitle ?: '博友们的最新文章'}">
|
||||
博友们的最新文章
|
||||
</p> <span class="wi-page-accent"></span> </div> <th:block th:if="${feedPage != null and !#lists.isEmpty(feedPage.items)}"> <div class="wi-subs__flow"> <article class="wi-subs__card" th:each="item : ${feedPage.items}" th:with="authorName=${item.author ?: '匿名'}"> <div class="wi-subs__author"> <img th:if="${item.authorLogo}" th:src="${item.authorLogo}" th:alt="${authorName}" class="wi-subs__avatar" loading="lazy" decoding="async"> <div th:unless="${item.authorLogo}" class="wi-subs__avatar wi-subs__avatar--placeholder"> <span th:text="${#strings.substring(authorName, 0, 1)}"></span> </div> <div class="wi-subs__author-info"> <a th:if="${item.authorUrl}" th:href="${item.authorUrl}" target="_blank" rel="noopener noreferrer" class="wi-subs__author-name" th:text="${authorName}"></a> <span th:unless="${item.authorUrl}" class="wi-subs__author-name" th:text="${authorName}"></span> <time class="wi-subs__date" th:if="${item.publishedAt}" th:text="${#temporals.format(item.publishedAt, 'yyyy-MM-dd')}"></time> </div> </div> <div class="wi-subs__body"> <a th:href="${item.url}" target="_blank" rel="noopener noreferrer" class="wi-subs__title-link"> <h2 class="wi-subs__post-title" th:text="${item.title}"></h2> </a> <p class="wi-subs__summary" th:if="${item.summary}" th:text="${item.summary}"></p> </div> </article> </div> <nav class="wi-subs__pagination" th:if="${loadingMode != 'infinite_scroll' and (hasCursor or feedPage.hasNext)}" th:with="permalink=${singlePage.status.permalink}"> <a th:if="${hasCursor}" th:href="@{${permalink}}" class="wi-subs__page-link" th:text="'较新'">
|
||||
</p> <span class="wi-page-accent"></span> </div> <th:block th:if="${feedPage != null and !#lists.isEmpty(feedPage.items)}"> <div class="wi-subs__flow"> <article class="wi-subs__card" th:each="item : ${feedPage.items}" th:with="authorName=${item.author ?: '匿名'}"> <div class="wi-subs__author"> <img th:if="${item.authorLogo}" th:src="${item.authorLogo}" th:alt="${authorName}" class="wi-subs__avatar" loading="lazy" decoding="async"> <div th:unless="${item.authorLogo}" class="wi-subs__avatar wi-subs__avatar--placeholder"> <span th:text="${!#strings.isEmpty(authorName) ? #strings.substring(authorName, 0, 1) : '·'}"></span> </div> <div class="wi-subs__author-info"> <a th:if="${item.authorUrl}" th:href="${item.authorUrl}" target="_blank" rel="noopener noreferrer" class="wi-subs__author-name" th:text="${authorName}"></a> <span th:unless="${item.authorUrl}" class="wi-subs__author-name" th:text="${authorName}"></span> <time class="wi-subs__date" th:if="${item.publishedAt}" th:text="${#temporals.format(item.publishedAt, 'yyyy-MM-dd')}"></time> </div> </div> <div class="wi-subs__body"> <a th:href="${item.url}" target="_blank" rel="noopener noreferrer" class="wi-subs__title-link"> <h2 class="wi-subs__post-title" th:text="${item.title}"></h2> </a> <p class="wi-subs__summary" th:if="${item.summary}" th:text="${item.summary}"></p> </div> </article> </div> <nav class="wi-subs__pagination" th:if="${loadingMode != 'infinite_scroll' and (hasCursor or feedPage.hasNext)}" th:with="permalink=${singlePage.status.permalink}"> <a th:if="${hasCursor}" th:href="@{${permalink}}" class="wi-subs__page-link" th:text="'较新'">
|
||||
较新
|
||||
</a> <span th:unless="${hasCursor}" class="wi-subs__page-link wi-subs__page-link--placeholder" aria-hidden="true"></span> <span class="wi-subs__page-info"></span> <a th:if="${feedPage.hasNext}" th:href="@{|${permalink}?beforePublishedAt=${feedPage.nextBeforePublishedAt}&beforeId=${feedPage.nextBeforeId}|}" class="wi-subs__page-link" th:text="'较旧'">
|
||||
较旧
|
||||
|
||||
+67
-71
@@ -194,9 +194,11 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
<button class="wi-wechat-qr__close" type="button" aria-label="Close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
<div class="wi-wechat-qr__title" th:text="'微信扫码分享'">微信扫码分享</div>
|
||||
<canvas class="wi-wechat-qr__canvas" id="wi-wechat-qr-canvas"></canvas>
|
||||
<div class="wi-wechat-qr__hint" th:text="'打开微信,扫描二维码即可分享'">打开微信,扫描二维码即可分享</div>
|
||||
<div class="wi-wechat-qr__title" th:text="'微信分享'">微信分享</div>
|
||||
<!-- 微信分享不加载任何远程二维码库:改为复制文章链接,由访客在微信内分享 -->
|
||||
<input class="wi-wechat-qr__url" id="wi-wechat-qr-url" type="text" readonly="readonly" aria-label="文章链接" />
|
||||
<button class="wi-wechat-qr__copy" id="wi-wechat-qr-copy" type="button" th:text="'复制链接'">复制链接</button>
|
||||
<div class="wi-wechat-qr__hint" th:text="'复制链接后,在微信中发送给好友'">复制链接后,在微信中发送给好友</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -295,37 +297,20 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
|
||||
<LightGallery />
|
||||
|
||||
<!-- 代码高亮:主题不再加载远程语法高亮运行时与自动加载插件(后者会按代码语言
|
||||
从 CDN 动态拉取并执行组件,属于远程动态加载)。语法高亮改由应用市场的
|
||||
代码高亮插件或「API 扩展包」服务端高亮输出,主题只自托管两套 token 配色。 -->
|
||||
<link
|
||||
th:if="${theme.config?.article?.article_code_theme == 'cold'}"
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css"
|
||||
href="/themes/warm-island/assets/vendor/prism-cold.css"
|
||||
/>
|
||||
<link
|
||||
th:if="${theme.config?.article?.article_code_theme != 'cold'}"
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css"
|
||||
href="/themes/warm-island/assets/vendor/prism-warm.css"
|
||||
/>
|
||||
|
||||
<script th:if="${theme.config?.article?.article_show_share != false and #strings.contains(theme.config?.article?.article_share_channels ?: 'qq,wechat,weibo', 'wechat')}" is:inline defer src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
|
||||
|
||||
<script is:inline defer src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
|
||||
<script is:inline defer src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
if (typeof Prism !== "undefined") {
|
||||
Prism.plugins.autoloader.languages_path = "https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/";
|
||||
var body = document.getElementById("wi-post-body");
|
||||
if (body) {
|
||||
body.querySelectorAll("pre code").forEach(function (block) {
|
||||
if (!block.classList.contains("language-")) {
|
||||
Prism.highlightElement(block);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script is:inline th:inline="none">
|
||||
(function () {
|
||||
var progressBar = document.getElementById("wi-reading-progress");
|
||||
@@ -673,58 +658,45 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
|
||||
var wechatBtn = document.querySelector(".wi-post__share-btn--wechat");
|
||||
var wechatQr = document.getElementById("wi-wechat-qr");
|
||||
var wechatQrCanvas = document.getElementById("wi-wechat-qr-canvas");
|
||||
var wechatUrlInput = document.getElementById("wi-wechat-qr-url");
|
||||
var wechatCopyBtn = document.getElementById("wi-wechat-qr-copy");
|
||||
var wechatQrClose = wechatQr ? wechatQr.querySelector(".wi-wechat-qr__close") : null;
|
||||
var wechatQrGenerated = false;
|
||||
|
||||
function generateQr(text, canvas) {
|
||||
var size = 200;
|
||||
var cellSize = 5;
|
||||
var qr = null;
|
||||
function resolveShareUrl() {
|
||||
var rawUrl = (wechatBtn && wechatBtn.getAttribute("data-url")) || window.location.pathname;
|
||||
return rawUrl.startsWith("http") ? rawUrl : window.location.origin + rawUrl;
|
||||
}
|
||||
|
||||
/* 复制链接后由访客在微信内分享,零远程脚本 */
|
||||
function copyShareUrl() {
|
||||
var url = resolveShareUrl();
|
||||
if (wechatUrlInput) wechatUrlInput.value = url;
|
||||
var done = function () {
|
||||
if (!wechatCopyBtn) return;
|
||||
wechatCopyBtn.textContent = "已复制";
|
||||
setTimeout(function () {
|
||||
wechatCopyBtn.textContent = "复制链接";
|
||||
}, 1800);
|
||||
};
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(url).then(done, function () {
|
||||
if (wechatUrlInput) wechatUrlInput.select();
|
||||
});
|
||||
} else if (wechatUrlInput) {
|
||||
wechatUrlInput.select();
|
||||
try {
|
||||
var typeNumber = 0;
|
||||
var errorCorrectionLevel = "M";
|
||||
var qrData = [];
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
qrData.push(text.charCodeAt(i));
|
||||
}
|
||||
qr = qrcode(typeNumber, errorCorrectionLevel);
|
||||
qr.addData(text);
|
||||
qr.make();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
var moduleCount = qr.getModuleCount();
|
||||
var margin = 4;
|
||||
var totalCells = moduleCount + margin * 2;
|
||||
var canvasSize = totalCells * cellSize;
|
||||
canvas.width = canvasSize;
|
||||
canvas.height = canvasSize;
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillRect(0, 0, canvasSize, canvasSize);
|
||||
ctx.fillStyle = "#000000";
|
||||
for (var row = 0; row < moduleCount; row++) {
|
||||
for (var col = 0; col < moduleCount; col++) {
|
||||
if (qr.isDark(row, col)) {
|
||||
ctx.fillRect((col + margin) * cellSize, (row + margin) * cellSize, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
document.execCommand("copy");
|
||||
done();
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (wechatBtn && wechatQr) {
|
||||
wechatBtn.addEventListener("click", function () {
|
||||
wechatQr.classList.add("wi-wechat-qr--visible");
|
||||
if (!wechatQrGenerated && wechatQrCanvas) {
|
||||
var rawUrl = wechatBtn.getAttribute("data-url") || window.location.pathname;
|
||||
var url = rawUrl.startsWith("http") ? rawUrl : window.location.origin + rawUrl;
|
||||
if (typeof qrcode !== "undefined") {
|
||||
generateQr(url, wechatQrCanvas);
|
||||
wechatQrGenerated = true;
|
||||
}
|
||||
}
|
||||
if (wechatUrlInput) wechatUrlInput.value = resolveShareUrl();
|
||||
});
|
||||
if (wechatCopyBtn) wechatCopyBtn.addEventListener("click", copyShareUrl);
|
||||
if (wechatQrClose) {
|
||||
wechatQrClose.addEventListener("click", function () {
|
||||
wechatQr.classList.remove("wi-wechat-qr--visible");
|
||||
@@ -1255,12 +1227,36 @@ import LightGallery from "../components/LightGallery.astro";
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.wi-wechat-qr__canvas {
|
||||
.wi-wechat-qr__url {
|
||||
display: block;
|
||||
margin: 0 auto var(--space-md);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 0 var(--space-sm, 0.5rem);
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
background: var(--bg-raised);
|
||||
color: var(--ink);
|
||||
font-size: var(--text-sm);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wi-wechat-qr__copy {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 0 var(--space-md);
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-size: var(--text-sm);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.wi-wechat-qr__copy:hover {
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.wi-wechat-qr__hint {
|
||||
|
||||
Reference in new issue
Block a user