style: add prettier and format code

This commit is contained in:
Daniil Zhuravlev
2025-07-13 01:44:53 +04:00
parent dcdd7c594b
commit 01a595b78f
105 changed files with 8691 additions and 8864 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: Deploy
on:
schedule:
- cron: '0 0 * * *'
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
+5 -5
View File
@@ -2,14 +2,14 @@
"line-length": false,
"link-fragments": false,
"no-inline-html": {
"allowed_elements": ["Badge", "ul", "ol", "li", "small", "sub", "sup", "br"]
"allowed_elements": ["Badge", "ul", "ol", "li", "small", "sub", "sup", "br"],
},
"fenced-code-language": false,
"no-duplicate-heading": {
"allow_different_nesting": true,
"siblings_only": true
"siblings_only": true,
},
"single-title": {
"front_matter_title": ""
}
}
"front_matter_title": "",
},
}
+2
View File
@@ -0,0 +1,2 @@
package-lock.json
pnpm-lock.yml
+179 -181
View File
@@ -1,46 +1,44 @@
import type { DefaultTheme, LocaleConfig } from 'vitepress'
import type { CustomConfig, Theme } from '../theme/types'
import { sections } from '../theme/plugins/section'
import 'dotenv/config'
import { resolveBaseUrl } from '../theme/utils'
import type { DefaultTheme, LocaleConfig } from "vitepress";
import type { CustomConfig, Theme } from "../theme/types";
import { sections } from "../theme/plugins/section";
import "dotenv/config";
import { resolveBaseUrl } from "../theme/utils";
export const SITE_NAME = 'Kotatsu Website'
export const META_DESCRIPTION = 'A simple and convenient open source manga reader from and for the community, where you can find and read your favorite manga easier than ever.'
export const SITE_NAME = "Kotatsu Website";
export const META_DESCRIPTION = "A simple and convenient open source manga reader from and for the community, where you can find and read your favorite manga easier than ever.";
export const searchLocale: Record<string, Partial<Omit<DefaultTheme.LocalSearchOptions, 'locales'>>> = {
export const searchLocale: Record<string, Partial<Omit<DefaultTheme.LocalSearchOptions, "locales">>> = {
root: {
translations: {
button: {
buttonText: 'Search',
buttonAriaLabel: 'Search'
buttonText: "Search",
buttonAriaLabel: "Search",
},
modal: {
resetButtonTitle: 'Reset search',
backButtonTitle: 'Close search',
noResultsText: 'No results for',
resetButtonTitle: "Reset search",
backButtonTitle: "Close search",
noResultsText: "No results for",
footer: {
selectText: 'to select',
selectKeyAriaLabel: 'enter',
navigateText: 'to navigate',
navigateUpKeyAriaLabel: 'up arrow',
navigateDownKeyAriaLabel: 'down arrow',
closeText: 'to close',
closeKeyAriaLabel: 'escape'
}
}
}
selectText: "to select",
selectKeyAriaLabel: "enter",
navigateText: "to navigate",
navigateUpKeyAriaLabel: "up arrow",
navigateDownKeyAriaLabel: "down arrow",
closeText: "to close",
closeKeyAriaLabel: "escape",
},
},
},
},
}
};
export const config: LocaleConfig<Theme.Config> = {
root: {
label: 'English',
lang: 'en',
label: "English",
lang: "en",
description: META_DESCRIPTION,
head: [
['meta', { property: 'og:site_name', content: SITE_NAME }],
],
head: [["meta", { property: "og:site_name", content: SITE_NAME }]],
themeConfig: {
nav: getNav(),
@@ -48,43 +46,43 @@ export const config: LocaleConfig<Theme.Config> = {
"/": defaultSidebar(),
},
outline: {
label: 'On this page',
level: 'deep',
label: "On this page",
level: "deep",
},
docFooter: {
prev: 'Previous page',
next: 'Next page',
prev: "Previous page",
next: "Next page",
},
editLink: {
pattern: 'https://github.com/KotatsuApp/website/edit/main/website/:path',
text: 'Suggest changes to this page',
pattern: "https://github.com/KotatsuApp/website/edit/main/website/:path",
text: "Suggest changes to this page",
},
footer: resolveBaseUrl(getFooter(), ''),
sections
footer: resolveBaseUrl(getFooter(), ""),
sections,
},
}
}
},
};
function defaultSidebar(): DefaultTheme.SidebarItem[] {
return [
{
return [
{
text: "General sections",
items: [
{
text: "Download",
link: "/download/",
},
/* {
items: [
{
text: "Download",
link: "/download/",
},
/* {
text: "Parsers",
link: "/parsers/",
}, */
{
text: "Changelogs",
link: "/changelogs/",
},
],
},
/* {
{
text: "Changelogs",
link: "/changelogs/",
},
],
},
/* {
text: "Other projects",
items: [
{
@@ -95,27 +93,27 @@ function defaultSidebar(): DefaultTheme.SidebarItem[] {
}
]
}, */
{
text: "Frequently Asked Questions",
items: [
{ text: "General", link: "/manuals/faq/general/" },
{
text: "Feed",
link: "/manuals/faq/feed/",
},
{
text: "Explore",
collapsed: true,
items: [
{
text: "Sources",
link: "/manuals/faq/explore/sources/"
},
/* {
{
text: "Frequently Asked Questions",
items: [
{ text: "General", link: "/manuals/faq/general/" },
{
text: "Feed",
link: "/manuals/faq/feed/",
},
{
text: "Explore",
collapsed: true,
items: [
{
text: "Sources",
link: "/manuals/faq/explore/sources/",
},
/* {
text: "Local storage",
link: "/manuals/faq/explore/local-storage",
}, */
/* {
/* {
text: "Bookmarks",
link: "/manuals/faq/explore/bookmarks",
},
@@ -123,138 +121,138 @@ function defaultSidebar(): DefaultTheme.SidebarItem[] {
text: "Random",
link: "/manuals/faq/explore/random",
}, */
{
text: "Downloads",
link: "/manuals/faq/explore/downloads/",
},
/* {
{
text: "Downloads",
link: "/manuals/faq/explore/downloads/",
},
/* {
text: "Suggestions",
link: "/manuals/faq/explore/suggestions",
}, */
],
},
{
text: "Reader",
link: "/manuals/faq/reader/",
},
{
text: "Settings",
link: "/manuals/faq/settings/",
},
{
text: "Miscellaneous",
link: "/manuals/faq/miscellaneous/"
}
],
},
{
text: "Guides",
items: [
{
text: "Getting started",
link: "/manuals/guides/getting-started/",
},
{
text: "Troubleshooting",
link: "/manuals/guides/troubleshooting/",
collapsed: true,
items: [
{
text: "Common issues",
link: "/manuals/guides/troubleshooting/common-issues/",
},
{
text: "Diagnosis",
link: "/manuals/guides/troubleshooting/diagnosis/",
},
],
},
{ text: "Backups", link: "/manuals/guides/backups/" },
{ text: "Tracking", link: "/manuals/guides/tracking/" },
{ text: "Categories", link: "/manuals/guides/categories/" },
{
text: "Synchronization",
link: "/manuals/guides/synchronization/"
}
],
},
{
text: "For developers",
items: [
{
text: "Contribute",
link: "/dev/contribute/",
},
{
text: "Parsers library",
link: "/dev/parsers-library/",
},
{
text: "Synchronization server",
link: "/dev/sync-server/",
},
{
text: "External plugins SDK",
link: "/dev/plugin-sdk/",
},
]
}
]
],
},
{
text: "Reader",
link: "/manuals/faq/reader/",
},
{
text: "Settings",
link: "/manuals/faq/settings/",
},
{
text: "Miscellaneous",
link: "/manuals/faq/miscellaneous/",
},
],
},
{
text: "Guides",
items: [
{
text: "Getting started",
link: "/manuals/guides/getting-started/",
},
{
text: "Troubleshooting",
link: "/manuals/guides/troubleshooting/",
collapsed: true,
items: [
{
text: "Common issues",
link: "/manuals/guides/troubleshooting/common-issues/",
},
{
text: "Diagnosis",
link: "/manuals/guides/troubleshooting/diagnosis/",
},
],
},
{ text: "Backups", link: "/manuals/guides/backups/" },
{ text: "Tracking", link: "/manuals/guides/tracking/" },
{ text: "Categories", link: "/manuals/guides/categories/" },
{
text: "Synchronization",
link: "/manuals/guides/synchronization/",
},
],
},
{
text: "For developers",
items: [
{
text: "Contribute",
link: "/dev/contribute/",
},
{
text: "Parsers library",
link: "/dev/parsers-library/",
},
{
text: "Synchronization server",
link: "/dev/sync-server/",
},
{
text: "External plugins SDK",
link: "/dev/plugin-sdk/",
},
],
},
];
}
function getNav(): DefaultTheme.NavItem[] {
return [
{
text: 'Get v{app_version}',
text: "Get v{app_version}",
activeMatch: "^/*?(download|changelogs)/*?$",
items: [
{
text: "Download",
link: "/download/",
},
{
text: "Changelogs",
link: "/changelogs/",
},
{
text: "Download",
link: "/download/",
},
{
text: "Changelogs",
link: "/changelogs/",
},
],
},
{
text: "User manual",
link: "/manuals/guides/getting-started/",
activeMatch: "/manuals/",
text: "User manual",
link: "/manuals/guides/getting-started/",
activeMatch: "/manuals/",
},
/* {
/* {
text: "Desktop app",
link: "/desktop/",
activeMatch: "/desktop/",
}, */
/* {
/* {
text: "News",
link: "/news/",
activeMatch: "/news/"
} */
]
];
}
function getFooter(): CustomConfig['footer'] {
function getFooter(): CustomConfig["footer"] {
return {
qrcodeTitle: 'Telegram Group',
qrcodeMessage: 'Contact us on Telegram',
qrcodeLink: 'https://t.me/kotatsuapp',
navigation: [
{
title: 'Legal',
items: [
{
text: 'Privacy',
link: '/privacy/',
},
{
text: 'DMCA disclaimer',
link: '/dmca/',
},
],
},
],
}
qrcodeTitle: "Telegram Group",
qrcodeMessage: "Contact us on Telegram",
qrcodeLink: "https://t.me/kotatsuapp",
navigation: [
{
title: "Legal",
items: [
{
text: "Privacy",
link: "/privacy/",
},
{
text: "DMCA disclaimer",
link: "/dmca/",
},
],
},
],
};
}
+115 -115
View File
@@ -1,128 +1,128 @@
import type { HeadConfig, TransformContext } from "vitepress"
import type { DocsPageData } from '../../theme/plugins/section'
import type { HeadConfig, TransformContext } from "vitepress";
import type { DocsPageData } from "../../theme/plugins/section";
function generateMeta(context: TransformContext, hostname: string) {
const head: HeadConfig[] = []
const { pageData }: { pageData: DocsPageData } = context
const head: HeadConfig[] = [];
const { pageData }: { pageData: DocsPageData } = context;
const url = `${hostname}/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, "$2")}`
const url = `${hostname}/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, "$2")}`;
// Fonts
head.push(['link', { rel: 'preconnect', href: 'https://fonts.googleapis.com' }])
head.push(['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }])
head.push(['link', { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap' }])
// Fonts
head.push(["link", { rel: "preconnect", href: "https://fonts.googleapis.com" }]);
head.push(["link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }]);
head.push(["link", { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap" }]);
// Icons & manifest
head.push(['link', { rel: 'icon', href: '/favicon.ico?v=2', sizes: 'any' }])
head.push(['link', { rel: 'icon', href: '/logo-compact.svg?v=2', type: 'image/svg+xml' }])
head.push(['link', { rel: 'apple-touch-icon', href: '/apple-touch-icon.png?v=2' }])
head.push(['link', { rel: 'manifest', href: '/site.webmanifest' }])
head.push(["link", { rel: "icon", href: "/favicon.ico?v=2", sizes: "any" }]);
head.push(["link", { rel: "icon", href: "/logo-compact.svg?v=2", type: "image/svg+xml" }]);
head.push(["link", { rel: "apple-touch-icon", href: "/apple-touch-icon.png?v=2" }]);
head.push(["link", { rel: "manifest", href: "/site.webmanifest" }]);
// Canonical
head.push(["link", { rel: "canonical", href: url }])
head.push(["meta", { property: "og:url", content: url }])
head.push(["meta", { name: "twitter:url", content: url }])
head.push(["meta", { name: "twitter:card", content: "summary_large_image" }])
head.push(["link", { rel: "canonical", href: url }]);
head.push(["meta", { property: "og:url", content: url }]);
head.push(["meta", { name: "twitter:url", content: url }]);
head.push(["meta", { name: "twitter:card", content: "summary_large_image" }]);
if (pageData.frontmatter.theme) {
head.push(["meta", { name: "theme-color", content: pageData.frontmatter.theme }])
}
if (pageData.frontmatter.type) {
head.push(["meta", { property: "og:type", content: pageData.frontmatter.type }])
}
if (pageData.frontmatter.customMetaTitle) {
head.push([
"meta",
{
property: "og:title",
content: pageData.frontmatter.customMetaTitle,
},
])
head.push([
"meta",
{
name: "twitter:title",
content: pageData.frontmatter.customMetaTitle,
},
])
head.push(["meta", { property: "og:site_name", content: "" }])
} else {
head.push(["meta", { property: "og:title", content: pageData.frontmatter.title }])
head.push(["meta", { name: "twitter:title", content: pageData.frontmatter.title }])
}
if (pageData.frontmatter.description) {
head.push([
"meta",
{
property: "og:description",
content: pageData.frontmatter.description,
},
])
head.push([
"meta",
{
name: "twitter:description",
content: pageData.frontmatter.description,
},
])
}
if (pageData.frontmatter.image) {
head.push([
"meta",
{
property: "og:image",
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
},
])
head.push([
"meta",
{
name: "twitter:image",
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
},
])
} else {
const url = pageData.filePath.replace("index.md", "").replace(".md", "")
const imageUrl = `${url}/__og_image__/og.png`.replace(/\/\//g, "/").replace(/^\//, "")
if (pageData.frontmatter.theme) {
head.push(["meta", { name: "theme-color", content: pageData.frontmatter.theme }]);
}
if (pageData.frontmatter.type) {
head.push(["meta", { property: "og:type", content: pageData.frontmatter.type }]);
}
if (pageData.frontmatter.customMetaTitle) {
head.push([
"meta",
{
property: "og:title",
content: pageData.frontmatter.customMetaTitle,
},
]);
head.push([
"meta",
{
name: "twitter:title",
content: pageData.frontmatter.customMetaTitle,
},
]);
head.push(["meta", { property: "og:site_name", content: "" }]);
} else {
head.push(["meta", { property: "og:title", content: pageData.frontmatter.title }]);
head.push(["meta", { name: "twitter:title", content: pageData.frontmatter.title }]);
}
if (pageData.frontmatter.description) {
head.push([
"meta",
{
property: "og:description",
content: pageData.frontmatter.description,
},
]);
head.push([
"meta",
{
name: "twitter:description",
content: pageData.frontmatter.description,
},
]);
}
if (pageData.frontmatter.image) {
head.push([
"meta",
{
property: "og:image",
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
},
]);
head.push([
"meta",
{
name: "twitter:image",
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
},
]);
} else {
const url = pageData.filePath.replace("index.md", "").replace(".md", "");
const imageUrl = `${url}/__og_image__/og.png`.replace(/\/\//g, "/").replace(/^\//, "");
head.push(["meta", { property: "og:image", content: `${hostname}/${imageUrl}` }])
head.push(["meta", { property: "og:image:width", content: "1200" }])
head.push(["meta", { property: "og:image:height", content: "628" }])
head.push(["meta", { property: "og:image:type", content: "image/png" }])
head.push(["meta", { property: "og:image:alt", content: pageData.frontmatter.title }])
head.push(["meta", { name: "twitter:image", content: `${hostname}/${imageUrl}` }])
head.push(["meta", { name: "twitter:image:width", content: "1200" }])
head.push(["meta", { name: "twitter:image:height", content: "628" }])
head.push(["meta", { name: "twitter:image:alt", content: pageData.frontmatter.title }])
}
if (pageData.frontmatter.tag) {
head.push(["meta", { property: "article:tag", content: pageData.frontmatter.tag }])
}
if (pageData.frontmatter.date) {
head.push([
"meta",
{
property: "article:published_time",
content: pageData.frontmatter.date,
},
])
}
if (pageData.lastUpdated && pageData.frontmatter.lastUpdated !== false) {
head.push([
"meta",
{
property: "article:modified_time",
content: new Date(pageData.lastUpdated).toISOString(),
},
])
}
if (pageData.filePath === "manga/index.md") {
head.push(["meta", { property: "al:android:url", content: "kotatsu://manga" }])
head.push(["meta", { property: "al:android:app_name", content: "Kotatsu" }])
head.push(["meta", { property: "al:android:package", content: "org.koitharu.kotatsu" }])
}
head.push(["meta", { property: "og:image", content: `${hostname}/${imageUrl}` }]);
head.push(["meta", { property: "og:image:width", content: "1200" }]);
head.push(["meta", { property: "og:image:height", content: "628" }]);
head.push(["meta", { property: "og:image:type", content: "image/png" }]);
head.push(["meta", { property: "og:image:alt", content: pageData.frontmatter.title }]);
head.push(["meta", { name: "twitter:image", content: `${hostname}/${imageUrl}` }]);
head.push(["meta", { name: "twitter:image:width", content: "1200" }]);
head.push(["meta", { name: "twitter:image:height", content: "628" }]);
head.push(["meta", { name: "twitter:image:alt", content: pageData.frontmatter.title }]);
}
if (pageData.frontmatter.tag) {
head.push(["meta", { property: "article:tag", content: pageData.frontmatter.tag }]);
}
if (pageData.frontmatter.date) {
head.push([
"meta",
{
property: "article:published_time",
content: pageData.frontmatter.date,
},
]);
}
if (pageData.lastUpdated && pageData.frontmatter.lastUpdated !== false) {
head.push([
"meta",
{
property: "article:modified_time",
content: new Date(pageData.lastUpdated).toISOString(),
},
]);
}
if (pageData.filePath === "manga/index.md") {
head.push(["meta", { property: "al:android:url", content: "kotatsu://manga" }]);
head.push(["meta", { property: "al:android:app_name", content: "Kotatsu" }]);
head.push(["meta", { property: "al:android:package", content: "org.koitharu.kotatsu" }]);
}
return head
return head;
}
export default generateMeta
export default generateMeta;
+83 -89
View File
@@ -1,115 +1,109 @@
import { mkdir, readFile, writeFile } from "node:fs/promises"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { createContentLoader } from "vitepress"
import type { ContentData, SiteConfig } from "vitepress"
import { type SatoriOptions, satoriVue } from "x-satori/vue"
import { renderAsync } from "@resvg/resvg-js"
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createContentLoader } from "vitepress";
import type { ContentData, SiteConfig } from "vitepress";
import { type SatoriOptions, satoriVue } from "x-satori/vue";
import { renderAsync } from "@resvg/resvg-js";
const __dirname = dirname(fileURLToPath(import.meta.url))
const __fonts = resolve(__dirname, "../../fonts")
const __dirname = dirname(fileURLToPath(import.meta.url));
const __fonts = resolve(__dirname, "../../fonts");
async function generateOgImages(config: SiteConfig) {
const pages = await createContentLoader("/website/**/*.md", { excerpt: true }).load()
const template = await readFile(resolve(__dirname, "../../theme/components/OgImageTemplate.vue"), "utf-8")
const pages = await createContentLoader("/website/**/*.md", { excerpt: true }).load();
const template = await readFile(resolve(__dirname, "../../theme/components/OgImageTemplate.vue"), "utf-8");
const fonts: SatoriOptions["fonts"] = [
{
name: "Montserrat",
data: await readFile(resolve(__fonts, "Montserrat-Regular.ttf")),
weight: 400,
style: "normal",
},
{
name: "Montserrat",
data: await readFile(resolve(__fonts, "Montserrat-Medium.ttf")),
weight: 500,
style: "normal",
},
{
name: "Montserrat",
data: await readFile(resolve(__fonts, "Montserrat-Bold.ttf")),
weight: 700,
style: "normal",
},
]
const fonts: SatoriOptions["fonts"] = [
{
name: "Montserrat",
data: await readFile(resolve(__fonts, "Montserrat-Regular.ttf")),
weight: 400,
style: "normal",
},
{
name: "Montserrat",
data: await readFile(resolve(__fonts, "Montserrat-Medium.ttf")),
weight: 500,
style: "normal",
},
{
name: "Montserrat",
data: await readFile(resolve(__fonts, "Montserrat-Bold.ttf")),
weight: 700,
style: "normal",
},
];
const filteredPages = pages.filter((p) => p.frontmatter.image === undefined)
const filteredPages = pages.filter((p) => p.frontmatter.image === undefined);
for (const page of filteredPages) {
await generateImage({
page,
template,
outDir: config.outDir,
fonts,
})
}
for (const page of filteredPages) {
await generateImage({
page,
template,
outDir: config.outDir,
fonts,
});
}
}
export default generateOgImages
export default generateOgImages;
interface GenerateImagesOptions {
page: ContentData
template: string
outDir: string
fonts: SatoriOptions["fonts"]
page: ContentData;
template: string;
outDir: string;
fonts: SatoriOptions["fonts"];
}
type Dirs= "FAQ" | "Guide" | "News" | "Sandbox" | "Dev" | "Other"
type Dirs = "FAQ" | "Guide" | "News" | "Sandbox" | "Dev" | "Other";
function getDir(url: string): Dirs {
if (url.startsWith("/manuals/faq/")) {
return "FAQ"
}
if (url.startsWith("/manuals/guides/")) {
return "Guide"
}
if (url.startsWith("/news/") && url !== "/news/") {
return "News"
}
if (url.startsWith("/sandbox/")) {
return "Sandbox"
}
if (url.startsWith("/dev/")) {
return "Dev"
if (url.startsWith("/manuals/faq/")) {
return "FAQ";
}
if (url.startsWith("/manuals/guides/")) {
return "Guide";
}
if (url.startsWith("/news/") && url !== "/news/") {
return "News";
}
if (url.startsWith("/sandbox/")) {
return "Sandbox";
}
if (url.startsWith("/dev/")) {
return "Dev";
}
return "Other"
return "Other";
}
async function generateImage({ page, template, outDir, fonts }: GenerateImagesOptions) {
const { frontmatter, url } = page
const { frontmatter, url } = page;
const options: SatoriOptions = {
width: 1200,
height: 628,
fonts,
props: {
title:
frontmatter.layout === "home"
? frontmatter.main.name ?? frontmatter.title
: frontmatter.customMetaTitle ?? frontmatter.title,
description:
frontmatter.layout === "home"
? frontmatter.main.tagline ?? frontmatter.description
: frontmatter.description,
dir: getDir(url),
},
}
const options: SatoriOptions = {
width: 1200,
height: 628,
fonts,
props: {
title: frontmatter.layout === "home" ? (frontmatter.main.name ?? frontmatter.title) : (frontmatter.customMetaTitle ?? frontmatter.title),
description: frontmatter.layout === "home" ? (frontmatter.main.tagline ?? frontmatter.description) : frontmatter.description,
dir: getDir(url),
},
};
const svg = await satoriVue(options, template)
const svg = await satoriVue(options, template);
const render = await renderAsync(svg, {
fitTo: {
mode: "width",
value: 1200,
},
})
const render = await renderAsync(svg, {
fitTo: {
mode: "width",
value: 1200,
},
});
const outputFolder = resolve(outDir, url.substring(1), "__og_image__")
const outputFile = resolve(outputFolder, "og.png")
const outputFolder = resolve(outDir, url.substring(1), "__og_image__");
const outputFile = resolve(outputFolder, "og.png");
await mkdir(outputFolder, { recursive: true })
await mkdir(outputFolder, { recursive: true });
return await writeFile(outputFile, render.asPng())
return await writeFile(outputFile, render.asPng());
}
+48 -57
View File
@@ -1,51 +1,53 @@
import type { Theme } from '../theme/types'
import { defineConfigWithTheme } from 'vitepress'
import { config as root, searchLocale as searchLocaleEn } from './en'
import { addPlugins } from '../theme/plugins/markdown'
import { sections, prepareData } from '../theme/plugins/section'
import { slugify } from 'transliteration'
import { fileURLToPath, URL } from 'node:url'
import { telegram } from '../../website/icons'
import { tabsMarkdownPlugin } from "vitepress-plugin-tabs"
import shortcode_plugin from "markdown-it-shortcode-tag"
import shortcodes from "./shortcodes"
import generateOgImages from "./hooks/generateOgImages"
import generateMeta from "./hooks/generateMeta"
import type { Theme } from "../theme/types";
import { defineConfigWithTheme } from "vitepress";
import { config as root, searchLocale as searchLocaleEn } from "./en";
import { addPlugins } from "../theme/plugins/markdown";
import { sections, prepareData } from "../theme/plugins/section";
import { slugify } from "transliteration";
import { fileURLToPath, URL } from "node:url";
import { telegram } from "../../website/icons";
import { tabsMarkdownPlugin } from "vitepress-plugin-tabs";
import shortcode_plugin from "markdown-it-shortcode-tag";
import shortcodes from "./shortcodes";
import generateOgImages from "./hooks/generateOgImages";
import generateMeta from "./hooks/generateMeta";
const SITE_HOST = 'https://kotatsu.app'
const SITE_TITLE = 'kotatsu.app'
const SITE_TITLE_SEPARATOR = ' / '
const SITE_HOST = "https://kotatsu.app";
const SITE_TITLE = "kotatsu.app";
const SITE_TITLE_SEPARATOR = " / ";
export default defineConfigWithTheme<Theme.Config>({
lastUpdated: true,
cleanUrls: true,
title: SITE_TITLE,
titleTemplate: ':title' + SITE_TITLE_SEPARATOR + SITE_TITLE,
srcDir: './website',
titleTemplate: ":title" + SITE_TITLE_SEPARATOR + SITE_TITLE,
srcDir: "./website",
markdown: {
theme: {
light: 'github-light',
dark: 'one-dark-pro',
light: "github-light",
dark: "one-dark-pro",
},
anchor: {
slugify(str) {
str = str.trim()
.replace(/^\d*/g, '') // Удаление чисел из начала строки
.replace(/[^a-zA-Zа-яА-ЯЁё0-9\-\s]/g, '') // Удаление ненужных символов
.replace(/\s\-\s/, '-').replace(/\-+/g, '-') // Избавление от повторяющихся символов
.replace(/^(.{25}[^\s]*).*/, '$1') // Ограничение количества символов
str = str
.trim()
.replace(/^\d*/g, "") // Удаление чисел из начала строки
.replace(/[^a-zA-Zа-яА-ЯЁё0-9\-\s]/g, "") // Удаление ненужных символов
.replace(/\s\-\s/, "-")
.replace(/\-+/g, "-") // Избавление от повторяющихся символов
.replace(/^(.{25}[^\s]*).*/, "$1"); // Ограничение количества символов
return encodeURIComponent(slugify(str, { lowercase: true }))
}
return encodeURIComponent(slugify(str, { lowercase: true }));
},
},
config (md) {
addPlugins(md)
md.use(tabsMarkdownPlugin)
md.use(shortcode_plugin, shortcodes)
config(md) {
addPlugins(md);
md.use(tabsMarkdownPlugin);
md.use(shortcode_plugin, shortcodes);
},
},
@@ -54,18 +56,18 @@ export default defineConfigWithTheme<Theme.Config>({
i18nRouting: true,
logo: {
light: '/logo.svg',
dark: '/logo-dark.svg',
light: "/logo.svg",
dark: "/logo-dark.svg",
},
socialLinks: [
{ icon: { svg: telegram }, link: 'https://t.me/kotatsuapp' },
{ icon: 'discord', link: 'https://discord.gg/NNJ5RgVBC5' },
{ icon: 'github', link: 'https://github.com/KotatsuApp/Kotatsu' },
{ icon: { svg: telegram }, link: "https://t.me/kotatsuapp" },
{ icon: "discord", link: "https://discord.gg/NNJ5RgVBC5" },
{ icon: "github", link: "https://github.com/KotatsuApp/Kotatsu" },
],
search: {
provider: 'local',
provider: "local",
options: {
locales: {
...searchLocaleEn,
@@ -73,7 +75,7 @@ export default defineConfigWithTheme<Theme.Config>({
},
},
sections
sections,
},
locales: {
@@ -81,29 +83,19 @@ export default defineConfigWithTheme<Theme.Config>({
},
transformPageData(pageData, { siteConfig }) {
return prepareData(pageData, siteConfig)
return prepareData(pageData, siteConfig);
},
transformHead: async (context) => generateMeta(context, SITE_HOST),
vite: {
define:{
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__:"false"
define: {
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false",
},
resolve: {
alias: [
'VPSidebar',
'VPNavBarTranslations',
'VPNavScreenTranslations',
'VPNavBar',
'VPNavBarMenu',
'VPNavScreenMenu',
'VPFooter',
].map(componentName => ({
alias: ["VPSidebar", "VPNavBarTranslations", "VPNavScreenTranslations", "VPNavBar", "VPNavBarMenu", "VPNavScreenMenu", "VPFooter"].map((componentName) => ({
find: new RegExp(`^.*\/${componentName}\.vue$`),
replacement: fileURLToPath(
new URL(`../theme/components/${componentName.replace(/^VP/, '')}.vue`, import.meta.url)
)
replacement: fileURLToPath(new URL(`../theme/components/${componentName.replace(/^VP/, "")}.vue`, import.meta.url)),
})),
},
},
@@ -112,8 +104,7 @@ export default defineConfigWithTheme<Theme.Config>({
hostname: SITE_HOST,
},
buildEnd: async (context) => {
generateOgImages(context)
}
})
generateOgImages(context);
},
});
+73 -73
View File
@@ -1,96 +1,96 @@
const iconMappings = {
about: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>information-outline</title><path d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" /></svg>',
bookmark: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>bookmark-outline</title><path d="M17,18L12,15.82L7,18V5H17M17,3H7A2,2 0 0,0 5,5V21L12,18L19,21V5C19,3.89 18.1,3 17,3Z" /></svg>',
bookPage: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 1l-5 5v11l5-4.5V1m2 4v13.5c-1.1-0.35-2.3-0.5-3.5-0.5-1.7 0-4.15 0.65-5.5 1.5V6c-1.45-1.1-3.55-1.5-5.5-1.5C4.55 4.5 2.45 4.9 1 6v14.65c0 0.25 0.25 0.5 0.5 0.5 0.1 0 0.15-0.05 0.25-0.05C3.1 20.45 5.05 20 6.5 20c1.95 0 4.05 0.4 5.5 1.5 1.35-0.85 3.8-1.5 5.5-1.5 1.65 0 3.35 0.3 4.75 1.05 0.1 0.05 0.15 0.05 0.25 0.05 0.25 0 0.5-0.25 0.5-0.5V6c-0.6-0.45-1.25-0.75-2-1M10 18.41C8.75 18.09 7.5 18 6.5 18c-1.06 0-2.32 0.19-3.5 0.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5c1.36 0 2.59 0.23 3.5 0.63v11.28z" /></svg>',
compassOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7,17L10.2,10.2L17,7L13.8,13.8L7,17M12,11.1A0.9,0.9 0 0,0 11.1,12A0.9,0.9 0 0,0 12,12.9A0.9,0.9 0 0,0 12.9,12A0.9,0.9 0 0,0 12,11.1M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></svg>',
dataPrivacy: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 14.4C17.6 14.4 18.1 14.9 18.1 15.5S17.6 16.6 17 16.6 15.9 16.1 15.9 15.5 16.4 14.4 17 14.4M17 17.5C16.3 17.5 14.8 17.9 14.8 18.6C15.3 19.3 16.1 19.8 17 19.8S18.7 19.3 19.2 18.6C19.2 17.9 17.7 17.5 17 17.5M18 11.1V6.3L10.5 3L3 6.3V11.2C3 15.7 6.2 20 10.5 21C11.1 20.9 11.6 20.7 12.1 20.5C13.2 22 15 23 17 23C20.3 23 23 20.3 23 17C23 14 20.8 11.6 18 11.1M11 17C11 17.6 11.1 18.1 11.2 18.6C11 18.7 10.7 18.8 10.5 18.9C7.3 17.9 5 14.7 5 11.2V7.6L10.5 5.2L16 7.6V11.1C13.2 11.6 11 14 11 17M17 21C14.8 21 13 19.2 13 17S14.8 13 17 13 21 14.8 21 17 19.2 21 17 21Z" /></svg>',
dice: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>dice-5-outline</title><path d="M19 5V19H5V5H19M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3M7.5 6C6.7 6 6 6.7 6 7.5S6.7 9 7.5 9 9 8.3 9 7.5 8.3 6 7.5 6M16.5 15C15.7 15 15 15.7 15 16.5C15 17.3 15.7 18 16.5 18C17.3 18 18 17.3 18 16.5C18 15.7 17.3 15 16.5 15M16.5 6C15.7 6 15 6.7 15 7.5S15.7 9 16.5 9C17.3 9 18 8.3 18 7.5S17.3 6 16.5 6M12 10.5C11.2 10.5 10.5 11.2 10.5 12S11.2 13.5 12 13.5 13.5 12.8 13.5 12 12.8 10.5 12 10.5M7.5 15C6.7 15 6 15.7 6 16.5C6 17.3 6.7 18 7.5 18S9 17.3 9 16.5C9 15.7 8.3 15 7.5 15Z" /></svg>',
dotsHorizontal: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z" /></svg>',
downloadOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z" /></svg>',
favourite: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>heart-outline</title><path d="M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z" /></svg>',
feed: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>rss</title><path d="M6.18,15.64A2.18,2.18 0 0,1 8.36,17.82C8.36,19 7.38,20 6.18,20C5,20 4,19 4,17.82A2.18,2.18 0 0,1 6.18,15.64M4,4.44A15.56,15.56 0 0,1 19.56,20H16.73A12.73,12.73 0 0,0 4,7.27V4.44M4,10.1A9.9,9.9 0 0,1 13.9,20H11.07A7.07,7.07 0 0,0 4,12.93V10.1Z" /></svg>',
history: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" /></svg>',
local: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 4v16H6V8.83L10.83 4H18m0-2h-8L4 8v12c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2V4c0-1.1-0.9-2-2-2zM9 7h2v4H9zm3 0h2v4h-2zm3 0h2v4h-2z" /></svg>',
paletteOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2C17.5,2 22,6 22,11A6,6 0 0,1 16,17H14.2C13.9,17 13.7,17.2 13.7,17.5C13.7,17.6 13.8,17.7 13.8,17.8C14.2,18.3 14.4,18.9 14.4,19.5C14.5,20.9 13.4,22 12,22M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C12.3,20 12.5,19.8 12.5,19.5C12.5,19.3 12.4,19.2 12.4,19.1C12,18.6 11.8,18.1 11.8,17.5C11.8,16.1 12.9,15 14.3,15H16A4,4 0 0,0 20,11C20,7.1 16.4,4 12,4M6.5,10C7.3,10 8,10.7 8,11.5C8,12.3 7.3,13 6.5,13C5.7,13 5,12.3 5,11.5C5,10.7 5.7,10 6.5,10M9.5,6C10.3,6 11,6.7 11,7.5C11,8.3 10.3,9 9.5,9C8.7,9 8,8.3 8,7.5C8,6.7 8.7,6 9.5,6M14.5,6C15.3,6 16,6.7 16,7.5C16,8.3 15.3,9 14.5,9C13.7,9 13,8.3 13,7.5C13,6.7 13.7,6 14.5,6M17.5,10C18.3,10 19,10.7 19,11.5C19,12.3 18.3,13 17.5,13C16.7,13 16,12.3 16,11.5C16,10.7 16.7,10 17.5,10Z" /></svg>',
sources: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 20h14v2H4c-1.1 0-2-0.9-2-2V6h2v14M22 4v12c0 1.1-0.9 2-2 2H8c-1.1 0-2-0.9-2-2V4c0-1.1 0.9-2 2-2h12c1.1 0 2 0.9 2 2m-2 0H8v12h12V4m-2 2h-5v7l2.5-1.5L18 13V6z" /></svg>',
services: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z" /></svg>',
threeDots: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>dots-vertical</title><path d="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z" /></svg>',
web: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-0.32-1.25-0.78-2.45-1.38-3.56 1.84 0.63 3.37 1.91 4.33 3.56zM12 4.04c0.83 1.2 1.48 2.53 1.91 3.96h-3.82c0.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s0.1-1.36 0.26-2h3.38c-0.08 0.66-0.14 1.32-0.14 2 0 0.68 0.06 1.34 0.14 2H4.26zm0.82 2h2.95c0.32 1.25 0.78 2.45 1.38 3.56-1.84-0.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c0.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-0.83-1.2-1.48-2.53-1.91-3.96h3.82c-0.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-0.09-0.66-0.16-1.32-0.16-2 0-0.68 0.07-1.35 0.16-2h4.68c0.09 0.65 0.16 1.32 0.16 2 0 0.68-0.07 1.34-0.16 2zm0.25 5.56c0.6-1.11 1.06-2.31 1.38-3.56h2.95c-0.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c0.08-0.66 0.14-1.32 0.14-2 0-0.68-0.06-1.34-0.14-2h3.38C19.9 10.64 20 11.31 20 12s-0.1 1.36-0.26 2h-3.38z" /></svg>'
}
about: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>information-outline</title><path d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" /></svg>',
bookmark: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>bookmark-outline</title><path d="M17,18L12,15.82L7,18V5H17M17,3H7A2,2 0 0,0 5,5V21L12,18L19,21V5C19,3.89 18.1,3 17,3Z" /></svg>',
bookPage: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 1l-5 5v11l5-4.5V1m2 4v13.5c-1.1-0.35-2.3-0.5-3.5-0.5-1.7 0-4.15 0.65-5.5 1.5V6c-1.45-1.1-3.55-1.5-5.5-1.5C4.55 4.5 2.45 4.9 1 6v14.65c0 0.25 0.25 0.5 0.5 0.5 0.1 0 0.15-0.05 0.25-0.05C3.1 20.45 5.05 20 6.5 20c1.95 0 4.05 0.4 5.5 1.5 1.35-0.85 3.8-1.5 5.5-1.5 1.65 0 3.35 0.3 4.75 1.05 0.1 0.05 0.15 0.05 0.25 0.05 0.25 0 0.5-0.25 0.5-0.5V6c-0.6-0.45-1.25-0.75-2-1M10 18.41C8.75 18.09 7.5 18 6.5 18c-1.06 0-2.32 0.19-3.5 0.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5c1.36 0 2.59 0.23 3.5 0.63v11.28z" /></svg>',
compassOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7,17L10.2,10.2L17,7L13.8,13.8L7,17M12,11.1A0.9,0.9 0 0,0 11.1,12A0.9,0.9 0 0,0 12,12.9A0.9,0.9 0 0,0 12.9,12A0.9,0.9 0 0,0 12,11.1M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></svg>',
dataPrivacy: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 14.4C17.6 14.4 18.1 14.9 18.1 15.5S17.6 16.6 17 16.6 15.9 16.1 15.9 15.5 16.4 14.4 17 14.4M17 17.5C16.3 17.5 14.8 17.9 14.8 18.6C15.3 19.3 16.1 19.8 17 19.8S18.7 19.3 19.2 18.6C19.2 17.9 17.7 17.5 17 17.5M18 11.1V6.3L10.5 3L3 6.3V11.2C3 15.7 6.2 20 10.5 21C11.1 20.9 11.6 20.7 12.1 20.5C13.2 22 15 23 17 23C20.3 23 23 20.3 23 17C23 14 20.8 11.6 18 11.1M11 17C11 17.6 11.1 18.1 11.2 18.6C11 18.7 10.7 18.8 10.5 18.9C7.3 17.9 5 14.7 5 11.2V7.6L10.5 5.2L16 7.6V11.1C13.2 11.6 11 14 11 17M17 21C14.8 21 13 19.2 13 17S14.8 13 17 13 21 14.8 21 17 19.2 21 17 21Z" /></svg>',
dice: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>dice-5-outline</title><path d="M19 5V19H5V5H19M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3M7.5 6C6.7 6 6 6.7 6 7.5S6.7 9 7.5 9 9 8.3 9 7.5 8.3 6 7.5 6M16.5 15C15.7 15 15 15.7 15 16.5C15 17.3 15.7 18 16.5 18C17.3 18 18 17.3 18 16.5C18 15.7 17.3 15 16.5 15M16.5 6C15.7 6 15 6.7 15 7.5S15.7 9 16.5 9C17.3 9 18 8.3 18 7.5S17.3 6 16.5 6M12 10.5C11.2 10.5 10.5 11.2 10.5 12S11.2 13.5 12 13.5 13.5 12.8 13.5 12 12.8 10.5 12 10.5M7.5 15C6.7 15 6 15.7 6 16.5C6 17.3 6.7 18 7.5 18S9 17.3 9 16.5C9 15.7 8.3 15 7.5 15Z" /></svg>',
dotsHorizontal: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z" /></svg>',
downloadOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z" /></svg>',
favourite: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>heart-outline</title><path d="M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z" /></svg>',
feed: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>rss</title><path d="M6.18,15.64A2.18,2.18 0 0,1 8.36,17.82C8.36,19 7.38,20 6.18,20C5,20 4,19 4,17.82A2.18,2.18 0 0,1 6.18,15.64M4,4.44A15.56,15.56 0 0,1 19.56,20H16.73A12.73,12.73 0 0,0 4,7.27V4.44M4,10.1A9.9,9.9 0 0,1 13.9,20H11.07A7.07,7.07 0 0,0 4,12.93V10.1Z" /></svg>',
history: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" /></svg>',
local: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 4v16H6V8.83L10.83 4H18m0-2h-8L4 8v12c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2V4c0-1.1-0.9-2-2-2zM9 7h2v4H9zm3 0h2v4h-2zm3 0h2v4h-2z" /></svg>',
paletteOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2C17.5,2 22,6 22,11A6,6 0 0,1 16,17H14.2C13.9,17 13.7,17.2 13.7,17.5C13.7,17.6 13.8,17.7 13.8,17.8C14.2,18.3 14.4,18.9 14.4,19.5C14.5,20.9 13.4,22 12,22M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C12.3,20 12.5,19.8 12.5,19.5C12.5,19.3 12.4,19.2 12.4,19.1C12,18.6 11.8,18.1 11.8,17.5C11.8,16.1 12.9,15 14.3,15H16A4,4 0 0,0 20,11C20,7.1 16.4,4 12,4M6.5,10C7.3,10 8,10.7 8,11.5C8,12.3 7.3,13 6.5,13C5.7,13 5,12.3 5,11.5C5,10.7 5.7,10 6.5,10M9.5,6C10.3,6 11,6.7 11,7.5C11,8.3 10.3,9 9.5,9C8.7,9 8,8.3 8,7.5C8,6.7 8.7,6 9.5,6M14.5,6C15.3,6 16,6.7 16,7.5C16,8.3 15.3,9 14.5,9C13.7,9 13,8.3 13,7.5C13,6.7 13.7,6 14.5,6M17.5,10C18.3,10 19,10.7 19,11.5C19,12.3 18.3,13 17.5,13C16.7,13 16,12.3 16,11.5C16,10.7 16.7,10 17.5,10Z" /></svg>',
sources: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 20h14v2H4c-1.1 0-2-0.9-2-2V6h2v14M22 4v12c0 1.1-0.9 2-2 2H8c-1.1 0-2-0.9-2-2V4c0-1.1 0.9-2 2-2h12c1.1 0 2 0.9 2 2m-2 0H8v12h12V4m-2 2h-5v7l2.5-1.5L18 13V6z" /></svg>',
services: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z" /></svg>',
threeDots: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>dots-vertical</title><path d="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z" /></svg>',
web: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-0.32-1.25-0.78-2.45-1.38-3.56 1.84 0.63 3.37 1.91 4.33 3.56zM12 4.04c0.83 1.2 1.48 2.53 1.91 3.96h-3.82c0.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s0.1-1.36 0.26-2h3.38c-0.08 0.66-0.14 1.32-0.14 2 0 0.68 0.06 1.34 0.14 2H4.26zm0.82 2h2.95c0.32 1.25 0.78 2.45 1.38 3.56-1.84-0.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c0.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-0.83-1.2-1.48-2.53-1.91-3.96h3.82c-0.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-0.09-0.66-0.16-1.32-0.16-2 0-0.68 0.07-1.35 0.16-2h4.68c0.09 0.65 0.16 1.32 0.16 2 0 0.68-0.07 1.34-0.16 2zm0.25 5.56c0.6-1.11 1.06-2.31 1.38-3.56h2.95c-0.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c0.08-0.66 0.14-1.32 0.14-2 0-0.68-0.06-1.34-0.14-2h3.38C19.9 10.64 20 11.31 20 12s-0.1 1.36-0.26 2h-3.38z" /></svg>',
};
interface Navigation {
name: string
icon?: string
dependsOn?: string
name: string;
icon?: string;
dependsOn?: string;
}
const navigationMappings: Record<string, Navigation> = {
// Main menus
"main_three_dots": { name: "Three dots", icon: iconMappings.threeDots },
"main_history": { name: "History", icon: iconMappings.history },
"main_favourites": { name: "Favourites", icon: iconMappings.favourite },
"main_explore": { name: "Explore", icon: iconMappings.compassOutline },
"main_feed": { name: "Feed", icon: iconMappings.feed },
"main_settings": { name: "Settings", dependsOn: "main_three_dots" },
// Main menus
main_three_dots: { name: "Three dots", icon: iconMappings.threeDots },
main_history: { name: "History", icon: iconMappings.history },
main_favourites: { name: "Favourites", icon: iconMappings.favourite },
main_explore: { name: "Explore", icon: iconMappings.compassOutline },
main_feed: { name: "Feed", icon: iconMappings.feed },
main_settings: { name: "Settings", dependsOn: "main_three_dots" },
// Explore
"explore_local_storage" : { name: "Local storage", icon: iconMappings.local, dependsOn: "main_explore" },
"explore_bookmarks" : { name: "Bookmarks", icon: iconMappings.bookmark, dependsOn: "main_explore" },
"explore_random" : { name: "Random", icon: iconMappings.dice, dependsOn: "main_explore" },
"explore_downloads" : { name: "Downloads", icon: iconMappings.downloadOutline, dependsOn: "main_explore" },
"explore_manage_sources" : { name: "Catalog", dependsOn: "main_explore" },
// Explore
explore_local_storage: { name: "Local storage", icon: iconMappings.local, dependsOn: "main_explore" },
explore_bookmarks: { name: "Bookmarks", icon: iconMappings.bookmark, dependsOn: "main_explore" },
explore_random: { name: "Random", icon: iconMappings.dice, dependsOn: "main_explore" },
explore_downloads: { name: "Downloads", icon: iconMappings.downloadOutline, dependsOn: "main_explore" },
explore_manage_sources: { name: "Catalog", dependsOn: "main_explore" },
// Details
"details_tracking" : { name: "Tracking", dependsOn: "main_three_dots" },
// Details
details_tracking: { name: "Tracking", dependsOn: "main_three_dots" },
// Favourites
"favourites_three_dots": { name: "Three dots", icon: iconMappings.threeDots, dependsOn: "main_favourites" },
"favourites_manage_categories" : { name: "Favourite categories", dependsOn: "favourites_three_dots" },
// Favourites
favourites_three_dots: { name: "Three dots", icon: iconMappings.threeDots, dependsOn: "main_favourites" },
favourites_manage_categories: { name: "Favourite categories", dependsOn: "favourites_three_dots" },
// Settings submenu
"appearance": { name: "Appearance", icon: iconMappings.paletteOutline, dependsOn: "main_settings" },
"sources": { name: "Manga sources", icon: iconMappings.sources, dependsOn: "main_settings" },
"reader": { name: "Reader settings", icon: iconMappings.bookPage, dependsOn: "main_settings" },
"network": { name: "Network", icon: iconMappings.web, dependsOn: "main_settings" },
"data": { name: "Data and privacy", icon: iconMappings.dataPrivacy, dependsOn: "main_settings" },
"downloads": { name: "Downloads", icon: iconMappings.downloadOutline, dependsOn: "main_settings" },
"checking": { name: "Check for new chapters", icon: iconMappings.feed, dependsOn: "main_settings" },
"services": { name: "Services", icon: iconMappings.services, dependsOn: "main_settings" },
"about": { name: "About", icon: iconMappings.about, dependsOn: "main_settings" },
// Settings submenu
appearance: { name: "Appearance", icon: iconMappings.paletteOutline, dependsOn: "main_settings" },
sources: { name: "Manga sources", icon: iconMappings.sources, dependsOn: "main_settings" },
reader: { name: "Reader settings", icon: iconMappings.bookPage, dependsOn: "main_settings" },
network: { name: "Network", icon: iconMappings.web, dependsOn: "main_settings" },
data: { name: "Data and privacy", icon: iconMappings.dataPrivacy, dependsOn: "main_settings" },
downloads: { name: "Downloads", icon: iconMappings.downloadOutline, dependsOn: "main_settings" },
checking: { name: "Check for new chapters", icon: iconMappings.feed, dependsOn: "main_settings" },
services: { name: "Services", icon: iconMappings.services, dependsOn: "main_settings" },
about: { name: "About", icon: iconMappings.about, dependsOn: "main_settings" },
// Sync
"sync_settings": { name: "Synchronization settings", dependsOn: "services" },
"server_address": { name: "Server address", dependsOn: "sync_settings" },
// Sync
sync_settings: { name: "Synchronization settings", dependsOn: "services" },
server_address: { name: "Server address", dependsOn: "sync_settings" },
// Backups
"periodic_backups": { name: "Periodic backups", dependsOn: "data" }
}
// Backups
periodic_backups: { name: "Periodic backups", dependsOn: "data" },
};
function generateNavigationHtml(navKey: string) {
const navData = navigationMappings[navKey]
const navData = navigationMappings[navKey];
if (!navData) {
return "<strong style='color:var(--vp-c-danger-1)'>Unsupported Navigation!</strong>"
}
if (!navData) {
return "<strong style='color:var(--vp-c-danger-1)'>Unsupported Navigation!</strong>";
}
const { name, icon, dependsOn } = navData
const { name, icon, dependsOn } = navData;
const iconHtml = icon ?? ""
let html = `<span class='shortcode navigation ${navKey}'>${iconHtml}<span class="name">${name}</span></span>`
const iconHtml = icon ?? "";
let html = `<span class='shortcode navigation ${navKey}'>${iconHtml}<span class="name">${name}</span></span>`;
if (dependsOn) {
html = `${generateNavigationHtml(dependsOn)} <span class='shortcode navigation direction'><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>arrow-right-thin</title><path d="M14 16.94V12.94H5.08L5.05 10.93H14V6.94L19 11.94Z" /></svg></span> ${html}`
}
if (dependsOn) {
html = `${generateNavigationHtml(dependsOn)} <span class='shortcode navigation direction'><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>arrow-right-thin</title><path d="M14 16.94V12.94H5.08L5.05 10.93H14V6.94L19 11.94Z" /></svg></span> ${html}`;
}
return html
return html;
}
const shortcodes = {
nav: {
render({ to }) {
return generateNavigationHtml(to)
},
},
}
nav: {
render({ to }) {
return generateNavigationHtml(to);
},
},
};
export default shortcodes
export default shortcodes;
+17 -41
View File
@@ -1,59 +1,35 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import { useData, withBase, useRoute } from 'vitepress'
import type { DocsPageData } from '../plugins/section'
import type { Ref } from "vue";
import { useData, withBase, useRoute } from "vitepress";
import type { DocsPageData } from "../plugins/section";
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
import VPLink from "vitepress/dist/client/theme-default/components/VPLink.vue";
const { page }: { page: Ref<DocsPageData> } = useData()
const route = useRoute()
const { page }: { page: Ref<DocsPageData> } = useData();
const route = useRoute();
</script>
<template>
<ol
v-if="page.breadcrumbs.length"
class="Breadcrumbs"
itemscope itemtype="https://schema.org/BreadcrumbList"
>
<ol v-if="page.breadcrumbs.length" class="Breadcrumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
<li
v-for="(item, idx) in page.breadcrumbs"
:key="idx"
itemscope itemprop="itemListElement" itemtype="https://schema.org/ListItem"
itemscope
itemprop="itemListElement"
itemtype="https://schema.org/ListItem"
class="item"
:class="{
active: item.link && withBase(item.link) === route.path,
}"
>
<VPLink
v-if="item.link && idx < page.breadcrumbs.length - 1"
:href="item.link"
:title="item.text"
itemprop="item"
class="link"
>
<span
itemprop="name"
class="name"
v-text="item.text"
/>
<VPLink v-if="item.link && idx < page.breadcrumbs.length - 1" :href="item.link" :title="item.text" itemprop="item" class="link">
<span itemprop="name" class="name" v-text="item.text" />
</VPLink>
<template v-else>
<span
:title="item.text"
itemprop="name"
class="name"
v-text="item.text"
/>
<link
v-if="item.link"
:href="item.link"
itemprop="item"
>
<span :title="item.text" itemprop="name" class="name" v-text="item.text" />
<link v-if="item.link" :href="item.link" itemprop="item" />
</template>
<meta
itemprop="position"
:content="idx.toString()"
>
<meta itemprop="position" :content="idx.toString()" />
</li>
</ol>
</template>
@@ -66,7 +42,7 @@ const route = useRoute()
color: var(--vp-c-text-2);
margin-bottom: 10px;
font-size: .9rem;
font-size: 0.9rem;
}
.item {
@@ -80,7 +56,7 @@ const route = useRoute()
}
& + .item::before {
content: '/';
content: "/";
font-weight: normal;
margin-right: 8px;
}
+23 -27
View File
@@ -1,35 +1,29 @@
<script setup lang="ts">
import { computed } from 'vue'
import { normalizeLink } from 'vitepress/dist/client/theme-default/support/utils'
import { EXTERNAL_URL_RE } from 'vitepress/dist/client/shared'
import { computed } from "vue";
import { normalizeLink } from "vitepress/dist/client/theme-default/support/utils";
import { EXTERNAL_URL_RE } from "vitepress/dist/client/shared";
const {tag,theme = "brand",href} = defineProps<{
tag?: string
theme?: 'brand' | 'alt' | 'sponsor'
text: string
href?: string
target?: string
}>()
const {
tag,
theme = "brand",
href,
} = defineProps<{
tag?: string;
theme?: "brand" | "alt" | "sponsor";
text: string;
href?: string;
target?: string;
}>();
const classes = computed(() => [
theme
])
const classes = computed(() => [theme]);
const isExternal = computed(() => href && EXTERNAL_URL_RE.test(href))
const isExternal = computed(() => href && EXTERNAL_URL_RE.test(href));
const buttonTag = computed(() => tag ?? (href ? 'a' : 'button'))
const buttonTag = computed(() => tag ?? (href ? "a" : "button"));
</script>
<template>
<component
:is="buttonTag"
class="Button"
:class="classes"
:href="href ? normalizeLink(href) : undefined"
:target="target ?? (isExternal ? '_blank' : undefined)"
:rel="isExternal ? 'noreferrer' : undefined"
:aria-label="text"
>
<component :is="buttonTag" class="Button" :class="classes" :href="href ? normalizeLink(href) : undefined" :target="target ?? (isExternal ? '_blank' : undefined)" :rel="isExternal ? 'noreferrer' : undefined" :aria-label="text">
{{ text }}
</component>
</template>
@@ -43,7 +37,9 @@ const buttonTag = computed(() => tag ?? (href ? 'a' : 'button'))
padding: 8px 18px;
font-size: 16px;
font-weight: 500;
transition: all 0.25s, color 0.25s;
transition:
all 0.25s,
color 0.25s;
}
.Button.alt {
@@ -67,7 +63,7 @@ const buttonTag = computed(() => tag ?? (href ? 'a' : 'button'))
}
.Button.cta:before {
content: '';
content: "";
position: absolute;
display: block;
width: 25px;
@@ -81,7 +77,7 @@ const buttonTag = computed(() => tag ?? (href ? 'a' : 'button'))
}
.Button.cta.question::before {
content: '';
content: "";
display: inline-block;
width: 25px;
height: 25px;
+62 -62
View File
@@ -1,85 +1,85 @@
<script setup lang="ts">
import { computed } from "vue"
import MarkdownIt from "markdown-it"
import { type AppRelease, data as release } from "../data/release.data"
import { computed } from "vue";
import MarkdownIt from "markdown-it";
import { type AppRelease, data as release } from "../data/release.data";
const {type} = defineProps<{ type: keyof AppRelease }>()
const { type } = defineProps<{ type: keyof AppRelease }>();
const md = new MarkdownIt()
const md = new MarkdownIt();
const changelog = computed(() => {
const flavoredString = (release[type].body ?? "")
.replace(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g, "[@$2](https://github.com/$2)")
.replace("https://github.com/KotatsuApp/Kotatsu/releases", "/changelogs/")
.replace(/## [ \t]*([^\n\r]*)/g, "### $1")
const flavoredString = (release[type].body ?? "")
.replace(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g, "[@$2](https://github.com/$2)")
.replace("https://github.com/KotatsuApp/Kotatsu/releases", "/changelogs/")
.replace(/## [ \t]*([^\n\r]*)/g, "### $1");
return md.render(flavoredString)
})
return md.render(flavoredString);
});
</script>
<template>
<section class="changelog">
<header class="changelog__header">
<h2 class="changelog__title">Changelog</h2>
</header>
<article class="changelog__content" v-html="changelog" />
</section>
<footer class="full-changelog">
<p>
View the full release
<a class="full-changelog__link" href="https://github.com/KotatsuApp/Kotatsu/releases/latest" target="_blank" rel="noopener">
here
</a>
</p>
</footer>
<section class="changelog">
<header class="changelog__header">
<h2 class="changelog__title">Changelog</h2>
</header>
<article class="changelog__content" v-html="changelog" />
</section>
<footer class="full-changelog">
<p>
View the full release
<a class="full-changelog__link" href="https://github.com/KotatsuApp/Kotatsu/releases/latest" target="_blank" rel="noopener"> here </a>
</p>
</footer>
</template>
<style scoped>
.changelog {
display: block;
border: 1px solid var(--vp-c-bg-soft);
border-radius: 12px;
background-color: var(--vp-c-bg-soft);
transition: border-color 0.25s, background-color 0.25s;
padding: 24px;
height: 100%;
margin: 1.5em auto 0.5em;
display: block;
border: 1px solid var(--vp-c-bg-soft);
border-radius: 12px;
background-color: var(--vp-c-bg-soft);
transition:
border-color 0.25s,
background-color 0.25s;
padding: 24px;
height: 100%;
margin: 1.5em auto 0.5em;
.changelog__header {
display: flex;
justify-content: center;
align-items: baseline;
margin: 0 0 1rem;
.changelog__header {
display: flex;
justify-content: center;
align-items: baseline;
margin: 0 0 1rem;
.changelog__title {
font-size: 1.5rem;
margin: 0;
padding: 0;
color: var(--vp-c-text-1);
border: none;
}
}
.changelog__title {
font-size: 1.5rem;
margin: 0;
padding: 0;
color: var(--vp-c-text-1);
border: none;
}
}
.changelog__content {
margin: 0;
.changelog__content {
margin: 0;
& > p {
margin: 0 0 1rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
}
& > p {
margin: 0 0 1rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
}
& svg {
font-size: 1.2em;
margin-right: 0.5rem;
vertical-align: middle;
}
}
& svg {
font-size: 1.2em;
margin-right: 0.5rem;
vertical-align: middle;
}
}
}
.full-changelog {
margin: 0 0 1rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
margin: 0 0 1rem;
color: var(--vp-c-text-2);
font-size: 0.875rem;
}
</style>
+33 -45
View File
@@ -1,57 +1,45 @@
<script setup lang="ts">
import MarkdownIt from "markdown-it"
import { data as changelogs } from "../data/changelogs.data"
import MarkdownIt from "markdown-it";
import { data as changelogs } from "../data/changelogs.data";
const md = new MarkdownIt()
const md = new MarkdownIt();
function renderMarkdown(string: string | null | undefined) {
const body = string ?? "No changelog provided."
const flavoredString = body
.split(/---\r\n\r\n### Checksums|---\r\n\r\nMD5/)[0]
.replace(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g, "[@$2](https://github.com/$2)")
.replace(/#(\d+)/g, "[#$1](https://github.com/KotatsuApp/Kotatsu/issues/$1)")
.replace(/^Check out the .*past release notes.* if you're.*$/m, "")
.replace(/https\:\/\/github.com\/KotatsuApp\/Kotatsu\/releases\/tag\/(.*?)/g, "#$1")
.replace(/## [ \t]*([^\n\r]*)/g, "### $1")
.trim()
const body = string ?? "No changelog provided.";
const flavoredString = body
.split(/---\r\n\r\n### Checksums|---\r\n\r\nMD5/)[0]
.replace(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g, "[@$2](https://github.com/$2)")
.replace(/#(\d+)/g, "[#$1](https://github.com/KotatsuApp/Kotatsu/issues/$1)")
.replace(/^Check out the .*past release notes.* if you're.*$/m, "")
.replace(/https\:\/\/github.com\/KotatsuApp\/Kotatsu\/releases\/tag\/(.*?)/g, "#$1")
.replace(/## [ \t]*([^\n\r]*)/g, "### $1")
.trim();
return md.render(flavoredString)
return md.render(flavoredString);
}
const dateFormatter = new Intl.DateTimeFormat("en", {
dateStyle: "medium",
})
dateStyle: "medium",
});
</script>
<template>
<div
class="release"
v-for="(release, index) of changelogs"
:key="release.id"
>
<h2 class="release__title"
:id="index === 0 ? 'latest' : release.tag_name">
<a
:href="release.html_url"
target="_blank"
>
{{ release.tag_name.replace("v", "") }}
</a>
<Badge v-if="index === 0" type="tip" text="Latest" />
<Badge v-if="release.tag_name.includes('a')" type="danger" text="Unstable" />
<Badge v-if="release.tag_name.includes('b')" type="warning" text="Unstable" />
<Badge v-if="release.tag_name.includes('rc')" type="warning" text="Unstable" />
<a
class="header-anchor"
:href="index === 0 ? '#latest' : `#${release.tag_name}`"
:aria-label="`Permalink to &quot;${release.tag_name}&quot;`"
/>
</h2>
<time class="release__date" :datetime="release.published_at">
{{ dateFormatter.format(new Date(release.published_at)) }}
</time>
<div v-html="renderMarkdown(release.body)" />
</div>
<div class="release" v-for="(release, index) of changelogs" :key="release.id">
<h2 class="release__title" :id="index === 0 ? 'latest' : release.tag_name">
<a :href="release.html_url" target="_blank">
{{ release.tag_name.replace("v", "") }}
</a>
<Badge v-if="index === 0" type="tip" text="Latest" />
<Badge v-if="release.tag_name.includes('a')" type="danger" text="Unstable" />
<Badge v-if="release.tag_name.includes('b')" type="warning" text="Unstable" />
<Badge v-if="release.tag_name.includes('rc')" type="warning" text="Unstable" />
<a class="header-anchor" :href="index === 0 ? '#latest' : `#${release.tag_name}`" :aria-label="`Permalink to &quot;${release.tag_name}&quot;`" />
</h2>
<time class="release__date" :datetime="release.published_at">
{{ dateFormatter.format(new Date(release.published_at)) }}
</time>
<div v-html="renderMarkdown(release.body)" />
</div>
</template>
<style scoped>
@@ -59,8 +47,8 @@ const dateFormatter = new Intl.DateTimeFormat("en", {
.release__title {
margin-bottom: 0;
display: flex;
align-items: center;
gap: 0.5rem;
align-items: center;
gap: 0.5rem;
}
.release__date {
+44 -65
View File
@@ -1,91 +1,70 @@
<script setup lang="ts">
import { computed, ref } from "vue"
import { computed, ref } from "vue";
const {body, author, tag} = defineProps<{ body: string; author: string; tag: string }>()
const { body, author, tag } = defineProps<{ body: string; author: string; tag: string }>();
function isHigherThan(tagName: string, reference: string) {
return reference.localeCompare(tagName, undefined, { numeric: true, sensitivity: "base" }) >= 0
return reference.localeCompare(tagName, undefined, { numeric: true, sensitivity: "base" }) >= 0;
}
const notMentioned = computed(() => {
return isHigherThan("v0.1", tag) ? ["Koitharu"] : []
})
return isHigherThan("v0.1", tag) ? ["Koitharu"] : [];
});
const nonExistent = ref<string[]>([])
const nonExistent = ref<string[]>([]);
const contributors = computed(() => {
const list = [...body.matchAll(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g)]
.map((match) => match[2])
const uncredited = author.includes("[bot]")
? notMentioned.value
: [author, ...notMentioned.value]
const list = [...body.matchAll(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g)].map((match) => match[2]);
const uncredited = author.includes("[bot]") ? notMentioned.value : [author, ...notMentioned.value];
return [...new Set([...uncredited, ...list])].filter((user) => !nonExistent.value.includes(user))
})
return [...new Set([...uncredited, ...list])].filter((user) => !nonExistent.value.includes(user));
});
const listFormatter = new Intl.ListFormat("en", {
style: "long",
type: "conjunction",
})
style: "long",
type: "conjunction",
});
const contributorsText = computed(() => {
if (contributors.value.length <= 3) {
return listFormatter.format(contributors.value)
}
if (contributors.value.length <= 3) {
return listFormatter.format(contributors.value);
}
return listFormatter.format([
...contributors.value.slice(0, 2),
`${contributors.value.at(-2)} other contributors`,
])
})
return listFormatter.format([...contributors.value.slice(0, 2), `${contributors.value.at(-2)} other contributors`]);
});
function addToNonExistent(user: string) {
if (!nonExistent.value.includes(user)) {
nonExistent.value.push(user)
}
if (!nonExistent.value.includes(user)) {
nonExistent.value.push(user);
}
}
</script>
<template>
<div v-if="contributors.length > 0" class="contributors">
<h3>Contributors</h3>
<ul class="contributors__list">
<li
class="contributor"
v-for="contributor of contributors"
:key="contributor"
>
<a
:href="`https://github.com/${contributor}`"
target="_blank"
:title="`${contributor} profile on GitHub`"
:aria-label="`${contributor} profile on GitHub`"
>
<img
:src="`https://github.com/${contributor}.png?size=32`"
:alt="`@${contributor} profile picture`"
loading="lazy"
class="contributor__avatar"
@error="addToNonExistent(contributor)"
>
</a>
</li>
</ul>
<div class="contributors__names">
{{ contributorsText }}
</div>
</div>
<div v-if="contributors.length > 0" class="contributors">
<h3>Contributors</h3>
<ul class="contributors__list">
<li class="contributor" v-for="contributor of contributors" :key="contributor">
<a :href="`https://github.com/${contributor}`" target="_blank" :title="`${contributor} profile on GitHub`" :aria-label="`${contributor} profile on GitHub`">
<img :src="`https://github.com/${contributor}.png?size=32`" :alt="`@${contributor} profile picture`" loading="lazy" class="contributor__avatar" @error="addToNonExistent(contributor)" />
</a>
</li>
</ul>
<div class="contributors__names">
{{ contributorsText }}
</div>
</div>
</template>
<style scoped>
.contrubutors {
.contrubutors__contributors__list {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
list-style-type: none;
padding-left: 0;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
list-style-type: none;
padding-left: 0;
}
.contrubutors__names {
@@ -94,17 +73,17 @@ function addToNonExistent(user: string) {
}
& li + li {
margin-top: 0
margin-top: 0;
}
}
.contributor {
.contributor__avatar {
width: 32px;
height: 32px;
border-radius: 50%;
box-shadow: var(--vp-shadow-1);
border: 1px solid var(--vp-c-divider);
height: 32px;
border-radius: 50%;
box-shadow: var(--vp-shadow-1);
border: 1px solid var(--vp-c-divider);
}
}
</style>
+66 -79
View File
@@ -1,67 +1,52 @@
<script setup lang="ts">
/// <reference types="@types/gtag.js" />
import { computed } from "vue"
import { data as release } from "../data/release.data"
import { computed } from "vue";
import { data as release } from "../data/release.data";
const downloadInformation = computed(() => ({
stable: {
tagName: release.stable.tag_name ?? "v0.0.0",
asset: (release.stable.assets ?? []).find((a) => /^kotatsu-(\d+\.)?(\d+\.)?(\*|\d+)-release.apk/.test(a.name)),
},
nightly: {
tagName: release.nightly.tag_name ?? "00000000",
asset: (release.nightly.assets ?? [])
.find(a => /^release.apk/.test(a.name)),
},
}))
stable: {
tagName: release.stable.tag_name ?? "v0.0.0",
asset: (release.stable.assets ?? []).find((a) => /^kotatsu-(\d+\.)?(\d+\.)?(\*|\d+)-release.apk/.test(a.name)),
},
nightly: {
tagName: release.nightly.tag_name ?? "00000000",
asset: (release.nightly.assets ?? []).find((a) => /^release.apk/.test(a.name)),
},
}));
function handleAnalytics() {
window.gtag?.("event", "Download", {
event_category: "App",
event_label: type === 'stable' ? 'Stable' : 'Nightly',
version: type === 'stable'
? release.stable.tag_name
: release.nightly.tag_name
})
window.gtag?.("event", "Download", {
event_category: "App",
event_label: type === "stable" ? "Stable" : "Nightly",
version: type === "stable" ? release.stable.tag_name : release.nightly.tag_name,
});
}
</script>
<template>
<div>
<div class="download-buttons">
<a
class="download-button primary"
:download="downloadInformation.stable.asset?.name"
:href="downloadInformation.stable.asset?.browser_download_url"
@click="handleAnalytics('stable')"
>
<span class="text">Stable</span>
<span class="version">{{ downloadInformation.stable.tagName }}</span>
</a>
<a
class="download-button secondary"
:download="downloadInformation.nightly.asset?.name"
:href="downloadInformation.nightly.asset?.browser_download_url"
@click="handleAnalytics('nightly')"
>
<span class="text">Nightly</span>
<span class="version">{{ downloadInformation.nightly.tagName.replace("nightly-", "") }}</span>
</a>
</div>
<span class="version-disclaimer">
Requires <strong>Android 5.0</strong> or higher.
</span>
</div>
<div>
<div class="download-buttons">
<a class="download-button primary" :download="downloadInformation.stable.asset?.name" :href="downloadInformation.stable.asset?.browser_download_url" @click="handleAnalytics('stable')">
<span class="text">Stable</span>
<span class="version">{{ downloadInformation.stable.tagName }}</span>
</a>
<a class="download-button secondary" :download="downloadInformation.nightly.asset?.name" :href="downloadInformation.nightly.asset?.browser_download_url" @click="handleAnalytics('nightly')">
<span class="text">Nightly</span>
<span class="version">{{ downloadInformation.nightly.tagName.replace("nightly-", "") }}</span>
</a>
</div>
<span class="version-disclaimer"> Requires <strong>Android 5.0</strong> or higher. </span>
</div>
</template>
<style scoped>
.download-buttons {
display: flex;
gap: 0.75em;
justify-content: center;
align-items: center;
margin: 0.75em auto;
display: flex;
gap: 0.75em;
justify-content: center;
align-items: center;
margin: 0.75em auto;
}
.download-button {
@@ -73,66 +58,68 @@ function handleAnalytics() {
padding: 8px 18px;
font-size: 16px;
font-weight: 500;
transition: all 0.25s, color 0.25s;
transition:
all 0.25s,
color 0.25s;
text-decoration: none !important;
}
.download-button.primary {
border-color: var(--vp-button-brand-border);
color: var(--vp-button-brand-text);
background-color: var(--vp-button-brand-bg);
border-color: var(--vp-button-brand-border);
color: var(--vp-button-brand-text);
background-color: var(--vp-button-brand-bg);
}
.download-button.primary:hover {
border-color: var(--vp-button-brand-hover-border);
color: var(--vp-button-brand-hover-text);
background-color: var(--vp-button-brand-hover-bg);
border-color: var(--vp-button-brand-hover-border);
color: var(--vp-button-brand-hover-text);
background-color: var(--vp-button-brand-hover-bg);
}
.download-button.primary:active {
border-color: var(--vp-button-brand-active-border);
color: var(--vp-button-brand-active-text);
background-color: var(--vp-button-brand-active-bg);
border-color: var(--vp-button-brand-active-border);
color: var(--vp-button-brand-active-text);
background-color: var(--vp-button-brand-active-bg);
}
.download-button.secondary {
border-color: var(--vp-button-alt-border);
color: var(--vp-button-alt-text);
background-color: var(--vp-button-alt-bg);
border-color: var(--vp-button-alt-border);
color: var(--vp-button-alt-text);
background-color: var(--vp-button-alt-bg);
}
.download-button.secondary:hover {
border-color: var(--vp-button-alt-hover-border);
color: var(--vp-button-alt-hover-text);
background-color: var(--vp-button-alt-hover-bg);
border-color: var(--vp-button-alt-hover-border);
color: var(--vp-button-alt-hover-text);
background-color: var(--vp-button-alt-hover-bg);
}
.download-button.secondary:active {
border-color: var(--vp-button-alt-active-border);
color: var(--vp-button-alt-active-text);
background-color: var(--vp-button-alt-active-bg);
border-color: var(--vp-button-alt-active-border);
color: var(--vp-button-alt-active-text);
background-color: var(--vp-button-alt-active-bg);
}
svg {
display: inline-block;
vertical-align: middle;
margin-right: 0.5em;
font-size: 1.25em;
display: inline-block;
vertical-align: middle;
margin-right: 0.5em;
font-size: 1.25em;
}
.text {
margin-right: 10px;
font-weight: bold;
margin-right: 10px;
font-weight: bold;
}
.version {
font-size: 1em;
font-size: 1em;
}
.version-disclaimer {
display: block;
text-align: center;
margin: 0.75em auto;
font-size: 0.75rem;
display: block;
text-align: center;
margin: 0.75em auto;
font-size: 0.75rem;
}
</style>
+9 -7
View File
@@ -1,12 +1,12 @@
<script setup lang="ts">
import VPImage from 'vitepress/dist/client/theme-default/components/VPImage.vue';
import type { DefaultTheme } from 'vitepress/theme'
import VPImage from "vitepress/dist/client/theme-default/components/VPImage.vue";
import type { DefaultTheme } from "vitepress/theme";
defineProps<{
image: DefaultTheme.ThemeableImage
title: string
details?: string
}>()
image: DefaultTheme.ThemeableImage;
title: string;
details?: string;
}>();
</script>
<template>
@@ -32,7 +32,9 @@ defineProps<{
border-radius: var(--vp-border-radius);
height: 100%;
box-shadow: var(--vp-shadow-6);
transition: border-color 0.25s, background-color 0.25s;
transition:
border-color 0.25s,
background-color 0.25s;
}
.box {
+5 -6
View File
@@ -1,14 +1,13 @@
<template>
<div class="FeatureTitle">
<div class="container">
<p class="title">What Kotatsu can do?</p>
<h1 class="subtitle">And here's what it does!</h1>
</div>
<div class="FeatureTitle">
<div class="container">
<p class="title">What Kotatsu can do?</p>
<h1 class="subtitle">And here's what it does!</h1>
</div>
</div>
</template>
<style scoped>
.FeatureTitle {
position: relative;
padding: 0 24px 24px 24px;
+20 -29
View File
@@ -1,47 +1,38 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { DefaultTheme } from 'vitepress/theme-without-fonts'
import { computed } from "vue";
import type { DefaultTheme } from "vitepress/theme-without-fonts";
import Feature from './Feature.vue'
import Feature from "./Feature.vue";
interface IFeature {
image?: DefaultTheme.ThemeableImage
title: string
details: string
link?: string
linkText?: string
image?: DefaultTheme.ThemeableImage;
title: string;
details: string;
link?: string;
linkText?: string;
}
const {features} = defineProps<{
features: IFeature[]
}>()
const { features } = defineProps<{
features: IFeature[];
}>();
const grid = computed(() => {
const length = features.length
const length = features.length;
if (length === 0) return
if (length === 2) return 'grid-2'
if (length === 3) return 'grid-3'
if (length % 3 === 0) return 'grid-6'
if (length > 3) return 'grid-4'
})
if (length === 0) return;
if (length === 2) return "grid-2";
if (length === 3) return "grid-3";
if (length % 3 === 0) return "grid-6";
if (length > 3) return "grid-4";
});
</script>
<template>
<div v-if="features" class="Features">
<div class="container">
<div class="items">
<div
v-for="feature in features"
:key="feature.title"
class="item"
:class="[grid]"
>
<Feature
:image="feature.image"
:title="feature.title"
:details="feature.details"
/>
<div v-for="feature in features" :key="feature.title" class="item" :class="[grid]">
<Feature :image="feature.image" :title="feature.title" :details="feature.details" />
</div>
</div>
</div>
+12 -27
View File
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { useQRCode } from '@vueuse/integrations/useQRCode'
import { useData, withBase } from 'vitepress'
import { socialList } from '../utils'
import Link from './Link.vue'
import GPL from '../icons/GPL.vue'
import { useQRCode } from "@vueuse/integrations/useQRCode";
import { useData, withBase } from "vitepress";
import { socialList } from "../utils";
import Link from "./Link.vue";
import GPL from "../icons/GPL.vue";
const { theme, frontmatter } = useData()
const qrcode = useQRCode(theme.value.footer.qrcodeLink)
const { theme, frontmatter } = useData();
const qrcode = useQRCode(theme.value.footer.qrcodeLink);
</script>
<template>
@@ -17,11 +17,7 @@ const qrcode = useQRCode(theme.value.footer.qrcodeLink)
<h3 class="footer__title">{{ item.title }}</h3>
<ul class="footer__list">
<li v-for="ic in item.items" class="footer__item">
<Link
:href="withBase(ic.link)"
:title="ic.text + '' + withBase(ic.link) + ''"
noIcon
>
<Link :href="withBase(ic.link)" :title="ic.text + '' + withBase(ic.link) + ''" noIcon>
{{ ic.text }}
</Link>
</li>
@@ -42,19 +38,8 @@ const qrcode = useQRCode(theme.value.footer.qrcodeLink)
</div>
<ul>
<li
class="footer__social-item"
v-for="item in socialList"
>
<a
:href="item.link"
:aria-label="item.title"
:title="item.title"
target="_blank"
class="footer__social-link"
rel="noopener noreferrer"
v-html="item.icon">
</a>
<li class="footer__social-item" v-for="item in socialList">
<a :href="item.link" :aria-label="item.title" :title="item.title" target="_blank" class="footer__social-link" rel="noopener noreferrer" v-html="item.icon"> </a>
</li>
</ul>
</div>
@@ -155,7 +140,7 @@ const qrcode = useQRCode(theme.value.footer.qrcodeLink)
opacity: 0.8;
&::after {
content: '+';
content: "+";
filter: invert(50%);
float: right;
width: 14px;
@@ -240,7 +225,7 @@ const qrcode = useQRCode(theme.value.footer.qrcodeLink)
.footer {
& > :last-child {
border-top: 1px solid var(--vp-c-divider);
}
}
}
.footer__socials {
+6 -6
View File
@@ -1,11 +1,11 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import { useData } from "vitepress";
import HomeHero from './HomeHero.vue'
import Features from './Features.vue';
import FeatureTitle from './FeatureTitle.vue';
import HomeHero from "./HomeHero.vue";
import Features from "./Features.vue";
import FeatureTitle from "./FeatureTitle.vue";
const { frontmatter: fm } = useData()
const { frontmatter: fm } = useData();
</script>
<template>
@@ -25,4 +25,4 @@ const { frontmatter: fm } = useData()
margin: 0 auto;
max-width: 1340px;
}
</style>
</style>
+17 -22
View File
@@ -1,28 +1,28 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import VPImage from 'vitepress/dist/client/theme-default/components/VPImage.vue'
import Button from './Button.vue'
import { type Ref, inject } from 'vue'
import type { DefaultTheme } from "vitepress/theme";
import VPImage from "vitepress/dist/client/theme-default/components/VPImage.vue";
import Button from "./Button.vue";
import { type Ref, inject } from "vue";
interface HeroAction {
theme?: 'brand' | 'alt'
text: string
link: string
theme?: "brand" | "alt";
text: string;
link: string;
}
interface Data {
image?: DefaultTheme.ThemeableImage
title: string
text: string
tagline: string
actions: HeroAction[]
image?: DefaultTheme.ThemeableImage;
title: string;
text: string;
tagline: string;
actions: HeroAction[];
}
defineProps<{
data: Data
}>()
data: Data;
}>();
const heroImageSlotExists = inject('hero-image-slot-exists') as Ref<boolean>
const heroImageSlotExists = inject("hero-image-slot-exists") as Ref<boolean>;
</script>
<template>
@@ -40,12 +40,7 @@ const heroImageSlotExists = inject('hero-image-slot-exists') as Ref<boolean>
</slot>
<div v-if="data.actions" class="actions">
<p v-for="action in data.actions" :key="action.link" class="action">
<Button
tag="a"
:theme="action.theme"
:text="action.text"
:href="action.link"
/>
<Button tag="a" :theme="action.theme" :text="action.text" :href="action.link" />
</p>
</div>
</div>
@@ -210,7 +205,7 @@ const heroImageSlotExists = inject('hero-image-slot-exists') as Ref<boolean>
.description {
line-height: 36px;
font-size: 24px;
max-width: 576px;
max-width: 576px;
}
.VPHomeHero.has-image .description {

Some files were not shown because too many files have changed in this diff Show More