Repository: getgridea/gridea Branch: master Commit: 48e4ab573bfa Files: 206 Total size: 508.3 KB Directory structure: gitextract_69c_nu5k/ ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README-ru.md ├── README-zh_CN.md ├── README-zh_TW.md ├── README.md ├── babel.config.js ├── package.json ├── postcss.config.js ├── public/ │ ├── app-icons/ │ │ └── gridea.icns │ ├── default-files/ │ │ ├── config/ │ │ │ ├── posts.json │ │ │ ├── setting.json │ │ │ └── theme.json │ │ ├── post-images/ │ │ │ └── .gitkeep │ │ ├── posts/ │ │ │ ├── about.md │ │ │ └── hello-gridea.md │ │ ├── static/ │ │ │ └── 404.html │ │ └── themes/ │ │ ├── fly/ │ │ │ ├── assets/ │ │ │ │ └── styles/ │ │ │ │ ├── _blocks/ │ │ │ │ │ ├── archives.less │ │ │ │ │ ├── fonts.less │ │ │ │ │ ├── footer.less │ │ │ │ │ ├── header.less │ │ │ │ │ ├── link.less │ │ │ │ │ ├── list.less │ │ │ │ │ ├── pagination.less │ │ │ │ │ ├── post.less │ │ │ │ │ ├── tag.less │ │ │ │ │ └── tags.less │ │ │ │ ├── _core/ │ │ │ │ │ ├── base.less │ │ │ │ │ ├── colors.less │ │ │ │ │ └── github.less │ │ │ │ └── main.less │ │ │ ├── config.json │ │ │ ├── style-override.js │ │ │ └── templates/ │ │ │ ├── archives.ejs │ │ │ ├── includes/ │ │ │ │ ├── footer.ejs │ │ │ │ ├── head.ejs │ │ │ │ ├── header.ejs │ │ │ │ ├── post-list-archives.ejs │ │ │ │ ├── post-list.ejs │ │ │ │ └── scripts.ejs │ │ │ ├── index.ejs │ │ │ ├── post.ejs │ │ │ ├── tag.ejs │ │ │ └── tags.ejs │ │ ├── notes/ │ │ │ ├── assets/ │ │ │ │ └── styles/ │ │ │ │ ├── abstracts/ │ │ │ │ │ └── varibles.less │ │ │ │ ├── components/ │ │ │ │ │ ├── about.less │ │ │ │ │ ├── archives.less │ │ │ │ │ ├── footer.less │ │ │ │ │ ├── header.less │ │ │ │ │ ├── home.less │ │ │ │ │ ├── post.less │ │ │ │ │ ├── tag.less │ │ │ │ │ └── tags.less │ │ │ │ ├── lib/ │ │ │ │ │ ├── colors.less │ │ │ │ │ ├── github.less │ │ │ │ │ └── modern-normalize.less │ │ │ │ └── main.less │ │ │ ├── config.json │ │ │ ├── style-override.js │ │ │ └── templates/ │ │ │ ├── archives.ejs │ │ │ ├── includes/ │ │ │ │ ├── disqus.ejs │ │ │ │ ├── footer.ejs │ │ │ │ ├── gitalk.ejs │ │ │ │ ├── head.ejs │ │ │ │ ├── header.ejs │ │ │ │ ├── pagination.ejs │ │ │ │ ├── post-list-archives.ejs │ │ │ │ └── post-list.ejs │ │ │ ├── index.ejs │ │ │ ├── post.ejs │ │ │ ├── tag.ejs │ │ │ └── tags.ejs │ │ ├── paper/ │ │ │ ├── assets/ │ │ │ │ └── styles/ │ │ │ │ ├── _core/ │ │ │ │ │ ├── base.less │ │ │ │ │ ├── colors.less │ │ │ │ │ └── github.less │ │ │ │ └── main.less │ │ │ ├── config.json │ │ │ ├── style-override.js │ │ │ └── templates/ │ │ │ ├── _blocks/ │ │ │ │ ├── head.ejs │ │ │ │ ├── header.ejs │ │ │ │ ├── pagination.ejs │ │ │ │ ├── post-list.ejs │ │ │ │ ├── scripts.ejs │ │ │ │ └── sidebar.ejs │ │ │ ├── archives.ejs │ │ │ ├── index.ejs │ │ │ ├── post.ejs │ │ │ ├── tag.ejs │ │ │ └── tags.ejs │ │ └── simple/ │ │ ├── assets/ │ │ │ └── styles/ │ │ │ ├── _core/ │ │ │ │ ├── a11y-dark.less │ │ │ │ ├── atom-one-dark.less │ │ │ │ ├── base.less │ │ │ │ ├── colors.less │ │ │ │ └── github.less │ │ │ └── main.less │ │ ├── config.json │ │ ├── style-override.js │ │ └── templates/ │ │ ├── _blocks/ │ │ │ ├── head.ejs │ │ │ ├── pagination.ejs │ │ │ ├── scripts.ejs │ │ │ └── sidebar.ejs │ │ ├── archives.ejs │ │ ├── index.ejs │ │ ├── post.ejs │ │ ├── tag.ejs │ │ └── tags.ejs │ └── index.html ├── src/ │ ├── App.vue │ ├── assets/ │ │ ├── locales-menu.ts │ │ ├── locales.ts │ │ └── styles/ │ │ ├── custom.less │ │ ├── main.less │ │ ├── tailwind.css │ │ ├── var.less │ │ └── zwicon.less │ ├── background.ts │ ├── components/ │ │ ├── AppSystem/ │ │ │ ├── Index.vue │ │ │ └── includes/ │ │ │ ├── LanguageSetting.vue │ │ │ ├── SourceFolderSetting.vue │ │ │ └── Version.vue │ │ ├── ColorCard/ │ │ │ └── Index.vue │ │ ├── EmojiCard/ │ │ │ └── Index.vue │ │ ├── FooterBox/ │ │ │ └── Index.vue │ │ ├── Main.vue │ │ ├── MonacoMarkdownEditor/ │ │ │ ├── Index.vue │ │ │ └── theme.js │ │ └── PostsCard/ │ │ └── Index.vue │ ├── helpers/ │ │ ├── analytics.ts │ │ ├── constants.ts │ │ ├── content-helper.ts │ │ ├── enums.ts │ │ ├── shortcut-keys.ts │ │ ├── slug.ts │ │ ├── utils.ts │ │ ├── vee-validate.ts │ │ └── words-count.ts │ ├── interfaces/ │ │ ├── menu.ts │ │ ├── post.ts │ │ ├── setting.ts │ │ ├── snackbar.ts │ │ ├── tag.ts │ │ └── theme.ts │ ├── main.ts │ ├── router.ts │ ├── server/ │ │ ├── app.ts │ │ ├── deploy.ts │ │ ├── events/ │ │ │ ├── deploy.ts │ │ │ ├── index.ts │ │ │ ├── menu.ts │ │ │ ├── post.ts │ │ │ ├── renderer.ts │ │ │ ├── setting.ts │ │ │ ├── site.ts │ │ │ ├── tag.ts │ │ │ └── theme.ts │ │ ├── interfaces/ │ │ │ ├── application.ts │ │ │ ├── menu.ts │ │ │ ├── post.ts │ │ │ ├── renderer.ts │ │ │ ├── setting.ts │ │ │ ├── tag.ts │ │ │ └── theme.ts │ │ ├── menus.ts │ │ ├── model.ts │ │ ├── plugins/ │ │ │ ├── deploys/ │ │ │ │ ├── gitproxy.ts │ │ │ │ ├── netlify.ts │ │ │ │ └── sftp.ts │ │ │ └── markdown.ts │ │ ├── posts.ts │ │ ├── renderer.ts │ │ ├── setting.ts │ │ ├── tags.ts │ │ └── theme.ts │ ├── server.ts │ ├── shims-tsx.d.ts │ ├── shims-vue.d.ts │ ├── shims.d.ts │ ├── store/ │ │ ├── index.ts │ │ └── modules/ │ │ └── site.ts │ ├── views/ │ │ ├── article/ │ │ │ ├── ArticleUpdate.vue │ │ │ └── Articles.vue │ │ ├── loading/ │ │ │ └── Index.vue │ │ ├── menu/ │ │ │ └── Index.vue │ │ ├── setting/ │ │ │ ├── Index.vue │ │ │ └── includes/ │ │ │ ├── BasicSetting.vue │ │ │ ├── CommentSetting.vue │ │ │ ├── DisqusSetting.vue │ │ │ └── GitalkSetting.vue │ │ ├── tags/ │ │ │ └── Index.vue │ │ └── theme/ │ │ ├── Index.vue │ │ └── includes/ │ │ ├── AvatarSetting.vue │ │ ├── BasicSetting.vue │ │ ├── CustomSetting.vue │ │ └── FaviconSetting.vue │ └── vue-bus.ts ├── tailwind.config.js ├── tsconfig.json └── vue.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .browserslistrc ================================================ > 1% last 2 versions not ie <= 8 ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true # Matches multiple files with brace expansion notation # Set default charset [*.{js,py}] charset = utf-8 # Tab indentation (no size specified) [Makefile] indent_style = tab # Indentation override for all JS under lib directory [*.{js,ts}] indent_style = space indent_size = 2 # Matches the exact files either package.json or .travis.yml [{package.json,.travis.yml}] indent_style = space indent_size = 2 ================================================ FILE: .eslintrc.js ================================================ module.exports = { root: true, env: { node: true, }, extends: ['plugin:vue/essential', '@vue/airbnb', '@vue/typescript'], rules: { // js 和 ts 不需要检查 import 的文件后缀 'import/extensions': [ 'error', 'always', { js: 'never', ts: 'never', }, ], 'no-restricted-syntax': [ 'error', 'WithStatement', 'BinaryExpression[operator=\'in\']', ], // 可以 debugger 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, // 不要分号 semi: [2, 'never'], // 全部单引号 quotes: [2, 'single'], // 对象缩写 'object-shorthand': 0, // 可以使用 console 'no-console': 0, // 允许使用匿名函数 'func-names': 0, // 允许属性的 key 值加引号 'quote-props': 0, // 允许对函数的参数赋值 'no-param-reassign': 0, // 函数的参数可以不使用 'no-unused-vars': 0, // 不用强制 export default 'import/prefer-default-export': 0, // 不禁止箭头函数直接return对象 'arrow-body-style': 0, // 允许空行 'no-trailing-spaces': ['error', { skipBlankLines: true }], // 允许short circuit evaluations 'no-unused-expressions': [ 'error', { allowShortCircuit: true, allowTernary: true }, ], // 最长字符 'max-len': ['error', { code: 1500 }], 'vue/no-parsing-error': [ 2, { 'invalid-first-character-of-tag-name': false, }, ], // no-plusplus 'no-plusplus': 0, 'class-methods-use-this': 0, 'no-irregular-whitespace': 0, 'consistent-return': 0, 'import/no-extraneous-dependencies': 0, 'global-require': 0, 'no-continue': 0, 'linebreak-style': 0, }, parserOptions: { parser: '@typescript-eslint/parser', }, } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug Report about: 事情不像预期的那样工作吗? title: '' labels: 'bug' assignees: '' --- ## 我的环境 | 名称 | 值 | | ------- | ---- | | 操作系统 | | | 软件版本 | | | 主题名称 | | --- ## 期望行为 ## 当前行为 ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature Request about: 想让我们为 Gridea 增加什么功能吗? title: 'feat: ' labels: 'Feature Request' assignees: '' --- ## 概述 ## 动机 ## 详细解释 ================================================ FILE: .github/ISSUE_TEMPLATE/question.md ================================================ --- name: Question about: 对 Gridea 有任何问题吗? title: '' labels: 'question' assignees: '' --- ================================================ FILE: .gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw* #Electron-builder output /dist_electron ================================================ FILE: .npmrc ================================================ registry=https://registry.npmjs.org ================================================ FILE: CHANGELOG.md ================================================ ## v0.9.1 `2020-01-01` - 修复首次安装使用 Gridea,无法预览 BUG ## v0.9.0 `2019-12-28` **注意:若你之前有手动放置文件或文件夹到构建后文件夹(output 文件夹)本次升级有非兼容升级,请看下升级说明,若没有,可直接升级啦** 升级说明: 需手动复制到博客源文件夹的 static 文件夹(若无此文件夹可新建一个,新版使用时会自动生成,有静态文件需求的可放在此文件夹,构建时会直接复制到 output 文件夹) ## 新增 - 新增 SFTP 部署 - 新增文章置顶功能 - 自定义配置支持图片类型和数组类型,增加文章数据卡片类型 - 自定义归档路径前缀 - 文章页与标签页支持精简 URL 与默认 URL - 新增菜单拖动排序 - 支持自定义模板渲染,具体可见[Gridea 文档](https://gridea.dev/docs) ## 修复 - 修复更改文章 URL 大小写会删除文章的 bug - 修复编辑器超出一屏后回车不会滚动的 bug - 修复 Linux(Ubuntu)初始化配置时,检测远程链接失败的 BUG ## 优化 - 文章内图片支持懒加载(基于 chrome 的lazy loading) - 升级 Electron 至 7.x - 标签页支持渲染列表隐藏文章(break change) - 增加预览唤出快捷键(Ctrl + P) - 优化编辑器 Windows 下字体显示 - 增加渲染过程错误日志弹窗 - 增加应用内通知系统 ## v0.8.3 `2019-09-23` 「全新体验,助你妙笔生花」 ## 新增 - 全新的内置编辑器,全新的编辑体验与快捷键 - 增加 Emoji 输入面板与文章信息统计 - 增加 `isHomepage`字段,赋予主题开发更多可能性 - Markdown 渲染支持 `Emoji` 与 `implict-figures` - 增加 GA 统计,为了更好的优化产品 - 新上架一款主题 「Tech」 Tech ## 修复 - 修复文章编辑页返回未保存提示,内容不丢失 - 修复创建文章时报 URL 冲突 BUG ## 优化 - 升级 Electron 版本到 6.0+,更快了 - 更改本地预览为 Server 模式 - 提升了阅读时间的准确度 - 更多细节优化,等你发现 ## v0.8.2 `2019-07-15` ## 新增 - 文章编辑页增加 `Command/Ctrl + S` 快捷键,快速保存文章 - 增加繁体中文显示 (thanks @joinmouse ) - Makrdown 渲染支持 `Mark`、`Sup`、`Abbr`、`Footnote` - 侧边栏新增直达站点按钮 - 增加上一篇文章渲染字段支持 `post.prevPost` - 文章增加 Reading Time(阅读时间,例如 `9 min read`) 字段 `post.stats` - Markdown 渲染支持设置图片大小,例如:`![test image](https://xxx/xxx.png =100x200)` ## 优化 - 更新 Electron 版本到 5.0.6,提升应用内菜单切换流畅性,更快了 - 优化编辑器**编辑体验**与应用内预览样式(**强烈推荐**) - 新增文章编辑时可新建标签 - 提升编辑器的安全性 - Notes 主题显示优化与增加目录显示(**需手动更新主题**) - 应用菜单多语言支持与优化 ## v0.8.1 `2019-05-26` ### 新增 - 增加 **Linux 版** - 增加 RSS 支持,并在默认主题中增加链接(⚠️**需手动更新主题**,或在原主题中添加链接为 `href="<%= themeConfig.domain %>/atom.xml"`) - 增加 **Task lists** 渲染支持 - 增加**编辑器粘贴剪切板中图片**功能 - 增加 [主题市场](https://gridea.dev/themes/) 与 [主题开发样板](https://github.com/getgridea/gridea-theme-starter),欢迎参与主题开发 ### 修复 - 修复文章标题包含特殊字符时渲染 BUG - 修复 KaTeX 公式渲染 BUG(⚠️**需手动更新主题**,若不更新主题,文章内使用 KaTeX 可能会造成文章公式显示异常) - 修复文章详情页 `site.posts` 包含隐藏文章 BUG - 修复应用内预览文章,点击链接为应用内打开 BUG - 修复编辑文章时,更改文章标题,文章 URL 自动变化 bug ### 优化 - 编辑器菜单栏置顶优化 - 优化安装包体积,减小 **25%** > **提示:** > 手动更新主题,需将`源文件夹/themes` 文件夹里的旧主题删除,重启应用即可 > (源文件夹默认为:`~/Documents/Gridea`,也可在应用内:**系统 -> 源文件夹** 进行查看) ## v0.8.0 `2019-04-14` ### 新增 - 全新品牌名 **Gridea**,更易读! - 全新 LOGO,更好记!thanks @Leohuaji - 全新视觉,更简约! - 新增主题自定义配置功能,你可以使用主题提供的社交、谷歌统计、自定义样式...等自定义配置,当然你也可以开发新的主题,提供更有趣的自定义配置,尽情发挥你的想象(⚠️**需手动更新主题**) - 新增一款主题 「Paper」,英文手写体风格,欢迎体验 - 封面图支持外链 - 文章支持 `@[toc]` 显示文章目录,同时增加 `post.toc` 字段供主题显示 - [**KaTeX**](https://katex.org/) 公式支持 - 全新项目主页,欢迎 [访问](https://gridea.dev) ### 修复 - 修复基本配置未填写时,点击检测远程链接导致后续操作失效 BUG - 修复文章 URL 中含有 `/` 导致新建文章保存失败 BUG #29 - 修复 Favicon 和 Avatar 图片缓存问题 #24 - 修复更改源文件夹,新文件夹为空时 BUG ### 优化 - 固定文章编辑页附属信息栏,长文章编辑体验增强 - 内置主题细节优化,适配主题自定义配置功能 > **提示:** > 若您是老用户,安装新版之后,需将 `源文件夹/themes` 文件夹里的旧主题删除,重启应用,方可使用内置主题的自定义配置功能 > (源文件夹默认为:旧:`~/Documents/hve-notes`,新:`~/Documents/Gridea`) ## v0.7.7 `2019-03-01` - 🛠 修复了应用中非第一页文章删除时 BUG ## v0.7.6 `2019-02-27` ### 新增 - 🔥 Windows 安装支持**自定义安装路径** - 🔥 增加**远程连接检测**功能 - 🔥 新增一款主题: **Simple** - 🔥 增加**自定义源文件夹**,可利用 OneDrive、百度网盘、iCloud、Dropbox 等进行多设备同步 - 🧩 增加文章目录(TOC)能力(数据支持,还需主题支持) ### 修复 - 🛠 文章时间渲染错误 - 🛠 所有文章隐藏渲染错误 - 🛠 部署除 master 分支外分支不成功问题 - 🛠 标签带空格使用时渲染错误 - 🛠 文章编辑全屏时无法点击输入菜单栏的 BUG - 🛠 文章快捷输入链接菜单BUG ### 优化 - 🌟 摘要分隔符判断逻辑 <!-- more --> 单独行为摘要渲染分割符 - 🌟 调整应用部分 UI - 🌟 增加应用退出快捷键 - 🌟 增加同步和检测远程连接时开发者工具控制台错误信息显示,更精准的定位同步问题 ## v0.7.5 - 🎈 新春快乐 🏮 🧨 🧧 `2019-01-30` - 🌈 重写应用 UI - 🔥 增加**归档页**渲染支持和**每页归档数**自定义功能 - 🔥 增加**标签页**渲染支持 - 🔥 增加文章 Link 和标签 Link 设置**默认生成方式**和**自定义**支持 - 🔥 增加**列表隐藏文章**功能,用于创建特殊页面使用(例如,**关于**页) - 🔥 增加**显示日期格式自定义**功能 - 🌟 优化默认主题「notes、fly」,支持归档页和标签页显示(️️⚡️ 需手动更新) - 🌟 更新文档 > 注: > - 更新主题,需将 `~/Documents/hve-notes/themes` 文件夹中主题删除,重新启动应用即可 ## v0.7.0 `2019-01-20` - 🔥 增加多语言支持:English、简体中文(默认) - 🔥 增加多平台部署支持:Github Pages、Coding Pages - 🔥 增加多评论系统支持:Gitalk、DisqusJS(不兼容更新 ⚡️) - 🌟 全新的文章编辑页,专注写作 - 🌟 优化多处交互体验 - 🌟 完善文档,更强大的主题开发能力 - 🌟 优化默认主题「notes、fly」:多评论支持及 UI 优化 > 注: > - 更新主题,需将 `~/Documents/hve-notes/themes` 文件夹中对应主题删除,重新启动应用即可 > - ⚡️ 若之前有设置过 Gitalk 配置,此版本更新后需重新设置一下(原 Gitalk 配置信息可见`~/Documents/hve-notes/config/setting.json`) ## v0.6.4 - 新增一款主题:「fly」,优化默认主题UI:「notes」 (若想更新 notes 主题,已安装旧版本用户需将旧版本应用文件夹 `~/Documents/hve-notes` 删除,安装新版应用即可(记得备份文件哦!)) - 新增头像设置功能 - 调整多处 UI - 完善主题开发文档 ## v0.6.3 - 🐛 更改构建配置,修复了应用初始化文件夹的 BUG(使用前,需要手动删除文档(Documents)目录下的 hve-notes 文件夹,然后打开应用即可正常初始化) - 修复 material icons load bug,感谢 @rosuH - 简单优化 favicon 页面 UI - 添加发布前配置简单校验 - 🙏 感谢支持 ## v0.6.2 - 优化默认主题 title、link 等 - 新增版本更新提醒 - 新增 CNAME 配置 - 新增 favicon 配置 - 新增 Gitalk 配置 - 修复 Windows 下文章更新 bug ## v0.6.1 - 增加文章删除功能 - 更新 footer 信息 - 更改顶部窗口控制方式 - 更改默认打开窗口大小 - windows版 和 mac 版同步发布 ✌ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 EryouHao Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README-ru.md ================================================

Gridea

Клиент для ведения блога

[Загрузить](https://github.com/getgridea/gridea/releases) | [Официальная страница](https://gridea.dev/) GitHub All Releases
[English](https://github.com/getgridea/gridea/blob/master/README.md) | Русский | [简体中文](https://github.com/getgridea/gridea/blob/master/README-zh_CN.md) | [繁體中文](https://github.com/getgridea/gridea/blob/master/README-zh_TW.md) **[CHANGELOG](https://github.com/getgridea/gridea/blob/master/CHANGELOG.md)** 👏 Спасибо, что используете **Gridea**! ✍️ **Gridea** это статичный клиент для ведения блога. Вы можете использовать его для записи событий своей жизни, заметок о настроении, полученых знаний, эссе и идей... ## Возможности👇 📝 Используйте самый крутой редактор **Markdown** для быстрого создания и редактирования материалов 🌉 Вставляйте изображения и диаграммы для обложки материала или вставляйте их в любое место статьи 🏷️ Создавайте рубрики и группируйте материалы 📋 Редактируйте меню и добавляйте туда любые ссылки 💻 Используйте статичный клиент для **Windows** или **MacOS** или даже **Linux** 🌎 Используйте **Github Pages** или **Coding Pages** чтобы поделиться материалами (в следующих версиях будет поддерживаться ещё больше платформ) 💬 Запросто настройте и получите доступ с системам комментирования [Gitalk](https://github.com/gitalk/gitalk) или [DisqusJS](https://github.com/SukkaW/DisqusJS) 🗺️ Наш клиент мультиязычен и имеет **упрощённый Китайский**、**традиционный Китайский**、 **Английский**、 **Русский**、 **Французский**、 **Японский** языки 🌁 Используйте любую тему по умолчанию прямо в клиенте или подключите любую стороннюю тему, почуствуйте свободу при настройке тем 🖥 Настройте папку для хранения материалов и синхронизируйте несколько устройств с помощью OneDrive, iCloud, Dropbox и т.д. 🌱 Конечно, **Gridea** ещё очень молода и имеет немного недостатков, но, поверьте, она продолжает двигаться вперед и развиваться 🏃 Надеемся, этот клиент станет вашим неразлучным партнером в творчестве Дайте полную волю своим талантам! 😘 Наслаждайтесь использованием~ ## Разработка Если вы хотите внести свой вклад в код, пожалуйста, проверьте [Contribution Guide (он правда на китайском)](https://github.com/getgridea/gridea/wiki/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97) на всякий случай. ``` shell $ # Node version > v10.0.0 is requied $ git clone https://github.com/getgridea/gridea.git $ cd gridea $ yarn $ yarn electron:serve $ yarn electron:build ``` ## Контакты и обратная связь [Канал в Telegram](https://t.me/joinchat/AAAAAEj82_lma0Y1wmyqUQ) | [Группа в Telegram](https://t.me/joinchat/IDY0ahRqb8NPodv95BNpBg) | QQ 1 Group: 970332209 | QQ 2 Group: 923131213 | Author Twitter: @EryouHao ## Примеры в скриншотах
## Участие в разработке Мы приветствуем все идеи и улучшения. Вы можете отправлять любые идеи в виде [pull requests](https://github.com/getgridea/gridea/pulls) или как GitHub [issues](https://github.com/getgridea/gridea/issues). ## Пожертвования разработчикам
## Сайт переводчика на Русский язык [Заглянуть ко мне на сайт](https://paul.bid/) там есть и другие интересные проекты 😉 ## Лицензия [MIT](https://github.com/getgridea/gridea/blob/master/LICENSE). Copyright (c) 2020-2023 EryouHao ================================================ FILE: README-zh_CN.md ================================================

Gridea

一个静态博客写作客户端

[下 载](https://github.com/getgridea/gridea/releases) | [主 页](https://gridea.dev/) GitHub All Releases
[English](https://github.com/getgridea/gridea/blob/master/README.md) | 简体中文 | [繁體中文](https://github.com/getgridea/gridea/blob/master/README-zh_TW.md) **[更新日志](https://github.com/getgridea/gridea/blob/master/CHANGELOG.md)** 👏 欢迎使用 **Gridea** ! ✍️ **Gridea** 一个静态博客写作客户端。你可以用它来记录你的生活、心情、知识、笔记、创意... ... ## 特性👇 📝 你可以使用最酷的 **Markdown** 语法,进行快速创作 🌉 你可以给文章配上精美的封面图和在文章任意位置插入图片 🏷️ 你可以对文章进行标签分组 📋 你可以自定义菜单,甚至可以创建外部链接菜单 💻 你可以在 **𝖶𝗂𝗇𝖽𝗈𝗐𝗌** 或 **𝖬𝖺𝖼𝖮𝖲** 或 **Linux** 设备上使用此客户端 🌎 你可以使用 **𝖦𝗂𝗍𝗁𝗎𝖻 𝖯𝖺𝗀𝖾𝗌** 或 **Coding Pages** 向世界展示,未来将支持更多平台 💬 你可以进行简单的配置,接入 [Gitalk](https://github.com/gitalk/gitalk) 或 [DisqusJS](https://github.com/SukkaW/DisqusJS) 评论系统 🇬🇧 你可以使用**中文简体**、**中文繁体**、**英语** 🌁 你可以任意使用应用内默认主题或任意第三方主题,强大的主题自定义能力 🖥 你可以自定义源文件夹,利用 OneDrive、百度网盘、iCloud、Dropbox 等进行多设备同步 🌱 当然 **Gridea** 还很年轻,有很多不足,但请相信,它会不停向前🏃 未来,它一定会成为你离不开的伙伴 尽情发挥你的才华吧! 😘 Enjoy~ ## 开发 如果你想贡献代码,请提前参阅[贡献指南](https://github.com/getgridea/gridea/wiki/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97) ``` shell $ # Node version > v10.0.0 is requied $ git clone https://github.com/getgridea/gridea.git $ cd gridea $ yarn $ yarn electron:serve $ yarn electron:build ``` ## 联系 [Telegram 频道](https://t.me/joinchat/AAAAAEj82_lma0Y1wmyqUQ) | [Telegram 群组](https://t.me/joinchat/IDY0ahRqb8NPodv95BNpBg) | QQ 1 群: 970332209(已满)| QQ 2 群: 923131213 | 作者推特: @EryouHao ## 示例截图
## 贡献 我们欢迎任何形式的贡献。你可以用 [pull requests](https://github.com/getgridea/gridea/pulls) 或 [issues](https://github.com/getgridea/gridea/issues) 的方式提交任何想法。 ## 支持
## License [MIT](https://github.com/getgridea/gridea/blob/master/LICENSE). Copyright (c) 2020 EryouHao ================================================ FILE: README-zh_TW.md ================================================

Gridea

一個靜態博客寫作客戶端

[下 載](https://github.com/getgridea/gridea/releases) | [主 頁](https://gridea.dev/) GitHub All Releases
[English](https://github.com/getgridea/gridea/blob/master/README.md) | [简体中文](https://github.com/getgridea/gridea/blob/master/README-zh_CN.md) | 繁體中文 **[更新日誌](https://github.com/getgridea/gridea/blob/master/CHANGELOG.md)** 👏 歡迎使用 **Gridea** ! ✍️ **Gridea** 一個靜態博客寫作客戶端。你可以用它來記錄你的生活、心情、知識、筆記、創意... ... ## 特性👇 📝 你可以使用最酷的 **Markdown** 語法,進行快速創作 🌉 你可以給文章配上精美的封面圖和在文章任意位置插入圖片 🏷️ 你可以對文章進行標籤分組 📋 你可以自定義菜單,甚至可以創建外部鏈接菜單 💻 你可以在 **𝖶𝗂𝗇𝖽𝗈𝗐𝗌** 或 **𝖬𝖺𝖼𝖮𝖲** 設備上使用此客戶端 🌎 你可以使用 **𝖦𝗂𝗍𝗁𝗎𝖻 𝖯𝖺𝗀𝖾𝗌** 或 **Coding Pages** 向世界展示,未來將支持更多平台 💬 你可以進行簡單的配置,接入 [Gitalk](https://github.com/gitalk/gitalk) 或 [DisqusJS](https://github.com/SukkaW/DisqusJS) 評論系統 🇬🇧 你可以使用**中文簡體**、**中文繁體**、**英語** 🌁 你可以任意使用應用內默認主題或任意第三方主題,強大的主題自定義能力 🖥 你可以自定義源文件夾,利用 OneDrive、百度網盤、iCloud、Dropbox 等進行多設備同步 🌱 當然 **Gridea** 還很年輕,有很多不足,但請相信,它會不停向前🏃 未來,它一定會成為你離不開的伙伴 盡情發揮你的才華吧! 😘 Enjoy~ ## 開發 如果你想貢獻代碼,請提前參閱[貢獻指南](https://github.com/getgridea/gridea/wiki/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97) ``` shell $ # Node version > v10.0.0 is requied $ git clone https://github.com/getgridea/gridea.git $ cd gridea $ yarn $ yarn electron:serve $ yarn electron:build ``` ## 聯繫 [Telegram 頻道](https://t.me/joinchat/AAAAAEj82_lma0Y1wmyqUQ) | [Telegram 群組](https://t.me/joinchat/IDY0ahRqb8NPodv95BNpBg) | QQ 1 群: 970332209(已满)| QQ 2 群: 923131213 | 作者推特: @EryouHao ## 示例截圖
## 貢獻 我們歡迎任何形式的貢獻。你可以用 [pull requests](https://github.com/getgridea/gridea/pulls) 或 [issues](https://github.com/getgridea/gridea/issues) 的方式提交任何想法。 ## 支持
## License [MIT](https://github.com/getgridea/gridea/blob/master/LICENSE). Copyright (c) 2020 EryouHao ================================================ FILE: README.md ================================================

Gridea

A static blog writing client

[Download](https://github.com/getgridea/gridea/releases) | [Homepage](https://gridea.dev/) GitHub All Releases
English | [Русский](https://github.com/getgridea/gridea/blob/master/README-ru.md) | [简体中文](https://github.com/getgridea/gridea/blob/master/README-zh_CN.md) | [繁體中文](https://github.com/getgridea/gridea/blob/master/README-zh_TW.md) **[CHANGELOG](https://github.com/getgridea/gridea/blob/master/CHANGELOG.md)** 👏 Welcome to use **Gridea**! ✍️ **Gridea** A static blog writing client. You can use it to record your life, mood, knowledge, notes and ideas... ## Features👇 📝 Use the coolest **Markdown** editor to create quickly 🌉 Insert pictures and article cover charts anywhere in the article 🏷️ Label and group articles 📋 Customize menus and even create external link menus 💻 Use this client on **Windows** or **MacOS** or **Linux** 🌎 Use **Github Pages** or **Coding Pages** to show the world that more platforms will be supported in the future 💬 Simply configure and access the [Gitalk](https://github.com/gitalk/gitalk) or [DisqusJS](https://github.com/SukkaW/DisqusJS) comment system 🗺️ Use **simplified Chinese**、**traditional Chinese**、 **English**、 **Russian**、 **French** 🌁 Use any default theme within the application or any third-party theme, free theme customization 🖥 Customize the source folder and synchronize multiple devices using OneDrive, iCloud, Dropbox, etc. 🌱 Of course **Gridea** is still very young and has many shortcomings, but please believe it will keep moving forward 🏃 In the future, it will surely become your inseparable partner Give full play to your talents! 😘 Enjoy~ ## Development If you want to contribute code, please check the [Contribution Guide](https://github.com/getgridea/gridea/wiki/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97) in advance. ``` shell $ # Node version > v10.0.0 is requied $ git clone https://github.com/getgridea/gridea.git $ cd gridea $ yarn $ yarn electron:serve $ yarn electron:build ``` ## Contact [Telegram Channel](https://t.me/joinchat/AAAAAEj82_lma0Y1wmyqUQ) | [Telegram Group](https://t.me/joinchat/IDY0ahRqb8NPodv95BNpBg) | QQ 1 Group: 970332209 | QQ 2 Group: 923131213 | Author Twitter: @EryouHao ## Example Screenshots
## Contributions We welcome all contributions. You can submit any ideas as [pull requests](https://github.com/getgridea/gridea/pulls) or as GitHub [issues](https://github.com/getgridea/gridea/issues). ## Donation
## License [MIT](https://github.com/getgridea/gridea/blob/master/LICENSE). Copyright (c) 2020-2023 EryouHao ================================================ FILE: babel.config.js ================================================ module.exports = { presets: [ '@vue/app', ], plugins: [ ['prismjs', { 'languages': ['javascript', 'css', 'markup', 'json', 'bash', 'sass', 'python', 'typescript', 'java', 'less', 'php', 'pug', 'jsx', 'c', 'ruby', 'rust', 'dart', 'stylus', 'swift', 'yaml', 'sql'], 'plugins': ['line-numbers'], 'theme': 'default', 'css': true, }], ], } ================================================ FILE: package.json ================================================ { "name": "gridea", "version": "0.9.3", "private": true, "description": "A static blog writing client. You can use it to record your life, mood, knowledge, notes and ideas...", "keywords": [ "gridea", "static-site", "static-site-generator" ], "homepage": "https://gridea.dev", "license": "MIT", "author": { "name": "EryouHao", "email": "eryouhao@gmail.com" }, "repository": "https://github.com/getgridea/gridea", "scripts": { "electron:build": "vue-cli-service electron:build", "electron:serve": "vue-cli-service electron:serve", "postinstall": "electron-builder install-app-deps", "lint": "vue-cli-service lint" }, "dependencies": { "@fontsource/noto-serif": "^4.5.9", "@iktakahiro/markdown-it-katex": "^3.1.0", "@sentry/electron": "^1.2.0", "ant-design-vue": "^1.3.5", "axios": "^0.27.2", "bluebird": "^3.5.3", "ejs": "^2.6.1", "electron-google-analytics": "^0.1.0", "electron-updater": "^4.2.0", "feed": "^2.0.4", "fs-extra": "^7.0.1", "gray-matter": "^4.0.1", "hpagent": "^1.0.0", "isomorphic-git": "1.17.1", "junk": "^3.1.0", "katex": "0.12.0", "less": "^3.9.0", "lowdb": "^1.0.0", "macaddress": "^0.2.9", "markdown-it": "^13.0.1", "markdown-it-abbr": "^1.0.4", "markdown-it-emoji": "^2.0.2", "markdown-it-footnote": "^3.0.3", "markdown-it-image-lazy-loading": "^1.2.0", "markdown-it-implicit-figures": "^0.9.0", "markdown-it-imsize": "^2.0.1", "markdown-it-mark": "^2.0.0", "markdown-it-sub": "^1.0.0", "markdown-it-sup": "^1.0.0", "markdown-it-toc-and-anchor": "^4.2.0", "moment": "^2.24.0", "monaco-markdown": "^0.0.6", "node-ssh": "^6.0.0", "normalize-path": "^3.0.0", "prismjs": "^1.16.0", "remixicon": "2.3.0", "shortid": "^2.2.14", "simple-get": "^4.0.1", "slug": "^0.9.3", "ssh2-sftp-client": "^4.2.4", "striptags": "^3.1.1", "transliteration": "^1.6.6", "url-join": "^4.0.1", "uuid": "^3.3.3", "v-emoji-picker": "^1.1.6", "vee-validate": "^3.1.3", "vue": "^2.6.10", "vue-class-component": "^6.0.0", "vue-i18n": "^8.7.0", "vue-property-decorator": "^7.3.0", "vue-router": "^3.0.1", "vue-shortkey": "^3.1.7", "vue2-transitions": "^0.2.3", "vuedraggable": "^2.23.2", "vuex": "^3.0.1", "vuex-class": "^0.3.1" }, "devDependencies": { "@types/bluebird": "^3.5.26", "@types/ejs": "^2.6.3", "@types/express": "^4.17.0", "@types/fs-extra": "^5.0.5", "@types/less": "^3.0.0", "@types/lowdb": "^1.0.7", "@types/markdown-it": "0.0.7", "@types/marked": "^0.6.5", "@types/normalize-path": "^3.0.0", "@types/prismjs": "^1.16.0", "@types/shortid": "0.0.29", "@types/slug": "^0.9.1", "@types/ssh2-sftp-client": "^4.1.1", "@types/url-join": "^4.0.0", "@types/uuid": "^3.4.5", "@vue/cli-plugin-babel": "^3.6.0", "@vue/cli-plugin-eslint": "^3.6.0", "@vue/cli-plugin-typescript": "^3.6.0", "@vue/cli-service": "^3.6.0", "@vue/eslint-config-airbnb": "^4.0.0", "@vue/eslint-config-typescript": "^4.0.0", "babel-eslint": "^10.0.1", "babel-plugin-prismjs": "^1.0.2", "electron": "^7.3.3", "eslint": "^5.16.0", "eslint-plugin-vue": "^5.2.2", "husky": "^1.3.1", "less-loader": "^4.1.0", "lint-staged": "^8.1.5", "markdown-it-task-lists": "^2.1.1", "postcss-import": "^12.0.1", "tailwindcss": "^1.1.4", "typescript": "^3.0.0", "vue-cli-plugin-electron-builder": "2.0.0-beta.2", "vue-template-compiler": "^2.6.10" }, "main": "background.js", "engines": { "node": ">=10.0.0" }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{js,ts,tsx,vue}": [ "vue-cli-service lint", "git add" ] }, "__npminstall_done": false } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: [ require('postcss-import'), require('tailwindcss'), require('autoprefixer'), ], } ================================================ FILE: public/default-files/config/posts.json ================================================ { "posts": [], "tags": [], "menus": [ { "link": "/", "name": "首页", "openType": "Internal" }, { "link": "/archives", "name": "归档", "openType": "Internal" }, { "link": "/tags", "name": "标签", "openType": "Internal" }, { "link": "/post/about", "name": "关于", "openType": "Internal" } ] } ================================================ FILE: public/default-files/config/setting.json ================================================ { "config": { "platform": "github", "domain": "", "repository": "", "branch": "", "username": "", "email": "", "token": "" }, "comment": { "commentPlatform": "gitalk", "disqusSetting": { "api": "", "apikey": "", "shortname": "" }, "gitalkSetting": { "clientId": "", "clientSecret": "", "owner": "", "repository": "" }, "showComment": false } } ================================================ FILE: public/default-files/config/theme.json ================================================ { "config": { "footerInfo": "Powered by Gridea", "postPageSize": 10, "archivesPageSize": 50, "showFeatureImage": true, "siteDescription": "温故而知新", "siteName": "Gridea", "themeName": "notes", "dateFormat": "YYYY-MM-DD", "postUrlFormat": "SLUG", "tagUrlFormat": "SHORT_ID", "feedCount": 10, "feedFullText": true }, "customConfig": {} } ================================================ FILE: public/default-files/post-images/.gitkeep ================================================ ================================================ FILE: public/default-files/posts/about.md ================================================ --- title: 关于 date: 2019-01-25 19:09:48 tags: published: true hideInList: true feature: --- > 欢迎来到我的小站呀,很高兴遇见你!🤝 ## 🏠 关于本站 ## 👨‍💻 博主是谁 ## ⛹ 兴趣爱好 ## 📬 联系我呀 ================================================ FILE: public/default-files/posts/hello-gridea.md ================================================ --- title: Hello Gridea date: 2018-12-12 tags: [Gridea] published: true hideInList: false feature: /post-images/hello-gridea.png --- 👏 欢迎使用 **Gridea** ! ✍️ **Gridea** 一个静态博客写作客户端。你可以用它来记录你的生活、心情、知识、笔记、创意... ... [Github](https://github.com/getgridea/gridea) [Gridea 主页](https://gridea.dev/) [示例网站](https://fehey.com/) ## 特性👇 📝 你可以使用最酷的 **Markdown** 语法,进行快速创作 🌉 你可以给文章配上精美的封面图和在文章任意位置插入图片 🏷️ 你可以对文章进行标签分组 📋 你可以自定义菜单,甚至可以创建外部链接菜单 💻 你可以在 **Windows**,**MacOS** 或 **Linux** 设备上使用此客户端 🌎 你可以使用 **𝖦𝗂𝗍𝗁𝗎𝖻 𝖯𝖺𝗀𝖾𝗌** 或 **Coding Pages** 向世界展示,未来将支持更多平台 💬 你可以进行简单的配置,接入 [Gitalk](https://github.com/gitalk/gitalk) 或 [DisqusJS](https://github.com/SukkaW/DisqusJS) 评论系统 🇬🇧 你可以使用**中文简体**或**英语** 🌁 你可以任意使用应用内默认主题或任意第三方主题,强大的主题自定义能力 🖥 你可以自定义源文件夹,利用 OneDrive、百度网盘、iCloud、Dropbox 等进行多设备同步 🌱 当然 **Gridea** 还很年轻,有很多不足,但请相信,它会不停向前 🏃 未来,它一定会成为你离不开的伙伴 尽情发挥你的才华吧! 😘 Enjoy~ ================================================ FILE: public/default-files/static/404.html ================================================ Page Not Found
4 0 4
Page not found
Back
================================================ FILE: public/default-files/themes/fly/assets/styles/_blocks/archives.less ================================================ .archives-container { padding: 32px 0; .year { font-size: 24px; margin: 24px 0; color: var(--c-base-purple); } .post { margin-bottom: 32px; .post-title { font-size: 18px; font-weight: normal; position: relative; transition: all 0.382s; &:before { content: ''; display: block; width: 6px; height: 6px; border-radius: 50%; background: var(--c-base-purple); position: absolute; left: 0px; top: 50%; transform: translateY(-50%); opacity: 0; transition: all 0.382s; } &:hover { transform: translateX(8px); &:before { left: -8px; opacity: 1; } } } } } ================================================ FILE: public/default-files/themes/fly/assets/styles/_blocks/fonts.less ================================================ @font-face { font-family: 'icomoon'; src: url('../media/fonts/icomoon.eot?pjaj8c'); src: url('../media/fonts/icomoon.eot?pjaj8c#iefix') format('embedded-opentype'), url('../media/fonts/icomoon.ttf?pjaj8c') format('truetype'), url('../media/fonts/icomoon.woff?pjaj8c') format('woff'), url('../media/fonts/icomoon.svg?pjaj8c#icomoon') format('svg'); font-weight: normal; font-style: normal; } [class^="icon-"], [class*=" icon-"] { /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'icomoon' !important; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-activity-outline:before { content: "\e900"; } .icon-alert-circle-outline:before { content: "\e901"; } .icon-alert-triangle-outline:before { content: "\e902"; } .icon-archive-outline:before { content: "\e903"; } .icon-arrow-back-outline:before { content: "\e904"; } .icon-arrow-circle-down-outline:before { content: "\e905"; } .icon-arrow-circle-left-outline:before { content: "\e906"; } .icon-arrow-circle-right-outline:before { content: "\e907"; } .icon-arrow-circle-up-outline:before { content: "\e908"; } .icon-arrow-down-outline:before { content: "\e909"; } .icon-arrow-downward-outline:before { content: "\e90a"; } .icon-arrow-forward-outline:before { content: "\e90b"; } .icon-arrow-ios-back-outline:before { content: "\e90c"; } .icon-arrow-ios-downward-outline:before { content: "\e90d"; } .icon-arrow-ios-forward-outline:before { content: "\e90e"; } .icon-arrow-ios-upward-outline:before { content: "\e90f"; } .icon-arrow-left-outline:before { content: "\e910"; } .icon-arrow-right-outline:before { content: "\e911"; } .icon-arrow-up-outline:before { content: "\e912"; } .icon-arrow-upward-outline:before { content: "\e913"; } .icon-arrowhead-down-outline:before { content: "\e914"; } .icon-arrowhead-left-outline:before { content: "\e915"; } .icon-arrowhead-right-outline:before { content: "\e916"; } .icon-arrowhead-up-outline:before { content: "\e917"; } .icon-at-outline:before { content: "\e918"; } .icon-attach-2-outline:before { content: "\e919"; } .icon-attach-outline:before { content: "\e91a"; } .icon-award-outline:before { content: "\e91b"; } .icon-backspace-outline:before { content: "\e91c"; } .icon-bar-chart-2-outline:before { content: "\e91d"; } .icon-bar-chart-outline:before { content: "\e91e"; } .icon-battery-outline:before { content: "\e91f"; } .icon-behance-outline:before { content: "\e920"; } .icon-bell-off-outline:before { content: "\e921"; } .icon-bell-outline:before { content: "\e922"; } .icon-bluetooth-outline:before { content: "\e923"; } .icon-book-open-outline:before { content: "\e924"; } .icon-book-outline:before { content: "\e925"; } .icon-bookmark-outline:before { content: "\e926"; } .icon-briefcase-outline:before { content: "\e927"; } .icon-browser-outline:before { content: "\e928"; } .icon-brush-outline:before { content: "\e929"; } .icon-bulb-outline:before { content: "\e92a"; } .icon-calendar-outline:before { content: "\e92b"; } .icon-camera-outline:before { content: "\e92c"; } .icon-car-outline:before { content: "\e92d"; } .icon-cast-outline:before { content: "\e92e"; } .icon-charging-outline:before { content: "\e92f"; } .icon-checkmark-circle-2-outline:before { content: "\e930"; } .icon-checkmark-circle-outline:before { content: "\e931"; } .icon-checkmark-outline:before { content: "\e932"; } .icon-checkmark-square-2-outline:before { content: "\e933"; } .icon-checkmark-square-outline:before { content: "\e934"; } .icon-chevron-down-outline:before { content: "\e935"; } .icon-chevron-left-outline:before { content: "\e936"; } .icon-chevron-right-outline:before { content: "\e937"; } .icon-chevron-up-outline:before { content: "\e938"; } .icon-clipboard-outline:before { content: "\e939"; } .icon-clock-outline:before { content: "\e93a"; } .icon-close-circle-outline:before { content: "\e93b"; } .icon-close-outline:before { content: "\e93c"; } .icon-close-square-outline:before { content: "\e93d"; } .icon-cloud-download-outline:before { content: "\e93e"; } .icon-cloud-upload-outline:before { content: "\e93f"; } .icon-code-download-outline:before { content: "\e940"; } .icon-code-outline:before { content: "\e941"; } .icon-collapse-outline:before { content: "\e942"; } .icon-color-palette-outline:before { content: "\e943"; } .icon-color-picker-outline:before { content: "\e944"; } .icon-compass-outline:before { content: "\e945"; } .icon-copy-outline:before { content: "\e946"; } .icon-corner-down-left-outline:before { content: "\e947"; } .icon-corner-down-right-outline:before { content: "\e948"; } .icon-corner-left-down-outline:before { content: "\e949"; } .icon-corner-left-up-outline:before { content: "\e94a"; } .icon-corner-right-down-outline:before { content: "\e94b"; } .icon-corner-right-up-outline:before { content: "\e94c"; } .icon-corner-up-left-outline:before { content: "\e94d"; } .icon-corner-up-right-outline:before { content: "\e94e"; } .icon-credit-card-outline:before { content: "\e94f"; } .icon-crop-outline:before { content: "\e950"; } .icon-cube-outline:before { content: "\e951"; } .icon-diagonal-arrow-left-down-outline:before { content: "\e952"; } .icon-diagonal-arrow-left-up-outline:before { content: "\e953"; } .icon-diagonal-arrow-right-down-outline:before { content: "\e954"; } .icon-diagonal-arrow-right-up-outline:before { content: "\e955"; } .icon-done-all-outline:before { content: "\e956"; } .icon-download-outline:before { content: "\e957"; } .icon-droplet-off-outline:before { content: "\e958"; } .icon-droplet-outline:before { content: "\e959"; } .icon-edit-2-outline:before { content: "\e95a"; } .icon-edit-outline:before { content: "\e95b"; } .icon-email-outline:before { content: "\e95c"; } .icon-expand-outline:before { content: "\e95d"; } .icon-external-link-outline:before { content: "\e95e"; } .icon-eye-off-2-outline:before { content: "\e95f"; } .icon-eye-off-outline:before { content: "\e960"; } .icon-eye-outline:before { content: "\e961"; } .icon-facebook-outline:before { content: "\e962"; } .icon-file-add-outline:before { content: "\e963"; } .icon-file-outline:before { content: "\e964"; } .icon-file-remove-outline:before { content: "\e965"; } .icon-file-text-outline:before { content: "\e966"; } .icon-film-outline:before { content: "\e967"; } .icon-flag-outline:before { content: "\e968"; } .icon-flash-off-outline:before { content: "\e969"; } .icon-flash-outline:before { content: "\e96a"; } .icon-flip-2-outline:before { content: "\e96b"; } .icon-flip-outline:before { content: "\e96c"; } .icon-folder-add-outline:before { content: "\e96d"; } .icon-folder-outline:before { content: "\e96e"; } .icon-folder-remove-outline:before { content: "\e96f"; } .icon-funnel-outline:before { content: "\e970"; } .icon-gift-outline:before { content: "\e971"; } .icon-github-outline:before { content: "\e972"; } .icon-globe-2-outline:before { content: "\e973"; } .icon-globe-outline:before { content: "\e974"; } .icon-google-outline:before { content: "\e975"; } .icon-grid-outline:before { content: "\e976"; } .icon-hard-drive-outline:before { content: "\e977"; } .icon-hash-outline:before { content: "\e978"; } .icon-headphones-outline:before { content: "\e979"; } .icon-heart-outline:before { content: "\e97a"; } .icon-home-outline:before { content: "\e97b"; } .icon-image-outline:before { content: "\e97c"; } .icon-inbox-outline:before { content: "\e97d"; } .icon-info-outline:before { content: "\e97e"; } .icon-keypad-outline:before { content: "\e97f"; } .icon-layers-outline:before { content: "\e980"; } .icon-layout-outline:before { content: "\e981"; } .icon-link-2-outline:before { content: "\e982"; } .icon-link-outline:before { content: "\e983"; } .icon-linkedin-outline:before { content: "\e984"; } .icon-list-outline:before { content: "\e985"; } .icon-loader-outline:before { content: "\e986"; } .icon-lock-outline:before { content: "\e987"; } .icon-log-in-outline:before { content: "\e988"; } .icon-log-out-outline:before { content: "\e989"; } .icon-map-outline:before { content: "\e98a"; } .icon-maximize-outline:before { content: "\e98b"; } .icon-menu-2-outline:before { content: "\e98c"; } .icon-menu-arrow-outline:before { content: "\e98d"; } .icon-menu-outline:before { content: "\e98e"; } .icon-message-circle-outline:before { content: "\e98f"; } .icon-message-square-outline:before { content: "\e990"; } .icon-mic-off-outline:before { content: "\e991"; } .icon-mic-outline:before { content: "\e992"; } .icon-minimize-outline:before { content: "\e993"; } .icon-minus-circle-outline:before { content: "\e994"; } .icon-minus-outline:before { content: "\e995"; } .icon-minus-square-outline:before { content: "\e996"; } .icon-monitor-outline:before { content: "\e997"; } .icon-moon-outline:before { content: "\e998"; } .icon-more-horizontal-outline:before { content: "\e999"; } .icon-more-vertical-outline:before { content: "\e99a"; } .icon-move-outline:before { content: "\e99b"; } .icon-music-outline:before { content: "\e99c"; } .icon-navigation-2-outline:before { content: "\e99d"; } .icon-navigation-outline:before { content: "\e99e"; } .icon-npm-outline:before { content: "\e99f"; } .icon-options-2-outline:before { content: "\e9a0"; } .icon-options-outline:before { content: "\e9a1"; } .icon-pantone-outline:before { content: "\e9a2"; } .icon-paper-plane-outline:before { content: "\e9a3"; } .icon-pause-circle-outline:before { content: "\e9a4"; } .icon-people-outline:before { content: "\e9a5"; } .icon-percent-outline:before { content: "\e9a6"; } .icon-person-add-outline:before { content: "\e9a7"; } .icon-person-delete-outline:before { content: "\e9a8"; } .icon-person-done-outline:before { content: "\e9a9"; } .icon-person-outline:before { content: "\e9aa"; } .icon-person-remove-outline:before { content: "\e9ab"; } .icon-phone-call-outline:before { content: "\e9ac"; } .icon-phone-missed-outline:before { content: "\e9ad"; } .icon-phone-off-outline:before { content: "\e9ae"; } .icon-phone-outline:before { content: "\e9af"; } .icon-pie-chart-outline:before { content: "\e9b0"; } .icon-pin-outline:before { content: "\e9b1"; } .icon-play-circle-outline:before { content: "\e9b2"; } .icon-plus-circle-outline:before { content: "\e9b3"; } .icon-plus-outline:before { content: "\e9b4"; } .icon-plus-square-outline:before { content: "\e9b5"; } .icon-power-outline:before { content: "\e9b6"; } .icon-pricetags-outline:before { content: "\e9b7"; } .icon-printer-outline:before { content: "\e9b8"; } .icon-question-mark-circle-outline:before { content: "\e9b9"; } .icon-question-mark-outline:before { content: "\e9ba"; } .icon-radio-button-off-outline:before { content: "\e9bb"; } .icon-radio-button-on-outline:before { content: "\e9bc"; } .icon-radio-outline:before { content: "\e9bd"; } .icon-recording-outline:before { content: "\e9be"; } .icon-refresh-outline:before { content: "\e9bf"; } .icon-repeat-outline:before { content: "\e9c0"; } .icon-rewind-left-outline:before { content: "\e9c1"; } .icon-rewind-right-outline:before { content: "\e9c2"; } .icon-save-outline:before { content: "\e9c3"; } .icon-scissors-outline:before { content: "\e9c4"; } .icon-search-outline:before { content: "\e9c5"; } .icon-settings-2-outline:before { content: "\e9c6"; } .icon-settings-outline:before { content: "\e9c7"; } .icon-shake-outline:before { content: "\e9c8"; } .icon-share-outline:before { content: "\e9c9"; } .icon-shield-off-outline:before { content: "\e9ca"; } .icon-shield-outline:before { content: "\e9cb"; } .icon-shopping-bag-outline:before { content: "\e9cc"; } .icon-shopping-cart-outline:before { content: "\e9cd"; } .icon-shuffle-2-outline:before { content: "\e9ce"; } .icon-shuffle-outline:before { content: "\e9cf"; } .icon-skip-back-outline:before { content: "\e9d0"; } .icon-skip-forward-outline:before { content: "\e9d1"; } .icon-slash-outline:before { content: "\e9d2"; } .icon-smartphone-outline:before { content: "\e9d3"; } .icon-speaker-outline:before { content: "\e9d4"; } .icon-square-outline:before { content: "\e9d5"; } .icon-star-outline:before { content: "\e9d6"; } .icon-stop-circle-outline:before { content: "\e9d7"; } .icon-sun-outline:before { content: "\e9d8"; } .icon-swap-outline:before { content: "\e9d9"; } .icon-sync-outline:before { content: "\e9da"; } .icon-text-outline:before { content: "\e9db"; } .icon-thermometer-minus-outline:before { content: "\e9dc"; } .icon-thermometer-outline:before { content: "\e9dd"; } .icon-thermometer-plus-outline:before { content: "\e9de"; } .icon-toggle-left-outline:before { content: "\e9df"; } .icon-toggle-right-outline:before { content: "\e9e0"; } .icon-trash-2-outline:before { content: "\e9e1"; } .icon-trash-outline:before { content: "\e9e2"; } .icon-trending-down-outline:before { content: "\e9e3"; } .icon-trending-up-outline:before { content: "\e9e4"; } .icon-tv-outline:before { content: "\e9e5"; } .icon-twitter-outline:before { content: "\e9e6"; } .icon-umbrella-outline:before { content: "\e9e7"; } .icon-undo-outline:before { content: "\e9e8"; } .icon-unlock-outline:before { content: "\e9e9"; } .icon-upload-outline:before { content: "\e9ea"; } .icon-video-off-outline:before { content: "\e9eb"; } .icon-video-outline:before { content: "\e9ec"; } .icon-volume-down-outline:before { content: "\e9ed"; } .icon-volume-mute-outline:before { content: "\e9ee"; } .icon-volume-off-outline:before { content: "\e9ef"; } .icon-volume-up-outline:before { content: "\e9f0"; } .icon-wifi-off-outline:before { content: "\e9f1"; } .icon-wifi-outline:before { content: "\e9f2"; } ================================================ FILE: public/default-files/themes/fly/assets/styles/_blocks/footer.less ================================================ .site-footer { text-align: center; font-size: 12px; padding: 32px 16px; .slogan { margin-bottom: 24px; } .social-container { margin-bottom: 24px; font-size: 16px; a { margin: 0 8px; } } } ================================================ FILE: public/default-files/themes/fly/assets/styles/_blocks/header.less ================================================ .site-header-container { position: fixed; right: 0; left: 0; z-index: 1024; background: #fff; height: 56px; } .site-header { height: 56px; padding: 0 16px; display: flex; align-items: center; justify-content: space-between; max-width: 768px; margin: 0 auto; box-shadow: inset 0px -1px 0px #fafafa; .left { display: flex; .avatar { border-radius: 50%; } .site-title { margin: 0 16px; font-size: 24px; line-height: 32px; } } .right { .icon { font-size: 24px; cursor: pointer; } } } .menu-container { display: none; position: fixed; top: 56px; left: 0; right: 0; bottom: 0; background: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 1024; .menu-list { display: inline-flex; flex-direction: column; align-items: center; justify-content: center; margin-top: -40px; } .menu { display: inline-block; padding: 2px 8px; font-size: 22px; margin: 16px 0; font-weight: bold; } } .fade-enter-active, .fade-leave-active { transition: opacity .5s; } .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { opacity: 0; } ================================================ FILE: public/default-files/themes/fly/assets/styles/_blocks/link.less ================================================ a, .link { color: var(--c-base-black); text-decoration: none; cursor: pointer; background: transparent 0 0; &:hover, &:focus { // text-decoration: underline; } &:hover, &:focus, &:active { outline: 0; } } a.purple-link { position: relative; background: linear-gradient(180deg,transparent 70%,rgba(101,125,225,.4) 0); } ================================================ FILE: public/default-files/themes/fly/assets/styles/_blocks/list.less ================================================ .main { max-width: 768px; margin: 0 auto; display: flex; min-height: 100vh; flex-direction: column; } .content-container { flex: 1; padding: 48px 16px 32px; } .post-item { display: flex; justify-content: space-between; padding: 32px 0; max-height: 200px; .content { flex: 1; // display: flex; // flex-direction: column; .post-title { font-size: 22px; color: #0C1E29; display: inline-block; } .post-abstract { font-size: 14px; margin: 16px 0 8px; flex: 1; color: #4E616C; overflow:hidden; line-height: 24px; position: relative; display: -webkit-box; overflow: hidden; -webkit-line-clamp: 3; -webkit-box-orient: vertical; // &:after { // content: ''; // position: absolute; // bottom: 0; // right: 0; // width: 120px; // height: 32px; // /* "transparent" only works here because == rgba(0,0,0,0) */ // background-image: linear-gradient(to right, transparent, white); // } strong { font-weight: bolder; } a { font-weight: 600; background: linear-gradient(180deg,transparent 70%,rgba(101,125,225,.4) 0); } code { padding: 0 3px; margin: 0 2px; background: rgba(195,195,195,0.41); font-size: 0.9em; border-radius: 2px; } } } .feature-container { margin-left: 16px; flex-shrink: 0; width: 240px; height: 135px; position: relative; overflow: hidden; background-size: cover; background-position: center; } } .post-info { color: var(--c-base-purple); font-size: 12px; span { margin-right: 16px; } .icon { font-size: 12px; margin-right: 4px; } a { color: var(--c-base-purple); } } @media (max-width: 600px) { .post-item { flex-direction: column; height: auto; padding: 24px 0; max-height: none; .content { order: 2; display: block; .post-title { margin-top: 16px; font-size: 18px; } } .feature-container { margin-left: 0; order: 1; height: 0; width: 100%; padding-top: 56%; position: relative; background-size: cover; background-position: center; // .feature { // width: 100%; // height: 100%; // position: absolute; // top: 0; // left: 0; // right: 0; // bottom: 0; // } } } } ================================================ FILE: public/default-files/themes/fly/assets/styles/_blocks/pagination.less ================================================ .pagination-container { padding: 24px 0; overflow: hidden; .prev, .next { display: flex; align-items: center; padding: 2px 8px; background: linear-gradient(180deg,transparent 70%,rgba(101,125,225,.4) 0); } .prev { float: left; } .next { float: right; } } ================================================ FILE: public/default-files/themes/fly/assets/styles/_blocks/post.less ================================================ .post-detail { padding: 32px 0; .post-title { font-size: 32px; margin: 24px 0; color: var(--c-base-black); } .post-detail-info { padding: 0 0 32px; } .feature-container { padding-top: 56.25%; background-size: cover; background-position: center; border-radius: 2px; box-shadow: 0 0 30px #eee; } } .next-post { text-align: center; .post-title { display: inline-block; font-size: 22px; } } .post-content { h1, h2, h3, h4, h5, h6 { padding: 16px 0; color: #0C1E29; } a { font-weight: 600; background: linear-gradient(180deg,transparent 70%,rgba(101,125,225,.4) 0); } img { display: block; box-shadow: 0 0 30px #eee; max-width: 100%; border-radius: 2px; margin: 16px auto; } p { line-height: 1.725; margin-bottom: 16px; color: var(--c-base-blacklight); code { padding: 0 3px; margin: 0 2px; background: rgba(195,195,195,0.41); font-size: 0.9em; border-radius: 2px; display: inline-block; } } code { color: #476582; padding: .25rem .5rem; margin: 0; font-size: .85em; background-color: rgba(27,31,35,.05); border-radius: 3px; display: inline-block; } blockquote { background: #f3f5f7; padding: 16px; border-left: 2px solid var(--c-base-purple); margin-bottom: 16px; p { margin-bottom: 0; } } pre { margin-bottom: 16px; code { font-size: 14px; font-family: 'Source Code Pro', Consolas, Menlo, Monaco, 'Courier New', monospace; padding: 2em 1em 1em; border-radius: 5px; line-height: 1.375; position: relative; background: #fafafa; display: block; &:after { content: 'CODE'; display: block; position: absolute; left: 8px; top: 4px; font-size: 14px; font-weight: bold; color: #ccc; } } } table { border-collapse: collapse; margin: 1rem 0; display: block; overflow-x: auto; } tr { border-top: 1px solid #dfe2e5; } td, th { border: 1px solid #dfe2e5; padding: .6em 1em; } ul, ol { color: var(--c-base-blacklight); padding-left: 24px; line-height: 1.725; margin-bottom: 16px; } strong { font-weight: bolder; } } ================================================ FILE: public/default-files/themes/fly/assets/styles/_blocks/tag.less ================================================ .current-tag-container { padding-top: 32px; color: var(--c-base-purple); } ================================================ FILE: public/default-files/themes/fly/assets/styles/_blocks/tags.less ================================================ .tags-container { padding: 32px 0; .tag { color: var(--c-base-purple); padding: 4px 8px; display: inline-block; border: 2px solid var(--c-base-purple); margin: 0 16px 16px 0; border-radius: 16px; } } ================================================ FILE: public/default-files/themes/fly/assets/styles/_core/base.less ================================================ /*! modern-normalize | MIT License | https://github.com/sindresorhus/modern-normalize */ /* Document ========================================================================== */ /** * Use a better box model (opinionated). */ html { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; margin: 0; padding: 0; } /** * Use a more readable tab size (opinionated). */ :root { -moz-tab-size: 4; tab-size: 4; } /** * 1. Correct the line height in all browsers. * 2. Prevent adjustments of font size after orientation changes in iOS. */ html { line-height: 1.15; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ } /* Sections ========================================================================== */ /** * Remove the margin in all browsers. */ body { margin: 0; font-size: 16px; } /** * Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) */ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; } /* Grouping content ========================================================================== */ /** * Add the correct height in Firefox. */ hr { height: 0; } /* Text-level semantics ========================================================================== */ /** * Add the correct text decoration in Chrome, Edge, and Safari. */ abbr[title] { text-decoration: underline dotted; } /** * Add the correct font weight in Chrome, Edge, and Safari. */ b, strong { font-weight: bolder; } /** * 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) * 2. Correct the odd `em` font sizing in all browsers. */ code, kbd, samp, pre { font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace; /* 1 */ font-size: 1em; /* 2 */ } /** * Add the correct font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` elements from affecting the line height in all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } /* Forms ========================================================================== */ /** * 1. Change the font styles in all browsers. * 2. Remove the margin in Firefox and Safari. */ button, input, optgroup, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 1 */ line-height: 1.15; /* 1 */ margin: 0; /* 2 */ } /** * Remove the inheritance of text transform in Edge and Firefox. * 1. Remove the inheritance of text transform in Firefox. */ button, select { /* 1 */ text-transform: none; } /** * Correct the inability to style clickable types in iOS and Safari. */ button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; } /** * Remove the inner border and padding in Firefox. */ button::-moz-focus-inner, [type='button']::-moz-focus-inner, [type='reset']::-moz-focus-inner, [type='submit']::-moz-focus-inner { border-style: none; padding: 0; } /** * Restore the focus styles unset by the previous rule. */ button:-moz-focusring, [type='button']:-moz-focusring, [type='reset']:-moz-focusring, [type='submit']:-moz-focusring { outline: 1px dotted ButtonText; } /** * Correct the padding in Firefox. */ fieldset { padding: 0.35em 0.75em 0.625em; } /** * Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers. */ legend { padding: 0; } /** * Add the correct vertical alignment in Chrome and Firefox. */ progress { vertical-align: baseline; } /** * Correct the cursor style of increment and decrement buttons in Safari. */ [type='number']::-webkit-inner-spin-button, [type='number']::-webkit-outer-spin-button { height: auto; } /** * 1. Correct the odd appearance in Chrome and Safari. * 2. Correct the outline style in Safari. */ [type='search'] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; /* 2 */ } /** * Remove the inner padding in Chrome and Safari on macOS. */ [type='search']::-webkit-search-decoration { -webkit-appearance: none; } /** * 1. Correct the inability to style clickable types in iOS and Safari. * 2. Change font properties to `inherit` in Safari. */ ::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */ font: inherit; /* 2 */ } /* Interactive ========================================================================== */ /* * Add the correct display in Chrome and Safari. */ summary { display: list-item; } ================================================ FILE: public/default-files/themes/fly/assets/styles/_core/colors.less ================================================ :root { --c-base-black: #0C1E29; --c-base-blacklight: #4E616C; --c-base-purple: #5978F3; --c-base-purplelight: #C2CCF2; --c-base-bluelight: hwb(199, 1%, 4%); --c-base-blue: hwb(209, 0%, 35%); --c-base-green: hwb(123, 40%, 27%); --c-base-graylight: hwb(0, 80%, 20%); --c-base-gray: hwb(0, 60%, 40%); --c-base-orange: hwb(22, 21%, 5%); --c-base-red: hwb(5, 24%, 9%); --c-base-white: hwb(0, 95%, 5%); --c-base-yellow: hwb(48, 6%, 5%); } ================================================ FILE: public/default-files/themes/fly/assets/styles/_core/github.less ================================================ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #333; background: #f8f8f8; } .hljs-comment, .hljs-quote { color: #998; font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-subst { color: #333; font-weight: bold; } .hljs-number, .hljs-literal, .hljs-variable, .hljs-template-variable, .hljs-tag .hljs-attr { color: #008080; } .hljs-string, .hljs-doctag { color: #d14; } .hljs-title, .hljs-section, .hljs-selector-id { color: #900; font-weight: bold; } .hljs-subst { font-weight: normal; } .hljs-type, .hljs-class .hljs-title { color: #458; font-weight: bold; } .hljs-tag, .hljs-name, .hljs-attribute { color: #000080; font-weight: normal; } .hljs-regexp, .hljs-link { color: #009926; } .hljs-symbol, .hljs-bullet { color: #990073; } .hljs-built_in, .hljs-builtin-name { color: #0086b3; } .hljs-meta { color: #999; font-weight: bold; } .hljs-deletion { background: #fdd; } .hljs-addition { background: #dfd; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: public/default-files/themes/fly/assets/styles/main.less ================================================ @import "_core/colors"; @import "_core/base"; @import "_core/github"; @import "_blocks/fonts"; @import "_blocks/footer"; @import "_blocks/header"; @import "_blocks/link"; @import "_blocks/list"; @import "_blocks/pagination"; @import "_blocks/post"; @import "_blocks/tag"; @import "_blocks/archives"; @import "_blocks/tags"; ================================================ FILE: public/default-files/themes/fly/config.json ================================================ { "name": "Fly", "version": "1.0.0", "author": "EryouHao", "repository": "https://github.com/getgridea/gridea-theme-fly", "customConfig": [ { "name": "skin", "label": "皮肤", "group": "皮肤", "value": "white", "type": "select", "options": [ { "label": "简约白", "value": "white" }, { "label": "低调黑", "value": "black" } ] }, { "name": "contentMaxWidth", "label": "内容区最大宽度", "group": "布局", "value": "800px", "type": "input", "note": "可填像素类型(如:320px)或百分比类型(如:38.2%)" }, { "name": "textSize", "label": "正文内容文字大小", "group": "布局", "value": "16px", "type": "input", "note": "px 或 rem(如 16px 或 1rem)" }, { "name": "github", "label": "Github", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "twitter", "label": "Twitter", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "weibo", "label": "微博", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "zhihu", "label": "知乎", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "facebook", "label": "Facebook", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "customCss", "label": "自定义CSS", "group": "自定义样式", "value": "", "type": "textarea", "note": "" }, { "name": "ga", "label": "跟踪 ID", "group": "谷歌统计", "value": "", "type": "input", "note": "UA-xxxxxxxxx-x" } ] } ================================================ FILE: public/default-files/themes/fly/style-override.js ================================================ const generateOverride = (params = {}) => { let result = '' // 暗黑皮肤 if (params.skin && params.skin !== 'white') { result += ` body { color: #dee2e6; } body, .site-header-container, .menu-container { background: #212529; } a, .link { color: #e9ecef; } .site-header { box-shadow: inset 0px -1px 0px #495057; } .post-item .content .post-title { color: #e9ecef; } .post-item .content .post-abstract { color: #868e96; } .post-info { color: #495057; } .post-info a { color: #5978f3; } a.purple-link, .post-content a { background: linear-gradient(180deg, transparent 70%, #5978f3 0); } .post-detail .post-title { color: #e9ecef; } .post-content p, .post-content table, .post-content ul, .post-content ol { color: #dee2e6; } .post-content h1, .post-content h2, .post-content h3, .post-content h4, .post-content h5, .post-content h6 { color: #e9ecef; } .post-detail .feature-container, .post-content img { box-shadow: none; } .post-content blockquote { background: #343a40; border-left: 2px solid #5978f3; } .post-content code { color: #e9ecef; } .post-content p code { background: #495057; } .post-content pre code { background: #000000; } .post-content pre code:after { color: #495057; } .hljs-keyword, .hljs-selector-tag, .hljs-subst { color: #d4bdbd; } .post-content tr { border-top: 1px solid #495057; } .post-content td, .post-content th { border: 1px solid #495057; } ` } // 内容区最大宽度 - contentMaxWidth if (params.contentMaxWidth && params.contentMaxWidth !== '800px') { result += ` .main { max-width: ${params.contentMaxWidth}; } ` } // 正文内容文字大小 - textSize if (params.textSize && params.textSize !== '16px') { result += ` body { font-size: ${params.textSize}; } ` } if (params.customCss) { result += ` ${params.customCss} ` } console.log('result', result) return result } module.exports = generateOverride ================================================ FILE: public/default-files/themes/fly/templates/archives.ejs ================================================ <%- include('./includes/head', { siteTitle: themeConfig.siteName }) %>
<%- include('./includes/header') %>
<%- include('./includes/post-list-archives') %>
<%- include('./includes/footer') %>
<%- include('./includes/scripts') %> ================================================ FILE: public/default-files/themes/fly/templates/includes/footer.ejs ================================================ ================================================ FILE: public/default-files/themes/fly/templates/includes/head.ejs ================================================ <%= siteTitle %> <% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %> <% if (commentSetting.commentPlatform === 'gitalk') { %> <% } %> <% if (commentSetting.commentPlatform === 'disqus') { %> <% } %> <% } %> <% if (site.customConfig.ga) { %> <% } %> ================================================ FILE: public/default-files/themes/fly/templates/includes/header.ejs ================================================
================================================ FILE: public/default-files/themes/fly/templates/includes/post-list-archives.ejs ================================================ <% let years = []; posts.forEach((item) => { const year = item.date.substring(0, 4); if (!years.includes(year)) { years.push(year); } }); %>
<% years.forEach(function(year) { %>

<%- year %>

<% posts.forEach(function(post) { %> <%if (post.date.indexOf(year) !== -1) { %>

<%= post.title %> <%= post.dateFormat %>

<% } %> <% }); %> <% }); %>
================================================ FILE: public/default-files/themes/fly/templates/includes/post-list.ejs ================================================ <% posts.forEach(function(post) { %>

<%= post.title %>

<%- post.abstract %>
<% if (themeConfig.showFeatureImage && post.feature) { %>
<% } %>
<% }); %>
<% if (pagination.prev) { %> <% } %> <% if (pagination.next) { %> <% } %>
================================================ FILE: public/default-files/themes/fly/templates/includes/scripts.ejs ================================================ <% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %> <% if (commentSetting.commentPlatform === 'gitalk') { %> <% } %> <% if (commentSetting.commentPlatform === 'disqus') { %> <% } %> <% } %> ================================================ FILE: public/default-files/themes/fly/templates/index.ejs ================================================ <%- include('./includes/head', { siteTitle: themeConfig.siteName }) %>
<%- include('./includes/header') %>
<%- include('./includes/post-list') %>
<%- include('./includes/footer') %>
<%- include('./includes/scripts') %> ================================================ FILE: public/default-files/themes/fly/templates/post.ejs ================================================ <%- include('./includes/head', { siteTitle: `${post.title} | ${themeConfig.siteName}` }) %>
<%- include('./includes/header') %>
<% if (themeConfig.showFeatureImage && post.feature) { %>
<% } %>

<%= post.title %>

<%- post.content %>
<% if (post.nextPost && !post.hideInList) { %> <% } %>
<% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %> <% if (commentSetting.commentPlatform === 'gitalk') { %>
<% } %> <% if (commentSetting.commentPlatform === 'disqus') { %>
<% } %> <% } %> <%- include('./includes/footer') %>
<%- include('./includes/scripts') %> ================================================ FILE: public/default-files/themes/fly/templates/tag.ejs ================================================ <%- include('./includes/head', { siteTitle: `${tag.name} | ${themeConfig.siteName}` }) %>
<%- include('./includes/header') %>

<%= tag.name %>

<%- include('./includes/post-list') %>
<%- include('./includes/footer') %>
<%- include('./includes/scripts') %> ================================================ FILE: public/default-files/themes/fly/templates/tags.ejs ================================================ <%- include('./includes/head', { siteTitle: themeConfig.siteName }) %>
<%- include('./includes/header') %>
<% tags.forEach((tag) => { %> <%= tag.name %> <% }); %>
<%- include('./includes/footer') %>
<%- include('./includes/scripts') %> ================================================ FILE: public/default-files/themes/notes/assets/styles/abstracts/varibles.less ================================================ @darker-main-color: #004cb3; @dark-main-color: #0061e6; @main-color: #006CFF; @light-main-color: #006cff21; @black: #000000; @white: #FFFFFF; @dark-gray: #5E5E5E; @default-gray: #CCCCCC; @light-gray: #E6E6E6; @lighter-gray: #F3F3F3; @darker-red: #af0900; @dark-red: #e20c00; @default-red: #FB0D00; @default-green: #11B711; @default-yellow: #FAAD14; @default-green-blue: #788F9A; @dark-text-color: #2b2b2b; @default-text-color: #444444; @light-text-color: #6a6a6a; ================================================ FILE: public/default-files/themes/notes/assets/styles/components/about.less ================================================ .about-page { padding: 24px 32px; } ================================================ FILE: public/default-files/themes/notes/assets/styles/components/archives.less ================================================ .archives-container { padding: 32px; flex: 1; .year { font-size: 1.375rem; font-weight: bold; margin: 24px 0 16px; color: @oc-gray-6; padding: 0 24px; } .post { padding: 16px 24px; display: block; .post-title { font-size: 16px; font-weight: 900; letter-spacing: .02em; } .time { font-size: 0.75rem; margin-top: 8px; color: @oc-gray-4; } } } @media (max-width: 600px) { .archives-container { padding: 16px; } } ================================================ FILE: public/default-files/themes/notes/assets/styles/components/footer.less ================================================ .site-footer { font-size: 12px; text-align: center; padding: 40px 24px; color: @oc-gray-6; display: flex; justify-content: center; align-items: center; } .rss { display: inline-flex; align-items: center; margin-left: 24px; } ================================================ FILE: public/default-files/themes/notes/assets/styles/components/header.less ================================================ .site-header { padding: 48px 0; text-align: center; .site-title { font-size: 32px; font-weight: bold; } .site-description { font-size: 16px; padding: 24px; color: @oc-gray-7; font-weight: lighter; } .menu-container { display: flex; justify-content: center; flex-wrap: wrap; a.menu { font-size: 16px; padding: 8px 16px; flex-shrink: 0; font-weight: 600; } } .avatar { margin-bottom: 24px; border-radius: 50%; width: 120px; height: 120px; } .social-container { padding: 16px; font-size: 18px; a { margin: 4px 8px; color: #868e96; } } } @media (max-width: 600px) { .site-header { padding: 24px 0 0; .avatar { width: 80px; height: 80px; } } } ================================================ FILE: public/default-files/themes/notes/assets/styles/components/home.less ================================================ .post-container { flex: 1; .post { padding-bottom: 32px; .post-title { font-size: 28px; text-align: center; padding: 24px 0; font-weight: 900; letter-spacing: .02em; } .post-info { text-align: center; font-size: 12px; padding-bottom: 24px; > span { color: @dark-gray; &:not(:first-child) { &:before { content: "/ "; font-size: 10px; color: rgba(0,0,0,.1); margin: 0 4px; } } } .post-tag { padding: 8px 8px; } } .post-feature-image { display: block; width: 100%; padding-top: 32.6%; border-radius: 2px; overflow: hidden; background-size: cover; background-position: center; transition: all 0.3s; img { width: 100%; // height: 100% } &:hover { transform: scale(1.0082); } } .post-abstract { padding: 24px 0; line-height: 1.5; font-size: 16px; strong { font-weight: bolder; } a { color: @main-color; transition: all 0.3s; &:hover { color: @dark-main-color; border-bottom: 1px dotted @dark-main-color; } } code { font-family: monospace; font-size: inherit; background-color: rgba(0,0,0,.06); padding: 0 2px; border: 1px solid rgba(0,0,0,.08); border-radius: 2px 2px; line-height: initial; word-wrap: break-word; text-indent: 0; } } } } .pagination-container { padding: 32px 16px; overflow: hidden; .prev-page { float: left; } .next-page { float: right; } .prev-page, .next-page { padding: 6px 12px; font-weight: bold; border-bottom: 2px solid transparent; &:hover { border-bottom: 2px solid; } } } @media (max-width: 600px) { .post-container { .post { padding: 16px 16px; .post-title { padding: 16px 0; font-size: 24px; } .post-abstract { padding: 16px 0; } .post-feature-image { padding-top: 56.25%; } } } } ================================================ FILE: public/default-files/themes/notes/assets/styles/components/post.less ================================================ .post-detail { flex: 1; .post { padding: 24px 32px; .post-feature-image { width: 100%; height: auto; margin-bottom: 24px; border-radius: 2px; } .post-title { font-size: 32px; text-align: center; padding: 24px 0; font-weight: 900; letter-spacing: 0.02em; } .post-info { text-align: center; font-size: 12px; padding-bottom: 24px; > span { color: @dark-gray; &:not(:first-child) { &:before { content: "/ "; font-size: 10px; color: rgba(0,0,0,.1); margin: 0 4px; } } } .post-tag { padding: 8px 8px; } } .post-content-wrapper { display: flex; } .post-content { width: 100%; flex-shrink: 0; font-family: "Noto Serif","PingFang SC","Hiragino Sans GB","Droid Sans Fallback","Microsoft YaHei",sans-serif; a { color: rgba(0,0,0,.98); word-wrap: break-word; text-decoration: none; border-bottom: 1px solid rgba(0,0,0,.26); &:hover { color: @dark-main-color; border-bottom: 1px solid @dark-main-color; } } img { display: block; box-shadow: 0 0 30px #eee; max-width: 100%; border-radius: 2px; margin: 24px auto; } p { line-height: 1.62; margin-bottom: 1.12em; font-size: 16px; letter-spacing: .05em; hyphens: auto; } p, li { code { font-family: 'Source Code Pro', Consolas, Menlo, Monaco, 'Courier New', monospace; line-height: initial; word-wrap: break-word; border-radius: 0; background-color: #fff5f5; color: #c53030; padding: .2em .33333333em; margin-left: .125em; margin-right: .125em; } } pre { margin-bottom: 1.5rem; padding: 0; position: relative; code { font-size: 0.96em; font-family: 'Source Code Pro', Consolas, Menlo, Monaco, 'Courier New', monospace; padding: 1em; border-radius: 5px; line-height: 1.5; } } blockquote { color: #9a9a9a; position: relative; padding: .4em 0 0 2.2em; font-size: .96em; &:before { position: absolute; top: -4px; left: 0; content: "\201c"; font: 700 62px/1 serif; color: rgba(0,0,0,.1); } } table { border-collapse: collapse; margin: 1rem 0; display: block; overflow-x: auto; } tr { border-top: 1px solid #dfe2e5; } td, th { border: 1px solid #dfe2e5; padding: .6em 1em; } ul, ol { padding-left: 35px; line-height: 1.725; margin-bottom: 16px; } ul { list-style-type: square; } h1, h2, h3, h4, h5, h6 { margin: 16px 0; font-weight: 700; padding-top: 16px; } h1 { font-size: 1.8em; } h2 { font-size: 1.42em; } h3 { font-size: 1.17em; } h4 { font-size: 1em; } h5 { font-size: 1em; } h6 { font-size: 1em; font-weight: 500; } hr { display: block; border: 0; margin: 2.24em auto 2.86em; &:before { color: rgba(0,0,0,.2); font-size: 1.1em; display: block; content: "* * *"; text-align: center; } } mark { background: #faf089; color: #744210; padding: .2em; } .footnotes { margin-left: auto; margin-right: auto; max-width: 760px; padding-left: 18px; padding-right: 18px; &:before { content: ""; display: block; border-top: 4px solid rgba(0,0,0,.1); width: 50%; max-width: 100px; margin: 40px 0 20px; } } .contains-task-list { list-style-type: none; padding-left: 30px; } .task-list-item { position: relative; } .task-list-item-checkbox { position: absolute; cursor: pointer; width: 16px; height: 16px; margin: 4px 0 0; top: -1px; left: -22px; transform-origin: center; transform: rotate(-90deg); transition: all .2s ease; &:checked { transform: rotate(0); &:before { border: transparent; background-color: @oc-green-5; } &:after { transform: rotate(-45deg) scale(1); } + .task-list-item-label { color: #a0a0a0; text-decoration: line-through; } } &:before { content: ""; width: 16px; height: 16px; box-sizing: border-box; display: inline-block; border: 1px solid #9ae6b4; border-radius: 2px; background-color: #fff; position: absolute; top: 0; left: 0; transition: all .2s ease; } &:after { content: ""; transform: rotate(-45deg) scale(0); width: 9px; height: 5px; border: 1px solid #fff; border-top: none; border-right: none; position: absolute; display: inline-block; top: 4px; left: 4px; transition: all .2s ease; } } } } } .next-post { text-align: center; padding: 24px 32px; .next { margin-bottom: 24px; color: @oc-gray-8; font-weight: lighter; } .post-title { font-size: 20px; font-weight: bold; letter-spacing: .02em; } } #gitalk-container, #disqus_thread { padding: 24px 32px; } .toc-container { .markdownIt-TOC { position: sticky; top: 32px; width: 200px; font-size: 12px; list-style: none; padding-left: 0; padding: 16px 8px; &:before { content: ""; position: absolute; top: 0; left: 8px; bottom: 0; width: 1px; background-color: #ebedef; opacity: .5; } } ul { list-style: none; } li { padding-left: 16px; a { color: @oc-gray-6; padding: 4px; display: block; transition: all 0.3s; &:hover { background: #fafafa; } &.current { color: @main-color; background: #fafafa; } } } } @media (max-width: 600px) { .post-detail { .post { padding: 16px; .post-title { font-size: 24px; padding: 16px 0; } } } } @media (max-width: 1150px) { .toc-container { display: none; } } ================================================ FILE: public/default-files/themes/notes/assets/styles/components/tag.less ================================================ .current-tag-container { .title { text-align: center; font-size: 18px; margin-bottom: 24px; } } ================================================ FILE: public/default-files/themes/notes/assets/styles/components/tags.less ================================================ .tags-container { padding: 32px 32px; flex: 1; text-align: center; .tag { display: inline-block; padding: 8px 16px; margin: 8px; background: @oc-gray-0; color: @oc-gray-7; border-radius: 2px; font-size: 14px; &:hover { background: @oc-gray-2; color: @oc-gray-9; } } } ================================================ FILE: public/default-files/themes/notes/assets/styles/lib/colors.less ================================================ // // // 𝗖 𝗢 𝗟 𝗢 𝗥 // v 1.6.3 // // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // General // ─────────────────────────────────── @oc-white: #ffffff; @oc-black: #000000; // Gray // ─────────────────────────────────── @oc-gray-list: #f8f9fa, #f1f3f5, #e9ecef, #dee2e6, #ced4da, #adb5bd, #868e96, #495057, #343a40, #212529; @oc-gray-0: extract(@oc-gray-list, 1); @oc-gray-1: extract(@oc-gray-list, 2); @oc-gray-2: extract(@oc-gray-list, 3); @oc-gray-3: extract(@oc-gray-list, 4); @oc-gray-4: extract(@oc-gray-list, 5); @oc-gray-5: extract(@oc-gray-list, 6); @oc-gray-6: extract(@oc-gray-list, 7); @oc-gray-7: extract(@oc-gray-list, 8); @oc-gray-8: extract(@oc-gray-list, 9); @oc-gray-9: extract(@oc-gray-list, 10); // Red // ─────────────────────────────────── @oc-red-list: #fff5f5, #ffe3e3, #ffc9c9, #ffa8a8, #ff8787, #ff6b6b, #fa5252, #f03e3e, #e03131, #c92a2a; @oc-red-0: extract(@oc-red-list, 1); @oc-red-1: extract(@oc-red-list, 2); @oc-red-2: extract(@oc-red-list, 3); @oc-red-3: extract(@oc-red-list, 4); @oc-red-4: extract(@oc-red-list, 5); @oc-red-5: extract(@oc-red-list, 6); @oc-red-6: extract(@oc-red-list, 7); @oc-red-7: extract(@oc-red-list, 8); @oc-red-8: extract(@oc-red-list, 9); @oc-red-9: extract(@oc-red-list, 10); // Pink // ─────────────────────────────────── @oc-pink-list: #fff0f6, #ffdeeb, #fcc2d7, #faa2c1, #f783ac, #f06595, #e64980, #d6336c, #c2255c, #a61e4d; @oc-pink-0: extract(@oc-pink-list, 1); @oc-pink-1: extract(@oc-pink-list, 2); @oc-pink-2: extract(@oc-pink-list, 3); @oc-pink-3: extract(@oc-pink-list, 4); @oc-pink-4: extract(@oc-pink-list, 5); @oc-pink-5: extract(@oc-pink-list, 6); @oc-pink-6: extract(@oc-pink-list, 7); @oc-pink-7: extract(@oc-pink-list, 8); @oc-pink-8: extract(@oc-pink-list, 9); @oc-pink-9: extract(@oc-pink-list, 10); // Grape // ─────────────────────────────────── @oc-grape-list: #f8f0fc, #f3d9fa, #eebefa, #e599f7, #da77f2, #cc5de8, #be4bdb, #ae3ec9, #9c36b5, #862e9c; @oc-grape-0: extract(@oc-grape-list, 1); @oc-grape-1: extract(@oc-grape-list, 2); @oc-grape-2: extract(@oc-grape-list, 3); @oc-grape-3: extract(@oc-grape-list, 4); @oc-grape-4: extract(@oc-grape-list, 5); @oc-grape-5: extract(@oc-grape-list, 6); @oc-grape-6: extract(@oc-grape-list, 7); @oc-grape-7: extract(@oc-grape-list, 8); @oc-grape-8: extract(@oc-grape-list, 9); @oc-grape-9: extract(@oc-grape-list, 10); // Violet // ─────────────────────────────────── @oc-violet-list: #f3f0ff, #e5dbff, #d0bfff, #b197fc, #9775fa, #845ef7, #7950f2, #7048e8, #6741d9, #5f3dc4; @oc-violet-0: extract(@oc-violet-list, 1); @oc-violet-1: extract(@oc-violet-list, 2); @oc-violet-2: extract(@oc-violet-list, 3); @oc-violet-3: extract(@oc-violet-list, 4); @oc-violet-4: extract(@oc-violet-list, 5); @oc-violet-5: extract(@oc-violet-list, 6); @oc-violet-6: extract(@oc-violet-list, 7); @oc-violet-7: extract(@oc-violet-list, 8); @oc-violet-8: extract(@oc-violet-list, 9); @oc-violet-9: extract(@oc-violet-list, 10); // Indigo // ─────────────────────────────────── @oc-indigo-list: #edf2ff, #dbe4ff, #bac8ff, #91a7ff, #748ffc, #5c7cfa, #4c6ef5, #4263eb, #3b5bdb, #364fc7; @oc-indigo-0: extract(@oc-indigo-list, 1); @oc-indigo-1: extract(@oc-indigo-list, 2); @oc-indigo-2: extract(@oc-indigo-list, 3); @oc-indigo-3: extract(@oc-indigo-list, 4); @oc-indigo-4: extract(@oc-indigo-list, 5); @oc-indigo-5: extract(@oc-indigo-list, 6); @oc-indigo-6: extract(@oc-indigo-list, 7); @oc-indigo-7: extract(@oc-indigo-list, 8); @oc-indigo-8: extract(@oc-indigo-list, 9); @oc-indigo-9: extract(@oc-indigo-list, 10); // Blue // ─────────────────────────────────── @oc-blue-list: #e7f5ff, #d0ebff, #a5d8ff, #74c0fc, #4dabf7, #339af0, #228be6, #1c7ed6, #1971c2, #1864ab; @oc-blue-0: extract(@oc-blue-list, 1); @oc-blue-1: extract(@oc-blue-list, 2); @oc-blue-2: extract(@oc-blue-list, 3); @oc-blue-3: extract(@oc-blue-list, 4); @oc-blue-4: extract(@oc-blue-list, 5); @oc-blue-5: extract(@oc-blue-list, 6); @oc-blue-6: extract(@oc-blue-list, 7); @oc-blue-7: extract(@oc-blue-list, 8); @oc-blue-8: extract(@oc-blue-list, 9); @oc-blue-9: extract(@oc-blue-list, 10); // Cyan // ─────────────────────────────────── @oc-cyan-list: #e3fafc, #c5f6fa, #99e9f2, #66d9e8, #3bc9db, #22b8cf, #15aabf, #1098ad, #0c8599, #0b7285; @oc-cyan-0: extract(@oc-cyan-list, 1); @oc-cyan-1: extract(@oc-cyan-list, 2); @oc-cyan-2: extract(@oc-cyan-list, 3); @oc-cyan-3: extract(@oc-cyan-list, 4); @oc-cyan-4: extract(@oc-cyan-list, 5); @oc-cyan-5: extract(@oc-cyan-list, 6); @oc-cyan-6: extract(@oc-cyan-list, 7); @oc-cyan-7: extract(@oc-cyan-list, 8); @oc-cyan-8: extract(@oc-cyan-list, 9); @oc-cyan-9: extract(@oc-cyan-list, 10); // Teal // ─────────────────────────────────── @oc-teal-list: #e6fcf5, #c3fae8, #96f2d7, #63e6be, #38d9a9, #20c997, #12b886, #0ca678, #099268, #087f5b; @oc-teal-0: extract(@oc-teal-list, 1); @oc-teal-1: extract(@oc-teal-list, 2); @oc-teal-2: extract(@oc-teal-list, 3); @oc-teal-3: extract(@oc-teal-list, 4); @oc-teal-4: extract(@oc-teal-list, 5); @oc-teal-5: extract(@oc-teal-list, 6); @oc-teal-6: extract(@oc-teal-list, 7); @oc-teal-7: extract(@oc-teal-list, 8); @oc-teal-8: extract(@oc-teal-list, 9); @oc-teal-9: extract(@oc-teal-list, 10); // Green // ─────────────────────────────────── @oc-green-list: #ebfbee, #d3f9d8, #b2f2bb, #8ce99a, #69db7c, #51cf66, #40c057, #37b24d, #2f9e44, #2b8a3e; @oc-green-0: extract(@oc-green-list, 1); @oc-green-1: extract(@oc-green-list, 2); @oc-green-2: extract(@oc-green-list, 3); @oc-green-3: extract(@oc-green-list, 4); @oc-green-4: extract(@oc-green-list, 5); @oc-green-5: extract(@oc-green-list, 6); @oc-green-6: extract(@oc-green-list, 7); @oc-green-7: extract(@oc-green-list, 8); @oc-green-8: extract(@oc-green-list, 9); @oc-green-9: extract(@oc-green-list, 10); // Lime // ─────────────────────────────────── @oc-lime-list: #f4fce3, #e9fac8, #d8f5a2, #c0eb75, #a9e34b, #94d82d, #82c91e, #74b816, #66a80f, #5c940d; @oc-lime-0: extract(@oc-lime-list, 1); @oc-lime-1: extract(@oc-lime-list, 2); @oc-lime-2: extract(@oc-lime-list, 3); @oc-lime-3: extract(@oc-lime-list, 4); @oc-lime-4: extract(@oc-lime-list, 5); @oc-lime-5: extract(@oc-lime-list, 6); @oc-lime-6: extract(@oc-lime-list, 7); @oc-lime-7: extract(@oc-lime-list, 8); @oc-lime-8: extract(@oc-lime-list, 9); @oc-lime-9: extract(@oc-lime-list, 10); // Yellow // ─────────────────────────────────── @oc-yellow-list: #fff9db, #fff3bf, #ffec99, #ffe066, #ffd43b, #fcc419, #fab005, #f59f00, #f08c00, #e67700; @oc-yellow-0: extract(@oc-yellow-list, 1); @oc-yellow-1: extract(@oc-yellow-list, 2); @oc-yellow-2: extract(@oc-yellow-list, 3); @oc-yellow-3: extract(@oc-yellow-list, 4); @oc-yellow-4: extract(@oc-yellow-list, 5); @oc-yellow-5: extract(@oc-yellow-list, 6); @oc-yellow-6: extract(@oc-yellow-list, 7); @oc-yellow-7: extract(@oc-yellow-list, 8); @oc-yellow-8: extract(@oc-yellow-list, 9); @oc-yellow-9: extract(@oc-yellow-list, 10); // Orange // ─────────────────────────────────── @oc-orange-list: #fff4e6, #ffe8cc, #ffd8a8, #ffc078, #ffa94d, #ff922b, #fd7e14, #f76707, #e8590c, #d9480f; @oc-orange-0: extract(@oc-orange-list, 1); @oc-orange-1: extract(@oc-orange-list, 2); @oc-orange-2: extract(@oc-orange-list, 3); @oc-orange-3: extract(@oc-orange-list, 4); @oc-orange-4: extract(@oc-orange-list, 5); @oc-orange-5: extract(@oc-orange-list, 6); @oc-orange-6: extract(@oc-orange-list, 7); @oc-orange-7: extract(@oc-orange-list, 8); @oc-orange-8: extract(@oc-orange-list, 9); @oc-orange-9: extract(@oc-orange-list, 10); ================================================ FILE: public/default-files/themes/notes/assets/styles/lib/github.less ================================================ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #333; background: #f9f7f3; } .hljs-comment, .hljs-quote { color: #998; font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-subst { color: #333; font-weight: bold; } .hljs-number, .hljs-literal, .hljs-variable, .hljs-template-variable, .hljs-tag .hljs-attr { color: #008080; } .hljs-string, .hljs-doctag { color: #d14; } .hljs-title, .hljs-section, .hljs-selector-id { color: #900; font-weight: bold; } .hljs-subst { font-weight: normal; } .hljs-type, .hljs-class .hljs-title { color: #458; font-weight: bold; } .hljs-tag, .hljs-name, .hljs-attribute { color: #000080; font-weight: normal; } .hljs-regexp, .hljs-link { color: #009926; } .hljs-symbol, .hljs-bullet { color: #990073; } .hljs-built_in, .hljs-builtin-name { color: #0086b3; } .hljs-meta { color: #999; font-weight: bold; } .hljs-deletion { background: #fdd; } .hljs-addition { background: #dfd; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: public/default-files/themes/notes/assets/styles/lib/modern-normalize.less ================================================ /*! modern-normalize | MIT License | https://github.com/sindresorhus/modern-normalize */ /* Document ========================================================================== */ /** * Use a better box model (opinionated). */ html { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } /** * Use a more readable tab size (opinionated). */ :root { -moz-tab-size: 4; tab-size: 4; } /** * 1. Correct the line height in all browsers. * 2. Prevent adjustments of font size after orientation changes in iOS. */ html { line-height: 1.15; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ } /* Sections ========================================================================== */ /** * Remove the margin in all browsers. */ body { margin: 0; } /** * Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) */ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; } /* Grouping content ========================================================================== */ /* Text-level semantics ========================================================================== */ /** * Add the correct text decoration in Chrome, Edge, and Safari. */ abbr[title] { text-decoration: underline dotted; } /** * Add the correct font weight in Chrome, Edge, and Safari. */ b, strong { font-weight: bolder; } /** * 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) * 2. Correct the odd `em` font sizing in all browsers. */ code, kbd, samp, pre { font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace; /* 1 */ font-size: 1em; /* 2 */ } /** * Add the correct font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` elements from affecting the line height in all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } /* Forms ========================================================================== */ /** * 1. Change the font styles in all browsers. * 2. Remove the margin in Firefox and Safari. */ button, input, optgroup, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 1 */ line-height: 1.15; /* 1 */ margin: 0; /* 2 */ } /** * Remove the inheritance of text transform in Edge and Firefox. * 1. Remove the inheritance of text transform in Firefox. */ button, select { /* 1 */ text-transform: none; } /** * Correct the inability to style clickable types in iOS and Safari. */ button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; } /** * Remove the inner border and padding in Firefox. */ button::-moz-focus-inner, [type='button']::-moz-focus-inner, [type='reset']::-moz-focus-inner, [type='submit']::-moz-focus-inner { border-style: none; padding: 0; } /** * Restore the focus styles unset by the previous rule. */ button:-moz-focusring, [type='button']:-moz-focusring, [type='reset']:-moz-focusring, [type='submit']:-moz-focusring { outline: 1px dotted ButtonText; } /** * Correct the padding in Firefox. */ fieldset { padding: 0.35em 0.75em 0.625em; } /** * Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers. */ legend { padding: 0; } /** * Add the correct vertical alignment in Chrome and Firefox. */ progress { vertical-align: baseline; } /** * Correct the cursor style of increment and decrement buttons in Safari. */ [type='number']::-webkit-inner-spin-button, [type='number']::-webkit-outer-spin-button { height: auto; } /** * 1. Correct the odd appearance in Chrome and Safari. * 2. Correct the outline style in Safari. */ [type='search'] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; /* 2 */ } /** * Remove the inner padding in Chrome and Safari on macOS. */ [type='search']::-webkit-search-decoration { -webkit-appearance: none; } /** * 1. Correct the inability to style clickable types in iOS and Safari. * 2. Change font properties to `inherit` in Safari. */ ::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */ font: inherit; /* 2 */ } /* Interactive ========================================================================== */ /* * Add the correct display in Chrome and Safari. */ summary { display: list-item; } ================================================ FILE: public/default-files/themes/notes/assets/styles/main.less ================================================ @import './lib/modern-normalize'; @import './lib/colors'; @import './abstracts/varibles'; *, *:before, *:after { margin: 0; padding: 0; } html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { border: 0; vertical-align: baseline; } html { font-size: 58%; } body { color: rgba(0,0,0,.86); font: 400 16px/1.42 -apple-system,BlinkMacSystemFont,"Helvetica Neue","PingFang SC","Hiragino Sans GB","Droid Sans Fallback","Microsoft YaHei",sans-serif; letter-spacing: .05em; } a { color: rgba(0,0,0,.98); text-decoration: none; transition: all 0.3s; &:hover { color: @main-color; } } body, div, a, p, ul, li, ol, h1, h2, h3, h4, h5, h6, table, tr, td { box-sizing: border-box; margin: 0; padding: 0; } .main { max-width: 800px; min-height: 100vh; margin: 0 auto; background: #fff; .main-content { flex: 1; display: flex; min-height: 100vh; flex-direction: column; padding: 0 24px; } } @import "./components/header.less"; @import "./components/home.less"; @import "./components/post.less"; @import "./components/archives.less"; @import "./components/tags.less"; @import "./components/tag.less"; @import "./components/about.less"; @import "./components/footer.less"; @import "./lib/github.less"; ================================================ FILE: public/default-files/themes/notes/config.json ================================================ { "name": "Notes", "version": "1.2.0", "author": "EryouHao", "repository": "https://github.com/getgridea/gridea-theme-notes", "customConfig": [ { "name": "contentMaxWidth", "label": "内容区最大宽度", "group": "布局", "value": "800px", "type": "input", "note": "可填像素类型(如:320px)或百分比类型(如:38.2%)" }, { "name": "textSize", "label": "正文内容文字大小", "group": "布局", "value": "16px", "type": "input", "note": "px 或 rem(如 16px 或 1rem)" }, { "name": "titleAlign", "label": "标题对齐", "group": "布局", "value": "center", "type": "radio", "options": [ { "label": "居中对齐", "value": "center" }, { "label": "左对齐", "value": "left" }, { "label": "右对齐", "value": "right" } ], "note": "包含标题及日期、标签等信息部分" }, { "name": "siteFont", "label": "网站字体", "group": "布局", "value": "-apple-system,BlinkMacSystemFont,'Helvetica Neue','PingFang SC','Hiragino Sans GB','Droid Sans Fallback','Microsoft YaHei',sans-serif", "type": "select", "options": [ { "label": "-apple-system,BlinkMacSystemFont,'Helvetica Neue','PingFang SC','Hiragino Sans GB','Droid Sans Fallback','Microsoft YaHei',sans-serif", "value": "-apple-system,BlinkMacSystemFont,'Helvetica Neue','PingFang SC','Hiragino Sans GB','Droid Sans Fallback','Microsoft YaHei',sans-serif" }, { "label": "Georgia, serif", "value": "Georgia, serif" } ], "note": "" }, { "name": "openPostToc", "label": "是否显示文章目录", "group": "布局", "value": true, "type": "switch", "note": "仅在宽屏时生效" }, { "name": "contentBgColor", "label": "内容区背景色", "group": "颜色", "value": "#ffffff", "type": "input", "card": "color", "note": "颜色字符串:(如:#EEEEEE、rgba(255, 255, 255, 0.9))" }, { "name": "pageBgColor", "label": "网页背景色", "group": "颜色", "value": "#ffffff", "type": "input", "card": "color", "note": "颜色字符串:(如:#EEEEEE、rgba(255, 255, 255, 0.9))" }, { "name": "textColor", "label": "文字颜色", "group": "颜色", "value": "rgba(0, 0, 0, 0.86)", "type": "input", "card": "color", "note": "颜色字符串:(如:#EEEEEE、rgba(255, 255, 255, 0.9))" }, { "name": "linkColor", "label": "链接颜色", "group": "颜色", "value": "rgba(0,0,0,.98)", "type": "input", "card": "color", "note": "颜色字符串:(如:#EEEEEE、rgba(255, 255, 255, 0.9))" }, { "name": "linkHoverColor", "label": "链接 Hover 颜色", "group": "颜色", "value": "#006CFF", "type": "input", "card": "color", "note": "颜色字符串:(如:#EEEEEE、rgba(255, 255, 255, 0.9))" }, { "name": "github", "label": "Github", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "twitter", "label": "Twitter", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "weibo", "label": "微博", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "zhihu", "label": "知乎", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "facebook", "label": "Facebook", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "customCss", "label": "自定义CSS", "group": "自定义样式", "value": "", "type": "textarea", "note": "" }, { "name": "ga", "label": "跟踪 ID", "group": "谷歌统计", "value": "", "type": "input", "note": "UA-xxxxxxxxx-x" }, { "name": "metaDescription", "label": "Meta Description", "group": "SEO", "value": "", "type": "input" } ] } ================================================ FILE: public/default-files/themes/notes/style-override.js ================================================ const generateOverride = (params = {}) => { let result = '' // 内容区最大宽度 - contentMaxWidth if (params.contentMaxWidth && params.contentMaxWidth !== '800px') { result += ` .main { max-width: ${params.contentMaxWidth}; } ` } // 正文内容文字大小 - textSize if (params.textSize && params.textSize !== '16px') { result += ` .post-detail .post .post-content p { font-size: ${params.textSize}; } ` } // 标题对齐 - titleAlign: center(默认)、left、right if (params.titleAlign) { result += ` .post-container .post .post-title { text-align: ${params.titleAlign}; } .post-container .post .post-info { text-align: ${params.titleAlign}; } .post-detail .post .post-title { text-align: ${params.titleAlign}; } .post-detail .post .post-info { text-align: ${params.titleAlign}; } ` } // 网站字体 if (params.siteFont) { result += ` body { font-family: ${params.siteFont}; } ` } // 是否显示文章目录 if (typeof params.openPostToc !== 'undefined' && !params.openPostToc) { result += ` .toc-container { display: none; } ` } // 内容区背景色 - contentBgColor if (params.contentBgColor && params.contentBgColor !== '#ffffff') { result += ` .main { background: ${params.contentBgColor}; } ` } // 网页背景色 - pageBgColor if (params.pageBgColor && params.pageBgColor !== '#ffffff') { result += ` body { background: ${params.pageBgColor}; } ` } // 文字颜色 - textColor if (params.textColor && params.textColor !== 'rgba(0, 0, 0, 0.86)') { result += ` body { color: ${params.textColor}; } ` } // 链接颜色 - linkColor if (params.linkColor && params.linkColor !== 'rgba(0,0,0,.98)') { result += ` a { color: ${params.linkColor}; } ` } // 链接 Hover 颜色 - linkHoverColor if (params.linkHoverColor && params.linkHoverColor !== '#006CFF') { result += ` a:hover { color: ${params.linkHoverColor}; } ` } if (params.customCss) { result += ` ${params.customCss} ` } console.log('result', result) return result } module.exports = generateOverride ================================================ FILE: public/default-files/themes/notes/templates/archives.ejs ================================================ <%- include('./includes/head', { siteTitle: themeConfig.siteName }) %>
<%- include('./includes/header') %> <%- include('./includes/post-list-archives') %> <%- include('./includes/pagination') %> <%- include('./includes/footer') %>
================================================ FILE: public/default-files/themes/notes/templates/includes/disqus.ejs ================================================
================================================ FILE: public/default-files/themes/notes/templates/includes/footer.ejs ================================================ ================================================ FILE: public/default-files/themes/notes/templates/includes/gitalk.ejs ================================================
================================================ FILE: public/default-files/themes/notes/templates/includes/head.ejs ================================================ <%= siteTitle %> <% if (site.customConfig.ga) { %> <% } %> ================================================ FILE: public/default-files/themes/notes/templates/includes/header.ejs ================================================ ================================================ FILE: public/default-files/themes/notes/templates/includes/pagination.ejs ================================================
<% if (pagination.prev) { %> 上一页 <% } %> <% if (pagination.next) { %> 下一页 <% } %>
================================================ FILE: public/default-files/themes/notes/templates/includes/post-list-archives.ejs ================================================ <% let years = []; posts.forEach((item) => { const year = item.date.substring(0, 4); if (!years.includes(year)) { years.push(year); } }); %>
<% years.forEach(function(year) { %>

<%- year %>

<% posts.forEach(function(post) { %> <%if (post.date.indexOf(year) !== -1) { %>

<%= post.title %>

<%= post.dateFormat %>
<% } %> <% }); %> <% }); %>
================================================ FILE: public/default-files/themes/notes/templates/includes/post-list.ejs ================================================
<% posts.forEach(function(post) { %>

<%= post.title %>

<% if (themeConfig.showFeatureImage && post.feature) { %> <% } %>
<%- post.abstract %>
<% }); %>
================================================ FILE: public/default-files/themes/notes/templates/index.ejs ================================================ <%- include('./includes/head', { siteTitle: themeConfig.siteName }) %>
<%- include('./includes/header') %> <%- include('./includes/post-list') %> <%- include('./includes/pagination') %> <%- include('./includes/footer') %>
================================================ FILE: public/default-files/themes/notes/templates/post.ejs ================================================ <%- include('./includes/head', { siteTitle: `${post.title} | ${themeConfig.siteName}` }) %>
<%- include('./includes/header') %>

<%= post.title %>

<% if (themeConfig.showFeatureImage && post.feature) { %> <% } %>
<%- post.content %>
<%- post.toc %>
<% if (post.nextPost && !post.hideInList) { %> <% } %> <% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %> <% if (commentSetting.commentPlatform === 'gitalk') { %> <%- include('./includes/gitalk') %> <% } %> <% if (commentSetting.commentPlatform === 'disqus') { %> <%- include('./includes/disqus') %> <% } %> <% } %> <%- include('./includes/footer') %>
================================================ FILE: public/default-files/themes/notes/templates/tag.ejs ================================================ <%- include('./includes/head', { siteTitle: `${tag.name} | ${themeConfig.siteName}` }) %>
<%- include('./includes/header') %>

标签:# <%= tag.name %>

<%- include('./includes/post-list') %> <%- include('./includes/pagination') %> <%- include('./includes/footer') %>
================================================ FILE: public/default-files/themes/notes/templates/tags.ejs ================================================ <%- include('./includes/head', { siteTitle: themeConfig.siteName }) %>
<%- include('./includes/header') %>
<% tags.forEach((tag) => { %> <%= tag.name %> <% }); %>
<%- include('./includes/footer') %>
================================================ FILE: public/default-files/themes/paper/assets/styles/_core/base.less ================================================ /*! modern-normalize | MIT License | https://github.com/sindresorhus/modern-normalize */ /* Document ========================================================================== */ /** * Use a better box model (opinionated). */ html { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; margin: 0; padding: 0; } /** * Use a more readable tab size (opinionated). */ :root { -moz-tab-size: 4; tab-size: 4; } /** * 1. Correct the line height in all browsers. * 2. Prevent adjustments of font size after orientation changes in iOS. */ html { line-height: 1.15; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ } /* Sections ========================================================================== */ /** * Remove the margin in all browsers. */ body { margin: 0; font-size: 14px; } /** * Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) */ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; } /* Grouping content ========================================================================== */ /** * Add the correct height in Firefox. */ hr { height: 0; } /* Text-level semantics ========================================================================== */ /** * Add the correct text decoration in Chrome, Edge, and Safari. */ abbr[title] { text-decoration: underline dotted; } /** * Add the correct font weight in Chrome, Edge, and Safari. */ b, strong { font-weight: bolder; } /** * 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) * 2. Correct the odd `em` font sizing in all browsers. */ code, kbd, samp, pre { font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace; /* 1 */ font-size: 1em; /* 2 */ } /** * Add the correct font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` elements from affecting the line height in all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } /* Forms ========================================================================== */ /** * 1. Change the font styles in all browsers. * 2. Remove the margin in Firefox and Safari. */ button, input, optgroup, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 1 */ line-height: 1.15; /* 1 */ margin: 0; /* 2 */ } /** * Remove the inheritance of text transform in Edge and Firefox. * 1. Remove the inheritance of text transform in Firefox. */ button, select { /* 1 */ text-transform: none; } /** * Correct the inability to style clickable types in iOS and Safari. */ button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; } /** * Remove the inner border and padding in Firefox. */ button::-moz-focus-inner, [type='button']::-moz-focus-inner, [type='reset']::-moz-focus-inner, [type='submit']::-moz-focus-inner { border-style: none; padding: 0; } /** * Restore the focus styles unset by the previous rule. */ button:-moz-focusring, [type='button']:-moz-focusring, [type='reset']:-moz-focusring, [type='submit']:-moz-focusring { outline: 1px dotted ButtonText; } /** * Correct the padding in Firefox. */ fieldset { padding: 0.35em 0.75em 0.625em; } /** * Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers. */ legend { padding: 0; } /** * Add the correct vertical alignment in Chrome and Firefox. */ progress { vertical-align: baseline; } /** * Correct the cursor style of increment and decrement buttons in Safari. */ [type='number']::-webkit-inner-spin-button, [type='number']::-webkit-outer-spin-button { height: auto; } /** * 1. Correct the odd appearance in Chrome and Safari. * 2. Correct the outline style in Safari. */ [type='search'] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; /* 2 */ } /** * Remove the inner padding in Chrome and Safari on macOS. */ [type='search']::-webkit-search-decoration { -webkit-appearance: none; } /** * 1. Correct the inability to style clickable types in iOS and Safari. * 2. Change font properties to `inherit` in Safari. */ ::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */ font: inherit; /* 2 */ } /* Interactive ========================================================================== */ /* * Add the correct display in Chrome and Safari. */ summary { display: list-item; } ================================================ FILE: public/default-files/themes/paper/assets/styles/_core/colors.less ================================================ // // // 𝗖 𝗢 𝗟 𝗢 𝗥 // v 1.6.3 // // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // General // ─────────────────────────────────── @oc-white: #ffffff; @oc-black: #000000; // Gray // ─────────────────────────────────── @oc-gray-list: #f8f9fa, #f1f3f5, #e9ecef, #dee2e6, #ced4da, #adb5bd, #868e96, #495057, #343a40, #212529; @oc-gray-0: extract(@oc-gray-list, 1); @oc-gray-1: extract(@oc-gray-list, 2); @oc-gray-2: extract(@oc-gray-list, 3); @oc-gray-3: extract(@oc-gray-list, 4); @oc-gray-4: extract(@oc-gray-list, 5); @oc-gray-5: extract(@oc-gray-list, 6); @oc-gray-6: extract(@oc-gray-list, 7); @oc-gray-7: extract(@oc-gray-list, 8); @oc-gray-8: extract(@oc-gray-list, 9); @oc-gray-9: extract(@oc-gray-list, 10); // Red // ─────────────────────────────────── @oc-red-list: #fff5f5, #ffe3e3, #ffc9c9, #ffa8a8, #ff8787, #ff6b6b, #fa5252, #f03e3e, #e03131, #c92a2a; @oc-red-0: extract(@oc-red-list, 1); @oc-red-1: extract(@oc-red-list, 2); @oc-red-2: extract(@oc-red-list, 3); @oc-red-3: extract(@oc-red-list, 4); @oc-red-4: extract(@oc-red-list, 5); @oc-red-5: extract(@oc-red-list, 6); @oc-red-6: extract(@oc-red-list, 7); @oc-red-7: extract(@oc-red-list, 8); @oc-red-8: extract(@oc-red-list, 9); @oc-red-9: extract(@oc-red-list, 10); // Pink // ─────────────────────────────────── @oc-pink-list: #fff0f6, #ffdeeb, #fcc2d7, #faa2c1, #f783ac, #f06595, #e64980, #d6336c, #c2255c, #a61e4d; @oc-pink-0: extract(@oc-pink-list, 1); @oc-pink-1: extract(@oc-pink-list, 2); @oc-pink-2: extract(@oc-pink-list, 3); @oc-pink-3: extract(@oc-pink-list, 4); @oc-pink-4: extract(@oc-pink-list, 5); @oc-pink-5: extract(@oc-pink-list, 6); @oc-pink-6: extract(@oc-pink-list, 7); @oc-pink-7: extract(@oc-pink-list, 8); @oc-pink-8: extract(@oc-pink-list, 9); @oc-pink-9: extract(@oc-pink-list, 10); // Grape // ─────────────────────────────────── @oc-grape-list: #f8f0fc, #f3d9fa, #eebefa, #e599f7, #da77f2, #cc5de8, #be4bdb, #ae3ec9, #9c36b5, #862e9c; @oc-grape-0: extract(@oc-grape-list, 1); @oc-grape-1: extract(@oc-grape-list, 2); @oc-grape-2: extract(@oc-grape-list, 3); @oc-grape-3: extract(@oc-grape-list, 4); @oc-grape-4: extract(@oc-grape-list, 5); @oc-grape-5: extract(@oc-grape-list, 6); @oc-grape-6: extract(@oc-grape-list, 7); @oc-grape-7: extract(@oc-grape-list, 8); @oc-grape-8: extract(@oc-grape-list, 9); @oc-grape-9: extract(@oc-grape-list, 10); // Violet // ─────────────────────────────────── @oc-violet-list: #f3f0ff, #e5dbff, #d0bfff, #b197fc, #9775fa, #845ef7, #7950f2, #7048e8, #6741d9, #5f3dc4; @oc-violet-0: extract(@oc-violet-list, 1); @oc-violet-1: extract(@oc-violet-list, 2); @oc-violet-2: extract(@oc-violet-list, 3); @oc-violet-3: extract(@oc-violet-list, 4); @oc-violet-4: extract(@oc-violet-list, 5); @oc-violet-5: extract(@oc-violet-list, 6); @oc-violet-6: extract(@oc-violet-list, 7); @oc-violet-7: extract(@oc-violet-list, 8); @oc-violet-8: extract(@oc-violet-list, 9); @oc-violet-9: extract(@oc-violet-list, 10); // Indigo // ─────────────────────────────────── @oc-indigo-list: #edf2ff, #dbe4ff, #bac8ff, #91a7ff, #748ffc, #5c7cfa, #4c6ef5, #4263eb, #3b5bdb, #364fc7; @oc-indigo-0: extract(@oc-indigo-list, 1); @oc-indigo-1: extract(@oc-indigo-list, 2); @oc-indigo-2: extract(@oc-indigo-list, 3); @oc-indigo-3: extract(@oc-indigo-list, 4); @oc-indigo-4: extract(@oc-indigo-list, 5); @oc-indigo-5: extract(@oc-indigo-list, 6); @oc-indigo-6: extract(@oc-indigo-list, 7); @oc-indigo-7: extract(@oc-indigo-list, 8); @oc-indigo-8: extract(@oc-indigo-list, 9); @oc-indigo-9: extract(@oc-indigo-list, 10); // Blue // ─────────────────────────────────── @oc-blue-list: #e7f5ff, #d0ebff, #a5d8ff, #74c0fc, #4dabf7, #339af0, #228be6, #1c7ed6, #1971c2, #1864ab; @oc-blue-0: extract(@oc-blue-list, 1); @oc-blue-1: extract(@oc-blue-list, 2); @oc-blue-2: extract(@oc-blue-list, 3); @oc-blue-3: extract(@oc-blue-list, 4); @oc-blue-4: extract(@oc-blue-list, 5); @oc-blue-5: extract(@oc-blue-list, 6); @oc-blue-6: extract(@oc-blue-list, 7); @oc-blue-7: extract(@oc-blue-list, 8); @oc-blue-8: extract(@oc-blue-list, 9); @oc-blue-9: extract(@oc-blue-list, 10); // Cyan // ─────────────────────────────────── @oc-cyan-list: #e3fafc, #c5f6fa, #99e9f2, #66d9e8, #3bc9db, #22b8cf, #15aabf, #1098ad, #0c8599, #0b7285; @oc-cyan-0: extract(@oc-cyan-list, 1); @oc-cyan-1: extract(@oc-cyan-list, 2); @oc-cyan-2: extract(@oc-cyan-list, 3); @oc-cyan-3: extract(@oc-cyan-list, 4); @oc-cyan-4: extract(@oc-cyan-list, 5); @oc-cyan-5: extract(@oc-cyan-list, 6); @oc-cyan-6: extract(@oc-cyan-list, 7); @oc-cyan-7: extract(@oc-cyan-list, 8); @oc-cyan-8: extract(@oc-cyan-list, 9); @oc-cyan-9: extract(@oc-cyan-list, 10); // Teal // ─────────────────────────────────── @oc-teal-list: #e6fcf5, #c3fae8, #96f2d7, #63e6be, #38d9a9, #20c997, #12b886, #0ca678, #099268, #087f5b; @oc-teal-0: extract(@oc-teal-list, 1); @oc-teal-1: extract(@oc-teal-list, 2); @oc-teal-2: extract(@oc-teal-list, 3); @oc-teal-3: extract(@oc-teal-list, 4); @oc-teal-4: extract(@oc-teal-list, 5); @oc-teal-5: extract(@oc-teal-list, 6); @oc-teal-6: extract(@oc-teal-list, 7); @oc-teal-7: extract(@oc-teal-list, 8); @oc-teal-8: extract(@oc-teal-list, 9); @oc-teal-9: extract(@oc-teal-list, 10); // Green // ─────────────────────────────────── @oc-green-list: #ebfbee, #d3f9d8, #b2f2bb, #8ce99a, #69db7c, #51cf66, #40c057, #37b24d, #2f9e44, #2b8a3e; @oc-green-0: extract(@oc-green-list, 1); @oc-green-1: extract(@oc-green-list, 2); @oc-green-2: extract(@oc-green-list, 3); @oc-green-3: extract(@oc-green-list, 4); @oc-green-4: extract(@oc-green-list, 5); @oc-green-5: extract(@oc-green-list, 6); @oc-green-6: extract(@oc-green-list, 7); @oc-green-7: extract(@oc-green-list, 8); @oc-green-8: extract(@oc-green-list, 9); @oc-green-9: extract(@oc-green-list, 10); // Lime // ─────────────────────────────────── @oc-lime-list: #f4fce3, #e9fac8, #d8f5a2, #c0eb75, #a9e34b, #94d82d, #82c91e, #74b816, #66a80f, #5c940d; @oc-lime-0: extract(@oc-lime-list, 1); @oc-lime-1: extract(@oc-lime-list, 2); @oc-lime-2: extract(@oc-lime-list, 3); @oc-lime-3: extract(@oc-lime-list, 4); @oc-lime-4: extract(@oc-lime-list, 5); @oc-lime-5: extract(@oc-lime-list, 6); @oc-lime-6: extract(@oc-lime-list, 7); @oc-lime-7: extract(@oc-lime-list, 8); @oc-lime-8: extract(@oc-lime-list, 9); @oc-lime-9: extract(@oc-lime-list, 10); // Yellow // ─────────────────────────────────── @oc-yellow-list: #fff9db, #fff3bf, #ffec99, #ffe066, #ffd43b, #fcc419, #fab005, #f59f00, #f08c00, #e67700; @oc-yellow-0: extract(@oc-yellow-list, 1); @oc-yellow-1: extract(@oc-yellow-list, 2); @oc-yellow-2: extract(@oc-yellow-list, 3); @oc-yellow-3: extract(@oc-yellow-list, 4); @oc-yellow-4: extract(@oc-yellow-list, 5); @oc-yellow-5: extract(@oc-yellow-list, 6); @oc-yellow-6: extract(@oc-yellow-list, 7); @oc-yellow-7: extract(@oc-yellow-list, 8); @oc-yellow-8: extract(@oc-yellow-list, 9); @oc-yellow-9: extract(@oc-yellow-list, 10); // Orange // ─────────────────────────────────── @oc-orange-list: #fff4e6, #ffe8cc, #ffd8a8, #ffc078, #ffa94d, #ff922b, #fd7e14, #f76707, #e8590c, #d9480f; @oc-orange-0: extract(@oc-orange-list, 1); @oc-orange-1: extract(@oc-orange-list, 2); @oc-orange-2: extract(@oc-orange-list, 3); @oc-orange-3: extract(@oc-orange-list, 4); @oc-orange-4: extract(@oc-orange-list, 5); @oc-orange-5: extract(@oc-orange-list, 6); @oc-orange-6: extract(@oc-orange-list, 7); @oc-orange-7: extract(@oc-orange-list, 8); @oc-orange-8: extract(@oc-orange-list, 9); @oc-orange-9: extract(@oc-orange-list, 10); ================================================ FILE: public/default-files/themes/paper/assets/styles/_core/github.less ================================================ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #333; background: #f8f8f8; } .hljs-comment, .hljs-quote { color: #998; font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-subst { color: #333; font-weight: bold; } .hljs-number, .hljs-literal, .hljs-variable, .hljs-template-variable, .hljs-tag .hljs-attr { color: #008080; } .hljs-string, .hljs-doctag { color: #d14; } .hljs-title, .hljs-section, .hljs-selector-id { color: #900; font-weight: bold; } .hljs-subst { font-weight: normal; } .hljs-type, .hljs-class .hljs-title { color: #458; font-weight: bold; } .hljs-tag, .hljs-name, .hljs-attribute { color: #000080; font-weight: normal; } .hljs-regexp, .hljs-link { color: #009926; } .hljs-symbol, .hljs-bullet { color: #990073; } .hljs-built_in, .hljs-builtin-name { color: #0086b3; } .hljs-meta { color: #999; font-weight: bold; } .hljs-deletion { background: #fdd; } .hljs-addition { background: #dfd; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: public/default-files/themes/paper/assets/styles/main.less ================================================ @import "_core/colors"; @import "_core/github"; body { background-image: url('../media/images/geometry2.png'); } #top { max-width: 1440px; margin-top: 3rem; } article .article-meta a { color: #ffffff; } .sidebar { .sidebar-title { margin-bottom: 24px; } ul { padding-left: 0px; } li { margin-bottom: 16px; } a.badge { background-image: none; margin: 4px; color: #ffffff; } } .info-container { text-align: center; .avatar { width: 120px; height: 120px; border-top-left-radius: 185px 160px; border-top-right-radius: 200px 195px; border-bottom-right-radius: 160px 195px; border-bottom-left-radius: 185px 190px; margin-bottom: 24px; } } .social-container { margin-top: 24px; a { background-image: none; margin: 4px; } } .tags-container { a { margin: 8px; background-image: none; color: #ffffff; } } .readmore-link { background-image: none; } h1 { font-size: 3.5rem; } article .article-title, h2 { font-size: 2.5rem; } nav ul.inline li a { display: block; } ================================================ FILE: public/default-files/themes/paper/config.json ================================================ { "name": "Paper", "version": "1.0.0", "author": "EryouHao", "repository": "https://github.com/getgridea/gridea-theme-paper", "customConfig": [ { "name": "readMoreText", "label": "Read More", "group": "文案", "value": "Read More", "type": "input", "note": "Read More" }, { "name": "nextArticleText", "label": "下一篇", "group": "文案", "value": "下一篇", "type": "input", "note": "下一篇" }, { "name": "prevPageText", "label": "上一页", "group": "文案", "value": "上一页", "type": "input", "note": "上一页" }, { "name": "nextPageText", "label": "下一页", "group": "文案", "value": "下一页", "type": "input", "note": "下一页" }, { "name": "latestArticleText", "label": "最新文章", "group": "文案", "value": "最新文章", "type": "input", "note": "最新文章" }, { "name": "tagListText", "label": "标签列表", "group": "文案", "value": "标签列表", "type": "input", "note": "标签列表" }, { "name": "contentBgColor", "label": "内容区背景色", "group": "颜色", "value": "#ffffff", "type": "input", "card": "color", "note": "颜色字符串:(如:#EEEEEE、rgba(255, 255, 255, 0.9))" }, { "name": "navBgColor", "label": "导航栏背景色", "group": "颜色", "value": "#ffffff", "type": "input", "card": "color", "note": "颜色字符串:(如:#EEEEEE、rgba(255, 255, 255, 0.9))" }, { "name": "github", "label": "Github", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "twitter", "label": "Twitter", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "weibo", "label": "微博", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "zhihu", "label": "知乎", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "facebook", "label": "Facebook", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "customCss", "label": "自定义CSS", "group": "自定义样式", "value": "", "type": "textarea", "note": "" }, { "name": "ga", "label": "跟踪 ID", "group": "谷歌统计", "value": "", "type": "input", "note": "UA-xxxxxxxxx-x" } ] } ================================================ FILE: public/default-files/themes/paper/style-override.js ================================================ const generateOverride = (params = {}) => { let result = '' // 内容区背景色 - contentBgColor if (params.contentBgColor && params.contentBgColor !== '#ffffff') { result += ` .paper { background: ${params.contentBgColor}; } ` } // 导航栏背景色 - contentBgColor if (params.navBgColor && params.navBgColor !== '#ffffff') { result += ` .navbar { background: ${params.navBgColor}; } ` } if (params.customCss) { result += ` ${params.customCss} ` } console.log('result', result) return result } module.exports = generateOverride ================================================ FILE: public/default-files/themes/paper/templates/_blocks/head.ejs ================================================ <%= siteTitle %> <% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %> <% if (commentSetting.commentPlatform === 'gitalk') { %> <% } %> <% if (commentSetting.commentPlatform === 'disqus') { %> <% } %> <% } %> <% if (site.customConfig.ga) { %> <% } %> ================================================ FILE: public/default-files/themes/paper/templates/_blocks/header.ejs ================================================ ================================================ FILE: public/default-files/themes/paper/templates/_blocks/pagination.ejs ================================================
<% if (pagination.prev) { %> <%= site.customConfig.prevPageText || '上一页' %> <% } %> <% if (pagination.next) { %> <%= site.customConfig.nextPageText || '下一页' %> <% } %>
================================================ FILE: public/default-files/themes/paper/templates/_blocks/post-list.ejs ================================================ <% posts.forEach(function(post) { %>

<%= post.title %>

<%= post.dateFormat %> <% post.tags.forEach(function(tag, tagIndex) { %> <%= tag.name %> <% }); %>

<%- post.abstract %>

<% }); %> ================================================ FILE: public/default-files/themes/paper/templates/_blocks/scripts.ejs ================================================ <% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %> <% if (commentSetting.commentPlatform === 'gitalk') { %> <% } %> <% if (commentSetting.commentPlatform === 'disqus') { %> <% } %> <% } %> ================================================ FILE: public/default-files/themes/paper/templates/_blocks/sidebar.ejs ================================================ ================================================ FILE: public/default-files/themes/paper/templates/archives.ejs ================================================ <%- include('./_blocks/head', { siteTitle: `文章归档 | ${themeConfig.siteName}` }) %> <%- include('./_blocks/header') %>
<% let years = []; posts.forEach((item) => { const year = item.date.substring(0, 4); if (!years.includes(year)) { years.push(year); } }); %>

文章归档

<% years.forEach(function(year) { %>

<%- year %>

<% posts.forEach(function(post) { %> <%if (post.date.indexOf(year) !== -1) { %> <% } %> <% }); %> <% }); %>
<%- include('./_blocks/pagination') %>
<%- include('./_blocks/sidebar') %> <%- include('./_blocks/scripts') %> ================================================ FILE: public/default-files/themes/paper/templates/index.ejs ================================================ <%- include('./_blocks/head', { siteTitle: themeConfig.siteName }) %> <%- include('./_blocks/header') %>
<%- include('./_blocks/post-list') %>
<%- include('./_blocks/pagination') %>
<%- include('./_blocks/sidebar') %>
<%- include('./_blocks/scripts') %> ================================================ FILE: public/default-files/themes/paper/templates/post.ejs ================================================ <%- include('./_blocks/head', { siteTitle: `${post.title} | ${themeConfig.siteName}` }) %> <%- include('./_blocks/header') %>

<%= post.title %>

<% if (post.feature) { %> <%= post.title %> <% } %>
<%- post.content %>
<% if (post.nextPost && !post.hideInList) { %> <% } %>
<% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %> <% if (commentSetting.commentPlatform === 'gitalk') { %>
<% } %> <% if (commentSetting.commentPlatform === 'disqus') { %>
<% } %> <% } %>
<%- include('./_blocks/sidebar') %>
<%- include('./_blocks/scripts') %> ================================================ FILE: public/default-files/themes/paper/templates/tag.ejs ================================================ <%- include('./_blocks/head', { siteTitle: `${tag.name} | ${themeConfig.siteName}` }) %> <%- include('./_blocks/header') %>

标签: <%= tag.name %>

<%- include('./_blocks/post-list') %>
<%- include('./_blocks/pagination') %>
<%- include('./_blocks/sidebar') %>
<%- include('./_blocks/scripts') %> ================================================ FILE: public/default-files/themes/paper/templates/tags.ejs ================================================ <%- include('./_blocks/head', { siteTitle: `标签列表 | ${themeConfig.siteName}` }) %> <%- include('./_blocks/header') %>

标签列表

<% tags.forEach(function(tag, tagIndex) { %> <%= tag.name %> <% }); %>
<%- include('./_blocks/sidebar') %>
<%- include('./_blocks/scripts') %> ================================================ FILE: public/default-files/themes/simple/assets/styles/_core/a11y-dark.less ================================================ /* a11y-dark theme */ /* Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css */ /* @author: ericwbailey */ /* Comment */ .hljs-comment, .hljs-quote { color: #d4d0ab; } /* Red */ .hljs-variable, .hljs-template-variable, .hljs-tag, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-regexp, .hljs-deletion { color: #ffa07a; } /* Orange */ .hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-literal, .hljs-type, .hljs-params, .hljs-meta, .hljs-link { color: #f5ab35; } /* Yellow */ .hljs-attribute { color: #ffd700; } /* Green */ .hljs-string, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #abe338; } /* Blue */ .hljs-title, .hljs-section { color: #00e0e0; } /* Purple */ .hljs-keyword, .hljs-selector-tag { color: #dcc6e0; } .hljs { display: block; overflow-x: auto; background: #2b2b2b; color: #f8f8f2; padding: 0.5em; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } @media screen and (-ms-high-contrast: active) { .hljs-addition, .hljs-attribute, .hljs-built_in, .hljs-builtin-name, .hljs-bullet, .hljs-comment, .hljs-link, .hljs-literal, .hljs-meta, .hljs-number, .hljs-params, .hljs-string, .hljs-symbol, .hljs-type, .hljs-quote { color: highlight; } .hljs-keyword, .hljs-selector-tag { font-weight: bold; } } ================================================ FILE: public/default-files/themes/simple/assets/styles/_core/atom-one-dark.less ================================================ /* Atom One Dark by Daniel Gamage Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax base: #282c34 mono-1: #abb2bf mono-2: #818896 mono-3: #5c6370 hue-1: #56b6c2 hue-2: #61aeee hue-3: #c678dd hue-4: #98c379 hue-5: #e06c75 hue-5-2: #be5046 hue-6: #d19a66 hue-6-2: #e6c07b */ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #abb2bf; background: #282c34; } .hljs-comment, .hljs-quote { color: #5c6370; font-style: italic; } .hljs-doctag, .hljs-keyword, .hljs-formula { color: #c678dd; } .hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst { color: #e06c75; } .hljs-literal { color: #56b6c2; } .hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string { color: #98c379; } .hljs-built_in, .hljs-class .hljs-title { color: #e6c07b; } .hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number { color: #d19a66; } .hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title { color: #61aeee; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } .hljs-link { text-decoration: underline; } ================================================ FILE: public/default-files/themes/simple/assets/styles/_core/base.less ================================================ /*! modern-normalize | MIT License | https://github.com/sindresorhus/modern-normalize */ /* Document ========================================================================== */ /** * Use a better box model (opinionated). */ html { box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; margin: 0; padding: 0; } /** * Use a more readable tab size (opinionated). */ :root { -moz-tab-size: 4; tab-size: 4; } /** * 1. Correct the line height in all browsers. * 2. Prevent adjustments of font size after orientation changes in iOS. */ html { line-height: 1.15; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ } /* Sections ========================================================================== */ /** * Remove the margin in all browsers. */ body { margin: 0; font-size: 14px; } /** * Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) */ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; } /* Grouping content ========================================================================== */ /** * Add the correct height in Firefox. */ hr { height: 0; } /* Text-level semantics ========================================================================== */ /** * Add the correct text decoration in Chrome, Edge, and Safari. */ abbr[title] { text-decoration: underline dotted; } /** * Add the correct font weight in Chrome, Edge, and Safari. */ b, strong { font-weight: bolder; } /** * 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) * 2. Correct the odd `em` font sizing in all browsers. */ code, kbd, samp, pre { font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace; /* 1 */ font-size: 1em; /* 2 */ } /** * Add the correct font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` elements from affecting the line height in all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } /* Forms ========================================================================== */ /** * 1. Change the font styles in all browsers. * 2. Remove the margin in Firefox and Safari. */ button, input, optgroup, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 1 */ line-height: 1.15; /* 1 */ margin: 0; /* 2 */ } /** * Remove the inheritance of text transform in Edge and Firefox. * 1. Remove the inheritance of text transform in Firefox. */ button, select { /* 1 */ text-transform: none; } /** * Correct the inability to style clickable types in iOS and Safari. */ button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; } /** * Remove the inner border and padding in Firefox. */ button::-moz-focus-inner, [type='button']::-moz-focus-inner, [type='reset']::-moz-focus-inner, [type='submit']::-moz-focus-inner { border-style: none; padding: 0; } /** * Restore the focus styles unset by the previous rule. */ button:-moz-focusring, [type='button']:-moz-focusring, [type='reset']:-moz-focusring, [type='submit']:-moz-focusring { outline: 1px dotted ButtonText; } /** * Correct the padding in Firefox. */ fieldset { padding: 0.35em 0.75em 0.625em; } /** * Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers. */ legend { padding: 0; } /** * Add the correct vertical alignment in Chrome and Firefox. */ progress { vertical-align: baseline; } /** * Correct the cursor style of increment and decrement buttons in Safari. */ [type='number']::-webkit-inner-spin-button, [type='number']::-webkit-outer-spin-button { height: auto; } /** * 1. Correct the odd appearance in Chrome and Safari. * 2. Correct the outline style in Safari. */ [type='search'] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; /* 2 */ } /** * Remove the inner padding in Chrome and Safari on macOS. */ [type='search']::-webkit-search-decoration { -webkit-appearance: none; } /** * 1. Correct the inability to style clickable types in iOS and Safari. * 2. Change font properties to `inherit` in Safari. */ ::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */ font: inherit; /* 2 */ } /* Interactive ========================================================================== */ /* * Add the correct display in Chrome and Safari. */ summary { display: list-item; } ================================================ FILE: public/default-files/themes/simple/assets/styles/_core/colors.less ================================================ // // // 𝗖 𝗢 𝗟 𝗢 𝗥 // v 1.6.3 // // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // General // ─────────────────────────────────── @oc-white: #ffffff; @oc-black: #000000; // Gray // ─────────────────────────────────── @oc-gray-list: #f8f9fa, #f1f3f5, #e9ecef, #dee2e6, #ced4da, #adb5bd, #868e96, #495057, #343a40, #212529; @oc-gray-0: extract(@oc-gray-list, 1); @oc-gray-1: extract(@oc-gray-list, 2); @oc-gray-2: extract(@oc-gray-list, 3); @oc-gray-3: extract(@oc-gray-list, 4); @oc-gray-4: extract(@oc-gray-list, 5); @oc-gray-5: extract(@oc-gray-list, 6); @oc-gray-6: extract(@oc-gray-list, 7); @oc-gray-7: extract(@oc-gray-list, 8); @oc-gray-8: extract(@oc-gray-list, 9); @oc-gray-9: extract(@oc-gray-list, 10); // Red // ─────────────────────────────────── @oc-red-list: #fff5f5, #ffe3e3, #ffc9c9, #ffa8a8, #ff8787, #ff6b6b, #fa5252, #f03e3e, #e03131, #c92a2a; @oc-red-0: extract(@oc-red-list, 1); @oc-red-1: extract(@oc-red-list, 2); @oc-red-2: extract(@oc-red-list, 3); @oc-red-3: extract(@oc-red-list, 4); @oc-red-4: extract(@oc-red-list, 5); @oc-red-5: extract(@oc-red-list, 6); @oc-red-6: extract(@oc-red-list, 7); @oc-red-7: extract(@oc-red-list, 8); @oc-red-8: extract(@oc-red-list, 9); @oc-red-9: extract(@oc-red-list, 10); // Pink // ─────────────────────────────────── @oc-pink-list: #fff0f6, #ffdeeb, #fcc2d7, #faa2c1, #f783ac, #f06595, #e64980, #d6336c, #c2255c, #a61e4d; @oc-pink-0: extract(@oc-pink-list, 1); @oc-pink-1: extract(@oc-pink-list, 2); @oc-pink-2: extract(@oc-pink-list, 3); @oc-pink-3: extract(@oc-pink-list, 4); @oc-pink-4: extract(@oc-pink-list, 5); @oc-pink-5: extract(@oc-pink-list, 6); @oc-pink-6: extract(@oc-pink-list, 7); @oc-pink-7: extract(@oc-pink-list, 8); @oc-pink-8: extract(@oc-pink-list, 9); @oc-pink-9: extract(@oc-pink-list, 10); // Grape // ─────────────────────────────────── @oc-grape-list: #f8f0fc, #f3d9fa, #eebefa, #e599f7, #da77f2, #cc5de8, #be4bdb, #ae3ec9, #9c36b5, #862e9c; @oc-grape-0: extract(@oc-grape-list, 1); @oc-grape-1: extract(@oc-grape-list, 2); @oc-grape-2: extract(@oc-grape-list, 3); @oc-grape-3: extract(@oc-grape-list, 4); @oc-grape-4: extract(@oc-grape-list, 5); @oc-grape-5: extract(@oc-grape-list, 6); @oc-grape-6: extract(@oc-grape-list, 7); @oc-grape-7: extract(@oc-grape-list, 8); @oc-grape-8: extract(@oc-grape-list, 9); @oc-grape-9: extract(@oc-grape-list, 10); // Violet // ─────────────────────────────────── @oc-violet-list: #f3f0ff, #e5dbff, #d0bfff, #b197fc, #9775fa, #845ef7, #7950f2, #7048e8, #6741d9, #5f3dc4; @oc-violet-0: extract(@oc-violet-list, 1); @oc-violet-1: extract(@oc-violet-list, 2); @oc-violet-2: extract(@oc-violet-list, 3); @oc-violet-3: extract(@oc-violet-list, 4); @oc-violet-4: extract(@oc-violet-list, 5); @oc-violet-5: extract(@oc-violet-list, 6); @oc-violet-6: extract(@oc-violet-list, 7); @oc-violet-7: extract(@oc-violet-list, 8); @oc-violet-8: extract(@oc-violet-list, 9); @oc-violet-9: extract(@oc-violet-list, 10); // Indigo // ─────────────────────────────────── @oc-indigo-list: #edf2ff, #dbe4ff, #bac8ff, #91a7ff, #748ffc, #5c7cfa, #4c6ef5, #4263eb, #3b5bdb, #364fc7; @oc-indigo-0: extract(@oc-indigo-list, 1); @oc-indigo-1: extract(@oc-indigo-list, 2); @oc-indigo-2: extract(@oc-indigo-list, 3); @oc-indigo-3: extract(@oc-indigo-list, 4); @oc-indigo-4: extract(@oc-indigo-list, 5); @oc-indigo-5: extract(@oc-indigo-list, 6); @oc-indigo-6: extract(@oc-indigo-list, 7); @oc-indigo-7: extract(@oc-indigo-list, 8); @oc-indigo-8: extract(@oc-indigo-list, 9); @oc-indigo-9: extract(@oc-indigo-list, 10); // Blue // ─────────────────────────────────── @oc-blue-list: #e7f5ff, #d0ebff, #a5d8ff, #74c0fc, #4dabf7, #339af0, #228be6, #1c7ed6, #1971c2, #1864ab; @oc-blue-0: extract(@oc-blue-list, 1); @oc-blue-1: extract(@oc-blue-list, 2); @oc-blue-2: extract(@oc-blue-list, 3); @oc-blue-3: extract(@oc-blue-list, 4); @oc-blue-4: extract(@oc-blue-list, 5); @oc-blue-5: extract(@oc-blue-list, 6); @oc-blue-6: extract(@oc-blue-list, 7); @oc-blue-7: extract(@oc-blue-list, 8); @oc-blue-8: extract(@oc-blue-list, 9); @oc-blue-9: extract(@oc-blue-list, 10); // Cyan // ─────────────────────────────────── @oc-cyan-list: #e3fafc, #c5f6fa, #99e9f2, #66d9e8, #3bc9db, #22b8cf, #15aabf, #1098ad, #0c8599, #0b7285; @oc-cyan-0: extract(@oc-cyan-list, 1); @oc-cyan-1: extract(@oc-cyan-list, 2); @oc-cyan-2: extract(@oc-cyan-list, 3); @oc-cyan-3: extract(@oc-cyan-list, 4); @oc-cyan-4: extract(@oc-cyan-list, 5); @oc-cyan-5: extract(@oc-cyan-list, 6); @oc-cyan-6: extract(@oc-cyan-list, 7); @oc-cyan-7: extract(@oc-cyan-list, 8); @oc-cyan-8: extract(@oc-cyan-list, 9); @oc-cyan-9: extract(@oc-cyan-list, 10); // Teal // ─────────────────────────────────── @oc-teal-list: #e6fcf5, #c3fae8, #96f2d7, #63e6be, #38d9a9, #20c997, #12b886, #0ca678, #099268, #087f5b; @oc-teal-0: extract(@oc-teal-list, 1); @oc-teal-1: extract(@oc-teal-list, 2); @oc-teal-2: extract(@oc-teal-list, 3); @oc-teal-3: extract(@oc-teal-list, 4); @oc-teal-4: extract(@oc-teal-list, 5); @oc-teal-5: extract(@oc-teal-list, 6); @oc-teal-6: extract(@oc-teal-list, 7); @oc-teal-7: extract(@oc-teal-list, 8); @oc-teal-8: extract(@oc-teal-list, 9); @oc-teal-9: extract(@oc-teal-list, 10); // Green // ─────────────────────────────────── @oc-green-list: #ebfbee, #d3f9d8, #b2f2bb, #8ce99a, #69db7c, #51cf66, #40c057, #37b24d, #2f9e44, #2b8a3e; @oc-green-0: extract(@oc-green-list, 1); @oc-green-1: extract(@oc-green-list, 2); @oc-green-2: extract(@oc-green-list, 3); @oc-green-3: extract(@oc-green-list, 4); @oc-green-4: extract(@oc-green-list, 5); @oc-green-5: extract(@oc-green-list, 6); @oc-green-6: extract(@oc-green-list, 7); @oc-green-7: extract(@oc-green-list, 8); @oc-green-8: extract(@oc-green-list, 9); @oc-green-9: extract(@oc-green-list, 10); // Lime // ─────────────────────────────────── @oc-lime-list: #f4fce3, #e9fac8, #d8f5a2, #c0eb75, #a9e34b, #94d82d, #82c91e, #74b816, #66a80f, #5c940d; @oc-lime-0: extract(@oc-lime-list, 1); @oc-lime-1: extract(@oc-lime-list, 2); @oc-lime-2: extract(@oc-lime-list, 3); @oc-lime-3: extract(@oc-lime-list, 4); @oc-lime-4: extract(@oc-lime-list, 5); @oc-lime-5: extract(@oc-lime-list, 6); @oc-lime-6: extract(@oc-lime-list, 7); @oc-lime-7: extract(@oc-lime-list, 8); @oc-lime-8: extract(@oc-lime-list, 9); @oc-lime-9: extract(@oc-lime-list, 10); // Yellow // ─────────────────────────────────── @oc-yellow-list: #fff9db, #fff3bf, #ffec99, #ffe066, #ffd43b, #fcc419, #fab005, #f59f00, #f08c00, #e67700; @oc-yellow-0: extract(@oc-yellow-list, 1); @oc-yellow-1: extract(@oc-yellow-list, 2); @oc-yellow-2: extract(@oc-yellow-list, 3); @oc-yellow-3: extract(@oc-yellow-list, 4); @oc-yellow-4: extract(@oc-yellow-list, 5); @oc-yellow-5: extract(@oc-yellow-list, 6); @oc-yellow-6: extract(@oc-yellow-list, 7); @oc-yellow-7: extract(@oc-yellow-list, 8); @oc-yellow-8: extract(@oc-yellow-list, 9); @oc-yellow-9: extract(@oc-yellow-list, 10); // Orange // ─────────────────────────────────── @oc-orange-list: #fff4e6, #ffe8cc, #ffd8a8, #ffc078, #ffa94d, #ff922b, #fd7e14, #f76707, #e8590c, #d9480f; @oc-orange-0: extract(@oc-orange-list, 1); @oc-orange-1: extract(@oc-orange-list, 2); @oc-orange-2: extract(@oc-orange-list, 3); @oc-orange-3: extract(@oc-orange-list, 4); @oc-orange-4: extract(@oc-orange-list, 5); @oc-orange-5: extract(@oc-orange-list, 6); @oc-orange-6: extract(@oc-orange-list, 7); @oc-orange-7: extract(@oc-orange-list, 8); @oc-orange-8: extract(@oc-orange-list, 9); @oc-orange-9: extract(@oc-orange-list, 10); ================================================ FILE: public/default-files/themes/simple/assets/styles/_core/github.less ================================================ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #333; background: #f8f8f8; } .hljs-comment, .hljs-quote { color: #998; font-style: italic; } .hljs-keyword, .hljs-selector-tag, .hljs-subst { color: #333; font-weight: bold; } .hljs-number, .hljs-literal, .hljs-variable, .hljs-template-variable, .hljs-tag .hljs-attr { color: #008080; } .hljs-string, .hljs-doctag { color: #d14; } .hljs-title, .hljs-section, .hljs-selector-id { color: #900; font-weight: bold; } .hljs-subst { font-weight: normal; } .hljs-type, .hljs-class .hljs-title { color: #458; font-weight: bold; } .hljs-tag, .hljs-name, .hljs-attribute { color: #000080; font-weight: normal; } .hljs-regexp, .hljs-link { color: #009926; } .hljs-symbol, .hljs-bullet { color: #990073; } .hljs-built_in, .hljs-builtin-name { color: #0086b3; } .hljs-meta { color: #999; font-weight: bold; } .hljs-deletion { background: #fdd; } .hljs-addition { background: #dfd; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } ================================================ FILE: public/default-files/themes/simple/assets/styles/main.less ================================================ @import "_core/colors"; @import "_core/base"; @import "_core/a11y-dark"; body { background: @oc-gray-0; color: @oc-gray-7; font-size: 16px; } a { text-decoration: none; color: @oc-gray-7; } .sidebar { width: 320px; position: fixed; left: 0; top: 0; bottom: 0; background-color: #7c8280; background-size: cover; background-position: center; background-image: url('../media/images/sidebar-bg.jpg'); display: flex; flex-direction: column; overflow-y: scroll; .menu-btn { display: none; } .top-container { text-align: center; padding: 48px 16px; flex: 1; .site-logo { width: 80px; height: 80px; border-radius: 50%; border: 2px solid #F1F3F5; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16); } .site-title { font-size: 24px; padding: 32px 0; color: @oc-gray-3; } .site-nav { display: block; padding: 8px 16px; margin: 16px 0; color: @oc-gray-3; transition: all 0.3s; &:hover { color: @oc-gray-0; } } } .bottom-container { padding: 24px 16px; color: @oc-gray-3; font-size: 12px; .site-description { padding: 16px 0; } a { color: #fff; } } } .main-container { margin-left: 320px; } .content-container { max-width: 1064px; margin: 0 auto; padding: 48px 32px; } .post-item { display: flex; padding-bottom: 32px; margin-bottom: 32px; border-bottom: 1px solid @oc-gray-4; &:last-of-type { border-bottom: none; } .left { flex: 1; .post-title { font-size: 24px; transition: all 0.3s; &:hover { color: @oc-gray-9; } } .post-date { font-size: 18px; padding: 24px 0; color: @oc-gray-5; } .post-abstract { line-height: 24px; font-size: 18px; color: @oc-gray-6; } } .right { flex-shrink: 0; margin-left: 24px; width: 38.2%; .feature-container { padding-top: 56.25%; background-size: cover; background-position: center; border-radius: 3px; box-shadow: 0 2px 5px rgba(0,0,25,0.1), 0 5px 75px 1px rgba(0,0,50,0.2); } } } .pagination-container { display: flex; justify-content: space-between; .prev, .next { display: inline-block; padding: 8px 16px; background: #fff; border: 1px solid @oc-gray-1; border-radius: 2px; transition: all 0.3s; &:hover { transform: translateY(-3px); border: 1px solid @oc-gray-3; } } } .post-detail { max-width: 720px; margin: 0 auto; .feature-container { padding-top: 56.25%; background-size: cover; background-position: center; border-radius: 3px; box-shadow: 0 2px 5px rgba(0,0,25,0.1), 0 5px 75px 1px rgba(0,0,50,0.2); margin-bottom: 24px; } .post-title { font-size: 40px; } .post-date { font-size: 18px; padding: 24px 0; color: @oc-gray-5; } .post-content { h1, h2, h3, h4, h5, h6 { margin: 16px 0; color: @oc-gray-8; } a { color: @oc-indigo-6; border-bottom: 1px dotted @oc-indigo-6; transition: all 0.3s; &:hover { color: @oc-indigo-8; border-bottom: 1px dotted @oc-indigo-8; } } img { display: block; box-shadow: 0 2px 5px rgba(0,0,25,0.1), 0 5px 75px 1px rgba(0,0,50,0.2); max-width: 100%; border-radius: 2px; margin: 24px auto; } p { line-height: 1.725; margin-bottom: 24px; font-size: 18px; color: @oc-gray-7; } p, ul, ol { code { padding: 0 3px; margin: 0 2px; // background: rgba(195,195,195,0.41); background: @oc-gray-2; font-size: 0.9em; border-radius: 2px; border: 1px solid @oc-gray-3; display: inline-block; line-height: 1.5; color: @oc-gray-6; } } blockquote { background: @oc-gray-2; padding: 16px; border-left: 3px solid @oc-violet-7; border-radius: 2px; margin-bottom: 16px; p { color: @oc-gray-6; margin-bottom: 0; } } pre { margin-bottom: 16px; code { font-size: 14px; font-family: 'Source Code Pro', Consolas, Menlo, Monaco, 'Courier New', monospace; padding: 2em 1em 1em; border-radius: 5px; line-height: 1.375; position: relative; background: @oc-gray-8; color: @oc-gray-1; display: block; &:after { content: 'CODE'; display: block; position: absolute; left: 8px; top: 4px; font-size: 14px; font-weight: bold; color: @oc-gray-7; } } } table { border-collapse: collapse; margin: 1rem 0; display: block; overflow-x: auto; } tr { border-top: 1px solid #dfe2e5; } td, th { border: 1px solid #dfe2e5; padding: .6em 1em; } ul, ol { color: var(--c-base-blacklight); padding-left: 24px; line-height: 1.725; margin-bottom: 16px; } strong { font-weight: bolder; } em { color: @oc-gray-6; } hr { height: 0; border: 2px solid #efefef; margin-bottom: 24px; } } } .tag { display: inline-block; font-size: 14px; padding: 8px 16px; border-radius: 16px; background: @oc-gray-2; color: @oc-gray-6; margin: 16px 16px 16px 0; transition: all 0.3s; &:hover { background: @oc-gray-3; color: @oc-gray-7; transform: translateY(-3px); } } .next-post { border-top: 1px solid @oc-gray-4; border-bottom: 1px solid @oc-gray-4; padding: 24px 0; margin: 32px 0; .post-title { font-size: 24px; } .next { color: @oc-gray-4; margin-bottom: 16px; } } .archives-title, .tag-list-title, .current-tag { color: @oc-gray-7; padding-bottom: 48px; font-size: 32px; } .archives-container { padding-bottom: 32px; .year { font-size: 16px; padding-bottom: 16px; border-bottom: 1px solid @oc-gray-4; margin: 16px 0; color: @oc-gray-6; } .post { padding-bottom: 16px; .post-title { font-size: 18px; transition: all 0.3s; &:hover { color: @oc-gray-9; } } } } // Mobile ----------------------- // @media (max-width: 800px) { .sidebar { position: relative; width: 100% !important; height: 80px; overflow: hidden; transition: height 0.382s ease-in-out; &.full-height { height: 100vh; } .sidebar-content { position: absolute; top: 0; bottom: 0; left: 0; right: 0; } .top-header-container { display: flex; justify-content: space-between; margin-top: 16px; .menu-btn { display: block; position: relative; width: 48px; height: 48px; .line { width: 32px; height: 2px; background: @oc-gray-2; border-radius: 2px; position: absolute; right: 0; top: 23px; } &:before, &:after { content: ''; display: block; width: 32px; height: 2px; background: @oc-gray-2; border-radius: 2px; position: absolute; right: 0; } &:before { top: 12px; } &:after { bottom: 12px; } } } .top-container { text-align: left; padding: 0 16px; .site-title-container { display: flex; align-items: center; } .site-logo { width: 48px; height: 48px; } .site-title { display: inline; padding: 0 8px; font-size: 18px; } } } .main-container { margin-left: 0 !important;; } .content-container { padding: 32px 16px; } .post-item { flex-direction: column-reverse; padding-bottom: 16px; margin-bottom: 16x; .right { width: 100%; margin-left: 0; margin-bottom: 16px; } .left { .post-date { font-size: 16px; padding: 16px 0; } .post-abstract { font-size: 16px; } } } .pagination-container { .prev, .next { &:hover { transform: translateY(0px); } } } .post-detail { .post-title { font-size: 28px; } .post-date { font-size: 16px; padding: 16px 0; } .feature-container { margin-bottom: 16px; } .post-content { p { font-size: 16px; } } } .next-post { margin: 24px 0; padding: 16px 0; } .archives-title, .tag-list-title, .current-tag { font-size: 28px; padding-bottom: 32px; } .tag { margin: 8px 8px 8px 0; &:hover { transform: translateY(0px); } } } .social-container { .social-link { color: #dee2e6; font-size: 16px; margin: 4px 8px; } } ================================================ FILE: public/default-files/themes/simple/config.json ================================================ { "name": "Simple", "version": "1.1.0", "author": "EryouHao", "customConfig": [ { "name": "sidebarWidth", "label": "菜单栏宽度", "group": "布局", "value": "320px", "type": "input", "note": "可填像素类型(如:320px)或百分比类型(如:38.2%)" }, { "name": "featureBorderRadius", "label": "封面图圆角", "group": "布局", "value": "3px", "type": "input", "note": "像素类型(如:3px)" }, { "name": "menuColor", "label": "菜单颜色", "group": "颜色", "value": "#dee2e6", "type": "input", "card": "color", "note": "颜色字符串:(如:#EEEEEE、rgba(255, 255, 255, 0.9))" }, { "name": "contentBgColor", "label": "内容区背景色", "group": "颜色", "value": "#f8f9fa", "type": "input", "card": "color", "note": "颜色字符串:(如:#EEEEEE、rgba(255, 255, 255, 0.9))" }, { "name": "renderKaTeX", "label": "是否渲染 KaTeX 公式", "group": "渲染", "value": false, "type": "switch" }, { "name": "renderCode", "label": "是否渲染代码高亮", "group": "渲染", "value": false, "type": "switch" }, { "name": "github", "label": "Github", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "twitter", "label": "Twitter", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "weibo", "label": "微博", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "zhihu", "label": "知乎", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "facebook", "label": "Facebook", "group": "社交", "value": "", "type": "input", "note": "链接地址" }, { "name": "customCss", "label": "自定义CSS", "group": "自定义样式", "value": "", "type": "textarea", "note": "" }, { "name": "ga", "label": "跟踪 ID", "group": "谷歌统计", "value": "", "type": "input", "note": "UA-xxxxxxxxx-x" } ] } ================================================ FILE: public/default-files/themes/simple/style-override.js ================================================ const generateOverride = (params = {}) => { let result = '' // 侧边栏宽度 - sidebarWidth if (params.sidebarWidth && params.sidebarWidth !== '320px') { result += ` .sidebar { width: ${params.sidebarWidth}; } .main-container { margin-left: ${params.sidebarWidth}; } ` } // 菜单颜色 - menuColor if (params.menuColor && params.menuColor !== '#dee2e6') { result += ` .sidebar .top-container .site-nav { color: ${params.menuColor}; } ` } // 封面图圆角 - featureBorderRadius if (params.featureBorderRadius && params.featureBorderRadius !== '3px') { result += ` .post-item .right .feature-container { border-radius: ${params.featureBorderRadius}; } ` } // 内容区背景色 - contentBgColor if (params.contentBgColor && params.contentBgColor !== '#f8f9fa') { result += ` body { background: ${params.contentBgColor}; } ` } if (params.customCss) { result += ` ${params.customCss} ` } console.log('result', result) return result } module.exports = generateOverride ================================================ FILE: public/default-files/themes/simple/templates/_blocks/head.ejs ================================================ <%= siteTitle %> <% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %> <% if (commentSetting.commentPlatform === 'gitalk') { %> <% } %> <% if (commentSetting.commentPlatform === 'disqus') { %> <% } %> <% } %> <% if (site.customConfig.ga) { %> <% } %> ================================================ FILE: public/default-files/themes/simple/templates/_blocks/pagination.ejs ================================================
<% if (pagination.prev) { %> <% } %> <% if (pagination.next) { %> <% } %>
================================================ FILE: public/default-files/themes/simple/templates/_blocks/scripts.ejs ================================================ <% if (site.customConfig.renderCode) { %> <% } %> <% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %> <% if (commentSetting.commentPlatform === 'gitalk') { %> <% } %> <% if (commentSetting.commentPlatform === 'disqus') { %> <% } %> <% } %> ================================================ FILE: public/default-files/themes/simple/templates/_blocks/sidebar.ejs ================================================ ================================================ FILE: public/default-files/themes/simple/templates/archives.ejs ================================================ <%- include('./_blocks/head', { siteTitle: `文章归档 | ${themeConfig.siteName}` }) %>
<%- include('./_blocks/sidebar') %>
<% let years = []; posts.forEach((item) => { const year = item.date.substring(0, 4); if (!years.includes(year)) { years.push(year); } }); %>

文章归档

<% years.forEach(function(year) { %>

<%- year %>

<% posts.forEach(function(post) { %> <%if (post.date.indexOf(year) !== -1) { %> <% } %> <% }); %> <% }); %>
<%- include('./_blocks/pagination') %>
<%- include('./_blocks/scripts') %> ================================================ FILE: public/default-files/themes/simple/templates/index.ejs ================================================ <%- include('./_blocks/head', { siteTitle: themeConfig.siteName }) %> <% if (site.customConfig.renderKaTeX) { %> <% } %>
<%- include('./_blocks/sidebar') %>
<% posts.forEach(function(post) { %>

<%= post.title %>

<%- post.abstract %>
<% if (themeConfig.showFeatureImage && post.feature) { %>
<% } %>
<% }); %> <%- include('./_blocks/pagination') %>
<%- include('./_blocks/scripts') %> ================================================ FILE: public/default-files/themes/simple/templates/post.ejs ================================================ <%- include('./_blocks/head', { siteTitle: `${post.title} | ${themeConfig.siteName}` }) %>
<%- include('./_blocks/sidebar') %>

<%= post.title %>

<% if (post.feature) { %>
<% } %>
<%- post.content %>
<% if (post.tags.length > 0) { %>
<% post.tags.forEach(function(tag, index) { %> <%= tag.name %> <% }); %>
<% } %> <% if (post.nextPost && !post.hideInList) { %> <% } %> <% if (typeof commentSetting !== 'undefined' && commentSetting.showComment) { %> <% if (commentSetting.commentPlatform === 'gitalk') { %>
<% } %> <% if (commentSetting.commentPlatform === 'disqus') { %>
<% } %> <% } %>
<%- include('./_blocks/scripts') %> ================================================ FILE: public/default-files/themes/simple/templates/tag.ejs ================================================ <%- include('./_blocks/head', { siteTitle: `${tag.name} | ${themeConfig.siteName}` }) %>
<%- include('./_blocks/sidebar') %>

标签: <%= tag.name %>

<% posts.forEach(function(post) { %>

<%= post.title %>

<%- post.abstract %>
<% if (themeConfig.showFeatureImage && post.feature) { %>
<% } %>
<% }); %> <%- include('./_blocks/pagination') %>
<%- include('./_blocks/scripts') %> ================================================ FILE: public/default-files/themes/simple/templates/tags.ejs ================================================ <%- include('./_blocks/head', { siteTitle: `标签列表 | ${themeConfig.siteName}` }) %>
<%- include('./_blocks/sidebar') %>

标签列表

<% tags.forEach((tag) => { %> <%= tag.name %> <% }); %>
<%- include('./_blocks/scripts') %> ================================================ FILE: public/index.html ================================================ Gridea
================================================ FILE: src/App.vue ================================================ ================================================ FILE: src/assets/locales-menu.ts ================================================ const messages: any = { 'zh-CN': { edit: '编辑', help: '帮助', save: '保存', undo: '撤销', redo: '重做', cut: '剪切', copy: '复制', paste: '粘贴', delete: '删除', selectall: '全选', toggledevtools: '开发者工具', close: '关闭', quit: '退出', }, 'zh-TW': { edit: '编辑', help: '帮助', save: '保存', undo: '撤銷', redo: '重做', cut: '剪切', copy: '複製', paste: '粘貼', delete: '刪除', selectall: '全選', toggledevtools: '開發者工具', close: '關閉', quit: '退出', }, 'en': { edit: 'Edit', help: 'Help', save: 'Save', undo: 'Undo', redo: 'Redo', cut: 'Cut', copy: 'Copy', paste: 'Paste', delete: 'Delete', selectall: 'Select All', toggledevtools: 'Toogle Developer Tools', close: 'Close Window', quit: 'Quit', }, 'fr-FR': { edit: 'Éditer', help: 'Aide', save: 'Enregistrer', undo: 'Annuler', redo: 'Refaire', cut: 'Couper', copy: 'Copier', paste: 'Coller', delete: 'Supprimer', selectall: 'Tout sélectionner', toggledevtools: 'Toogle Developer Tools', close: 'Fermer', quit: 'Quitter', }, 'ru': { edit: 'Редактировать', help: 'Помощь', save: 'Сохранить', undo: 'Отменить', redo: 'Повторить', cut: 'Вырезать', copy: 'Копировать', paste: 'Вставить', delete: 'Удалить', selectall: 'Выделить всё', toggledevtools: 'Инструменты разработчика', close: 'Закрыть окно', quit: 'Выход', }, 'ja-JP': { edit: '編集', help: 'ヘルプ', save: 'セーブ', undo: '取り消す', redo: 'やり直す', cut: 'カット', copy: 'コピー', paste: 'ペースト', delete: '削除', selectall: '全て選択', toggledevtools: '開発者ツール', close: '閉じる', quit: '終了', } } export default messages ================================================ FILE: src/assets/locales.ts ================================================ const message = { zhHans: { preview: '预 览', syncSite: '同 步', newVersion: '有新版本', article: '文 章', menu: '菜 单', tag: '标 签', theme: '主 题', remote: '远 程', system: '系 统', renderSuccess: '渲染完毕,快去预览吧!', renderError: '渲染失败,请检查 hosts 文件中 127.0.0.1 是否指向 localhost。确认配置正确后,尝试重启应用。', syncWarning: '必须完成配置才能同步哦!', syncSuccess: '同步成功啦!', syncError1: '同步遇到了错误,请查阅', syncError2: '来寻找解决方案', newVersionTips: '有新版本发布,快去下载新版本吧!', newArticle: '新文章', publish: '发布', published: '已发布', draft: '草稿', title: '标题', status: '状态', createAt: '创建时间', actions: '操作', deleteWarning: '删除后不可撤销,你确定要删除吗?', warning: '警告', articleDelete: '文章已删除', cancel: '取 消', select: '选 择', featureImage: '封面图', saveDraft: '存草稿', save: '保 存', newMenu: '新菜单', name: '名称', openType: '打开方式', link: 'Link', menuSuccess: '菜单已保存', menuDelete: '菜单已删除', draftSuccess: '已存为草稿', saveSuccess: '已保存', newTag: '新标签', tagName: '标签名', selectTheme: '选择主题', siteName: '网站名称', siteDescription: '网站描述', footerInfo: '底部信息', isShowFeatureImage: '显示封面图', articlesPerPage: '每页文章数', archivesPerPage: '每页归档数', basicSetting: '基础配置', commentSetting: '评论配置', faviconSetting: '网页图标', avatarSetting: '头像配置', domain: '域 名', repository: '仓库名称', branch: '分 支', username: '仓库用户名', email: '邮 箱', isShowComment: '是否显示评论', domainShouldStartsWithWarn: '域名应以 \'https://\' 或 \'http://\' 开头', basicSettingSuccess: '基础配置已保存', commentSettingSuccess: '评论配置已保存', faviconSettingSuccess: '网页图标已保存', avatarSettingSuccess: '头像配置已保存', saved: '已保存', syncing: '同步中,请耐心等待...', articleDefault: '文章 URL 默认格式', tagDefault: '标签 URL 默认格式', hideInList: '列表中隐藏', dateFormat: '日期格式', htmlSupport: '支持 HTML', change: '更 换', editorTip: '你可以插入单独行的 为摘要分隔标识(此行之前内容为摘要)', saveError: '保存失败', privateKeyTip: '请填写绝对路径,例如:/home/username/.ssh/id_rsa', remotePathTip: '请填写绝对路径,例如:/home/username/www/', testConnection: '检测远程连接', connectSuccess: '远程连接成功', connectFailed: '远程连接失败,请检查仓库、用户名和令牌设置', sourceFolder: '站点源文件路径', language: '语 言', inConfig: '配置中', searchArticle: '搜索文章', deleteSelected: '已选', inputContent: '输入内容', postUrlRepeatTip: '文章的 URL 与其他文章重复', postUrlIncludeTip: 'URL 不可包含 /', onlyPicDrag: '仅支持图片拖拽', themeConfigSaved: '主题配置已保存', reset: '重置', reseted: '已重置', noCustomConfigTip: '当前主题暂无自定义配置', customConfig: '自定义配置', moreThemes: '更多主题', postSettings: '文章设置', back: '返回', savedIn: '保存于', or: '或', starSupport: 'Star 支持作者!', showAllPost: '显示全文', showAbstract: '仅显示摘要', unsavedWarning: '你将丢失所有的未保存的更改,是否继续?', noSaveAndBack: '继续', insertImage: '插入图片', insertMore: '插入摘要分隔符', writingIn: '写作于', words: '字 数', readingTime: '阅读时间', version: '版本', token: '令 牌', tokenUsername: '令牌用户名', platform: '平 台', topArticles: '置顶文章', default: '默认', external: '外链', pathContainHttps: '路径必须包含 http 或 https', articleUrlPath: '文章 URL 路径', concise: '精简', tagUrlPath: '标签 URL 路径', archivePathPrefix: '归档路径前缀', showFullText: '显示全文', showAbstractOnly: '仅显示摘要', numberArticlesRSS: 'RSS/Feed 文章数量', Proxy: 'HTTP代理', ProxyAddress: '地址', ProxyPort: '端口', }, zh_TW: { preview: '預 覽', syncSite: '同 步', newVersion: '有新版本', article: '文 章', menu: '菜 單', tag: '標 簽', theme: '主 題', remote: '遠 程', system: '系 統', renderSuccess: '渲染完毕,快去预览吧!', renderError: '渲染失敗,請檢查 hosts 文件中 127.0.0.1 是否指向 localhost。確認配置正確後,嘗試重啟應用。', syncWarning: '必須完成配置才能同步哦!', syncSuccess: '同步成功啦!', syncError1: '同步遇到了错误,请查閱', syncError2: '來尋找解決方案', newVersionTips: '有新版本發布,快去下載新版本吧!', newArticle: '新文章', publish: '發布', published: '已發布', draft: '草稿', title: '標題', status: '狀態', createAt: '創建時間', actions: '操作', deleteWarning: '刪除後不可撤銷,你確定要刪除嗎?', warning: '警告', articleDelete: '文章已刪除', cancel: '取 消', select: '選 擇', featureImage: '封面圖', saveDraft: '存草稿', save: '保 存', newMenu: '新菜單', name: '名稱', openType: '打開方式', link: 'Link', menuSuccess: '菜單已保存', menuDelete: '菜單已刪除', draftSuccess: '已存為草稿', saveSuccess: '已保存', newTag: '新標籤', tagName: '標籤名', selectTheme: '選擇主題', siteName: '網站名稱', siteDescription: '網站描述', footerInfo: '底部信息', isShowFeatureImage: '顯示封面圖', articlesPerPage: '每頁文章數', archivesPerPage: '每頁歸檔數', basicSetting: '基礎配置', commentSetting: '評論配置', faviconSetting: '網頁圖標', avatarSetting: '頭像配置', domain: '域 名', repository: '倉庫名稱', branch: '分 支', username: '倉庫用戶名', email: '郵 箱', isShowComment: '是否顯示評論', domainShouldStartsWithWarn: '域名應以 \'https://\' 或 \'http://\' 開頭', basicSettingSuccess: '基礎配置已保存', commentSettingSuccess: '評論配置已保存', faviconSettingSuccess: '網頁圖標配置已保存', avatarSettingSuccess: '頭像配置已保存', saved: '已保存', syncing: '同步中,請耐心等待...', articleDefault: '文章 URL 默認格式', tagDefault: '標籤 URL 默認格式', hideInList: '列表中隱藏', dateFormat: '日期格式', htmlSupport: '支持 HTML', change: '更 換', editorTip: '你可以插入單獨行的 為摘要分隔標識(此行之前內容為摘要)', saveError: '保存失敗', privateKeyTip: '請填寫絕對路徑,例如:/home/username/.ssh/id_rsa', remotePathTip: '請填寫絕對路徑,例如:/home/username/www/', testConnection: '檢測遠程連接', connectSuccess: '遠程連接成功', connectFailed: '遠程連接失敗,請檢查倉庫、用戶名和令牌設置', sourceFolder: '站点源文件路径', language: '語 言', inConfig: '配置中', searchArticle: '搜索文章', deleteSelected: '已選', inputContent: '輸入內容', postUrlRepeatTip: '文章的 URL 與其他文章重複', postUrlIncludeTip: 'URL 不可包含 /', onlyPicDrag: '僅支持圖片拖拽', themeConfigSaved: '主題配置已保存', reset: '重置', reseted: '已重置', noCustomConfigTip: '當前主題暫無自定義配置', customConfig: '自定義配置', moreThemes: '更多主題', postSettings: '文章設置', back: '返回', savedIn: '保存於', or: '或', starSupport: 'Star 支持作者!', showAllPost: '顯示全文', showAbstract: '僅顯示摘要', unsavedWarning: '你將丟失所有的未保存的更改,是否繼續?', noSaveAndBack: '繼續', insertImage: '插入圖片', insertMore: '插入摘要分隔符', writingIn: '寫作於', words: '字 數', readingTime: '閱讀時間', version: '版本', token: '令 牌', tokenUsername: '令牌用户名', platform: '平 臺', topArticles: '置顶文章', default: '默认', external: '外链', pathContainHttps: '路径必须包含 http 或 https', articleUrlPath: '文章 URL 路径', concise: '精简', tagUrlPath: '标签 URL 路径', archivePathPrefix: '归档路径前缀', showFullText: '显示全文', showAbstractOnly: '仅显示摘要', numberArticlesRSS: 'RSS/Feed 文章数量', Proxy: 'HTTP代理', ProxyAddress: '地址', ProxyPort: '端口', }, en: { preview: 'Preview', syncSite: 'Sync Website', newVersion: 'New version', article: 'Article', menu: 'Menu', tag: 'Tag', theme: 'Theme', remote: 'Server', system: 'System', renderSuccess: 'Congratulations, the rendering is complete, go ahead and preview.', renderError: 'Rendering failed, please check whether 127.0.0.1 in the hosts file points to localhost. After confirming that the configuration is correct, try to restart the application.', syncWarning: 'You must complete the configuration to synchronize!', syncSuccess: 'Synchronization succeeded!', syncError1: 'Sorry, synchronization encountered an error, please refer to', syncError2: 'to find a solution', newVersionTips: 'There is a new version released, go and download the new version!', newArticle: 'New', publish: 'Publish', published: 'Published', draft: 'Draft', title: 'Title', status: 'Status', createAt: 'Create Time', actions: 'Actions', deleteWarning: 'After deletion, it cannot be revoked. Are you sure you want to delete it?', warning: 'Warning', articleDelete: 'Article deleted', cancel: 'Cancel', select: 'Select', featureImage: 'Feature Image', saveDraft: 'Save as Draft', save: 'Save', newMenu: 'New', name: 'Name', openType: 'Open Type', link: 'Link', menuSuccess: 'Menu saved', menuDelete: 'Menu deleted', draftSuccess: 'Saved as draft', saveSuccess: 'Saved successfully', newTag: 'New', tagName: 'Tag Name', selectTheme: 'Select Theme', siteName: 'Site Name', siteDescription: 'Site Description', footerInfo: 'Footer Information', isShowFeatureImage: 'Feature image', articlesPerPage: 'Articles per page', archivesPerPage: 'Archives per page', basicSetting: 'Basic settings', commentSetting: 'Comment settings', faviconSetting: 'Favicon setting', avatarSetting: 'Avatar setting', domain: 'Domain', repository: 'Repository Name', branch: 'Branch', username: 'Branch Username', email: 'Email', isShowComment: 'Show comments', domainShouldStartsWithWarn: 'Domain should starts with \'https://\' or \'http://\' ', basicSettingSuccess: 'The basic setting saved', commentSettingSuccess: 'The comment setting saved', faviconSettingSuccess: 'The favicon setting saved', avatarSettingSuccess: 'The avatar setting saved', saved: 'Saved', syncing: 'Synchronizing, please wait...', articleDefault: 'Article URL Default', tagDefault: 'Tag URL Default', hideInList: 'Hide in list', dateFormat: 'Date format', htmlSupport: 'HTML is supported in this field', change: 'Change', editorTip: 'You can insert a separate line is the abstract separator identifier ( the content before this line is the abstract)', saveError: 'Save failed', privateKeyTip: 'Please fill in the absolute path, for example: /home/username/.ssh/id_rsa', remotePathTip: 'Please fill in the absolute path, for example::/home/username/www/', testConnection: 'Test Connection', connectSuccess: 'Remote connection succeeded', connectFailed: 'Remote connection failed, please check repository, username and token settings', sourceFolder: 'Site source file path', language: 'Language', inConfig: 'In configuration', searchArticle: 'Articles search', deleteSelected: 'Selected', inputContent: 'Input content', postUrlRepeatTip: 'The URL of the article is duplicated with other articles.', postUrlIncludeTip: 'URL cannot contain /', onlyPicDrag: 'Only picture dragging is supported', themeConfigSaved: 'The theme configuration has been saved', reset: 'Reset', reseted: 'Reset', noCustomConfigTip: 'There is no custom configuration for the theme', customConfig: 'Custom configuration', moreThemes: 'More Themes', postSettings: 'Post Settings', back: 'Back', savedIn: 'Saved in', or: 'or', starSupport: 'Give us a star!', showAllPost: 'Show full content', showAbstract: 'Show abstract only', unsavedWarning: 'You will lose all unsaved changes, do you want to continue?', noSaveAndBack: 'Continue', insertImage: 'Insert image', insertMore: 'Insert summary separator', writingIn: 'Writing in', words: 'Words', readingTime: 'Reading time', version: 'Version', token: 'Token', tokenUsername: 'Token Username', platform: 'Platform', topArticles: 'Top articles', default: 'Default', external: 'External', pathContainHttps: 'The path must contain either http or https', articleUrlPath: 'Article URL path', concise: 'concise', tagUrlPath: 'Tag URL path', archivePathPrefix: 'Archive path prefix', showFullText: 'Show full text', showAbstractOnly: 'Show abstract only', numberArticlesRSS: 'Number articles RSS/Feed', Proxy: 'HTTP Proxy', ProxyAddress: 'Proxy Address', ProxyPort: 'Proxy Port', }, fr_FR: { preview: 'Aperçu', syncSite: 'Synchroniser', newVersion: 'Nouvelle version', article: 'Article', menu: 'Menu', tag: 'Tag', theme: 'Thème', remote: 'Serveur', system: 'Système', renderSuccess: 'Félicitations, le rendu est terminé et regardez en avant-première.', renderError: 'Le rendu a échoué, veuillez vérifier si 127.0.0.1 dans le fichier hosts pointe vers localhost. Après avoir vérifié que la configuration est correcte, essayez de redémarrer l\'application.', syncWarning: 'Vous devez compléter la configuration pour synchroniser !', syncSuccess: 'La synchronisation a réussi !', syncError1: 'Désolé, la synchronisation a rencontré une erreur, veuillez vous référer à', syncError2: 'pour trouver une solution', newVersionTips: 'Une nouvelle version est disponible, téléchargez la nouvelle version!', newArticle: 'Nouveau', publish: 'Publier', published: 'Publié', draft: 'Brouillon', title: 'Titre', status: 'Status', createAt: 'Heure de création', actions: 'Actions', deleteWarning: 'Après la suppression, celui-ci ne peut plus être révoqué. Êtes-vous sûr de vouloir supprimer ?', warning: 'Attention', articleDelete: 'Article supprimé', cancel: 'Annuler', select: 'Selectionner', featureImage: 'Image de fond', saveDraft: 'Enregristrer en brouillon', save: 'Enregistrer', newMenu: 'Nouveau', name: 'Nom', openType: 'type d\'ouverture', link: 'Lien', menuSuccess: 'Menu enregistré', menuDelete: 'Menu supprimé', draftSuccess: 'Enregistré en brouillon', saveSuccess: 'Sauvegardé avec succès', newTag: 'Nouveau tag', tagName: 'Nom du tag', selectTheme: 'Sélectionnez un thème', siteName: 'Nom du site', siteDescription: 'Description du site', footerInfo: 'Informations sur le Footer', isShowFeatureImage: 'Image de fond', articlesPerPage: 'Articles par page', archivesPerPage: 'Archives par page', basicSetting: 'Paramètres de base', commentSetting: 'Paramétrage des commentaires', faviconSetting: 'Paramètres du Favicon', avatarSetting: 'Paramètres de l\'avatar', domain: 'Domaine', repository: 'Nom du repository', branch: 'Branche', username: 'Nom d\'utilisateur', email: 'Email', isShowComment: 'Afficher les commentaires', domainShouldStartsWithWarn: 'Le domaine doit commencer par \'https://\' or \'http://\' ', basicSettingSuccess: 'Les réglages de base sont enregistrés', commentSettingSuccess: 'Les réglages des commentaires sont enregistrés', faviconSettingSuccess: 'Les réglages du favicon sont enregistrés', avatarSettingSuccess: 'Les réglages de l\'avatar sont enregistrés', saved: 'Enregistré', syncing: 'Synchronisation, veuillez patienter...', articleDefault: 'URL de l\'article par défaut', tagDefault: 'URL de tag par défault', hideInList: 'Cacher dans la liste', dateFormat: 'Format de la date', htmlSupport: 'Gestion du HTML', change: 'Changer', editorTip: 'Vous pouvez insérer une ligne séparée c\'est un identifiant pour séparer le résumé (le contenu avant cette ligne est le résumé)', saveError: 'Enregistrement échoué', privateKeyTip: 'Veuillez indiquer le chemin absolu, par exemple: /home/username/.ssh/id_rsa', remotePathTip: 'Veuillez indiquer le chemin absolu, par exemple: /home/username/www/', testConnection: 'Test de connexion', connectSuccess: 'Connexion à distance a réussi', connectFailed: 'La connexion à distance a échoué, veuillez vérifier les paramètres du référentiel, du nom d\'utilisateur et du token', sourceFolder: 'Chemin d\'accès au fichier source du site', language: 'Langue', inConfig: 'En configuration', searchArticle: 'Rechercher des articles', deleteSelected: 'Sélectionné', inputContent: 'Saisie du contenu', postUrlRepeatTip: 'L\'URL de l\'article est dupliquée avec d\'autres articles.', postUrlIncludeTip: 'L\'URL ne peut pas contenir /', onlyPicDrag: 'Seul le dragage d\'images est autorisé', themeConfigSaved: 'La configuration du thème a été sauvegardée', reset: 'Réinitialiser', reseted: 'Réinitialiser', noCustomConfigTip: 'Il n\'y a pas de configuration personnalisée pour le thème', customConfig: 'Configuration personnalisée', moreThemes: 'Autres thèmes', postSettings: 'Paramètres des postes', back: 'Retour', savedIn: 'Sauvegardé dans', or: 'ou', starSupport: 'Donnez-nous une étoile !', showAllPost: 'Afficher tout les postes', showAbstract: 'Afficher uniquement le résumé', unsavedWarning: 'Vous allez perdre tous les changements non sauvegardés, voulez-vous continuer ?', noSaveAndBack: 'Continuer', insertImage: 'Insérer une image', insertMore: 'Insérer un séparateur de résumé', writingIn: 'Ecrire en', words: 'Mots', readingTime: 'Temps de lecture', version: 'Version', token: 'Token', tokenUsername: 'Token Username', platform: 'Plate-forme', topArticles: 'Articles en tête', default: 'Par défaut', external: 'Externe', pathContainHttps: 'L\'URL doit contenir soit \'http\' ou \'https\'', articleUrlPath: 'URL des articles', concise: 'simplifié', tagUrlPath: 'URL des tags', archivePathPrefix: 'Préfix du chemin des Archives', showFullText: 'Tout afficher', showAbstractOnly: 'Afficher seulement le résumé', numberArticlesRSS: 'Nombre d\'articles RSS/Feed', Proxy: 'HTTP Proxy', ProxyAddress: 'Proxy Address', ProxyPort: 'Proxy Port', }, ru: { preview: 'Предпросмотр', syncSite: 'Синхронизировать сайт', newVersion: 'Новая версия', article: 'Запись', menu: 'Меню', tag: 'Тег', theme: 'Тема', remote: 'Сервер', system: 'Система', renderSuccess: 'Поздравляем, рендеринг завершен, переходите к предпросмотру.', renderError: 'Не удалось выполнить рендеринг, пожалуйста, проверьте, указывает ли 127.0.0.1 в файле hosts на localhost. После подтверждения правильности настроек попробуйте перезапустить приложение.', syncWarning: 'Для синхронизации следует завершить настройку!', syncSuccess: 'Синхронизация выполнена успешно!', syncError1: 'Извините, при синхронизации произошла ошибка, пожалуйста, обратитесь к', syncError2: 'чтобы найти решение', newVersionTips: 'Выпущена новая версия, пожалуйста, зайдите и скачайте новую версию!', newArticle: 'Новая запись', publish: 'Опубликовать', published: 'Опубликовано', draft: 'Черновик', title: 'Заголовок', status: 'Статус', createAt: 'Дата и время создания', actions: 'Действия', deleteWarning: 'После удаления вернуть всё назад не получится. Вы уверены, что хотите удалить?', warning: 'Предупреждение', articleDelete: 'Запись удалена', cancel: 'Отмена', select: 'Выбрать', featureImage: 'Изображение записи', saveDraft: 'Сохранить как Черновик', save: 'Сохранить', newMenu: 'Добавить', name: 'Название', openType: 'Тип ссылки', link: 'Ссылка', menuSuccess: 'Меню сохранено', menuDelete: 'Меню удалено', draftSuccess: 'Сохранено как Черновик', saveSuccess: 'Успешно сохранено', newTag: 'Добавить тег', tagName: 'Название тега', selectTheme: 'Выберите Тему', siteName: 'Название сайта', siteDescription: 'Описание сайта', footerInfo: 'Информация в подвале сайта', isShowFeatureImage: 'Изображения записей', articlesPerPage: 'Записей на странице', archivesPerPage: 'Архивных записей на странице', basicSetting: 'Основные настройки', commentSetting: 'Настройки комментариев', faviconSetting: 'Настройки иконки сайта', avatarSetting: 'Настройки аватара', domain: 'Домен', repository: 'Название репозитория', branch: 'Ветка', username: 'Имя пользователя ветки', email: 'Email', isShowComment: 'Показывать комментарии', domainShouldStartsWithWarn: 'Домен должен начинаться с \'https://\' или \'http://\' ', basicSettingSuccess: 'Основные настройки сохранены', commentSettingSuccess: 'Настройки комментариев сохранены', faviconSettingSuccess: 'Новая иконка сайта сохранена', avatarSettingSuccess: 'Новый аватар сохранён', saved: 'Сохранено', syncing: 'Идёт синхронизация, пожалуйста подождите...', articleDefault: 'URL записи по умолчанию', tagDefault: 'URL тега по умолчанию', hideInList: 'Скрыть из списка записей', dateFormat: 'Формат даты', htmlSupport: 'В этом поле поддерживается HTML', change: 'Изменить', editorTip: 'Вы можете вставить отдельную строку с тегом — это тег разделителя (содержимое перед этой строкой будет отображаться на странице со списком записей)', saveError: 'Ошибка сохранения', privateKeyTip: 'Пожалуйста, введите абсолютный путь, например: /home/username/.ssh/id_rsa', remotePathTip: 'Пожалуйста, введите абсолютный путь, например: /home/username/www/', testConnection: 'Проверка соединения', connectSuccess: 'Удалённое подключение выполнено успешно', connectFailed: 'Ошибка при удалённом подключении. Пожалуйста, проверьте настройки репозитория, имени пользователя и токена.', sourceFolder: 'Путь к исходным файлам сайта', language: 'Язык', inConfig: 'Применение настроек', searchArticle: 'Поиск записей', deleteSelected: 'Выбранное', inputContent: 'Введите содержимое', postUrlRepeatTip: 'URL-адрес записи в точности повторяет URL-адрес других записей.', postUrlIncludeTip: 'URL-адрес не может содержать /', onlyPicDrag: 'Поддерживается только перетаскивание изображений', themeConfigSaved: 'Настройки темы были сохранены', reset: 'Сбросить', reseted: 'Сброшено', noCustomConfigTip: 'Пользовательских настроек для этой темы не предусмотрено', customConfig: 'Пользовательские настройки', moreThemes: 'Ещё больше тем', postSettings: 'Настройки записи', back: 'Назад', savedIn: 'Сохранено в', or: 'или', starSupport: 'Оцените нас!', showAllPost: 'Показывать содержимое записей полностью', showAbstract: 'Показывать краткое описание записей', unsavedWarning: 'Вы потеряете все несохраненные изменения, хотите продолжить?', noSaveAndBack: 'Продолжить', insertImage: 'Вставить изображение', insertMore: 'Вставить разделитель', writingIn: 'Создано с помощью', words: 'Слов(а)', readingTime: 'Время чтения', version: 'Версия', token: 'Токен', tokenUsername: 'Имя пользователя токена', platform: 'Платформа', topArticles: 'Закрепить запись', default: 'По умолчанию', external: 'Ссылка на изображение', pathContainHttps: 'Ссылка должна содержать либо http, либо https', articleUrlPath: 'URL-адрес записи', concise: 'Компактный вид', tagUrlPath: 'URL-адрес тега', archivePathPrefix: 'Префикс для архива', showFullText: 'Отображать содержимое полностью', showAbstractOnly: 'Отображать краткое содержимое', numberArticlesRSS: 'Количество записей в RSS/Feed', Proxy: 'HTTP Прокси', ProxyAddress: 'Прокси адрес', ProxyPort: 'Прокси порт', }, ja_JP: { preview: 'プレビュー', syncSite: 'サイトを同期化', newVersion: '新しいバージョン', article: '文書', menu: 'メニュー', tag: 'タグ', theme: 'テーマ', remote: 'リモート', system: 'システム', renderSuccess: 'レンダリングは完成しました、プレビューしてください。', renderError: 'レンダリングに失敗しました。hostsファイルの127.0.0.1がlocalhostを指しているかどうかを確認してください。構成が正しいことを確認した後、アプリケーションを再起動してみてください。', syncWarning: 'サイトを同期化する前に、システムの配置を完成してください!', syncSuccess: 'サイトの同期化は完成しました!', syncError1: '同期化の途中でエラーが発生しました、ご確認ください。', syncError2: '解決策を見つかりましょう', newVersionTips: '新しいバージョンがリリースしました、アップデートしましょう!', newArticle: '新規文書', publish: '公開', published: '公開済み', draft: '下書き', title: 'タイトル', status: 'ステータス', createAt: '作成日', actions: '操作', deleteWarning: '完全に削除してもよろしいですか?', warning: 'ウォーニング', articleDelete: '文書を削除しました。', cancel: '取り消す', select: '選択', featureImage: '表紙イメージ', saveDraft: '下書き保存', save: 'セーブ', newMenu: '新規メニュー', name: '名前', openType: '開く方式', link: 'リンク', menuSuccess: 'メニューを保存しました', menuDelete: 'メニューを削除しました', draftSuccess: '下書きを保存しました', saveSuccess: 'セーブしました', newTag: '新規タグ', tagName: 'タグ名', selectTheme: 'テーマ選択', siteName: 'サイト名', siteDescription: 'サイト紹介', footerInfo: 'フッター', isShowFeatureImage: '表紙イメージを表示します', articlesPerPage: '毎ページの文書数', archivesPerPage: '毎ページのアーカイブ数', basicSetting: 'システム設定', commentSetting: 'コメント設定', faviconSetting: 'タブアイコン', avatarSetting: 'ユーザーアイコン', domain: 'ドメイン名', repository: 'レポジトリ名', branch: 'ブランチ', username: 'ユーザー名', email: 'メールアドレス', isShowComment: 'コメント表示', domainShouldStartsWithWarn: 'ドメイン名は必ず \'https://\' 或 \'http://\' で始まります', basicSettingSuccess: 'システム設定を保存しました', commentSettingSuccess: 'コメント設定を保存しました', faviconSettingSuccess: 'タブアイコンを保存しました', avatarSettingSuccess: 'ユーザーアイコンを保存しました', saved: '保存しました', syncing: '同期中、しばらくお待ちください…', articleDefault: '文書URLのデフォルトフォーマット', tagDefault: 'タグURLのデフォルトフォーマット', hideInList: 'リストに隠す', dateFormat: '日付フォーマット', htmlSupport: 'HTMLサポート', change: '更新', editorTip: '文書の後ろに一行の を追加したら、この部分を摘要としてを表示できます。', saveError: '保存失敗しました', privateKeyTip: '絶対パスを記入してください、例えば:「/home/username/.ssh/id_rsa」', remotePathTip: '絶対パスを記入してください、例えば:「/home/username/www/」', testConnection: '接続をテストしています', connectSuccess: '接続は成功しました', connectFailed: '接続が失敗しました。リポジトリ名、ユーザー名とトークンを確認してください。', sourceFolder: 'サイトのソースファイルのパス', language: '言語', inConfig: '配置中', searchArticle: '文書検索', deleteSelected: '選択済み', inputContent: '内容を入力してください', postUrlRepeatTip: '文書のURLは他の文書と重複しました', postUrlIncludeTip: 'URLの中には / を含めません', onlyPicDrag: 'イメージをドラッグのみです', themeConfigSaved: 'テームの設定を保存しました', reset: 'リセット', reseted: 'リセット完成しました', noCustomConfigTip: '現在のテーマはカスタム設定はありません', customConfig: 'カスタム設定', moreThemes: 'テーマストアー', postSettings: '文書設定', back: '戻る', savedIn: '上書き保存', or: 'または', starSupport: 'Starで開発者をサポートします', showAllPost: '全ての文書を表示します', showAbstract: '摘要のみを表示します', unsavedWarning: '未保存の変更内容を全て削除します、よろしいですか?', noSaveAndBack: 'つづく', insertImage: 'イマージを挿入します', insertMore: '摘要標記を挿入します', writingIn: '作成地', words: '字数', readingTime: '読む時間', version: 'バージョン', token: 'トークン', tokenUsername: 'トークンのユーザー名', platform: 'プラットホーム', topArticles: '固定文書', default: 'デフォルト', external: '外部リンク', pathContainHttps: 'パスは必ずhttpまたはhttpsを含めます', articleUrlPath: '文書のURLのパス', concise: '簡素化', tagUrlPath: 'タグURLのパス', archivePathPrefix: 'アーカイブのパスの接頭辞', showFullText: '全文を表示します', showAbstractOnly: '摘要のみを表示します', numberArticlesRSS: 'RSS/Feed 文書数量', Proxy: 'HTTPプロキシ', ProxyAddress: 'プロキシアドレス', ProxyPort: 'プロキシポート', } } export default message ================================================ FILE: src/assets/styles/custom.less ================================================ .ant-table { background: #fff; color: #555; } .ant-table-tbody { .ant-table-row:nth-of-type(2n) { background: #fafafa; } } .ant-table-tbody > tr > td { border-bottom: 0; } .ant-table-thead > tr > th { background: #ffffff; color: #1b1b18b0; font-weight: normal; } .ant-table-thead > tr.ant-table-row-hover:not(.ant-table-expanded-row) > td, .ant-table-tbody > tr.ant-table-row-hover:not(.ant-table-expanded-row) > td, .ant-table-thead > tr:hover:not(.ant-table-expanded-row) > td, .ant-table-tbody > tr:hover:not(.ant-table-expanded-row) > td { background: transparent; } .ant-table-thead > tr > th, .ant-table-tbody > tr > td { padding: 12px 12px; } .ant-table-tbody > tr.ant-table-row-selected td { background: transparent; } .tool-container { padding: 0px 16px 7px; margin-bottom: 16px; position: fixed; top: 8px; left: 200px; right: 0px; z-index: 1; background: #fff; border-bottom: 1px solid #e8e8e88a; -webkit-app-region: drag; .btn { margin-left: 16px; -webkit-app-region: no-drag; } .op-btn { height: 30px; line-height: 30px; padding: 0 16px; font-size: 18px; border-radius: 20px; margin-left: 8px; outline: none; transition: all 0.3s; display: flex; justify-content: center; align-items: center; i { font-weight: bold; } &:hover { background: #efefef; color: #515457; } &:focus { background: #efefef; } &.save-btn { color: #38A169; &:hover { background: #9AE6B4; color: #22543D; } &:focus { background: #68D391; } } } } .content-container { margin-top: 48px; } .ant-input, .ant-input-group-addon, .ant-select-selection, .ant-input-number-input, .ant-input-number { background: #FAFAFA; border-color: #EAEAEA; &:focus { box-shadow: none; background: #fff; border-color: #999; } } .ant-input-number-focused { box-shadow: none; } .ant-select-dropdown-menu-item-active, .ant-select-dropdown-menu-item:hover { background: #f7f7f7; } .ant-btn { font-size: 13px; box-shadow: none; [class^='zwicon-']:not(:last-child) { font-size: 16px; margin-right: 8px; } } .ant-btn-primary { box-shadow: none; text-shadow: none; } .ant-menu-inline > .ant-menu-item { height: 36px; line-height: 36px; } .ant-menu-item, .ant-menu-submenu-title { transition: none; } .ant-menu-item:active, .ant-menu-submenu-title:active { background: inherit; } .ant-pagination-prev .ant-pagination-item-link, .ant-pagination-next .ant-pagination-item-link, .ant-pagination-item { border: none; &:hover { background: #fafafa; } } .ant-pagination-item-active { &, &:hover { background: #eaeaea; } } .ant-menu.ant-menu-dark .ant-menu-item-selected, .ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected { position: relative; } .ant-checkbox-inner:after { left: 3.571429px; } .ant-checkbox-checked .ant-checkbox-inner { background-color: #fff; &:after { border-color: @primary-color; } } .ant-tabs-nav { .ant-tabs-tab { padding: 4px 8px; margin-bottom: 8px; color: #b1b1b1; border-radius: 6px; transition: all 0.3s; &:hover { background: #efefef; } } .ant-tabs-tab-active { color: #1b1b18; } } .ant-modal-confirm .ant-modal-body { padding: 24px 16px 16px; } .tip-text { background: #fafafa; padding: 4px 8px; border-radius: 2px; line-height: 1.25; font-size: 12px; color: #6669; } .ant-tabs-ink-bar { height: 0; } .ant-message { z-index: 3010; top: 8px; font-size: 12px; } .ant-message-notice-content { padding: 8px 16px; border-radius: 16px; box-shadow: 0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)!important; background: #000; color: #fafafa; } .ant-drawer-close-x { width: 32px; height: 32px; line-height: 32px; border-radius: 50%; background: #f3f7f9; margin: 12px; font-size: 14px; display: inline-flex; justify-content: center; align-items: center; } .ant-modal-close { &:focus { outline: none; } } .anticon { vertical-align: unset; } .ant-drawer-header { border-bottom: none; } .ant-form-item-control { padding-right: 2px; } .ant-menu-inline .ant-menu-item, .ant-menu-inline .ant-menu-submenu-title { width: 90%; margin-left: 5%; border-radius: 6px; color: #666; } .menu-tab { .ant-tabs-top-bar { position: fixed; top: 8px; left: 200px; right: 0; background: #fff; z-index: 100; padding-left: 24px; } .ant-tabs-content { padding-top: 44px; } } .ant-tabs-bar { border-bottom: 1px solid #e8e8e88a; } .ant-tag { border: none; line-height: 22px; } .ant-notification-notice { padding: 8px 12px; box-shadow: 0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05)!important; } .ant-notification-notice-description { font-size: 12px; line-height: 16px; } .ant-notification-notice-with-icon { .ant-notification-notice-message, .ant-notification-notice-description { margin-left: 40px; } } .ant-notification-notice-close { right: 4px; top: 2px; } .ant-modal-content { border-radius: 10px; overflow: hidden; box-shadow: 0px 20px 100px 0px rgba(0,0,0,0.15); } .ant-modal-header { border-bottom-color: #fafafa; } .ant-form-explain, .ant-form-extra { font-size: 12px; } .ant-tooltip { font-size: 12px; } .ant-tooltip-arrow, .ant-popover-arrow { display: none; } .ant-tooltip-inner { padding: 4px 8px; min-height: auto; } .ant-drawer-mask, .ant-modal-mask { backdrop-filter: saturate(180%) blur(20px); background-color: rgba(242,242,242,0.5); } .ant-drawer.ant-drawer-open .ant-drawer-mask { opacity: 1; } .ant-drawer-right.ant-drawer-open .ant-drawer-content-wrapper { box-shadow: 0px 20px 100px 0px rgba(0,0,0,0.15); } .ant-popover-inner { border: 1px solid #eee; box-shadow: 0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -1px rgba(0,0,0,.06)!important; } ::selection { background: #bed6fb; color: @primary-color; } .ant-radio-inner:after { width: 14px; height: 14px; left: 0; top: 0; } .ant-radio-wrapper { padding: 2px 0 2px 8px; border-radius: 2px; &:hover { transition: all 0.3s; background: #efefef; } } .ant-slider-rail, .ant-slider-track, .ant-slider-step { height: 2px; top: 5px; } .ant-drawer-close:focus { outline: none; } ================================================ FILE: src/assets/styles/main.less ================================================ // @import "./custom.less"; @import "~ant-design-vue/dist/antd.less"; @import "./var.less"; @import "./custom.less"; @import "./zwicon.less"; ================================================ FILE: src/assets/styles/tailwind.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; .transition { transition: all .1s ease-in !important; } @variants responsive, hover { .translate-r-2px { transform: translateX(2px) !important; } .transition-fast { transition: all .2s ease !important; } } ================================================ FILE: src/assets/styles/var.less ================================================ @primary-color: #1b1b18; @primary-bg: #f7f6f6; @danger-color: #fa5252; @border-radius-base : 6px; @link-color: #1a5ccf; @border-color: #e8e8e88a; @btn-default-border: #eaeaea; @label-color: #1b1b18; @font-family: PingFang SC,-apple-system,SF UI Text,Lucida Grande,STheiti,Microsoft YaHei,sans-serif; @input-hover-border-color: #999; ================================================ FILE: src/assets/styles/zwicon.less ================================================ @font-face { font-family: 'zwicon'; src: url('../fonts/zwicon.eot?k483k8'); src: url('../fonts/zwicon.eot?k483k8#iefix') format('embedded-opentype'), url('../fonts/zwicon.ttf?k483k8') format('truetype'), url('../fonts/zwicon.woff?k483k8') format('woff'), url('../fonts/zwicon.svg?k483k8#zwicon') format('svg'); font-weight: normal; font-style: normal; } i { /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'zwicon' !important; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .zwicon-align-bottom:before { content: "\e900"; } .zwicon-align-h:before { content: "\e901"; } .zwicon-align-left:before { content: "\e902"; } .zwicon-align-right:before { content: "\e903"; } .zwicon-align-top:before { content: "\e904"; } .zwicon-align-v:before { content: "\e905"; } .zwicon-distribute-h:before { content: "\e906"; } .zwicon-distribute-v:before { content: "\e907"; } .zwicon-arrow-bottom-left:before { content: "\e908"; } .zwicon-arrow-bottom-right:before { content: "\e909"; } .zwicon-arrow-circle-down:before { content: "\e90a"; } .zwicon-arrow-circle-left:before { content: "\e90b"; } .zwicon-arrow-circle-right:before { content: "\e90c"; } .zwicon-arrow-circle-up:before { content: "\e90d"; } .zwicon-arrow-down:before { content: "\e90e"; } .zwicon-arrow-left:before { content: "\e90f"; } .zwicon-arrow-right:before { content: "\e910"; } .zwicon-arrow-square-down:before { content: "\e911"; } .zwicon-arrow-square-left:before { content: "\e912"; } .zwicon-arrow-square-right:before { content: "\e913"; } .zwicon-arrow-square-up:before { content: "\e914"; } .zwicon-arrow-to-top:before { content: "\e915"; } .zwicon-arrow-top-left:before { content: "\e916"; } .zwicon-arrow-top-right:before { content: "\e917"; } .zwicon-arrow-up:before { content: "\e918"; } .zwicon-back:before { content: "\e919"; } .zwicon-chevron-double-down:before { content: "\e91a"; } .zwicon-chevron-double-left:before { content: "\e91b"; } .zwicon-chevron-double-right:before { content: "\e91c"; } .zwicon-chevron-double-up:before { content: "\e91d"; } .zwicon-chevron-down:before { content: "\e91e"; } .zwicon-chevron-left:before { content: "\e91f"; } .zwicon-chevron-right:before { content: "\e920"; } .zwicon-chevron-up:before { content: "\e921"; } .zwicon-collapse-alt:before { content: "\e922"; } .zwicon-collapse-alt2:before { content: "\e923"; } .zwicon-collapse-down:before { content: "\e924"; } .zwicon-collapse-left:before { content: "\e925"; } .zwicon-collapse-right:before { content: "\e926"; } .zwicon-collapse-up:before { content: "\e927"; } .zwicon-collapse:before { content: "\e928"; } .zwicon-continue:before { content: "\e929"; } .zwicon-expand-alt:before { content: "\e92a"; } .zwicon-expand-alt2:before { content: "\e92b"; } .zwicon-expand-down:before { content: "\e92c"; } .zwicon-expand-h:before { content: "\e92d"; } .zwicon-expand-left:before { content: "\e92e"; } .zwicon-expand-right:before { content: "\e92f"; } .zwicon-expand-up:before { content: "\e930"; } .zwicon-expand-v:before { content: "\e931"; } .zwicon-expand:before { content: "\e932"; } .zwicon-loop:before { content: "\e933"; } .zwicon-prioritize-down:before { content: "\e934"; } .zwicon-prioritize-up:before { content: "\e935"; } .zwicon-redo:before { content: "\e936"; } .zwicon-refresh-double:before { content: "\e937"; } .zwicon-refresh-left:before { content: "\e938"; } .zwicon-refresh-right:before { content: "\e939"; } .zwicon-restart:before { content: "\e93a"; } .zwicon-split-h:before { content: "\e93b"; } .zwicon-split-v:before { content: "\e93c"; } .zwicon-undo:before { content: "\e93d"; } .zwicon-cell-border-bottom:before { content: "\e93e"; } .zwicon-cell-border-full:before { content: "\e93f"; } .zwicon-cell-border-left:before { content: "\e940"; } .zwicon-cell-border-right:before { content: "\e941"; } .zwicon-cell-border-top:before { content: "\e942"; } .zwicon-cell-empty:before { content: "\e943"; } .zwicon-cell-full:before { content: "\e944"; } .zwicon-cell-split-h:before { content: "\e945"; } .zwicon-cell-split-v:before { content: "\e946"; } .zwicon-cell-split:before { content: "\e947"; } .zwicon-archive:before { content: "\e948"; } .zwicon-document:before { content: "\e949"; } .zwicon-folder-delete:before { content: "\e94a"; } .zwicon-folder-minus:before { content: "\e94b"; } .zwicon-folder-open:before { content: "\e94c"; } .zwicon-folder-plus:before { content: "\e94d"; } .zwicon-folder:before { content: "\e94e"; } .zwicon-note:before { content: "\e94f"; } .zwicon-notebook:before { content: "\e950"; } .zwicon-script:before { content: "\e951"; } .zwicon-sticker:before { content: "\e952"; } .zwicon-sticky-notes:before { content: "\e953"; } .zwicon-tray-delete:before { content: "\e954"; } .zwicon-tray-empty:before { content: "\e955"; } .zwicon-tray-export:before { content: "\e956"; } .zwicon-tray-import:before { content: "\e957"; } .zwicon-tray-minus:before { content: "\e958"; } .zwicon-tray-plus:before { content: "\e959"; } .zwicon-tray-stack:before { content: "\e95a"; } .zwicon-tray:before { content: "\e95b"; } .zwicon-artboard:before { content: "\e95c"; } .zwicon-brush:before { content: "\e95d"; } .zwicon-clipboard:before { content: "\e95e"; } .zwicon-copy-alt:before { content: "\e95f"; } .zwicon-copy:before { content: "\e960"; } .zwicon-crop:before { content: "\e961"; } .zwicon-cut-alt:before { content: "\e962"; } .zwicon-cut:before { content: "\e963"; } .zwicon-drafting-compass:before { content: "\e964"; } .zwicon-duplicate-alt:before { content: "\e965"; } .zwicon-duplicate:before { content: "\e966"; } .zwicon-eraser:before { content: "\e967"; } .zwicon-eye-dropper:before { content: "\e968"; } .zwicon-group:before { content: "\e969"; } .zwicon-layer-stack:before { content: "\e96a"; } .zwicon-magic-wand:before { content: "\e96b"; } .zwicon-marker:before { content: "\e96c"; } .zwicon-paint-bucket:before { content: "\e96d"; } .zwicon-paint-roller:before { content: "\e96e"; } .zwicon-palette:before { content: "\e96f"; } .zwicon-paste-alt:before { content: "\e970"; } .zwicon-paste:before { content: "\e971"; } .zwicon-pen-circle:before { content: "\e972"; } .zwicon-pen:before { content: "\e973"; } .zwicon-pencil-circle:before { content: "\e974"; } .zwicon-pencil:before { content: "\e975"; } .zwicon-ruler-combined:before { content: "\e976"; } .zwicon-ruler-h:before { content: "\e977"; } .zwicon-ruler-v:before { content: "\e978"; } .zwicon-stamp:before { content: "\e979"; } .zwicon-table:before { content: "\e97a"; } .zwicon-activity:before { content: "\e97b"; } .zwicon-android:before { content: "\e97c"; } .zwicon-apple:before { content: "\e97d"; } .zwicon-bolt:before { content: "\e97e"; } .zwicon-cloud-download:before { content: "\e97f"; } .zwicon-cloud-minus:before { content: "\e980"; } .zwicon-cloud-plus:before { content: "\e981"; } .zwicon-cloud-upload:before { content: "\e982"; } .zwicon-code:before { content: "\e983"; } .zwicon-command:before { content: "\e984"; } .zwicon-database:before { content: "\e985"; } .zwicon-deploy:before { content: "\e986"; } .zwicon-git-branch:before { content: "\e987"; } .zwicon-git-commit:before { content: "\e988"; } .zwicon-git-fork:before { content: "\e989"; } .zwicon-git-merge:before { content: "\e98a"; } .zwicon-git-pull:before { content: "\e98b"; } .zwicon-ios:before { content: "\e98c"; } .zwicon-lan-connection:before { content: "\e98d"; } .zwicon-lan-error:before { content: "\e98e"; } .zwicon-lan:before { content: "\e98f"; } .zwicon-osx:before { content: "\e990"; } .zwicon-repository:before { content: "\e991"; } .zwicon-web:before { content: "\e992"; } .zwicon-window-delete:before { content: "\e993"; } .zwicon-window-minus:before { content: "\e994"; } .zwicon-window-plus:before { content: "\e995"; } .zwicon-window:before { content: "\e996"; } .zwicon-windows:before { content: "\e997"; } .zwicon-airpods-alt:before { content: "\e998"; } .zwicon-airpods:before { content: "\e999"; } .zwicon-apple-watch-smile:before { content: "\e99a"; } .zwicon-apple-watch-time:before { content: "\e99b"; } .zwicon-apple-watch:before { content: "\e99c"; } .zwicon-cable-hdmi:before { content: "\e99d"; } .zwicon-cable-jack:before { content: "\e99e"; } .zwicon-cable-lan:before { content: "\e99f"; } .zwicon-cable-lightning:before { content: "\e9a0"; } .zwicon-cable-magsafe:before { content: "\e9a1"; } .zwicon-cable-usb:before { content: "\e9a2"; } .zwicon-cardboard-vr:before { content: "\e9a3"; } .zwicon-controller-alt:before { content: "\e9a4"; } .zwicon-controller:before { content: "\e9a5"; } .zwicon-desktop:before { content: "\e9a6"; } .zwicon-devices:before { content: "\e9a7"; } .zwicon-floppy:before { content: "\e9a8"; } .zwicon-gameboy:before { content: "\e9a9"; } .zwicon-hard-drive:before { content: "\e9aa"; } .zwicon-headphone:before { content: "\e9ab"; } .zwicon-imac:before { content: "\e9ac"; } .zwicon-ipad-h:before { content: "\e9ad"; } .zwicon-ipad:before { content: "\e9ae"; } .zwicon-iphone-h:before { content: "\e9af"; } .zwicon-iphone-x-h:before { content: "\e9b0"; } .zwicon-iphone-x:before { content: "\e9b1"; } .zwicon-iphone:before { content: "\e9b2"; } .zwicon-keyboard:before { content: "\e9b3"; } .zwicon-laptop:before { content: "\e9b4"; } .zwicon-mac-pro:before { content: "\e9b5"; } .zwicon-macbook-pro:before { content: "\e9b6"; } .zwicon-memory-card:before { content: "\e9b7"; } .zwicon-mouse:before { content: "\e9b8"; } .zwicon-phone-andorid-h:before { content: "\e9b9"; } .zwicon-phone-andorid:before { content: "\e9ba"; } .zwicon-phone-holding-double:before { content: "\e9bb"; } .zwicon-phone-holding:before { content: "\e9bc"; } .zwicon-plug:before { content: "\e9bd"; } .zwicon-printer:before { content: "\e9be"; } .zwicon-server-stack:before { content: "\e9bf"; } .zwicon-smart-glasses:before { content: "\e9c0"; } .zwicon-smart-tv:before { content: "\e9c1"; } .zwicon-smart-watch-time:before { content: "\e9c2"; } .zwicon-smart-watch:before { content: "\e9c3"; } .zwicon-tablet-h:before { content: "\e9c4"; } .zwicon-tablet:before { content: "\e9c5"; } .zwicon-terminal:before { content: "\e9c6"; } .zwicon-virtual-reality:before { content: "\e9c7"; } .zwicon-voice-assistant:before { content: "\e9c8"; } .zwicon-edit-circle:before { content: "\e9c9"; } .zwicon-edit-pencil:before { content: "\e9ca"; } .zwicon-edit-square-feather:before { content: "\e9cb"; } .zwicon-edit-square:before { content: "\e9cc"; } .zwicon-file-archive:before { content: "\e9cd"; } .zwicon-file-audio:before { content: "\e9ce"; } .zwicon-file-cloud:before { content: "\e9cf"; } .zwicon-file-download:before { content: "\e9d0"; } .zwicon-file-empty:before { content: "\e9d1"; } .zwicon-file-export:before { content: "\e9d2"; } .zwicon-file-font:before { content: "\e9d3"; } .zwicon-file-graphic:before { content: "\e9d4"; } .zwicon-file-image:before { content: "\e9d5"; } .zwicon-file-import:before { content: "\e9d6"; } .zwicon-file-pdf:before { content: "\e9d7"; } .zwicon-file-search:before { content: "\e9d8"; } .zwicon-file-sketch:before { content: "\e9d9"; } .zwicon-file-table:before { content: "\e9da"; } .zwicon-file-upload:before { content: "\e9db"; } .zwicon-file-vector:before { content: "\e9dc"; } .zwicon-file-video:before { content: "\e9dd"; } .zwicon-filter-alt:before { content: "\e9de"; } .zwicon-filter:before { content: "\e9df"; } .zwicon-slider-circle-h:before { content: "\e9e0"; } .zwicon-slider-circle-v:before { content: "\e9e1"; } .zwicon-slider-rectangle-h:before { content: "\e9e2"; } .zwicon-slider-rectangle-v:before { content: "\e9e3"; } .zwicon-sort-alphabetic-down:before { content: "\e9e4"; } .zwicon-sort-alphabetic-up:before { content: "\e9e5"; } .zwicon-sort-amount-down:before { content: "\e9e6"; } .zwicon-sort-amount-up:before { content: "\e9e7"; } .zwicon-sort-numeric-down:before { content: "\e9e8"; } .zwicon-sort-numeric-up:before { content: "\e9e9"; } .zwicon-toggle-switch:before { content: "\e9ea"; } .zwicon-bar-code-scan:before { content: "\e9eb"; } .zwicon-bar-code:before { content: "\e9ec"; } .zwicon-bid:before { content: "\e9ed"; } .zwicon-bill:before { content: "\e9ee"; } .zwicon-bitcoin-sign:before { content: "\e9ef"; } .zwicon-bull-horn:before { content: "\e9f0"; } .zwicon-coin:before { content: "\e9f1"; } .zwicon-credit-card:before { content: "\e9f2"; } .zwicon-diamond:before { content: "\e9f3"; } .zwicon-dollar-sign:before { content: "\e9f4"; } .zwicon-euro-sign:before { content: "\e9f5"; } .zwicon-hammer:before { content: "\e9f6"; } .zwicon-line-chart:before { content: "\e9f7"; } .zwicon-lira-sign:before { content: "\e9f8"; } .zwicon-money-bill:before { content: "\e9f9"; } .zwicon-money-stack:before { content: "\e9fa"; } .zwicon-package:before { content: "\e9fb"; } .zwicon-piggy-bank:before { content: "\e9fc"; } .zwicon-pound-sign:before { content: "\e9fd"; } .zwicon-price-tag:before { content: "\e9fe"; } .zwicon-qr-code-scan:before { content: "\e9ff"; } .zwicon-qr-code:before { content: "\ea00"; } .zwicon-receipt:before { content: "\ea01"; } .zwicon-rubel-sign:before { content: "\ea02"; } .zwicon-rupee-sign:before { content: "\ea03"; } .zwicon-sale-badge:before { content: "\ea04"; } .zwicon-shopping-bag-alt:before { content: "\ea05"; } .zwicon-shopping-bag:before { content: "\ea06"; } .zwicon-shopping-cart:before { content: "\ea07"; } .zwicon-store:before { content: "\ea08"; } .zwicon-wallet:before { content: "\ea09"; } .zwicon-won-sign:before { content: "\ea0a"; } .zwicon-yen-sign:before { content: "\ea0b"; } .zwicon-flip-left-alt:before { content: "\ea0c"; } .zwicon-flip-left:before { content: "\ea0d"; } .zwicon-flip-right-alt:before { content: "\ea0e"; } .zwicon-flip-right:before { content: "\ea0f"; } .zwicon-double-tap-two:before { content: "\ea10"; } .zwicon-double-tap:before { content: "\ea11"; } .zwicon-drag:before { content: "\ea12"; } .zwicon-flick-left-two:before { content: "\ea13"; } .zwicon-flick-left:before { content: "\ea14"; } .zwicon-flick-right-two:before { content: "\ea15"; } .zwicon-flick-right:before { content: "\ea16"; } .zwicon-horns:before { content: "\ea17"; } .zwicon-pinch:before { content: "\ea18"; } .zwicon-point:before { content: "\ea19"; } .zwicon-press:before { content: "\ea1a"; } .zwicon-scroll-down-two:before { content: "\ea1b"; } .zwicon-scroll-down:before { content: "\ea1c"; } .zwicon-scroll-h-two:before { content: "\ea1d"; } .zwicon-scroll-h:before { content: "\ea1e"; } .zwicon-scroll-up-two:before { content: "\ea1f"; } .zwicon-scroll-up:before { content: "\ea20"; } .zwicon-scroll-v-two:before { content: "\ea21"; } .zwicon-scroll-v:before { content: "\ea22"; } .zwicon-shaka:before { content: "\ea23"; } .zwicon-spread:before { content: "\ea24"; } .zwicon-tap-two:before { content: "\ea25"; } .zwicon-tap:before { content: "\ea26"; } .zwicon-two-drag:before { content: "\ea27"; } .zwicon-add-item-alt:before { content: "\ea28"; } .zwicon-add-item:before { content: "\ea29"; } .zwicon-add-note:before { content: "\ea2a"; } .zwicon-add-to-list:before { content: "\ea2b"; } .zwicon-at:before { content: "\ea2c"; } .zwicon-attach-document:before { content: "\ea2d"; } .zwicon-paperclip:before { content: "\ea2e"; } .zwicon-battery-full:before { content: "\ea30"; } .zwicon-battery-low:before { content: "\ea31"; } .zwicon-battery-mid:before { content: "\ea32"; } .zwicon-battery-v:before { content: "\ea33"; } .zwicon-bell-alt-ring:before { content: "\ea34"; } .zwicon-bell-alt:before { content: "\ea35"; } .zwicon-bell-slash:before { content: "\ea36"; } .zwicon-bell-snooze:before { content: "\ea37"; } .zwicon-bell:before { content: "\ea38"; } .zwicon-block:before { content: "\ea39"; } .zwicon-book-alt:before { content: "\ea3a"; } .zwicon-book:before { content: "\ea3b"; } .zwicon-bookmark:before { content: "\ea3c"; } .zwicon-briefcase:before { content: "\ea3d"; } .zwicon-calendar-day:before { content: "\ea3e"; } .zwicon-calendar-month:before { content: "\ea3f"; } .zwicon-calendar-never:before { content: "\ea40"; } .zwicon-calendar-week:before { content: "\ea41"; } .zwicon-calendar:before { content: "\ea42"; } .zwicon-call-in:before { content: "\ea43"; } .zwicon-call-out:before { content: "\ea44"; } .zwicon-chat:before { content: "\ea45"; } .zwicon-checkmark-circle:before { content: "\ea46"; } .zwicon-checkmark-square:before { content: "\ea47"; } .zwicon-checkmark:before { content: "\ea48"; } .zwicon-clock:before { content: "\ea49"; } .zwicon-close-circle:before { content: "\ea4a"; } .zwicon-close-square:before { content: "\ea4b"; } .zwicon-close:before { content: "\ea4c"; } .zwicon-cog:before { content: "\ea4d"; } .zwicon-comment:before { content: "\ea4e"; } .zwicon-compass:before { content: "\ea4f"; } .zwicon-delete:before { content: "\ea50"; } .zwicon-download:before { content: "\ea51"; } .zwicon-earth-alt:before { content: "\ea52"; } .zwicon-earth:before { content: "\ea53"; } .zwicon-exclamation-triangle:before { content: "\ea54"; } .zwicon-exclamation-mark:before { content: "\ea2f"; } .zwicon-export:before { content: "\ea55"; } .zwicon-eye-slash:before { content: "\ea56"; } .zwicon-eye:before { content: "\ea57"; } .zwicon-face-id:before { content: "\ea58"; } .zwicon-flag:before { content: "\ea59"; } .zwicon-grid:before { content: "\ea5a"; } .zwicon-hamburger-menu:before { content: "\ea5b"; } .zwicon-heart:before { content: "\ea5c"; } .zwicon-home:before { content: "\ea5d"; } .zwicon-import:before { content: "\ea5e"; } .zwicon-info-circle:before { content: "\ea5f"; } .zwicon-lifebelt:before { content: "\ea60"; } .zwicon-link:before { content: "\ea61"; } .zwicon-lock-alt:before { content: "\ea62"; } .zwicon-lock:before { content: "\ea63"; } .zwicon-mail:before { content: "\ea64"; } .zwicon-map-marker:before { content: "\ea65"; } .zwicon-minus-circle:before { content: "\ea66"; } .zwicon-minus-square:before { content: "\ea67"; } .zwicon-minus:before { content: "\ea68"; } .zwicon-more-h:before { content: "\ea69"; } .zwicon-more-v:before { content: "\ea6a"; } .zwicon-my-location:before { content: "\ea6b"; } .zwicon-password:before { content: "\ea6c"; } .zwicon-phone:before { content: "\ea6d"; } .zwicon-pin:before { content: "\ea6e"; } .zwicon-plus-circle:before { content: "\ea6f"; } .zwicon-plus-square:before { content: "\ea70"; } .zwicon-plus:before { content: "\ea71"; } .zwicon-search:before { content: "\ea72"; } .zwicon-send:before { content: "\ea73"; } .zwicon-share:before { content: "\ea74"; } .zwicon-shortcut:before { content: "\ea75"; } .zwicon-sign-in:before { content: "\ea76"; } .zwicon-sign-out:before { content: "\ea77"; } .zwicon-thumbs-down:before { content: "\ea78"; } .zwicon-thumbs-up:before { content: "\ea79"; } .zwicon-trash:before { content: "\ea7a"; } .zwicon-unlink:before { content: "\ea7b"; } .zwicon-upload:before { content: "\ea7c"; } .zwicon-user-circle:before { content: "\ea7d"; } .zwicon-user-delete:before { content: "\ea7e"; } .zwicon-user-follow:before { content: "\ea7f"; } .zwicon-user-minus:before { content: "\ea80"; } .zwicon-user-plus:before { content: "\ea81"; } .zwicon-user:before { content: "\ea82"; } .zwicon-users:before { content: "\ea83"; } .zwicon-history:before { content: "\ea84"; } .zwicon-task:before { content: "\ea85"; } .zwicon-bottom-bar:before { content: "\ea86"; } .zwicon-content-left:before { content: "\ea87"; } .zwicon-content-right:before { content: "\ea88"; } .zwicon-desktop-1:before { content: "\ea89"; } .zwicon-desktop-2:before { content: "\ea8a"; } .zwicon-desktop-3:before { content: "\ea8b"; } .zwicon-half-h:before { content: "\ea8c"; } .zwicon-half-v:before { content: "\ea8d"; } .zwicon-layout-1:before { content: "\ea8e"; } .zwicon-layout-2:before { content: "\ea8f"; } .zwicon-layout-3:before { content: "\ea90"; } .zwicon-layout-4:before { content: "\ea91"; } .zwicon-layout-5:before { content: "\ea92"; } .zwicon-left-bar:before { content: "\ea93"; } .zwicon-margin:before { content: "\ea94"; } .zwicon-right-bar:before { content: "\ea95"; } .zwicon-sidebar:before { content: "\ea96"; } .zwicon-three-h:before { content: "\ea97"; } .zwicon-three-v:before { content: "\ea98"; } .zwicon-to-bottom:before { content: "\ea99"; } .zwicon-to-left:before { content: "\ea9a"; } .zwicon-to-right:before { content: "\ea9b"; } .zwicon-to-top:before { content: "\ea9c"; } .zwicon-top-bar:before { content: "\ea9d"; } .zwicon-airplay:before { content: "\ea9e"; } .zwicon-broadcast:before { content: "\ea9f"; } .zwicon-camera-alt:before { content: "\eaa0"; } .zwicon-camera-alt2:before { content: "\eaa1"; } .zwicon-camera:before { content: "\eaa2"; } .zwicon-cast:before { content: "\eaa3"; } .zwicon-collapse-wide:before { content: "\eaa4"; } .zwicon-collapse1:before { content: "\eaa5"; } .zwicon-disk:before { content: "\eaa6"; } .zwicon-expand-wide:before { content: "\eaa7"; } .zwicon-expand1:before { content: "\eaa8"; } .zwicon-film-alt:before { content: "\eaa9"; } .zwicon-film-play:before { content: "\eaaa"; } .zwicon-film:before { content: "\eaab"; } .zwicon-image-circle:before { content: "\eaac"; } .zwicon-image-gallery:before { content: "\eaad"; } .zwicon-image-wide:before { content: "\eaae"; } .zwicon-image:before { content: "\eaaf"; } .zwicon-microphone-mute:before { content: "\eab0"; } .zwicon-microphone:before { content: "\eab1"; } .zwicon-next-alt:before { content: "\eab2"; } .zwicon-next:before { content: "\eab3"; } .zwicon-panorama-h:before { content: "\eab4"; } .zwicon-pause-alt:before { content: "\eab5"; } .zwicon-pause:before { content: "\eab6"; } .zwicon-play-alt:before { content: "\eab7"; } .zwicon-play:before { content: "\eab8"; } .zwicon-previous-alt:before { content: "\eab9"; } .zwicon-previous:before { content: "\eaba"; } .zwicon-shuffle:before { content: "\eabb"; } .zwicon-video-alt:before { content: "\eabc"; } .zwicon-video-camera:before { content: "\eabd"; } .zwicon-video:before { content: "\eabe"; } .zwicon-volume-low:before { content: "\eabf"; } .zwicon-volume-max:before { content: "\eac0"; } .zwicon-volume-mid:before { content: "\eac1"; } .zwicon-volume-min:before { content: "\eac2"; } .zwicon-wide-angle:before { content: "\eac3"; } .zwicon-exclude:before { content: "\eac4"; } .zwicon-flatten:before { content: "\eac5"; } .zwicon-intersect:before { content: "\eac6"; } .zwicon-substract-back:before { content: "\eac7"; } .zwicon-substract-front:before { content: "\eac8"; } .zwicon-unite:before { content: "\eac9"; } .zwicon-height:before { content: "\eaca"; } .zwicon-resize-alt:before { content: "\eacb"; } .zwicon-resize:before { content: "\eacc"; } .zwicon-scale-down:before { content: "\eacd"; } .zwicon-scale-up:before { content: "\eace"; } .zwicon-scale:before { content: "\eacf"; } .zwicon-width:before { content: "\ead0"; } .zwicon-rotate-axis-x:before { content: "\ead1"; } .zwicon-rotate-axis-xy:before { content: "\ead2"; } .zwicon-rotate-axis-y:before { content: "\ead3"; } .zwicon-rotate-left:before { content: "\ead4"; } .zwicon-rotate-right:before { content: "\ead5"; } .zwicon-rotate-shape:before { content: "\ead6"; } .zwicon-cursor-square:before { content: "\ead7"; } .zwicon-cursor:before { content: "\ead8"; } .zwicon-select-cursor:before { content: "\ead9"; } .zwicon-select:before { content: "\eada"; } .zwicon-shape-circle:before { content: "\eadb"; } .zwicon-shape-cone:before { content: "\eadc"; } .zwicon-shape-cube:before { content: "\eadd"; } .zwicon-shape-cylinder:before { content: "\eade"; } .zwicon-shape-octagonal:before { content: "\eadf"; } .zwicon-shape-polygon:before { content: "\eae0"; } .zwicon-shape-sphere:before { content: "\eae1"; } .zwicon-shape-square:before { content: "\eae2"; } .zwicon-laugh:before { content: "\eae3"; } .zwicon-neutral:before { content: "\eae4"; } .zwicon-sad:before { content: "\eae5"; } .zwicon-smile:before { content: "\eae6"; } .zwicon-bold:before { content: "\eae7"; } .zwicon-draw-text-field:before { content: "\eae8"; } .zwicon-font-height:before { content: "\eae9"; } .zwicon-font-size:before { content: "\eaea"; } .zwicon-font-width:before { content: "\eaeb"; } .zwicon-font:before { content: "\eaec"; } .zwicon-heading:before { content: "\eaed"; } .zwicon-indent-left-alt:before { content: "\eaee"; } .zwicon-indent-left:before { content: "\eaef"; } .zwicon-indent-right-alt:before { content: "\eaf0"; } .zwicon-indent-right:before { content: "\eaf1"; } .zwicon-italic:before { content: "\eaf2"; } .zwicon-list-bullet:before { content: "\eaf3"; } .zwicon-list-number:before { content: "\eaf4"; } .zwicon-outdent-left:before { content: "\eaf5"; } .zwicon-outdent-right:before { content: "\eaf6"; } .zwicon-paragraph:before { content: "\eaf7"; } .zwicon-text-align-center:before { content: "\eaf8"; } .zwicon-text-align-justify:before { content: "\eaf9"; } .zwicon-text-align-left:before { content: "\eafa"; } .zwicon-text-align-right:before { content: "\eafb"; } .zwicon-text-cursor:before { content: "\eafc"; } .zwicon-text-decoration:before { content: "\eafd"; } .zwicon-text-field:before { content: "\eafe"; } .zwicon-text:before { content: "\eaff"; } .zwicon-underline:before { content: "\eb00"; } .zwicon-wrap-img-left:before { content: "\eb01"; } .zwicon-wrap-img-right:before { content: "\eb02"; } .zwicon-wrap-left:before { content: "\eb03"; } .zwicon-wrap-right:before { content: "\eb04"; } .zwicon-transform-left:before { content: "\eb05"; } .zwicon-transform-right:before { content: "\eb06"; } .zwicon-ab-testing:before { content: "\eb07"; } .zwicon-agile:before { content: "\eb08"; } .zwicon-backlog:before { content: "\eb09"; } .zwicon-design-studio:before { content: "\eb0a"; } .zwicon-design-validation:before { content: "\eb0b"; } .zwicon-information-architecture:before { content: "\eb0c"; } .zwicon-interview:before { content: "\eb0d"; } .zwicon-kanban-board:before { content: "\eb0e"; } .zwicon-lego-serious-play:before { content: "\eb0f"; } .zwicon-paper-prototype:before { content: "\eb10"; } .zwicon-persona:before { content: "\eb11"; } .zwicon-prototype-mobile:before { content: "\eb12"; } .zwicon-prototype:before { content: "\eb13"; } .zwicon-responsive:before { content: "\eb14"; } .zwicon-screen-flow:before { content: "\eb15"; } .zwicon-stand-up:before { content: "\eb16"; } .zwicon-sticky-notes1:before { content: "\eb17"; } .zwicon-usability:before { content: "\eb18"; } .zwicon-user-flow:before { content: "\eb19"; } .zwicon-user-interview:before { content: "\eb1a"; } .zwicon-user-journey:before { content: "\eb1b"; } .zwicon-cloud:before { content: "\eb1c"; } .zwicon-cloudy-day:before { content: "\eb1d"; } .zwicon-cloudy-night:before { content: "\eb1e"; } .zwicon-heavy-rain-day:before { content: "\eb1f"; } .zwicon-heavy-rain-night:before { content: "\eb20"; } .zwicon-heavy-rain:before { content: "\eb21"; } .zwicon-heavy-wind:before { content: "\eb22"; } .zwicon-mild-rain-day:before { content: "\eb23"; } .zwicon-mild-rain-night:before { content: "\eb24"; } .zwicon-mild-rain:before { content: "\eb25"; } .zwicon-moon:before { content: "\eb26"; } .zwicon-rain-day:before { content: "\eb27"; } .zwicon-rain-night:before { content: "\eb28"; } .zwicon-rain:before { content: "\eb29"; } .zwicon-snow-day:before { content: "\eb2a"; } .zwicon-snow-night:before { content: "\eb2b"; } .zwicon-snow:before { content: "\eb2c"; } .zwicon-storm-day:before { content: "\eb2d"; } .zwicon-storm-night:before { content: "\eb2e"; } .zwicon-storm:before { content: "\eb2f"; } .zwicon-sun:before { content: "\eb30"; } .zwicon-temperature:before { content: "\eb31"; } .zwicon-wind-alt:before { content: "\eb32"; } .zwicon-wind-cloudy-day:before { content: "\eb33"; } .zwicon-wind-cloudy-night:before { content: "\eb34"; } .zwicon-wind-cloudy:before { content: "\eb35"; } .zwicon-wind:before { content: "\eb36"; } ================================================ FILE: src/background.ts ================================================ import { app, protocol, BrowserWindow, Menu, shell, } from 'electron' import { createProtocol, } from 'vue-cli-plugin-electron-builder/lib' import { autoUpdater } from 'electron-updater' import { init } from '@sentry/electron/dist/main' import App from './server/app' import messages from './assets/locales-menu' import initServer from './server' init({ dsn: 'https://6a6dacc57a6a4e27a88eb31596c152f8@sentry.io/1887150' }) const isDevelopment = process.env.NODE_ENV !== 'production' // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let win: any let menu: Menu let httpServer: any // Standard scheme must be registered before the app is ready protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true } }]) function createWindow() { // Create the browser window. const winOption: any = { width: 1200, height: 800, minHeight: 642, minWidth: 1000, webPreferences: { webSecurity: false, // FIXED: Not allowed to load local resource nodeIntegration: true, enableRemoteModule: true, // FIXED: 兼容 electron@11.0.1 }, // frame: false, // 去除默认窗口栏 titleBarStyle: 'hiddenInset' as ('hidden' | 'default' | 'hiddenInset' | 'customButtonsOnHover' | undefined), } if (process.platform !== 'darwin') { winOption.icon = `${__dirname}/app-icons/gridea.png` } win = new BrowserWindow(winOption) win.setTitle('Gridea') if (process.env.WEBPACK_DEV_SERVER_URL) { // Load the url of the dev server if in development mode win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string) if (!process.env.IS_TEST) { win.webContents.openDevTools() } } else { createProtocol('app') // Load the index.html when not in development win.loadURL('app://./index.html') autoUpdater.checkForUpdatesAndNotify() } win.on('closed', () => { win = null }) const locale: string = app.getLocale() || 'zh-CN' const menuLabels = messages[locale] || messages['zh-CN'] // menu const template: any = [ { label: menuLabels.edit, submenu: [ { label: menuLabels.save, accelerator: 'CmdOrCtrl+S', click: () => { win.webContents.send('click-menu-save') }, }, { type: 'separator' }, { role: 'undo', label: menuLabels.undo }, { role: 'redo', label: menuLabels.redo }, { type: 'separator' }, { role: 'cut', label: menuLabels.cut }, { role: 'copy', label: menuLabels.copy }, { role: 'paste', label: menuLabels.paste }, { role: 'delete', label: menuLabels.delete }, { role: 'selectall', label: menuLabels.selectall }, { role: 'toggledevtools', label: menuLabels.toggledevtools }, { type: 'separator' }, { role: 'close', label: menuLabels.close }, { role: 'quit', label: menuLabels.quit }, ], }, { role: 'windowMenu', }, { role: menuLabels.help, submenu: [ { label: 'Learn More', click() { shell.openExternal('https://github.com/getgridea/gridea') }, }, ], }, ] menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) const s = initServer() httpServer = s.server const setting = { mainWindow: win, app, baseDir: __dirname, previewServer: s.app, } // Init app const appInstance = new App(setting) console.log('Main process runing...', appInstance.appDir) // DELETE ME } // Quit when all windows are closed. app.on('window-all-closed', () => { httpServer && httpServer.close() // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (win === null) { createWindow() } }) // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on('ready', async () => { // if (isDevelopment && !process.env.IS_TEST) { // // Install Vue Devtools // await installVueDevtools() // } createWindow() }) // Exit cleanly on request from parent process in development mode. if (isDevelopment) { if (process.platform === 'win32') { process.on('message', (data) => { if (data === 'graceful-exit') { app.quit() } }) } else { process.on('SIGTERM', () => { app.quit() }) } } // ipcMain.on('min-window', () => { // if (win) { // win.minimize() // } // }) // ipcMain.on('max-window', () => { // if (win) { // if (win.isMaximized()) { // win.unmaximize() // } else { // win.maximize() // } // } // }) // ipcMain.on('close-window', () => { // if (win) { // win.close() // } // }) /** * Auto Updater * * Uncomment the following code below and install `electron-updater` to * support auto updating. Code Signing with a valid certificate is required. * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating */ /* import { autoUpdater } from 'electron-updater' autoUpdater.on('update-downloaded', () => { autoUpdater.quitAndInstall() }) app.on('ready', () => { if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates() }) */ ================================================ FILE: src/components/AppSystem/Index.vue ================================================ ================================================ FILE: src/components/AppSystem/includes/LanguageSetting.vue ================================================ ================================================ FILE: src/components/AppSystem/includes/SourceFolderSetting.vue ================================================ ================================================ FILE: src/components/AppSystem/includes/Version.vue ================================================ ================================================ FILE: src/components/ColorCard/Index.vue ================================================ ================================================ FILE: src/components/EmojiCard/Index.vue ================================================ ================================================ FILE: src/components/FooterBox/Index.vue ================================================ ================================================ FILE: src/components/Main.vue ================================================ ================================================ FILE: src/components/MonacoMarkdownEditor/Index.vue ================================================ ================================================ FILE: src/components/MonacoMarkdownEditor/theme.js ================================================ export default { 'base': 'vs', 'inherit': true, 'rules': [ { 'foreground': '999999', 'token': 'comment', }, { 'foreground': 'e88501', 'token': 'string', }, { 'foreground': '999999', 'token': 'string.link', }, { 'foreground': '999999', 'token': 'variable.source', }, { 'foreground': '4C51BF', 'token': 'variable', }, { 'foreground': '2B6CB0', 'token': 'markup.list', }, { 'foreground': '2B6CB0', 'token': 'markup.underline.link', }, { 'foreground': '46a609', 'token': 'constant.numeric', }, { 'foreground': '39946a', 'token': 'constant.language', }, { 'foreground': 'b7791f', 'token': 'keyword', }, { 'fontStyle': 'bold', 'token': 'markup.heading', }, { 'fontStyle': 'bold', 'token': 'markup.bold', }, { 'fontStyle': 'italic', 'token': 'markup.italic', }, // ie bold/italic/heading/list marks { 'foreground': '999999', 'token': 'punctuation.definition.constant.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.bold.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.italic.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.heading.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.heading.begin.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.heading.end.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.heading.setext.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.list_item.markdown', }, { 'foreground': '999999', 'token': 'markup.list.numbered.bullet.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.bold.begin.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.bold.end.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.italic.begin.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.italic.end.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.variable.begin.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.variable.end.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.link.begin.markdown', }, { 'foreground': '999999', 'token': 'punctuation.definition.link.end.markdown', }, { 'foreground': 'b7791f', 'token': 'support.constant.property-value', }, { 'foreground': 'b7791f', 'token': 'constant.other.color', }, { 'foreground': '96dc5f', 'token': 'keyword.other.unit', }, { 'foreground': '484848', 'token': 'keyword.operator', }, { 'foreground': 'c52727', 'token': 'storage', }, { 'foreground': '858585', 'token': 'entity.other.inherited-class', }, { 'foreground': '606060', 'token': 'entity.name.tag', }, { 'foreground': 'bf78cc', 'token': 'constant.character.entity', }, { 'foreground': 'bf78cc', 'token': 'support.class.js', }, { 'foreground': '606060', 'token': 'entity.other.attribute-name', }, { 'foreground': 'c52727', 'token': 'meta.selector.css', }, { 'foreground': 'c52727', 'token': 'entity.name.tag.css', }, { 'foreground': 'c52727', 'token': 'entity.other.attribute-name.id.css', }, { 'foreground': 'c52727', 'token': 'entity.other.attribute-name.class.css', }, { 'foreground': '484848', 'token': 'meta.property-name.css', }, { 'foreground': 'c52727', 'token': 'support.function', }, { 'background': 'ff002a', 'token': 'invalid', }, { 'foreground': 'c52727', 'token': 'punctuation.section.embedded', }, { 'foreground': '606060', 'token': 'punctuation.definition.tag', }, { 'foreground': 'bf78cc', 'token': 'constant.other.color.rgb-value.css', }, { 'foreground': 'bf78cc', 'token': 'support.constant.property-value.css', }, ], 'colors': { 'editor.foreground': '#333333', 'editor.background': '#FFFFFF', 'editor.selectionBackground': '#BDD5FC', 'editor.lineHighlightBackground': '#FFFBD1', 'editorCursor.foreground': '#000000', 'editorWhitespace.foreground': '#BFBFBF', 'textLink.foreground': '#666', }, } ================================================ FILE: src/components/PostsCard/Index.vue ================================================ ================================================ FILE: src/helpers/analytics.ts ================================================ import GA from 'electron-google-analytics' import macaddress from 'macaddress' import * as pkg from '../../package.json' const isDevelopment = process.env.NODE_ENV !== 'production' interface EvOptions { evLabel?: any evValue?: any } const hostname = 'http://client.gridea.dev' class Analytics { private readonly ga: any private clientId: any constructor() { this.ga = new GA('UA-113307620-4', { debug: isDevelopment }) this.ga.set('version', (pkg as any).version) } public getClientId(callback: any) { if (this.clientId) { callback(this.clientId) } macaddress.one((err: any, mac: string) => { this.clientId = mac callback(mac) }) } public async pageView(url: string, title?: string) { this.getClientId(async (clientId: any) => { try { await this.ga.pageview(hostname, url, title, 1, clientId) } catch (e) { console.error(e) } }) } public async event(evCategory: string, evAction: string, options: EvOptions) { this.getClientId(async (clientId: any) => { try { await this.ga.event(evCategory, evAction, { ...options, clientID: clientId, }) } catch (e) { console.error(e) } }) } public async exception(exDesc: string, exFatal: any) { this.getClientId(async (clientId: any) => { try { await this.ga.exception(exDesc, exFatal) } catch (e) { console.error(e) } }) } } export default new Analytics() ================================================ FILE: src/helpers/constants.ts ================================================ export const UrlFormats = [ { text: 'Slug', value: 'SLUG', }, { text: 'Short ID', value: 'SHORT_ID', }, ] export const DEFAULT_POST_PAGE_SIZE = 10 export const DEFAULT_ARCHIVES_PAGE_SIZE = 50 export const DEFAULT_FEED_COUNT = 10 export const DEFAULT_ARCHIVES_PATH = 'archives' export const DEFAULT_POST_PATH = 'post' export const DEFAULT_TAG_PATH = 'tag' ================================================ FILE: src/helpers/content-helper.ts ================================================ export default class ContentHelper { localReg: RegExp domainReg: RegExp featureDomainReg: RegExp featureLocalReg: RegExp constructor() { this.localReg = /\(file.*\/post-images\//g this.domainReg = /\(.*\/post-images\//g this.featureDomainReg = /\.*\/post-images\//g this.featureLocalReg = /file.*\/post-images\//g } /** * 将文章中本地图片路径,变更为线上路径 * @param content 内容 * @param domainPath 线上路径 */ changeImageUrlLocalToDomain(content: string, domainPath: string) { domainPath = domainPath.replace(/\\/g, '/') return content.replace(this.localReg, `(${domainPath}/post-images/`) } /** * 将文章中线上图片路径,变更为本地路径 * @param content 内容 * @param localPath 本地路径 */ changeImageUrlDomainToLocal(content: string, localPath: string) { localPath = localPath.replace(/\\/g, '/') return content.replace(this.domainReg, `(file://${localPath}/post-images/`) } /** * 将 feature 图片路径,变更为本地路径 */ changeFeatureImageUrlDomainToLocal(content: string, localPath: string) { return content.replace(this.featureDomainReg, `file://${localPath}/post-images/`) } /** * 将 feature 本地图片路径,变更为线上路径 */ changeFeatureImageUrlLocalToDomain(content: string, domainPath: string) { let url = content.replace(this.featureLocalReg, `${domainPath}/post-images/`) url = url.replace(/\\/g, '/') return url } } ================================================ FILE: src/helpers/enums.ts ================================================ export enum MenuTypes { Internal = 'Internal', External = 'External', } export enum UrlFormats { Slug = 'SLUG', ShortId = 'SHORT_ID', } ================================================ FILE: src/helpers/shortcut-keys.ts ================================================ export default [ { name: '编辑', list: [ { title: '保存文章', keyboard: ['⌘', 'S'], }, { title: '剪切', keyboard: ['⌘', 'X'], }, { title: '复制', keyboard: ['⌘', 'C'], }, { title: '粘贴', keyboard: ['⌘', 'V'], }, ], }, { name: 'Markdown', list: [ { title: '标题降级 (# -)', keyboard: ['⌃', '⇧', '['], }, { title: '标题升级 (# +)', keyboard: ['⌃', '⇧', ']'], }, { title: '加粗 (**)', keyboard: ['⌘', 'B'], }, { title: '行内 Code(`)', keyboard: ['⌘', '`'], }, { title: '斜体 (*)', keyboard: ['⌘', 'I'], }, { title: '列表 (-)', keyboard: ['⌘', 'L'], }, { title: 'LaTeX ($)', keyboard: ['⌘', 'M'], }, { title: 'LaTeX ($$)', keyboard: ['⇧', '⌘', 'M'], }, { title: '删除线 (~~)', keyboard: ['⌥', 'S'], }, ], }, { name: '其他', list: [ { title: '格式化文档', keyboard: ['⇧', '⌥', 'F'], }, ], }, ] ================================================ FILE: src/helpers/slug.ts ================================================ /* tslint:disable */ const { transliterate } = require('transliteration') const slug = require('slug') /* * Custom mode of rfc3986 without unicode symbols */ slug.defaults.modes['rfc3986-non-unicode'] = { replacement: '-', // replace spaces with replacement symbols: false, // replace unicode symbols or not remove: /[.]/g, // (optional) regex to remove characters lower: true, // result in lower case charmap: slug.charmap, // replace special characters multicharmap: slug.multicharmap, // replace multi-characters } slug.defaults.modes['rfc3986-non-unicode-with-dots'] = { replacement: '-', // replace spaces with replacement symbols: false, // replace unicode symbols or not lower: true, // result in lower case charmap: slug.charmap, // replace special characters multicharmap: slug.multicharmap, // replace multi-characters } slug.defaults.modes['rfc3986-non-unicode-with-dots-no-lower'] = { replacement: '-', // replace spaces with replacement symbols: false, // replace unicode symbols or not lower: false, // result in lower case charmap: slug.charmap, // replace special characters multicharmap: slug.multicharmap, // replace multi-characters } slug.defaults.mode = 'rfc3986-non-unicode' /** * Slugify 文本 * @param textToSlugify 待 slugify 的文本 * @param filenameMode * @param saveLowerChars */ function createSlug(textToSlugify: any, filenameMode = false, saveLowerChars = false) { textToSlugify = transliterate(textToSlugify) if (!filenameMode) { if (saveLowerChars) { slug.defaults.mode = 'rfc3986-non-unicode-with-dots-no-lower' } textToSlugify = slug(textToSlugify) slug.defaults.mode = 'rfc3986-non-unicode' } else { slug.defaults.mode = 'rfc3986-non-unicode-with-dots' textToSlugify = slug(textToSlugify) slug.defaults.mode = 'rfc3986-non-unicode' } return textToSlugify } export default createSlug ================================================ FILE: src/helpers/utils.ts ================================================ import markdown from '../server/plugins/markdown' /** * Add single-quoted to string type field, in order to be compatible with many special characters * eg. true, false, 1, [, ], {, }, ,, #, <, >, @, */ export function formatYamlString(string: any) { return string.replace(/'/g, '\'\'') } export const formatThemeCustomConfigToRender = (config: any, currentThemeConfig: any) => { for (const configItem of currentThemeConfig) { const configValue = config[configItem.name] if (configItem.type === 'markdown') { if (!configValue) continue config[configItem.name] = markdown.render(configValue) } else if (configItem.type === 'array' && configValue) { for (let arrItemIndex = 0; arrItemIndex < configValue.length; arrItemIndex += 1) { const foundConfigItem = currentThemeConfig.find((i: any) => i.name === configItem.name) const arrayItemKeys = Object.keys(configValue[arrItemIndex]) for (let keyIndex = 0; keyIndex < arrayItemKeys.length; keyIndex += 1) { const key = arrayItemKeys[keyIndex] const foundMarkdownField = foundConfigItem.arrayItems.find((i: any) => i.name === key && i.type === 'markdown') if (foundMarkdownField) { const fieldValue = configValue[arrItemIndex][key] if (!fieldValue) continue configValue[arrItemIndex][key] = markdown.render(fieldValue) } } } } } return config } ================================================ FILE: src/helpers/vee-validate.ts ================================================ import { required } from 'vee-validate/dist/rules' import { extend } from 'vee-validate' extend('required', { ...required, message: '此项是必填项', }) ================================================ FILE: src/helpers/words-count.ts ================================================ import striptags from 'striptags' const CN_PATTERN = /[\u4E00-\u9FA5]/g const EN_PATTERN = /[a-zA-Z0-9_\u0392-\u03c9\u0400-\u04FF]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af\u0400-\u04FF]+|[\u00E4\u00C4\u00E5\u00C5\u00F6\u00D6]+|\w+/g function countContent(content: any): [number, number] { if (typeof content !== 'string') { throw new Error('[word-counter] content must be string type') } let cn = 0 let en = 0 if (content.length > 0) { content = striptags(content) cn = (content.match(CN_PATTERN) || []).length en = (content.replace(CN_PATTERN, '').match(EN_PATTERN) || []).length } return [cn, en] } export function wordCount(content?: any, transformFn?: (count: number) => any): any { const [cn, en] = countContent(content) const count = cn + en if (typeof transformFn === 'function') { return transformFn(count) } return count } interface TimeConfig { cn?: number en?: number } export function timeCalc(content?: any, { cn = 300, en = 160 }: TimeConfig = {}): { minius: number, second: number, } { const [cnCount, enCount] = countContent(content) const minius = cnCount / cn + enCount / en return { minius: minius === 0 ? 0 : Math.ceil(minius), second: Math.floor(minius * 60), } } ================================================ FILE: src/interfaces/menu.ts ================================================ export interface IMenu { name: string openType: string link: string } ================================================ FILE: src/interfaces/post.ts ================================================ export interface IPostData { title: string date: string published: boolean hideInList: boolean tags?: [] feature: string isTop: boolean } export interface IPost { content: string data: IPostData fileName: string } ================================================ FILE: src/interfaces/setting.ts ================================================ export interface ISetting { platform: 'github' | 'coding' | 'sftp' | 'gitee' | 'netlify' domain: string repository: string branch: string username: string email: string tokenUsername: string token: string cname: string port: string server: string password: string privateKey: string remotePath: string proxyPath: string proxyPort: string enabledProxy: 'direct' | 'proxy' netlifyAccessToken: string netlifySiteId: string [index: string]: string } export interface IDisqusSetting { api: string apikey: string shortname: string } export interface IGitalkSetting { clientId: string clientSecret: string repository: string owner: string } export interface ICommentSetting { commentPlatform: string showComment: boolean disqusSetting: IDisqusSetting gitalkSetting: IGitalkSetting } ================================================ FILE: src/interfaces/snackbar.ts ================================================ export default interface ISnackbar { /** * 通知颜色:success, info, error 或其他颜色 * default: success */ color?: string snackbar?: boolean message?: string bottom?: boolean } ================================================ FILE: src/interfaces/tag.ts ================================================ export interface ITag { name: string used: boolean slug?: string } ================================================ FILE: src/interfaces/theme.ts ================================================ export interface ITheme { themeName: string postPageSize: number archivesPageSize: number siteName: string siteDescription: string footerInfo: string showFeatureImage: boolean postUrlFormat: string tagUrlFormat: string dateFormat: string feedFullText: boolean feedCount: number archivesPath: string postPath: string tagPath: string } ================================================ FILE: src/main.ts ================================================ import Vue from 'vue' import moment from 'moment' import Antd from 'ant-design-vue' import 'remixicon/fonts/remixicon.css' import 'katex/dist/katex.min.css' import '@fontsource/noto-serif/index.css' import '@/assets/styles/tailwind.css' import '@/assets/styles/main.less' import VueI18n from 'vue-i18n' import Prism from 'prismjs' import VueShortkey from 'vue-shortkey' import { remote } from 'electron' import * as Sentry from '@sentry/electron' import locale from './assets/locales' import App from './App.vue' import router from './router' import store from './store/index' import VueBus from './vue-bus' import ga from './helpers/analytics' import './helpers/vee-validate' ga.event('Client', 'show', { evLabel: 'startup', }) Sentry.init({ dsn: 'https://6a6dacc57a6a4e27a88eb31596c152f8@sentry.io/1887150' }) const defaultLocale = ({ 'zh-CN': 'zhHans', 'zh-TW': 'zh_TW', 'en-US': 'en', } as any)[remote.app.getLocale() || 'zh-CN'] Vue.use(VueI18n) const i18n = new VueI18n({ locale: localStorage.getItem('language') || defaultLocale, messages: locale as any, silentTranslationWarn: true, }) Prism.highlightAll() Vue.use(Antd) Vue.use(VueBus) Vue.use(VueShortkey) Vue.prototype.$moment = moment Vue.config.productionTip = false new Vue({ router, store, i18n, render: h => h(App), mounted() { router.push('/') }, }).$mount('#app') ================================================ FILE: src/router.ts ================================================ import Vue from 'vue' import Router from 'vue-router' import macaddress from 'macaddress' import Main from './components/Main.vue' // import ArticleUpdate from './views/article/ArticleUpdate.vue' import Articles from './views/article/Articles.vue' import Menu from './views/menu/Index.vue' import Tags from './views/tags/Index.vue' import Theme from './views/theme/Index.vue' import Setting from './views/setting/Index.vue' import Loading from './views/loading/Index.vue' import ga from './helpers/analytics' Vue.use(Router) const router = new Router({ base: process.env.BASE_URL, routes: [ { path: '/', name: 'main', component: Main, children: [ { path: '/articles', name: 'articles', component: Articles, }, { path: '/menu', name: 'menu', component: Menu, }, { path: '/tags', name: 'tags', component: Tags, }, { path: '/theme', name: 'theme', component: Theme, }, { path: '/setting', name: 'setting', component: Setting, }, { path: '/loading', name: 'loading', component: Loading, }, { path: '*', redirect: '/articles', }, ], }, ], }) router.afterEach((to, from) => { ga.pageView(to.fullPath, to.name) }) export default router ================================================ FILE: src/server/app.ts ================================================ import { BrowserWindow, app } from 'electron' import * as fse from 'fs-extra' import * as path from 'path' import express from 'express' import EventClasses from './events/index' import Posts from './posts' import Tags from './tags' import Menus from './menus' import Theme from './theme' import Renderer from './renderer' import Setting from './setting' import Deploy from './deploy' import { IApplicationDb, IApplicationSetting } from './interfaces/application' import { DEFAULT_POST_PAGE_SIZE, DEFAULT_ARCHIVES_PAGE_SIZE, DEFAULT_FEED_COUNT, DEFAULT_ARCHIVES_PATH, DEFAULT_POST_PATH, DEFAULT_TAG_PATH, } from '../helpers/constants' // eslint-disable-next-line declare const __static: string export default class App { mainWindow: BrowserWindow app: any baseDir: string appDir: string previewServer: any db: IApplicationDb buildDir: string constructor(setting: IApplicationSetting) { this.mainWindow = setting.mainWindow this.app = setting.app this.baseDir = setting.baseDir this.appDir = path.join(this.app.getPath('documents'), 'gridea') this.previewServer = setting.previewServer this.buildDir = path.join(this.app.getPath('home'), '.gridea', 'output') this.db = { posts: [], tags: [], menus: [], themeConfig: { themeName: '', postPageSize: DEFAULT_POST_PAGE_SIZE, archivesPageSize: DEFAULT_ARCHIVES_PAGE_SIZE, siteName: '', siteDescription: '', footerInfo: 'Powered by Gridea', showFeatureImage: true, domain: '', postUrlFormat: 'SLUG', tagUrlFormat: 'SLUG', dateFormat: 'YYYY-MM-DD', feedFullText: true, feedCount: DEFAULT_FEED_COUNT, archivesPath: DEFAULT_ARCHIVES_PATH, postPath: DEFAULT_POST_PATH, tagPath: DEFAULT_TAG_PATH, }, themeCustomConfig: {}, currentThemeConfig: [], themes: [], setting: { platform: 'github', domain: '', repository: '', branch: '', username: '', email: '', tokenUsername: '', token: '', cname: '', port: '22', server: '', password: '', privateKey: '', remotePath: '', proxyPath: '', proxyPort: '', enabledProxy: 'direct', netlifySiteId: '', netlifyAccessToken: '', }, commentSetting: { showComment: false, commentPlatform: 'gitalk', gitalkSetting: { clientId: '', clientSecret: '', repository: '', owner: '', }, disqusSetting: { api: '', apikey: '', shortname: '', }, }, } this.checkDir() } /** * Load site config and data */ public async loadSite() { const postsInstance = new Posts(this) const posts = await postsInstance.list() const tagsInstance = new Tags(this) const tags = await tagsInstance.list() const menusInstance = new Menus(this) const menus = await menusInstance.list() const themeInstance = new Theme(this) const themeConfig = await themeInstance.getThemeConfig() const themes = await themeInstance.getThemeList() const themeCustomConfig = await themeInstance.getThemeCustomConfig() const currentThemeConfig = await themeInstance.getCurrentThemeCustomConfig() const settingInstance = new Setting(this) const setting = await settingInstance.getSetting() const commentSetting = await settingInstance.getCommentSetting() const deployInstance = new Deploy(this) this.db = { posts, tags, menus, themeConfig: Object.assign(this.db.themeConfig, themeConfig), themeCustomConfig, currentThemeConfig, themes, setting, commentSetting: commentSetting || this.db.commentSetting, } this.updateStaticServer() this.initEvents() return { ...this.db, currentThemeConfig, appDir: this.appDir, mainWindow: this.mainWindow, } } public renderHtml() { const renderer = new Renderer(this) console.log(renderer) // renderer.renderPostList() } public async saveSourceFolderSetting(sourceFolderPath: string = '') { try { const appConfigFolder = path.join(this.app.getPath('home'), '.gridea') const appConfigPath = path.join(appConfigFolder, 'config.json') const jsonString = `{"sourceFolder": "${sourceFolderPath || this.appDir}"}` fse.writeFileSync(appConfigPath, jsonString) const appConfig = fse.readJsonSync(appConfigPath) this.appDir = appConfig.sourceFolder this.updateStaticServer() this.checkDir() return true } catch (e) { console.log(e) return false } } /** * Check if the hve-next folder exists, if it does not exist, it is initialized */ private async checkDir() { // Check if there is a .hve-notes folder, if it exists, load it, otherwise use the default configuration. const appConfigFolderOld = path.join(this.app.getPath('home'), '.hve-notes') // < 0.7.7 const appConfigFolder = path.join(this.app.getPath('home'), '.gridea') const appConfigPath = path.join(appConfigFolder, 'config.json') let defaultAppDir = path.join(this.app.getPath('documents'), 'Gridea') defaultAppDir = defaultAppDir.replace(/\\/g, '/') try { // if exist `.hve-notes` config folder, change folder name to `.gridea` try { if (!fse.pathExistsSync(appConfigFolder) && fse.pathExistsSync(appConfigFolderOld)) { fse.renameSync(appConfigFolderOld, appConfigFolder) } } catch (e) { console.log('Rename Error: ', e) } if (!fse.pathExistsSync(appConfigFolder)) { fse.mkdirSync(appConfigFolder) const jsonString = `{"sourceFolder": "${defaultAppDir}"}` fse.writeFileSync(appConfigPath, jsonString) } const buildDir = path.join(appConfigFolder, 'output') if (!fse.pathExistsSync(buildDir)) { fse.mkdirSync(buildDir) } const appConfig = fse.readJsonSync(appConfigPath) this.appDir = appConfig.sourceFolder // Site folder exists if (fse.pathExistsSync(this.appDir)) { // Check if the `images`, `config`, 'output', `post-images`, 'posts', 'themes', 'static' folder exists, if it does not exist, copy it from default-files ['images', 'config', 'post-images', 'posts', 'themes', 'static'].forEach((folder: string) => { const folderPath = path.join(this.appDir, folder) if (!fse.pathExistsSync(folderPath)) { fse.copySync( path.join(__static, 'default-files', folder), folderPath, ) } }) // Check default theme folder if includes [notes、fly、simple、paper] themes this.checkTheme('notes') this.checkTheme('fly') this.checkTheme('simple') this.checkTheme('paper') // move output/favicon.ico to Gridea/favicon.ico const outputFavicon = path.join(this.buildDir, 'favicon.ico') const sourceFavicon = path.join(this.appDir, 'favicon.ico') const existFaviconOutput = fse.pathExistsSync(outputFavicon) const existFaviconSource = fse.pathExistsSync(sourceFavicon) if (existFaviconOutput && !existFaviconSource) { fse.moveSync(outputFavicon, sourceFavicon) } return } // Site folder not exists this.appDir = defaultAppDir const jsonString = `{"sourceFolder": "${defaultAppDir}"}` fse.writeFileSync(appConfigPath, jsonString) fse.mkdirSync(this.appDir) fse.copySync( path.join(__static, 'default-files'), path.join(this.appDir), ) } catch (e) { console.log('Error', e) } finally { this.initEvents() } } /** * Check whether the theme is included, and if not, initialize one copy of the theme within the application. */ private checkTheme(themeName: string): void { const themePath = path.join(this.appDir, 'themes', themeName) if (!fse.pathExistsSync(themePath)) { fse.copySync( path.join(__static, 'default-files', 'themes', themeName), themePath, ) } } private updateStaticServer(): void { function removeMiddleware(route: any, i: number, routes: any) { if (route.handle.name === 'serveStatic') { routes.splice(i, 1) console.log('Preview server: Removed old static route') } } const routers = this.previewServer._router // eslint-disable-line no-underscore-dangle if (routers) { const routesStack = routers.stack routesStack.forEach(removeMiddleware) } this.previewServer.use(express.static(`${this.buildDir}`)) console.log(`Preview server: Static dir change to ${this.buildDir}`) } private initEvents(): void { const { SiteEvents, PostEvents, TagEvents, MenuEvents, ThemeEvents, RendererEvents, SettingEvents, DeployEvents, } = EventClasses const site = new SiteEvents(this) const post = new PostEvents(this) const tag = new TagEvents(this) const menu = new MenuEvents(this) const theme = new ThemeEvents(this) const renderer = new RendererEvents(this) const setting = new SettingEvents(this) const deploy = new DeployEvents(this) } } ================================================ FILE: src/server/deploy.ts ================================================ import fs from 'fs' import moment from 'moment' // @ts-ignore import Model from './model' import GitProxy from './plugins/deploys/gitproxy' const git = require('isomorphic-git') export default class Deploy extends Model { outputDir: string = this.buildDir remoteUrl = '' platformAddress = '' http = new GitProxy(this) constructor(appInstance: any) { super(appInstance) const { setting } = this.db this.platformAddress = ({ github: 'github.com', coding: 'e.coding.net', gitee: 'gitee.com', } as any)[setting.platform || 'github'] const preUrl = ({ github: `${setting.username}:${setting.token}`, coding: `${setting.tokenUsername}:${setting.token}`, gitee: `${setting.username}:${setting.token}`, } as any)[setting.platform || 'github'] this.remoteUrl = `https://${preUrl}@${this.platformAddress}/${setting.username}/${setting.repository}.git` } /** * Check whether the remote connection is normal */ async remoteDetect() { const result = { success: true, message: [''], } try { const { setting } = this.db let isRepo = false try { await git.currentBranch({ fs, dir: this.outputDir }) isRepo = true } catch (e) { console.log('Not a repo', e.message) } if (!setting.username || !setting.repository || !setting.token) { return { success: false, message: 'Username、repository、token is required', } } if (!isRepo) { await git.init({ fs, dir: this.outputDir }) await git.setConfig({ fs, dir: this.outputDir, path: 'user.name', value: setting.username, }) await git.setConfig({ fs, dir: this.outputDir, path: 'user.email', value: setting.email, }) } await git.addRemote({ fs, dir: this.outputDir, remote: 'origin', url: this.remoteUrl, force: true, }) const info = await git.getRemoteInfo({ http: this.http, url: this.remoteUrl, }) console.log('info', info) result.message = info.capabilities } catch (e) { console.log('Test Remote Error: ', e) result.success = false result.message = e.message } return result } async publish() { await this.remoteDetect() this.db.themeConfig.domain = this.db.setting.domain let result = { success: true, message: '', localBranchs: {}, } let isRepo = false try { await git.currentBranch({ fs, dir: this.outputDir }) isRepo = true } catch (e) { console.log('Not a repo', e.message) } if (isRepo) { result = await this.commonPush() } else { // result = await this.firstPush() } return result } async commonPush() { console.log('common push') const { setting } = this.db const localBranchs = {} try { const statusSummary = await git.status({ fs, dir: this.outputDir, filepath: '.' }) console.log('statusSummary', statusSummary) await git.addRemote({ fs, dir: this.outputDir, remote: 'origin', url: this.remoteUrl, force: true, }) if (statusSummary !== 'unmodified') { await git.add({ fs, dir: this.outputDir, filepath: '.' }) await git.commit({ fs, dir: this.outputDir, message: `update from gridea: ${moment().format('YYYY-MM-DD HH:mm:ss')}`, }) } await this.checkCurrentBranch() const pushRes = await git.push({ fs, http: this.http, dir: this.outputDir, remote: 'origin', ref: setting.branch, force: true, }) console.log('pushRes', pushRes) return { success: true, data: pushRes, message: '', localBranchs, } } catch (e) { console.log(e) return { success: false, message: e.message, data: localBranchs, localBranchs, } } } /** * Check whether the branch needs to be switched, * FIXME: if branch is change, then the fist push is not work. so need to push again. */ async checkCurrentBranch() { const { setting } = this.db const currentBranch = await git.currentBranch({ fs, dir: this.outputDir, fullname: false }) const localBranches = await git.listBranches({ fs, dir: this.outputDir }) if (currentBranch !== setting.branch) { if (!localBranches.includes(setting.branch)) { await git.branch({ fs, dir: this.outputDir, ref: setting.branch }) } await git.checkout({ fs, dir: this.outputDir, ref: setting.branch }) } } } ================================================ FILE: src/server/events/deploy.ts ================================================ import { ipcMain, IpcMainEvent } from 'electron' import Deploy from '../deploy' import Renderer from '../renderer' import SftpDeploy from '../plugins/deploys/sftp' import NetlifyDeploy from '../plugins/deploys/netlify' export default class DeployEvents { constructor(appInstance: any) { const { platform } = appInstance.db.setting const deploy = new Deploy(appInstance) const sftp = new SftpDeploy(appInstance) const renderer = new Renderer(appInstance) const netlify = new NetlifyDeploy(appInstance) ipcMain.removeAllListeners('site-publish') ipcMain.removeAllListeners('site-published') ipcMain.removeAllListeners('remote-detect') ipcMain.removeAllListeners('remote-detected') ipcMain.on('site-publish', async (event: IpcMainEvent, params: any) => { console.log(platform) const client = ({ 'github': deploy, 'coding': deploy, 'gitee': deploy, 'sftp': sftp, 'netlify': netlify, } as any)[platform] // render renderer.db.themeConfig.domain = renderer.db.setting.domain await renderer.renderAll() // publish const result = await client.publish() event.sender.send('site-published', result) }) ipcMain.on('remote-detect', async (event: IpcMainEvent, params: any) => { const client = ({ 'github': deploy, 'coding': deploy, 'gitee': deploy, 'sftp': sftp, 'netlify': netlify, } as any)[platform] const result = await client.remoteDetect() event.sender.send('remote-detected', result) }) } } ================================================ FILE: src/server/events/index.ts ================================================ import SiteEvents from './site' import PostEvents from './post' import TagEvents from './tag' import MenuEvents from './menu' import ThemeEvents from './theme' import RendererEvents from './renderer' import SettingEvents from './setting' import DeployEvents from './deploy' export default { SiteEvents, PostEvents, TagEvents, MenuEvents, ThemeEvents, RendererEvents, SettingEvents, DeployEvents, } ================================================ FILE: src/server/events/menu.ts ================================================ import { ipcMain, IpcMainEvent } from 'electron' import Menus from '../menus' import { IMenu } from '../interfaces/menu' export default class MenuEvents { constructor(appInstance: any) { const menus = new Menus(appInstance) ipcMain.removeAllListeners('menu-delete') ipcMain.removeAllListeners('menu-deleted') ipcMain.removeAllListeners('menu-save') ipcMain.removeAllListeners('menu-saved') ipcMain.removeAllListeners('menu-sort') ipcMain.removeAllListeners('menu-sorted') ipcMain.on('menu-delete', async (event: IpcMainEvent, menuName: string) => { const data = await menus.deleteMenu(menuName) event.sender.send('menu-deleted', data) }) ipcMain.on('menu-save', async (event: IpcMainEvent, menu: IMenu) => { const data = await menus.saveMenu(menu) event.sender.send('menu-saved', data) }) ipcMain.on('menu-sort', async (event: IpcMainEvent, menuList: IMenu[]) => { const data = await menus.saveMenus(menuList) event.sender.send('menu-sorted', data) }) } } ================================================ FILE: src/server/events/post.ts ================================================ import { ipcMain, IpcMainEvent } from 'electron' import { IPost, IPostDb } from '../interfaces/post' import Posts from '../posts' export default class PostEvents { constructor(appInstance: any) { ipcMain.removeAllListeners('app-post-create') ipcMain.removeAllListeners('app-post-created') ipcMain.removeAllListeners('app-post-delete') ipcMain.removeAllListeners('app-post-deleted') ipcMain.removeAllListeners('app-post-list-delete') ipcMain.removeAllListeners('app-post-list-deleted') ipcMain.removeAllListeners('image-upload') ipcMain.removeAllListeners('image-uploaded') const posts = new Posts(appInstance) ipcMain.on('app-post-create', async (event: IpcMainEvent, post: IPost) => { const data = await posts.savePostToFile(post) event.sender.send('app-post-created', data) }) ipcMain.on('app-post-delete', async (event: IpcMainEvent, post: IPostDb) => { const data = await posts.deletePost(post) event.sender.send('app-post-deleted', data) }) ipcMain.on('app-post-list-delete', async (event: IpcMainEvent, postList: IPostDb[]) => { let data: any = false for (const post of postList) { data = posts.deletePost(post) } event.sender.send('app-post-list-deleted', data) }) ipcMain.on('image-upload', async (event: IpcMainEvent, files: any[]) => { console.log('执行了上传图片', files) const data = await posts.uploadImages(files) event.sender.send('image-uploaded', data) }) } } ================================================ FILE: src/server/events/renderer.ts ================================================ import { ipcMain, IpcMainEvent } from 'electron' import Renderer from '../renderer' export default class RendererEvents { constructor(appInstance: any) { const renderer = new Renderer(appInstance) ipcMain.removeAllListeners('html-render') ipcMain.removeAllListeners('html-rendered') ipcMain.on('html-render', async (event: IpcMainEvent, params: any) => { if (renderer.db.themeConfig.themeName) { await renderer.preview() } event.sender.send('html-rendered', null) }) } } ================================================ FILE: src/server/events/setting.ts ================================================ import { ipcMain, IpcMainEvent } from 'electron' import Setting from '../setting' import { ICommentSetting } from '../interfaces/setting' import { ISetting } from '../../interfaces/setting' export default class SettingEvents { constructor(appInstance: any) { const settingInstance = new Setting(appInstance) ipcMain.removeAllListeners('setting-save') ipcMain.removeAllListeners('setting-saved') ipcMain.removeAllListeners('comment-setting-save') ipcMain.removeAllListeners('comment-setting-saved') ipcMain.removeAllListeners('favicon-upload') ipcMain.removeAllListeners('favicon-uploaded') ipcMain.removeAllListeners('avatar-upload') ipcMain.removeAllListeners('avatar-uploaded') ipcMain.on('setting-save', async (event: IpcMainEvent, setting: ISetting) => { const data = await settingInstance.saveSetting(setting) event.sender.send('setting-saved', data) }) ipcMain.on('comment-setting-save', async (event: IpcMainEvent, setting: ICommentSetting) => { const data = await settingInstance.saveCommentSetting(setting) event.sender.send('comment-setting-saved', data) }) ipcMain.on('favicon-upload', async (event: IpcMainEvent, filePath: string) => { console.log('执行了上传图片', filePath) const data = await settingInstance.uploadFavicon(filePath) event.sender.send('favicon-uploaded', data) }) ipcMain.on('avatar-upload', async (event: IpcMainEvent, filePath: string) => { console.log('执行了上传头像', filePath) const data = await settingInstance.uploadAvatar(filePath) event.sender.send('avatar-uploaded', data) }) } } ================================================ FILE: src/server/events/site.ts ================================================ import { ipcMain, IpcMainEvent } from 'electron' export default class SiteEvents { constructor(appInstance: any) { /** * load site config and data */ ipcMain.removeAllListeners('app-site-reload') ipcMain.removeAllListeners('app-site-loaded') ipcMain.removeAllListeners('app-source-folder-setting') ipcMain.removeAllListeners('app-source-folder-set') ipcMain.removeAllListeners('app-preview-server-port-get') ipcMain.removeAllListeners('app-preview-server-port-got') ipcMain.on('app-site-reload', async (event: IpcMainEvent, params: any) => { const result = await appInstance.loadSite() event.sender.send('app-site-loaded', result) }) ipcMain.on('app-source-folder-setting', async (event: IpcMainEvent, params: string) => { const result = await appInstance.saveSourceFolderSetting(params) event.sender.send('app-source-folder-set', result) }) ipcMain.on('app-preview-server-port-get', async (event: IpcMainEvent, params: string) => { const port = await appInstance.previewServer.get('port') event.sender.send('app-preview-server-port-got', port) }) } } ================================================ FILE: src/server/events/tag.ts ================================================ import { ipcMain, IpcMainEvent } from 'electron' import Tags from '../tags' import { ITag } from '../interfaces/tag' export default class TagEvents { constructor(appInstance: any) { const tags = new Tags(appInstance) ipcMain.removeAllListeners('tag-delete') ipcMain.removeAllListeners('tag-deleted') ipcMain.removeAllListeners('tag-save') ipcMain.removeAllListeners('tag-saved') ipcMain.on('tag-delete', async (event: IpcMainEvent, tagName: string) => { const data = await tags.deleteTag(tagName) event.sender.send('tag-deleted', data) }) ipcMain.on('tag-save', async (event: IpcMainEvent, tag: ITag) => { const data = await tags.saveTag(tag) event.sender.send('tag-saved', data) }) } } ================================================ FILE: src/server/events/theme.ts ================================================ import { ipcMain, IpcMainEvent } from 'electron' import { ITheme } from '../interfaces/theme' import Theme from '../theme' export default class ThemeEvents { constructor(appInstance: any) { const theme = new Theme(appInstance) ipcMain.removeAllListeners('theme-save') ipcMain.removeAllListeners('theme-saved') ipcMain.removeAllListeners('theme-custom-config-save') ipcMain.removeAllListeners('theme-custom-config-saved') ipcMain.on('theme-save', async (event: IpcMainEvent, themeConfig: ITheme) => { const config = await theme.saveThemeConfig(themeConfig) event.sender.send('theme-saved', config) }) ipcMain.on('theme-custom-config-save', async (event: IpcMainEvent, config: any) => { const result = await theme.saveThemeCustomConfig(config) event.sender.send('theme-custom-config-saved', result) }) } } ================================================ FILE: src/server/interfaces/application.ts ================================================ import { BrowserWindow } from 'electron' import { IPostDb } from './post' import { ITag } from './tag' import { ITheme } from './theme' import { IMenu } from './menu' import { ICommentSetting } from './setting' import { ISetting } from '../../interfaces/setting' export interface IApplicationSetting { mainWindow: BrowserWindow app: any baseDir: string previewServer: any } export interface IApplicationDb { posts: IPostDb[] tags: ITag[] menus: IMenu[] themeConfig: ITheme themeCustomConfig: any themes: any[] setting: ISetting commentSetting: ICommentSetting currentThemeConfig: any[] } export interface IApplication { mainWindow: BrowserWindow app: any baseDir: string appDir: string buildDir: string db: IApplicationDb } ================================================ FILE: src/server/interfaces/menu.ts ================================================ export interface IMenu { name: string index?: number openType: string link: string } ================================================ FILE: src/server/interfaces/post.ts ================================================ import { ITag } from './tag' export interface IPost { title: string fileName: string tags: string[] date: string content: string published: boolean hideInList: boolean isTop: boolean featureImage: { name?: string, path?: string, type?: string, } /** 外链封面图 */ featureImagePath: string deleteFileName?: string } export interface IPostData { title: string date: string published: boolean hideInList: boolean isTop: boolean tags?: [] feature: string } export interface IPostDb { content: string abstract: string, data: IPostData fileName: string } export interface ITagRenderData extends ITag { link: string } export interface ISiteTagsData extends ITagRenderData { count: number } export interface IStats { text: string minutes: number time: number words: number } export interface IPostRenderData { content: string fileName: string abstract: string title: string tags: ITagRenderData[] date: string dateFormat: string feature: string link: string hideInList: boolean isTop: boolean toc?: any stats: IStats nextPost?: IPostRenderData prevPost?: IPostRenderData description: string } ================================================ FILE: src/server/interfaces/renderer.ts ================================================ export interface IPagination { prev: string, next: string, } ================================================ FILE: src/server/interfaces/setting.ts ================================================ export interface IDisqusSetting { api: string apikey: string shortname: string } export interface IGitalkSetting { clientId: string clientSecret: string repository: string owner: string } export interface ICommentSetting { commentPlatform: string showComment: boolean disqusSetting: IDisqusSetting gitalkSetting: IGitalkSetting } ================================================ FILE: src/server/interfaces/tag.ts ================================================ export interface ITag { name: string used: boolean slug?: string index?: number } ================================================ FILE: src/server/interfaces/theme.ts ================================================ export interface ITheme { themeName: string postPageSize: number archivesPageSize: number siteName: string siteDescription: string footerInfo: string showFeatureImage: boolean domain: string postUrlFormat: string tagUrlFormat: string dateFormat: string feedFullText: boolean feedCount: number archivesPath: string postPath: string tagPath: string } ================================================ FILE: src/server/menus.ts ================================================ import Model from './model' import { IMenu } from './interfaces/menu' export default class Menus extends Model { list() { const menus = this.$posts.get('menus').value() return menus } public async saveMenu(menu: IMenu) { const menus = await this.$posts.get('menus').value() if (typeof menu.index === 'number') { const { index } = menu delete menu.index menus[index] = menu } else { delete menu.index menus.push(menu) } await this.$posts.set('menus', menus).write() return menus } public async saveMenus(menus: IMenu[]) { await this.$posts.set('menus', menus).write() return menus } public async deleteMenu(menuValue: string) { const menu = await this.$posts.get('menus').remove({ name: menuValue }).write() return menu } } ================================================ FILE: src/server/model.ts ================================================ import path from 'path' import low from 'lowdb' import FileSync from 'lowdb/adapters/FileSync' import { BrowserWindow } from 'electron' import { IApplicationDb, IApplication } from './interfaces/application' export default class Model { appDir: string buildDir: string $setting: any $posts: any $theme: any db: IApplicationDb mainWindow: BrowserWindow constructor(appInstance: IApplication) { this.appDir = appInstance.appDir this.buildDir = appInstance.buildDir this.db = appInstance.db this.mainWindow = appInstance.mainWindow this.initDataStore() } private initDataStore(): void { const settingAdapter = new FileSync(path.join(this.appDir, 'config/setting.json')) const setting = low(settingAdapter) this.$setting = setting const postsAdapter = new FileSync(path.join(this.appDir, 'config/posts.json')) const posts = low(postsAdapter) this.$posts = posts const themeAdapter = new FileSync(path.join(this.appDir, 'config/theme.json')) const theme = low(themeAdapter) this.$theme = theme } } ================================================ FILE: src/server/plugins/deploys/gitproxy.ts ================================================ import { IApplicationDb } from '../../interfaces/application' const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent') const get = require('simple-get') export default class GitProxy { db: IApplicationDb constructor(appInstance: any) { this.db = appInstance.db // console.log('instance git proxy',this.db.setting) } public async request({ url, method, headers, body, }: { url: any; method: any; headers: any; body: any; }) { const { setting } = this.db body = await this.mergeBuffers(body) const proxy = url.startsWith('https:') ? { Agent: HttpsProxyAgent } : { Agent: HttpProxyAgent } const agent = setting.enabledProxy === 'proxy' ? new proxy.Agent({ proxy: `http://${setting.proxyPath}:${setting.proxyPort}`, }) : undefined // const agent = new proxy.Agent({ proxy: 'http://127.0.0.1:1081' }) return new Promise((resolve, reject) => get( { url, method, agent, headers, body, }, (err: any, res: any) => (err ? reject(err) : resolve(this.transformResponse(res))), )) } private async mergeBuffers(data: any[] | Uint8Array) { if (!Array.isArray(data)) return data if (data.length === 1 && data[0] instanceof Buffer) return data[0] const buffers = [] let offset = 0 let size = 0 for await (const chunk of data) { buffers.push(chunk) size += chunk.byteLength } data = new Uint8Array(size) for (const buffer of buffers) { data.set(buffer, offset) offset += buffer.byteLength } return Buffer.from(data.buffer) } private async transformResponse(res: { url: any; method: any; statusCode: any; statusMessage: any; headers: any; }) { const { url, method, statusCode, statusMessage, headers, } = res return { url, method, statusCode, statusMessage, headers, body: res, } } } ================================================ FILE: src/server/plugins/deploys/netlify.ts ================================================ import fs from 'fs' import path from 'path' import axios from 'axios' import normalizePath from 'normalize-path' import crypto from 'crypto' import util from 'util' import Model from '../../model' import { IApplication } from '../../interfaces/application' const asyncReadFile = util.promisify(fs.readFile) export default class NetlifyApi extends Model { private apiUrl: string private accessToken: string private siteId: string private inputDir: string constructor(appInstance: IApplication) { super(appInstance) this.apiUrl = 'https://api.netlify.com/api/v1/' this.accessToken = appInstance.db.setting.netlifyAccessToken this.siteId = appInstance.db.setting.netlifySiteId this.inputDir = appInstance.buildDir } async request(method: 'GET' | 'PUT' | 'POST', endpoint: string, data?: any) { const endpointUrl = this.apiUrl + endpoint.replace(':site_id', this.siteId) const { setting } = this.db const proxy = setting.enabledProxy === 'proxy' ? { host: setting.proxyPath, port: Number(setting.proxyPort), } : undefined return axios( endpointUrl, { method, headers: { 'User-Agent': 'Gridea', 'Authorization': `Bearer ${this.accessToken}`, }, data, proxy, }, ) } async remoteDetect() { try { const res = await this.request('GET', 'sites/:site_id/') if (res.status === 200) { return { success: true, message: res.data, } } return { success: false, message: res.data, } } catch (e) { return { success: false, message: e, } } } async publish() { const result = { success: true, message: '同步成功', data: null, } try { const localFilesList = await this.prepareLocalFilesList() const deployData = await this.request('POST', 'sites/:site_id/deploys', localFilesList) const deployId = deployData.data.id const hashOfFilesToUpload = deployData.data.required const filesToUpload = this.getFilesToUpload(localFilesList, hashOfFilesToUpload) for (let i = 0; i < filesToUpload.length; i += 1) { const filePath = filesToUpload[i] try { // eslint-disable-next-line no-await-in-loop const res = await this.uploadFile(filePath, deployId) if (res.status === 422) { return Promise.reject(res) } } catch (e) { try { // eslint-disable-next-line no-await-in-loop const res = await this.uploadFile(filePath, deployId) if (res.status === 422) { return Promise.reject(res) } } catch (error) { return Promise.reject(error) } } } return result } catch (e) { result.success = false result.message = `[Server] 同步失败: ${e.message}` } } async prepareLocalFilesList() { const tempFileList: any = this.readDirRecursiveSync(this.inputDir) const fileList: any = {} for (const filePath of tempFileList) { if (fs.lstatSync(path.join(this.inputDir, filePath)).isDirectory()) { continue } // eslint-disable-next-line no-await-in-loop const fileHash = await this.getFileHash(path.join(this.inputDir, filePath)) const fileKey = `/${filePath}`.replace(/\/\//gmi, '/') fileList[fileKey] = fileHash } return Promise.resolve({ files: fileList }) } readDirRecursiveSync(dir: string, fileList?: any) { const files = fs.readdirSync(dir) fileList = fileList || [] files.forEach((file) => { if (this.fileIsDirectory(dir, file)) { fileList = this.readDirRecursiveSync(path.join(dir, file), fileList) return } if (this.fileIsNotExcluded(file)) { fileList.push(this.getFilePath(dir, file)) } }) return fileList } fileIsDirectory(dir: string, file: string) { return fs.statSync(path.join(dir, file)).isDirectory() } fileIsNotExcluded(file: string) { return file.indexOf('.') !== 0 || file === '.htaccess' || file === '_redirects' } getFilePath(dir: string, file: string, includeInputDir = false) { if (!includeInputDir) { dir = dir.replace(this.inputDir, '') } return normalizePath(path.join(dir, file)) } getFileHash(fileName: string) { return new Promise((resolve, reject) => { const shaSumCalculator = crypto.createHash('sha1') try { const fileStream = fs.createReadStream(fileName) fileStream.on('data', fileContentChunk => shaSumCalculator.update(fileContentChunk)) fileStream.on('end', () => resolve(shaSumCalculator.digest('hex'))) } catch (e) { return reject(e) } }) } getFilesToUpload(filesList: any, hashesToUpload: any) { const filePaths = Object.keys(filesList.files) const filesToUpload = [] const foundedHashes = [] for (let i = 0; i < filePaths.length; i++) { const filePath = filePaths[i] if (hashesToUpload.indexOf(filesList.files[filePath]) > -1) { filesToUpload.push(filePath.replace(/\/\//gmi, '/')) foundedHashes.push(filesList.files[filePath]) } } return filesToUpload } async uploadFile(filePath: any, deployID: any) { const endpointUrl = `${this.apiUrl}deploys/${deployID}/files${filePath}` const fullFilePath = this.getFilePath(this.inputDir, filePath, true) const fileContent = await asyncReadFile(fullFilePath) return axios(endpointUrl, { method: 'PUT', headers: { 'User-Agent': 'Gridea', 'Content-Type': 'application/octet-stream', 'Authorization': `Bearer ${this.accessToken}`, }, data: fileContent, }) } } ================================================ FILE: src/server/plugins/deploys/sftp.ts ================================================ import * as fse from 'fs-extra' // import * as fs from 'fs' import path from 'path' import SftpClient from 'ssh2-sftp-client' import NodeSsh from 'node-ssh' import normalizePath from 'normalize-path' import Model from '../../model' type sftpConnectConfig = { host: string; port: number; type?: string; username: string; password?: string; privateKey?: string | Buffer; } export default class SftpDeploy extends Model { // connect: SftpClient constructor(appInstance: any) { super(appInstance) // this.connect = new SftpClient() console.log('instance sftp deploy') } async remoteDetect() { const result = { success: true, message: '', } const client = new SftpClient() const { setting } = this.db const connectConfig: sftpConnectConfig = { host: setting.server, port: Number(setting.port), username: setting.username, } if (setting.privateKey) { try { connectConfig.privateKey = fse.readFileSync(setting.privateKey) } catch (e) { console.error('SFTP Test Remote Error: ', e.message) result.success = false result.message = e.message return result } } else { connectConfig.password = setting.password } const testFilename = 'gridea.txt' const localTestFilePath = normalizePath(path.join(this.appDir, testFilename)) const remoteTestFilePath = normalizePath(path.join(setting.remotePath, testFilename)) try { await client.connect(connectConfig) await client.list('/') try { fse.writeFileSync(localTestFilePath, 'This is gridea test file. you can delete it.') await client.put(localTestFilePath, remoteTestFilePath) await client.delete(remoteTestFilePath) } catch (e) { console.error('SFTP Test Remote Error: ', e.message) result.success = false result.message = e.message } finally { if (fse.existsSync(localTestFilePath)) { fse.unlinkSync(localTestFilePath) } } } catch (e) { console.error('SFTP Test Remote Error: ', e.message) result.success = false result.message = e.message } finally { await client.end() } return result } async publish() { const result = { success: true, message: '', } const client = new NodeSsh() const { setting } = this.db const connectConfig: sftpConnectConfig = { host: setting.server, port: Number(setting.port), type: 'sftp', username: setting.username, } // node-ssh: privateKey is path string. if (setting.privateKey) { connectConfig.privateKey = setting.privateKey } else { connectConfig.password = setting.password } const localPath = normalizePath(path.join(this.buildDir)) const remotePath = normalizePath(path.join(setting.remotePath)) try { await client.connect(connectConfig) try { await client.exec(`rm -rf ${remotePath}`) await client.mkdir(remotePath) const res = await client.putDirectory(localPath, remotePath, { recursive: true, concurrency: 1, // 解决同步丢失js、css、图片文件问题 validate: function (itemPath: string) { const baseName = path.basename(itemPath) return baseName.substr(0, 1) !== '.' // do not allow dot files && baseName !== 'node_modules' // do not allow node_modules }, }) } catch (e) { console.error('SFTP Publish Error: ', e.message) result.success = false result.message = e.message } } catch (e) { console.error('SFTP Publish Error: ', e.message) result.success = false result.message = e.message } finally { await client.dispose() } return result } } ================================================ FILE: src/server/plugins/markdown.ts ================================================ import MarkdownIt from 'markdown-it' import MarkdownItKatex from '@iktakahiro/markdown-it-katex' import markdownItTocAndAnchor from 'markdown-it-toc-and-anchor' import MarkdownItTaskLists from 'markdown-it-task-lists' import MarkdownItMark from 'markdown-it-mark' import MarkdownItSup from 'markdown-it-sup' import MarkdownItSub from 'markdown-it-sub' import MarkdownItAbbr from 'markdown-it-abbr' import MarkdownItFootnote from 'markdown-it-footnote' import MarkdownItImsize from 'markdown-it-imsize' import MarkdownItEmoji from 'markdown-it-emoji' import MarkdownItImplicitFigures from 'markdown-it-implicit-figures' import MarkdownItImageLazyLoading from 'markdown-it-image-lazy-loading' const markdownIt = new MarkdownIt({ html: true, breaks: true, }) const BAD_PROTO_RE = /^(vbscript|javascript|data):/ const GOOD_DATA_RE = /^data:image\/(gif|png|jpeg|webp);/ markdownIt.validateLink = function (url) { url = url.trim().toLowerCase() return BAD_PROTO_RE.test(url) ? (!!GOOD_DATA_RE.test(url)) : true } markdownIt.use(MarkdownItKatex) markdownIt.use(markdownItTocAndAnchor, { anchorLink: false, }) markdownIt.use(MarkdownItTaskLists, { label: true, labelAfter: true, }) markdownIt.use(MarkdownItMark) markdownIt.use(MarkdownItSup) markdownIt.use(MarkdownItSub) markdownIt.use(MarkdownItAbbr) markdownIt.use(MarkdownItFootnote) markdownIt.use(MarkdownItImsize) markdownIt.use(MarkdownItEmoji) markdownIt.use(MarkdownItImplicitFigures, { dataType: true, //
, default: false figcaption: false, //
alternative text
, default: false tabindex: true, //
..., default: false link: false, // , default: false }) markdownIt.use(MarkdownItImageLazyLoading) export default markdownIt ================================================ FILE: src/server/posts.ts ================================================ import * as fs from 'fs' import * as fse from 'fs-extra' import * as path from 'path' import matter from 'gray-matter' import moment from 'moment' import Bluebird from 'bluebird' import junk from 'junk' import Model from './model' import { IPost, IPostDb } from './interfaces/post' import ContentHelper from '../helpers/content-helper' import { formatYamlString } from '../helpers/utils' Bluebird.promisifyAll(fs) export default class Posts extends Model { postDir: string postImageDir: string constructor(appInstance: any) { super(appInstance) this.postDir = path.join(this.appDir, 'posts') this.postImageDir = `${this.appDir}/post-images` } public async savePosts() { const resultList: any = [] const requestList: any = [] let files = await fse.readdir(this.postDir) files = files.filter(junk.not) files.forEach((item) => { requestList.push(fs.readFileSync(path.join(this.postDir, item), 'utf8')) }) const results = await Bluebird.all(requestList) const fixedResults = JSON.parse(JSON.stringify(results)) /** * The format of the correction `tag` is changed from a string to an array, and the article source file is updated. from v0.7.6 */ await Promise.all(results.map(async (result: any, index: any) => { const postMatter = matter(result) const data = (postMatter.data as any) data.title = formatYamlString(data.title) if (data && data.date) { if (typeof data.date === 'string') { data.date = moment(data.date).format('YYYY-MM-DD HH:mm:ss') } else { data.date = moment(data.date).subtract(8, 'hours').format('YYYY-MM-DD HH:mm:ss') } } // If there is a `tag` and it is of string type, it is corrected to array type. if (data && typeof data.tags === 'string') { const tagReg = /tags: [^\s[]/i const newTagString = data.tags.split(' ').toString() if (tagReg.test(result)) { const mdStr = `--- title: '${data.title}' date: ${data.date} tags: [${newTagString}] published: ${data.published || false} hideInList: ${data.hideInList || false} feature: ${data.feature || ''} isTop: ${data.isTop || false} --- ${postMatter.content}` fixedResults[index] = mdStr fse.writeFileSync(`${this.postDir}/${files[index]}`, mdStr) } } })) fixedResults.forEach((result: any, index: any) => { const postMatter = matter(result) const data = (postMatter.data as any) // Remove useless `'` in formatYamlString generate if (data && data.title) { data.title = String(data.title).replace(/''/g, '\'') } // Fix matter's formatted `date` problem if (data && data.date) { if (typeof data.date === 'string') { data.date = moment(data.date).format('YYYY-MM-DD HH:mm:ss') } else { data.date = moment(data.date).subtract(8, 'hours').format('YYYY-MM-DD HH:mm:ss') } } delete postMatter.orig // Remove orig const post = { ...postMatter, abstract: '', fileName: '', } const moreReg = /\n\s*\s*\n/i const matchMore = moreReg.exec(post.content) if (matchMore) { post.abstract = (post.content).substring(0, matchMore.index) // Abstract } post.fileName = files[index].substring(0, files[index].length - 3) // To be optimized! resultList.push(post) }) const list: any = [] resultList.forEach((item: any) => { // Articles migrated from hexo or other platforms do not have a `published` field if (item.data.published === undefined) { item.data.published = false } // Articles migrated from other platforms or old articles do not have `hideInList` fields if (item.data.hideInList === undefined) { item.data.hideInList = false } // Articles migrated from other platforms or old articles do not have `isTop` fields if (item.data.isTop === undefined) { item.data.isTop = false } list.push(item) }) list.sort((a: any, b: any) => moment(b.data.date).unix() - moment(a.data.date).unix()) this.$posts.set('posts', list).write() return true } async list() { await this.savePosts() const posts = await this.$posts.get('posts').value() const helper = new ContentHelper() const list = posts.map((post: IPostDb) => { const item = JSON.parse(JSON.stringify(post)) item.content = helper.changeImageUrlDomainToLocal(item.content, this.appDir) item.data.feature = item.data.feature && !item.data.feature.includes('http') ? helper.changeFeatureImageUrlDomainToLocal(item.data.feature, this.appDir) : item.data.feature return item }) return list } /** * Save Post to file * @param post */ async savePostToFile(post: IPost): Promise { const helper = new ContentHelper() const content = helper.changeImageUrlLocalToDomain(post.content, this.db.setting.domain) const extendName = (post.featureImage.name || 'jpg').split('.').pop() post.title = formatYamlString(post.title) const mdStr = `--- title: '${post.title}' date: ${post.date} tags: [${post.tags.join(',')}] published: ${post.published} hideInList: ${post.hideInList} feature: ${post.featureImage.name ? `/post-images/${post.fileName}.${extendName}` : post.featureImagePath} isTop: ${post.isTop} --- ${content}` try { // If exist feature image if (post.featureImage.path) { const filePath = `${this.postImageDir}/${post.fileName}.${extendName}` if (post.featureImage.path !== filePath) { fse.copySync(post.featureImage.path, filePath) // Clean the old file if (post.featureImage.path.includes(this.postImageDir)) { fse.removeSync(post.featureImage.path) } } } // Write file must use fse, beause fs.writeFile need callback await fse.writeFile(`${this.postDir}/${post.fileName}.md`, mdStr) // Clean the old file if (post.deleteFileName) { fse.removeSync(`${this.postDir}/${post.deleteFileName}.md`) } } catch (e) { console.error('ERROR: ', e) } return post } async deletePost(post: IPostDb) { try { const postUrl = `${this.postDir}/${post.fileName}.md` fse.removeSync(postUrl) // Clean feature image if (post.data.feature) { fse.removeSync(post.data.feature.replace('file://', '')) } // Clean post content image const imageReg = /(!\[.*?\]\()(.+?)(\))/g const imageList = post.content.match(imageReg) if (imageList) { const postImagePaths = imageList.map((item: string) => { const index = item.indexOf('(') return item.substring(index + 1, item.length - 1) }) postImagePaths.forEach(async (filePath: string) => { fse.removeSync(filePath.replace('file://', '')) }) } return true } catch (e) { console.error('Delete Error', e) return false } } async uploadImages(files: any[]) { await fse.ensureDir(this.postImageDir) const results = [] for (const file of files) { const extendName = file.name.split('.').pop() const newFileName = new Date().getTime() const filePath = `${this.postImageDir}/${newFileName}.${extendName}` fse.copySync(file.path, filePath) results.push(filePath) } return results } } ================================================ FILE: src/server/renderer.ts ================================================ import * as fs from 'fs' import urlJoin from 'url-join' import Bluebird from 'bluebird' import * as fse from 'fs-extra' import ejs from 'ejs' import moment from 'moment' import less from 'less' import { Feed } from 'feed' import junk from 'junk' import { wordCount, timeCalc } from '../helpers/words-count' import Model from './model' import ContentHelper from '../helpers/content-helper' import { formatThemeCustomConfigToRender } from '../helpers/utils' import { IPostDb, IPostRenderData, ITagRenderData, ISiteTagsData, } from './interfaces/post' import { ITag } from './interfaces/tag' import { DEFAULT_POST_PAGE_SIZE, DEFAULT_ARCHIVES_PAGE_SIZE } from '../helpers/constants' import markdown from './plugins/markdown' import { IMenu } from './interfaces/menu' Bluebird.promisifyAll(fs) const helper = new ContentHelper() export default class Renderer extends Model { outputDir: string = this.buildDir themePath: string = '' postsData: IPostRenderData[] = [] tagsData: ISiteTagsData[] = [] menuData: IMenu[] = [] siteData: any = {} previewPort: number utils: any = {} constructor(appInstance: any) { super(appInstance) this.previewPort = appInstance.previewServer.get('port') this.loadConfig() this.utils.now = Date.now() this.utils.moment = moment } async preview() { this.db.themeConfig.domain = `http://localhost:${this.previewPort}` await this.renderAll() } async renderAll() { await this.clearOutputFolder() await this.formatDataForRender() await this.buildCss() // Render post list page await this.renderPostList('') // Render archives page await this.renderPostList(urlJoin('/', this.db.themeConfig.archivesPath)) // Render tag list page await this.renderTags() await this.renderPostDetail() await this.renderTagDetail() // Need before `renderCustomPage`, because maybe theme custom page include a `404 page` await this.copyFiles() // Render custom page await this.renderCustomPage() await this.buildCname() await this.buildFeed() } /** * Load Config */ async loadConfig() { this.themePath = urlJoin(this.appDir, 'themes', this.db.themeConfig.themeName) fse.ensureDirSync(urlJoin(this.outputDir)) } /** * Format data for rendering pages */ public formatDataForRender(): any { const { themeConfig } = this.db this.postsData = this.db.posts.filter((item: IPostDb) => item.data.published) .map((item: IPostDb) => { const currentTags = item.data.tags || [] let toc = '' const content = markdown.render(helper.changeImageUrlLocalToDomain(item.content, themeConfig.domain), { tocCallback(tocMarkdown: any, tocArray: any, tocHtml: any) { toc = tocHtml }, }) let words = 0 wordCount(content, (count: number) => { words = count }) const reading = timeCalc(content) const stats = { text: `${reading.minius} min read`, time: reading.second * 1000, // ms words, minutes: reading.minius, } const result: IPostRenderData = { content, fileName: item.fileName, abstract: markdown.render(helper.changeImageUrlLocalToDomain(item.abstract, themeConfig.domain)), title: item.data.title, tags: this.db.tags .filter((tag: ITag) => currentTags.find(i => i === tag.name)) .map((tag: ITag) => ({ ...tag, link: urlJoin(themeConfig.domain, themeConfig.tagPath, `${tag.slug}`, '/') })), date: item.data.date, dateFormat: (themeConfig.dateFormat && moment(item.data.date).format(themeConfig.dateFormat)) || item.data.date, feature: item.data.feature && !item.data.feature.includes('http') ? `${helper.changeFeatureImageUrlLocalToDomain(item.data.feature, themeConfig.domain)}` : item.data.feature || '', link: urlJoin(themeConfig.domain, themeConfig.postPath, item.fileName, '/'), hideInList: !!item.data.hideInList, isTop: !!item.data.isTop, stats, description: `${content.replace(/<[^>]*>/g, '').substring(0, 120)}${content[121] ? '...' : ''}`, } result.toc = toc return result }) .sort((a: IPostRenderData, b: IPostRenderData) => moment(b.date).unix() - moment(a.date).unix()) this.tagsData = [] this.postsData.forEach((item: IPostRenderData) => { if (!item.hideInList) { item.tags.forEach((tag: ITagRenderData) => { const foundTag = this.tagsData.find((t: ITagRenderData) => t.link === tag.link) if (!foundTag) { this.tagsData.push({ ...tag, count: 1, }) } else { foundTag.count += 1 } }) } }) this.menuData = this.db.menus.map((menu: IMenu) => { let link = menu.link.replace(this.db.setting.domain, this.db.themeConfig.domain) const isSiteLink = menu.link.includes(this.db.setting.domain) if (isSiteLink) { link = `${link}` } return { ...menu, link, } }) this.siteData = { posts: this.postsData, tags: this.tagsData, menus: this.menuData, themeConfig: this.db.themeConfig, customConfig: formatThemeCustomConfigToRender(this.db.themeCustomConfig, this.db.currentThemeConfig), utils: this.utils, isHomepage: false, } } /** * Render the article list, excluding hidden articles. * if extraPath exist, render archive template, if not render index template */ public async renderPostList(archivePath: string) { const { postPageSize, archivesPageSize, domain } = this.db.themeConfig // Compatible: < v0.7.0 const pageSize = archivePath ? archivesPageSize || DEFAULT_ARCHIVES_PAGE_SIZE : postPageSize || DEFAULT_POST_PAGE_SIZE let excludeHidePostsData = this.postsData.filter((item: IPostRenderData) => !item.hideInList) const renderTemplatePath = urlJoin(this.themePath, 'templates', `${archivePath ? 'archives.ejs' : 'index.ejs'}`) // If it is not archives, sort by `isTop` then to render if (!archivePath) { const isTopPosts = excludeHidePostsData.filter((item: IPostRenderData) => item.isTop) const notTopPosts = excludeHidePostsData.filter((item: IPostRenderData) => !item.isTop) excludeHidePostsData = isTopPosts.concat(notTopPosts) } const renderData: any = { menus: this.menuData, posts: [], pagination: { prev: '', next: '', }, themeConfig: this.db.themeConfig, site: this.siteData, } let html = '' const outputFolder = urlJoin(this.outputDir, archivePath) let renderPath = urlJoin(outputFolder, 'index.html') const renderFile = async (path: string, data: any) => { await ejs.renderFile(path, data, {}, async (err: any, str) => { if (err) { console.error('❌ Render post list error') this.mainWindow.webContents.send('log-error', { type: 'Render post list error', message: err.message, }) } if (str) { html = str } }) } // If there is no article to render if (!excludeHidePostsData.length) { renderData.site.isHomepage = !archivePath fse.ensureDirSync(outputFolder) renderFile(renderTemplatePath, renderData) await fs.writeFileSync(renderPath, html) return } for (let i = 0; i * pageSize < excludeHidePostsData.length; i += 1) { renderData.posts = excludeHidePostsData.slice(i * pageSize, (i + 1) * pageSize) renderData.site.isHomepage = !archivePath && !i if (i === 0 && excludeHidePostsData.length > pageSize) { fse.ensureDirSync(urlJoin(this.outputDir, archivePath, 'page')) renderData.pagination.next = urlJoin(domain, archivePath, 'page', '2') } else if (i > 0 && excludeHidePostsData.length > pageSize) { fse.ensureDirSync(urlJoin(this.outputDir, archivePath, 'page', `${i + 1}`)) renderPath = urlJoin(this.outputDir, archivePath, 'page', `${i + 1}`, 'index.html') renderData.pagination.prev = i === 1 ? urlJoin(domain, archivePath, '/') : urlJoin(domain, archivePath, 'page', `${i}/`) renderData.pagination.next = (i + 1) * pageSize < excludeHidePostsData.length ? urlJoin(domain, archivePath, 'page', `${i + 2}/`) : '' } else { fse.ensureDirSync(urlJoin(this.outputDir, archivePath)) } renderFile(renderTemplatePath, renderData) console.log('👏 PostList Page:', renderPath) fs.writeFileSync(renderPath, html) } } /** * Render the article details page, including hidden articles. */ async renderPostDetail() { for (let i = 0; i < this.postsData.length; i += 1) { const post: IPostRenderData = { ...this.postsData[i] } if (!post.hideInList) { if (i < this.postsData.length - 1) { const nexPost = this.postsData.slice(i + 1, this.postsData.length).find((item: IPostRenderData) => !item.hideInList) if (nexPost) { post.nextPost = nexPost } } if (i > 0) { const prevPost = this.postsData.slice(0, i).reverse().find((item: IPostRenderData) => !item.hideInList) if (prevPost) { post.prevPost = prevPost } } } const renderData = { menus: this.menuData, post, themeConfig: this.db.themeConfig, commentSetting: this.db.commentSetting, site: this.siteData, } let html = '' ejs.renderFile(urlJoin(this.themePath, 'templates', 'post.ejs'), renderData, {}, async (err: any, str) => { if (err) { console.error('❌ Render post detail error') this.mainWindow.webContents.send('log-error', { type: 'Render post detail error', message: err.message, }) } if (str) { html = str } }) const renderFolerPath = urlJoin(this.outputDir, `${this.db.themeConfig.postPath}`, post.fileName) fse.ensureDirSync(renderFolerPath) fs.writeFileSync(urlJoin(renderFolerPath, 'index.html'), html) } } /** * Render tags page */ async renderTags() { const tagsFolder = urlJoin(this.outputDir, 'tags') const renderPath = urlJoin(tagsFolder, 'index.html') const renderData = { tags: this.tagsData, menus: this.menuData, themeConfig: this.db.themeConfig, site: this.siteData, } let html = '' fse.ensureDirSync(tagsFolder) await ejs.renderFile(urlJoin(this.themePath, 'templates', 'tags.ejs'), renderData, {}, async (err: any, str) => { if (err) { console.log('❌ Render tags page error', err) this.mainWindow.webContents.send('log-error', { type: 'Render tags page error', message: err.message, }) } if (str) { html = str } }) console.log('👏 Tags Page:', renderPath) fs.writeFileSync(renderPath, html) } /** * Render tag detail page */ async renderTagDetail() { const usedTags = this.db.tags.filter((tag: ITag) => tag.used) const { postPageSize, domain, tagPath } = this.db.themeConfig // Compatible: < v0.7.0 const pageSize = postPageSize || DEFAULT_POST_PAGE_SIZE for (const usedTag of usedTags) { const posts = this.postsData.filter((post: IPostRenderData) => { return post.tags.find((tag: ITagRenderData) => tag.slug === usedTag.slug) }) const currentTag = usedTag const tagFolderPath = urlJoin(this.outputDir, tagPath, `${currentTag.slug}`) const tagDomainPath = urlJoin(domain, tagPath, `${currentTag.slug}`) fse.ensureDirSync(tagFolderPath) for (let i = 0; i * pageSize < posts.length; i += 1) { const renderData = { tag: currentTag, menus: this.menuData, posts: posts.slice(i * pageSize, (i + 1) * pageSize), pagination: { prev: '', next: '', }, themeConfig: this.db.themeConfig, site: this.siteData, } // Paging let renderPath = urlJoin(tagFolderPath, 'index.html') if (i === 0 && posts.length > pageSize) { fse.ensureDirSync(urlJoin(tagFolderPath, 'page')) renderData.pagination.next = urlJoin(tagDomainPath, 'page', '2') } else if (i > 0 && posts.length > pageSize) { fse.ensureDirSync(urlJoin(tagFolderPath, 'page', `${i + 1}`)) renderPath = urlJoin(tagFolderPath, 'page', `${i + 1}`, 'index.html') renderData.pagination.prev = i === 1 ? tagDomainPath : urlJoin(tagDomainPath, 'page', `${i}/`) renderData.pagination.next = (i + 1) * pageSize < posts.length ? urlJoin(tagDomainPath, 'page', `${i + 2}/`) : '' } let html = '' ejs.renderFile(urlJoin(this.themePath, 'templates', 'tag.ejs'), renderData, {}, async (err: any, str) => { if (err) { console.log('❌ Render tag detail error', err) this.mainWindow.webContents.send('log-error', { type: 'Render tag detail error', message: err.message, }) } if (str) { html = str } }) console.log('👏 Tag Page:', renderPath) fs.writeFileSync(renderPath, html) } } } /** * Render custom page, eg. friends.ejs, about.ejs, home.ejs, projects.ejs... */ async renderCustomPage() { const files = fse.readdirSync(urlJoin(this.themePath, 'templates'), { withFileTypes: true }) const customTemplates = files .filter(item => !item.isDirectory()) .map(item => item.name) .filter(junk.not) .filter((name: string) => { return ![ 'index.ejs', 'post.ejs', 'tag.ejs', 'tags.ejs', 'archives.ejs', // 👇 Gridea protected word, because these filename is gridea folder's name 'images.ejs', 'media.ejs', 'post-images.ejs', 'styles.ejs', 'tag.ejs', 'tags.ejs', ].includes(name) }) const renderData = { menus: this.menuData, themeConfig: this.db.themeConfig, commentSetting: this.db.commentSetting, site: this.siteData, } customTemplates.forEach(async (name: string) => { let renderFolder = urlJoin(this.outputDir, name.substring(0, name.length - 4)) let renderPath = urlJoin(renderFolder, 'index.html') let html = '' if (name === '404.ejs') { renderFolder = this.outputDir renderPath = urlJoin(renderFolder, '404.html') } fse.ensureDirSync(renderFolder) await ejs.renderFile(urlJoin(this.themePath, 'templates', name), renderData, async (err: any, str) => { if (err) { console.error('❌ Render custom page error', err) this.mainWindow.webContents.send('log-error', { type: 'Render custom page error', message: err.message, }) } if (str) { html = str } }) fse.writeFileSync(renderPath, html) console.log('✅ Render custom page success', renderPath) }) } /** * Build CSS and write file */ async buildCss() { const lessFilePath = urlJoin(this.themePath, 'assets', 'styles', 'main.less') const cssFolderPath = urlJoin(this.outputDir, 'styles') fse.ensureDirSync(cssFolderPath) const lessString = fs.readFileSync(lessFilePath, 'utf8') return new Promise((resolve, reject) => { less.render(lessString, { filename: lessFilePath }, async (err: any, cssString: Less.RenderOutput) => { if (err) { console.log(err) reject(err) } let { css } = cssString // if have override const customConfig = this.db.themeCustomConfig const currentThemePath = urlJoin(this.appDir, 'themes', this.db.themeConfig.themeName) const styleOverridePath = urlJoin(currentThemePath, 'style-override.js') const existOverrideFile = await fse.pathExists(styleOverridePath) if (existOverrideFile) { // clean cache delete __non_webpack_require__.cache[__non_webpack_require__.resolve(styleOverridePath)] const generateOverride = __non_webpack_require__(styleOverridePath) const customCss = generateOverride(customConfig) css += customCss } fs.writeFileSync(urlJoin(cssFolderPath, 'main.css'), css) resolve(true) }) }) } /** * Create CNAME file */ async buildCname() { const cnamePath = urlJoin(this.outputDir, 'CNAME') if (this.db.setting.cname) { fs.writeFileSync(cnamePath, this.db.setting.cname) } else { fse.removeSync(cnamePath) } } /** * Build Feed */ async buildFeed() { const DEFAULT_FEED_COUNT = 10 const feedFilename = 'atom.xml' const { themeConfig } = this.db const feed = new Feed({ title: themeConfig.siteName, description: themeConfig.siteDescription, id: themeConfig.domain, link: themeConfig.domain, image: urlJoin(themeConfig.domain, 'images', 'avatar.png'), favicon: urlJoin(themeConfig.domain, 'favicon.ico'), copyright: `All rights reserved ${(new Date()).getFullYear()}, ${themeConfig.siteName}`, feedLinks: { atom: urlJoin(themeConfig.domain, feedFilename), }, }) const postsData = this.postsData .filter((item: IPostRenderData) => !item.hideInList) .slice(0, themeConfig.feedCount || DEFAULT_FEED_COUNT) const feedFullText = (typeof themeConfig.feedFullText) === 'undefined' ? true : themeConfig.feedFullText postsData.forEach((post: IPostRenderData) => { feed.addItem({ title: post.title, id: post.link, link: post.link, description: post.abstract, content: feedFullText ? post.content : post.abstract, image: post.feature, date: new Date(post.date), }) }) fs.writeFileSync(urlJoin(this.outputDir, feedFilename), feed.atom1()) } /** * Copy file to output folder */ async copyFiles() { const postImageInputPath = urlJoin(this.appDir, 'post-images') const postImageOutputPath = urlJoin(this.outputDir, 'post-images') fse.ensureDirSync(postImageOutputPath) fse.copySync(postImageInputPath, postImageOutputPath) const imagesInputPath = urlJoin(this.appDir, 'images') const imagesOutputPath = urlJoin(this.outputDir, 'images') fse.ensureDirSync(imagesOutputPath) fse.copySync(imagesInputPath, imagesOutputPath) const mediaInputPath = urlJoin(this.themePath, 'assets', 'media') const mediaOutputPath = urlJoin(this.outputDir, 'media') fse.ensureDirSync(mediaInputPath) fse.copySync(mediaInputPath, mediaOutputPath) // Copy /static const staticFilesPath = urlJoin(this.appDir, 'static') if (fse.existsSync(staticFilesPath)) { fse.copySync(staticFilesPath, this.outputDir) } // Copy favicon.ico const faviconInputPath = urlJoin(this.appDir, 'favicon.ico') if (fse.existsSync(faviconInputPath)) { fse.copyFileSync(faviconInputPath, urlJoin(this.outputDir, 'favicon.ico')) } } async clearOutputFolder() { try { fse.emptyDirSync(this.outputDir) } catch (e) { console.log('Delete file error', e) } } } ================================================ FILE: src/server/setting.ts ================================================ import * as fse from 'fs-extra' import * as path from 'path' import Model from './model' import { ICommentSetting } from './interfaces/setting' import { ISetting } from '../interfaces/setting' export default class Setting extends Model { getSetting() { const setting = this.$setting.get('config').value() return setting } getGitalkSetting() { const setting = this.$setting.get('gitalk').value() return setting } getCommentSetting() { const setting = this.$setting.get('comment').value() return setting } public async saveSetting(setting: ISetting) { await this.$setting.set('config', setting).write() return true } public async saveCommentSetting(setting: ICommentSetting) { await this.$setting.set('comment', setting).write() return true } async uploadFavicon(filePath: string) { const faviconPath = path.join(this.appDir, 'favicon.ico') fse.copySync(filePath, faviconPath) } async uploadAvatar(filePath: string) { const avatarPath = path.join(this.appDir, 'images/avatar.png') fse.copySync(filePath, avatarPath) } } ================================================ FILE: src/server/tags.ts ================================================ import shortid from 'shortid' import Model from './model' import { ITag } from './interfaces/tag' import slug from '../helpers/slug' import { UrlFormats } from '../helpers/enums' export default class Tags extends Model { public async saveTags() { const posts = this.$posts.get('posts').value() let list: any = [] posts.forEach((post: any) => { if (Array.isArray(post.data.tags)) { list = list.concat(post.data.tags) } }) list = Array.from(new Set([...list])) const themeConfig = await this.$theme.get('config').value() const tagUrlFormat = themeConfig.tagUrlFormat || UrlFormats.Slug let existUsedTags = this.$posts.get('tags').filter({ used: true }).value() // If you delete an article after using a tag, there may be a tag unused state. existUsedTags = existUsedTags.map((tag: ITag) => { return { ...tag, used: list.includes(tag.name), } }) const unusedTags = this.$posts.get('tags').filter({ used: false }).value() // The tag of the imported article is newly used const newUsedTags = list .filter((item: any) => !existUsedTags.find((tag: ITag) => tag.name === item)) .map((item: any) => { const foundItem = unusedTags.find((tag: ITag) => tag.name === item) if (foundItem) { // remove from unusedTags const foundItemIndex = unusedTags.indexOf(foundItem) unusedTags.splice(foundItemIndex, 1) return { ...foundItem, used: true, } } return { name: item, slug: tagUrlFormat === UrlFormats.Slug ? slug(item) : shortid.generate(), used: true, } }) const tags = [...newUsedTags, ...existUsedTags, ...unusedTags] this.$posts.set('tags', tags).write() } async list() { await this.saveTags() const tags = await this.$posts.get('tags').value() return tags } public async saveTag(tag: ITag) { const tags = await this.$posts.get('tags').value() if (typeof tag.index === 'number' && tag.index >= 0) { tags[tag.index] = tag } else { tags.push(tag) } await this.$posts.set('tags', tags).write() return tags } public async deleteTag(tagValue: string) { const tag = await this.$posts.get('tags').remove({ name: tagValue }).write() return tag } } ================================================ FILE: src/server/theme.ts ================================================ import * as path from 'path' import * as fse from 'fs-extra' import junk from 'junk' import Model from './model' import { ITheme } from './interfaces/theme' export default class Theme extends Model { themeDir: string themeList: string[] themeConfig: any currentThemePath = '' constructor(appInstance: any) { super(appInstance) this.themeDir = path.join(this.appDir, 'themes') this.themeConfig = {} this.themeList = [] } /** * Get the theme list */ async getThemeList() { let themes = await fse.readdir(this.themeDir) themes = themes.filter(junk.not) const result = await Promise.all(themes.map(async (item: string) => { const data = { folder: item, name: item, version: '', author: '', repository: '', } const themeConfigPath = path.join(this.themeDir, item, 'config.json') if (fse.existsSync(themeConfigPath)) { const config = fse.readJSONSync(themeConfigPath) data.name = config.name data.version = config.version data.author = config.repository data.repository = config.repository } return data })) return result } /** * Get the theme configuration */ async getThemeConfig() { this.themeConfig = await this.$theme.get('config').value() this.currentThemePath = path.join(this.appDir, 'themes', this.themeConfig.themeName) return this.themeConfig } /** * Save the theme configuration */ public async saveThemeConfig(themeConfig: ITheme) { await this.$theme.set('config', themeConfig).write() // If there is a backup of the custom configuration, copy the backup to the custom configuration const themeConfigBackupPath = path.join(this.appDir, 'config', `theme.${themeConfig.themeName}.config.json`) const existThemeConfigBackupFile = await fse.pathExists(themeConfigBackupPath) if (existThemeConfigBackupFile) { const config = fse.readJSONSync(themeConfigBackupPath) await this.$theme.set('customConfig', config).write() } else { await this.$theme.set('customConfig', {}).write() } return themeConfig } /** * Save the theme custom configuration */ public async saveThemeCustomConfig(config: any) { if (Object.keys(config).length > 0) { // Save the picture type configuration const toPath = path.join(this.appDir, 'themes', this.db.themeConfig.themeName, 'assets', 'media', 'images') const includedArrayTypeImages: string[] = [] for (const configItem of this.db.currentThemeConfig) { const configValue = config[configItem.name] // Picture upload config type data need to upload image to folder if (configItem.type === 'picture-upload') { if ( typeof configValue === 'string' && configValue !== configItem.value && !configValue.startsWith('/media/') ) { const extendName = configValue.split('.').pop() const fileName = `custom-${configItem.name}.${extendName}` fse.ensureDirSync(toPath) fse.copySync(configValue, path.join(toPath, fileName)) // Change value to finally value config[configItem.name] = path.join('/', 'media', 'images', fileName) } else if (typeof configValue === 'undefined' || configValue === configItem.value) { const currentConfigValue = this.db.themeCustomConfig[configItem.name] if (currentConfigValue && currentConfigValue !== configItem.value) { const extendName = this.db.themeCustomConfig[configItem.name].split('.').pop() const fileName = `custom-${configItem.name}.${extendName}` fse.removeSync(path.join(toPath, fileName)) } } } // Array config type data need to find image config to upload folder if (configItem.type === 'array') { for (let arrItemIndex = 0; arrItemIndex < configValue.length; arrItemIndex += 1) { const foundConfigItem = this.db.currentThemeConfig.find((i: any) => i.name === configItem.name) const arrayItemKeys = Object.keys(configValue[arrItemIndex]) for (let keyIndex = 0; keyIndex < arrayItemKeys.length; keyIndex += 1) { const key = arrayItemKeys[keyIndex] const foundPictureTypeField = foundConfigItem.arrayItems.find((i: any) => i.name === key && i.type === 'picture-upload') if (foundPictureTypeField) { const fieldValue = configValue[arrItemIndex][key] if ( typeof fieldValue === 'string' && fieldValue !== foundPictureTypeField.value && !fieldValue.startsWith('/media/') ) { const extendName = fieldValue.split('.').pop() const fileName = `custom-array-${configItem.name}-${new Date().getTime()}-${key}.${extendName}` fse.ensureDirSync(toPath) fse.copySync(fieldValue, path.join(toPath, fileName)) // Change value to finally value configValue[arrItemIndex][key] = path.join('/', 'media', 'images', fileName) includedArrayTypeImages.push(configValue[arrItemIndex][key]) } else if (typeof fieldValue === 'undefined' || fieldValue === foundPictureTypeField.value) { console.log('run...') } else { includedArrayTypeImages.push(fieldValue) } } } } } } // Remove unused array type config images const assetsFolderPath = path.join(this.appDir, 'themes', this.db.themeConfig.themeName, 'assets') const imagesFolderPath = path.join(assetsFolderPath, 'media', 'images') if (fse.existsSync(imagesFolderPath)) { const files = await fse.readdirSync(imagesFolderPath, { withFileTypes: true }) const arrayTypeImages = files .filter(item => !item.isDirectory()) .map(item => path.join('/', 'media', 'images', item.name)) .filter(item => item.includes('custom-array')) arrayTypeImages.forEach((name: string) => { if (!includedArrayTypeImages.includes(name)) { fse.removeSync(path.join(assetsFolderPath, name)) } }) } } await this.$theme.set('customConfig', config).write() // Backup theme custom config const themeConfigBackupPath = path.join(this.appDir, 'config', `theme.${this.db.themeConfig.themeName}.config.json`) fse.writeJSONSync(themeConfigBackupPath, config) return config } /** * Get the theme custom configuration */ public async getThemeCustomConfig() { const config = await this.$theme.get('customConfig').value() return config } /** * Get current theme custom configuration */ public async getCurrentThemeCustomConfig() { const themeConfigPath = path.join(this.currentThemePath, 'config.json') const existThemeConfigFile = await fse.pathExists(themeConfigPath) if (existThemeConfigFile) { const themeConfig = fse.readJSONSync(themeConfigPath) if (themeConfig && themeConfig.customConfig) { return themeConfig.customConfig } } return [] } } ================================================ FILE: src/server.ts ================================================ import express from 'express' export default function initServer() { const app = express() let server: any = null function listen(port: number) { server = app.listen(port, 'localhost').on('error', (err: NodeJS.ErrnoException) => { if (err) { if (err.message === 'getaddrinfo ENOTFOUND localhost') { // Fixed: 修复渲染服务在找不到 localhost 时崩溃问题 console.log('\x1B[31m%s\x1B[0m', 'Localhost is not found so that the preview server is not working. Please check your hosts file and then restart the application.') } else { console.log(`Preview server port ${port} is busy, trying with port ${port + 1}`) listen(port + 1) } } }).on('listening', () => { app.set('port', port) console.log(`Preview server is running on port : ${port}`) }) } listen(4000) return { server, app, } } ================================================ FILE: src/shims-tsx.d.ts ================================================ import Vue, { VNode } from 'vue' declare global { namespace JSX { // tslint:disable no-empty-interface interface Element extends VNode {} // tslint:disable no-empty-interface interface ElementClass extends Vue {} interface IntrinsicElements { [elem: string]: any } } } ================================================ FILE: src/shims-vue.d.ts ================================================ declare module '*.vue' { import { ComponentOptions } from 'vue' export default Vue } declare module '*.json' { const data: any export default data } declare module 'vue2-transitions' declare module '@iktakahiro/markdown-it-katex' declare module 'markdown-it-toc-and-anchor' declare module 'markdown-it-task-lists' declare module 'markdown-it-abbr' declare module 'markdown-it-footnote' declare module 'markdown-it-mark' declare module 'markdown-it-sub' declare module 'markdown-it-sup' declare module 'markdown-it-imsize' declare module 'markdown-it-emoji' declare module 'markdown-it-implicit-figures' declare module 'markdown-it-image-lazy-loading' declare module 'electron-google-analytics' declare module 'macaddress' declare module 'v-emoji-picker' declare module 'vue-shortkey' declare module 'easy-ftp' declare module 'node-ssh' declare module 'vuedraggable' { export interface DraggedContext { index: number; futureIndex: number; element: T; } export interface DropContext { index: number; component: Vue; element: T; } export interface Rectangle { top: number; right: number; bottom: number; left: number; width: number; height: number; } export interface MoveEvent { originalEvent: DragEvent; dragged: Element; draggedContext: DraggedContext; draggedRect: Rectangle; related: Element; relatedContext: DropContext; relatedRect: Rectangle; from: Element; to: Element; willInsertAfter: boolean; isTrusted: boolean; } const draggableComponent: ComponentOptions export default draggableComponent } ================================================ FILE: src/shims.d.ts ================================================ import { AllElectron } from 'electron' declare module 'vue/types/vue' { interface Vue { readonly $electron: AllElectron, $bus: any, } } ================================================ FILE: src/store/index.ts ================================================ import Vue from 'vue' import Vuex from 'vuex' import site from './modules/site' Vue.use(Vuex) export default new Vuex.Store({ modules: { site, }, strict: process.env.NODE_ENV !== 'production', }) ================================================ FILE: src/store/modules/site.ts ================================================ import { ActionTree, Module, MutationTree } from 'vuex' import { IPost } from '../../interfaces/post' import { ITag } from '../../interfaces/tag' import { ITheme } from '../../interfaces/theme' import { IMenu } from '../../interfaces/menu' import { ISetting, ICommentSetting } from '../../interfaces/setting' import { DEFAULT_POST_PAGE_SIZE, DEFAULT_ARCHIVES_PAGE_SIZE, DEFAULT_FEED_COUNT, DEFAULT_ARCHIVES_PATH, DEFAULT_POST_PATH, DEFAULT_TAG_PATH, } from '../../helpers/constants' export interface Site { appDir: string config: any posts: IPost[] tags: ITag[] menus: IMenu[] themeConfig: ITheme themeCustomConfig: any currentThemeConfig: any themes: string[] setting: ISetting commentSetting: ICommentSetting } const siteState: Site = { appDir: '', config: {}, posts: [], tags: [], menus: [], themeConfig: { themeName: '', postPageSize: DEFAULT_POST_PAGE_SIZE, archivesPageSize: DEFAULT_ARCHIVES_PAGE_SIZE, siteName: '', siteDescription: '', footerInfo: 'Powered by Gridea', showFeatureImage: true, postUrlFormat: 'SLUG', tagUrlFormat: 'SLUG', dateFormat: 'YYYY-MM-DD', feedCount: DEFAULT_FEED_COUNT, feedFullText: true, archivesPath: DEFAULT_ARCHIVES_PATH, postPath: DEFAULT_POST_PATH, tagPath: DEFAULT_TAG_PATH, }, themeCustomConfig: {}, currentThemeConfig: {}, themes: [], setting: { platform: 'github', domain: '', repository: '', branch: '', username: '', email: '', tokenUsername: '', token: '', cname: '', port: '22', server: '', password: '', privateKey: '', remotePath: '', proxyPath: '', proxyPort: '', enabledProxy: 'direct', netlifySiteId: '', netlifyAccessToken: '', }, commentSetting: { showComment: false, commentPlatform: 'gitalk', gitalkSetting: { clientId: '', clientSecret: '', repository: '', owner: '', }, disqusSetting: { api: '', apikey: '', shortname: '', }, }, } const mutations: MutationTree = { updateSite(state, siteData: Site) { console.log('data', siteData) state.appDir = siteData.appDir state.posts = siteData.posts state.tags = siteData.tags state.menus = siteData.menus state.config = siteData.config state.themeConfig = siteData.themeConfig state.themeConfig.postUrlFormat = siteData.themeConfig.postUrlFormat || 'SLUG' state.themeConfig.tagUrlFormat = siteData.themeConfig.tagUrlFormat || 'SLUG' state.themeConfig.dateFormat = siteData.themeConfig.dateFormat || 'YYYY-MM-DD' state.themeConfig.postPageSize = siteData.themeConfig.postPageSize || DEFAULT_POST_PAGE_SIZE state.themeConfig.archivesPageSize = siteData.themeConfig.archivesPageSize || DEFAULT_ARCHIVES_PAGE_SIZE state.themeConfig.feedCount = siteData.themeConfig.feedCount || DEFAULT_FEED_COUNT state.themeConfig.feedFullText = (typeof siteData.themeConfig.feedFullText) === 'undefined' ? true : siteData.themeConfig.feedFullText // from > 0.8.0 state.themes = siteData.themes state.setting = siteData.setting state.commentSetting = siteData.commentSetting state.themeCustomConfig = siteData.themeCustomConfig state.currentThemeConfig = siteData.currentThemeConfig }, updatePosts(state, posts: IPost[]) { state.posts = posts }, } const actions: ActionTree = { updatePosts({ commit }, posts: IPost[]) { commit('updatePosts', posts) }, updateSite({ commit }, siteData: Site) { console.log('siteData:', siteData) commit('updateSite', siteData) }, } const module: Module = { namespaced: true, state: siteState, mutations, actions, } export default module ================================================ FILE: src/views/article/ArticleUpdate.vue ================================================ ================================================ FILE: src/views/article/Articles.vue ================================================ ================================================ FILE: src/views/loading/Index.vue ================================================ ================================================ FILE: src/views/menu/Index.vue ================================================ ================================================ FILE: src/views/setting/Index.vue ================================================ ================================================ FILE: src/views/setting/includes/BasicSetting.vue ================================================ ================================================ FILE: src/views/setting/includes/CommentSetting.vue ================================================ ================================================ FILE: src/views/setting/includes/DisqusSetting.vue ================================================ ================================================ FILE: src/views/setting/includes/GitalkSetting.vue ================================================ ================================================ FILE: src/views/tags/Index.vue ================================================ ================================================ FILE: src/views/theme/Index.vue ================================================ ================================================ FILE: src/views/theme/includes/AvatarSetting.vue ================================================ ================================================ FILE: src/views/theme/includes/BasicSetting.vue ================================================ ================================================ FILE: src/views/theme/includes/CustomSetting.vue ================================================ ================================================ FILE: src/views/theme/includes/FaviconSetting.vue ================================================ ================================================ FILE: src/vue-bus.ts ================================================ import _Vue from 'vue' declare module 'vue/types/vue' { interface Vue { $bus: any } } class VueBus { static install(Vue: any, options: any) { const bus = new Vue() Vue.bus = bus Vue.prototype.$bus = bus } } // eslint-disable-next-line if ('Vue' in window) { _Vue.use(VueBus) } export default VueBus ================================================ FILE: tailwind.config.js ================================================ module.exports = { prefix: '', important: false, separator: ':', theme: { screens: { sm: '640px', md: '768px', lg: '1024px', xl: '1280px', }, colors: { transparent: 'transparent', black: '#000', white: '#fff', gray: { 100: '#FAFAFA', 200: '#EAEAEA', 300: '#999', 400: '#888', 500: '#666', 600: '#444', 700: '#333', 800: '#111', 900: '#000', }, red: { 100: '#fff5f5', 200: '#fed7d7', 300: '#feb2b2', 400: '#fc8181', 500: '#f56565', 600: '#e53e3e', 700: '#c53030', 800: '#9b2c2c', 900: '#742a2a', }, orange: { 100: '#fffaf0', 200: '#feebc8', 300: '#fbd38d', 400: '#f6ad55', 500: '#ed8936', 600: '#dd6b20', 700: '#c05621', 800: '#9c4221', 900: '#7b341e', }, yellow: { 100: '#fffff0', 200: '#fefcbf', 300: '#faf089', 400: '#f6e05e', 500: '#ecc94b', 600: '#d69e2e', 700: '#b7791f', 800: '#975a16', 900: '#744210', }, green: { 100: '#f0fff4', 200: '#c6f6d5', 300: '#9ae6b4', 400: '#68d391', 500: '#48bb78', 600: '#38a169', 700: '#2f855a', 800: '#276749', 900: '#22543d', }, teal: { 100: '#e6fffa', 200: '#b2f5ea', 300: '#81e6d9', 400: '#4fd1c5', 500: '#38b2ac', 600: '#319795', 700: '#2c7a7b', 800: '#285e61', 900: '#234e52', }, blue: { 100: '#ebf8ff', 200: '#bee3f8', 300: '#90cdf4', 400: '#63b3ed', 500: '#4299e1', 600: '#3182ce', 700: '#2b6cb0', 800: '#2c5282', 900: '#2a4365', }, indigo: { 100: '#ebf4ff', 200: '#c3dafe', 300: '#a3bffa', 400: '#7f9cf5', 500: '#667eea', 600: '#5a67d8', 700: '#4c51bf', 800: '#434190', 900: '#3c366b', }, purple: { 100: '#faf5ff', 200: '#e9d8fd', 300: '#d6bcfa', 400: '#b794f4', 500: '#9f7aea', 600: '#805ad5', 700: '#6b46c1', 800: '#553c9a', 900: '#44337a', }, pink: { 100: '#fff5f7', 200: '#fed7e2', 300: '#fbb6ce', 400: '#f687b3', 500: '#ed64a6', 600: '#d53f8c', 700: '#b83280', 800: '#97266d', 900: '#702459', }, }, spacing: { px: '1px', '0': '0', '1': '0.25rem', '2': '0.5rem', '3': '0.75rem', '4': '1rem', '5': '1.25rem', '6': '1.5rem', '8': '2rem', '10': '2.5rem', '12': '3rem', '16': '4rem', '20': '5rem', '24': '6rem', '32': '8rem', '40': '10rem', '48': '12rem', '56': '14rem', '64': '16rem', }, backgroundColor: theme => theme('colors'), backgroundPosition: { bottom: 'bottom', center: 'center', left: 'left', 'left-bottom': 'left bottom', 'left-top': 'left top', right: 'right', 'right-bottom': 'right bottom', 'right-top': 'right top', top: 'top', }, backgroundSize: { auto: 'auto', cover: 'cover', contain: 'contain', }, borderColor: theme => ({ ...theme('colors'), default: theme('colors.gray.300', 'currentColor'), }), borderRadius: { none: '0', sm: '0.125rem', default: '0.25rem', lg: '0.5rem', full: '9999px', }, borderWidth: { default: '1px', '0': '0', '2': '2px', '4': '4px', '8': '8px', }, boxShadow: { default: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)', inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)', outline: '0 0 0 3px rgba(66, 153, 225, 0.5)', none: 'none', }, container: {}, cursor: { auto: 'auto', default: 'default', pointer: 'pointer', wait: 'wait', text: 'text', move: 'move', 'not-allowed': 'not-allowed', }, fill: { current: 'currentColor', }, flex: { '1': '1 1 0%', auto: '1 1 auto', initial: '0 1 auto', none: 'none', }, flexGrow: { '0': '0', default: '1', }, flexShrink: { '0': '0', default: '1', }, fontFamily: { sans: [ '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"', ], serif: [ 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif', ], mono: [ 'Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace', ], }, fontSize: { xs: '0.75rem', sm: '0.875rem', base: '1rem', lg: '1.125rem', xl: '1.25rem', '2xl': '1.5rem', '3xl': '1.875rem', '4xl': '2.25rem', '5xl': '3rem', '6xl': '4rem', }, fontWeight: { hairline: '100', thin: '200', light: '300', normal: '400', medium: '500', semibold: '600', bold: '700', extrabold: '800', black: '900', }, height: theme => ({ auto: 'auto', ...theme('spacing'), full: '100%', screen: '100vh', }), inset: { '0': '0', auto: 'auto', }, letterSpacing: { tighter: '-0.05em', tight: '-0.025em', normal: '0', wide: '0.025em', wider: '0.05em', widest: '0.1em', }, lineHeight: { none: '1', tight: '1.25', snug: '1.375', normal: '1.5', relaxed: '1.625', loose: '2', }, listStyleType: { none: 'none', disc: 'disc', decimal: 'decimal', }, margin: (theme, { negative }) => ({ auto: 'auto', ...theme('spacing'), ...negative(theme('spacing')), }), maxHeight: { full: '100%', screen: '100vh', }, maxWidth: { xs: '20rem', sm: '24rem', md: '28rem', lg: '32rem', xl: '36rem', '2xl': '42rem', '3xl': '48rem', '4xl': '56rem', '5xl': '64rem', '6xl': '72rem', full: '100%', }, minHeight: { '0': '0', full: '100%', screen: '100vh', }, minWidth: { '0': '0', full: '100%', }, objectPosition: { bottom: 'bottom', center: 'center', left: 'left', 'left-bottom': 'left bottom', 'left-top': 'left top', right: 'right', 'right-bottom': 'right bottom', 'right-top': 'right top', top: 'top', }, opacity: { '0': '0', '25': '0.25', '50': '0.5', '75': '0.75', '100': '1', }, order: { first: '-9999', last: '9999', none: '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', '10': '10', '11': '11', '12': '12', }, padding: theme => theme('spacing'), placeholderColor: theme => theme('colors'), stroke: { current: 'currentColor', }, textColor: theme => theme('colors'), width: theme => ({ auto: 'auto', ...theme('spacing'), '1/2': '50%', '1/3': '33.333333%', '2/3': '66.666667%', '1/4': '25%', '2/4': '50%', '3/4': '75%', '1/5': '20%', '2/5': '40%', '3/5': '60%', '4/5': '80%', '1/6': '16.666667%', '2/6': '33.333333%', '3/6': '50%', '4/6': '66.666667%', '5/6': '83.333333%', '1/12': '8.333333%', '2/12': '16.666667%', '3/12': '25%', '4/12': '33.333333%', '5/12': '41.666667%', '6/12': '50%', '7/12': '58.333333%', '8/12': '66.666667%', '9/12': '75%', '10/12': '83.333333%', '11/12': '91.666667%', full: '100%', screen: '100vw', }), zIndex: { auto: 'auto', '0': '0', '10': '10', '20': '20', '30': '30', '40': '40', '50': '50', }, }, variants: { accessibility: ['responsive', 'focus'], alignContent: ['responsive'], alignItems: ['responsive'], alignSelf: ['responsive'], appearance: ['responsive'], backgroundAttachment: ['responsive'], backgroundColor: ['responsive', 'hover', 'focus'], backgroundPosition: ['responsive'], backgroundRepeat: ['responsive'], backgroundSize: ['responsive'], borderCollapse: ['responsive'], borderColor: ['responsive', 'hover', 'focus'], borderRadius: ['responsive'], borderStyle: ['responsive'], borderWidth: ['responsive'], boxShadow: ['responsive', 'hover', 'focus'], cursor: ['responsive'], display: ['responsive'], fill: ['responsive'], flex: ['responsive'], flexDirection: ['responsive'], flexGrow: ['responsive'], flexShrink: ['responsive'], flexWrap: ['responsive'], float: ['responsive'], fontFamily: ['responsive'], fontSize: ['responsive'], fontSmoothing: ['responsive'], fontStyle: ['responsive'], fontWeight: ['responsive', 'hover', 'focus'], height: ['responsive'], inset: ['responsive'], justifyContent: ['responsive'], letterSpacing: ['responsive'], lineHeight: ['responsive'], listStylePosition: ['responsive'], listStyleType: ['responsive'], margin: ['responsive'], maxHeight: ['responsive'], maxWidth: ['responsive'], minHeight: ['responsive'], minWidth: ['responsive'], objectFit: ['responsive'], objectPosition: ['responsive'], opacity: ['responsive', 'hover', 'focus'], order: ['responsive'], outline: ['responsive', 'focus'], overflow: ['responsive'], padding: ['responsive'], placeholderColor: ['responsive', 'focus'], pointerEvents: ['responsive'], position: ['responsive'], resize: ['responsive'], stroke: ['responsive'], tableLayout: ['responsive'], textAlign: ['responsive'], textColor: ['responsive', 'hover', 'focus'], textDecoration: ['responsive', 'hover', 'focus'], textTransform: ['responsive'], userSelect: ['responsive'], verticalAlign: ['responsive'], visibility: ['responsive'], whitespace: ['responsive'], width: ['responsive'], wordBreak: ['responsive'], zIndex: ['responsive'], }, corePlugins: {}, plugins: [], } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "module": "esnext", "strict": true, "jsx": "preserve", "importHelpers": true, "moduleResolution": "node", "experimentalDecorators": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "sourceMap": true, "baseUrl": ".", "skipLibCheck": true, "allowJs": true, "types": [ "webpack-env" ], "paths": { "@/*": [ "src/*" ], "*": ["node_modules/*"] }, "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ] }, "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx" ], "exclude": [ "node_modules", "node_modules/gray-matter/gray-matter.d.ts" ] } ================================================ FILE: vue.config.js ================================================ const path = require('path') function resolve(dir) { return path.join(__dirname, dir) } module.exports = { css: { loaderOptions: { less: { import: [ resolve('src/assets/styles/var.less'), ], modifyVars: { 'btn-height-base': '30px', 'input-height-base': '30px', }, javascriptEnabled: true, }, }, }, pluginOptions: { electronBuilder: { nodeIntegration: true, builderOptions: { productName: 'Gridea', win: { icon: './public/app-icons/gridea.ico', // target: [ // { // target: 'nsis', // arch: [ // 'ia32', // 'x64', // ], // }, // ], }, mac: { icon: './public/app-icons/gridea.icns', }, linux: { icon: './public/app-icons/gridea.png', target: [ { target: 'AppImage', }, { target: 'deb', }, { target: 'snap', }, ], }, asar: false, nsis: { oneClick: false, // 是否一键安装 allowElevation: true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。 allowToChangeInstallationDirectory: true, // 允许修改安装目录 createDesktopShortcut: true, // 创建桌面图标 createStartMenuShortcut: true, // 创建开始菜单图标 shortcutName: 'Gridea', // 图标名称 }, publish: ['github'], }, // mainProcessWatch: [ // 'src/server/**/*', // ], }, }, }