Initial commit

pull/1/head
Zakhar Timoshenko 3 years ago
commit bd2ba8dd3f

@ -0,0 +1,31 @@
name: Deploy
on:
workflow_dispatch: {}
push:
branches:
- master
jobs:
deploy:
if: github.repository_owner == 'ztimms73'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm
- run: pnpm i --frozen-lockfile
- name: Build
run: pnpm run build
env:
NODE_OPTIONS: "--max_old_space_size=4096"
- uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
ARGS: '--archive --compress --delete --exclude="/redirect.php"'
SOURCE: ".vitepress/dist/"
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
REMOTE_USER: ${{ secrets.REMOTE_USER }}
TARGET: ${{ secrets.REMOTE_TARGET }}

19
.gitignore vendored

@ -0,0 +1,19 @@
.DS_Store
node_modules/
.yarn
.env.local
.env.*.local
.vitepress/cache
.vitepress/dist
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.temp
.idea
package-lock.json
pnpm-lock.yaml

@ -0,0 +1,15 @@
{
"line-length": false,
"link-fragments": false,
"no-inline-html": {
"allowed_elements": ["Badge", "ul", "ol", "li", "small", "sub", "sup", "br"]
},
"fenced-code-language": false,
"no-duplicate-heading": {
"allow_different_nesting": true,
"siblings_only": true
},
"single-title": {
"front_matter_title": ""
}
}

@ -0,0 +1 @@
package-import-method=clone-or-copy

@ -0,0 +1,223 @@
import type { DefaultTheme, LocaleConfig } from 'vitepress'
import type { Theme } from '../theme/types'
import { components } from '../theme/plugins/component'
import 'dotenv/config'
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'>>> = {
root: {
translations: {
button: {
buttonText: 'Search',
buttonAriaLabel: 'Search'
},
modal: {
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'
}
}
}
},
}
export const config: LocaleConfig<Theme.Config> = {
root: {
label: 'English',
lang: 'en',
description: META_DESCRIPTION,
head: [
['meta', { property: 'og:site_name', content: SITE_NAME }],
],
themeConfig: {
nav: getNav(),
sidebar: {
"/": defaultSidebar(),
},
outline: {
label: 'On this page',
level: 'deep',
},
docFooter: {
prev: 'Previous page',
next: 'Next page',
},
editLink: {
pattern: 'https://github.com/KotatsuApp/kotatsuapp.github.io/edit/master/website/:path',
text: 'Suggest changes to this page',
},
footer: {
message: "<a href=\"https://www.gnu.org/licenses/gpl-3.0.en.html\" target=\"_blank\">GPL-3.0 Licensed</a> <span class=\"divider\">|</span> <a href=\"/privacy/\">Privacy policy</a>",
copyright: `Copyright © 2020 - ${new Date().getFullYear()} Kotatsu Developers`,
},
components: components.filter(component => component.path.startsWith('components/')),
},
}
}
function defaultSidebar(): DefaultTheme.SidebarItem[] {
return [
{
text: "General sections",
items: [
{
text: "Download",
link: "/download/",
},
{
text: "Parsers",
link: "/parsers/",
},
{
text: "Changelogs",
link: "/changelogs/",
},
{
text: "Contribute",
link: "/contribute/",
},
],
},
{
text: "Frequently Asked Questions",
items: [
{ text: "General", link: "/manuals/faq/general" },
{
text: "Feed",
link: "/manuals/faq/feed",
},
{
text: "Explore",
link: "/manuals/faq/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",
},
{
text: "Random",
link: "/manuals/faq/explore/random",
},
{
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: "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: "Local storage",
link: "/manuals/guides/local-storage/",
collapsed: true,
items: [
{
text: "Advanced editing",
link: "/manuals/guides/local-source/advanced",
},
],
},
{
text: "Reader settings",
link: "/manuals/guides/reader-settings",
},
],
},
]
}
function getNav(): DefaultTheme.NavItem[] {
return [
{
text: 'Get v{app_version}',
activeMatch: "^/*?(download|changelogs)/*?$",
items: [
{
text: "Download",
link: "/download/",
},
{
text: "Changelogs",
link: "/changelogs/",
},
],
},
{
text: "Manual",
link: "/manuals/guides/getting-started",
activeMatch: "/manuals/",
},
{
text: "News",
link: "/news/",
activeMatch: "/news/",
},
{
text: "Account",
link: "/account/",
activeMatch: "/account/"
}
]
}

@ -0,0 +1,111 @@
import type { HeadConfig, TransformContext } from "vitepress"
import type { DocsPageData } from '../../theme/plugins/component'
function generateMeta(context: TransformContext, hostname: string) {
const head: HeadConfig[] = []
const { pageData }: { pageData: DocsPageData } = context
const url = `${hostname}/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, "$2")}`
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(/^\//, "")
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(),
},
])
}
return head
}
export default generateMeta

@ -0,0 +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"
const __dirname = dirname(fileURLToPath(import.meta.url))
const __fonts = resolve(__dirname, "../../fonts")
async function generateOgImages(config: SiteConfig) {
const pages = await createContentLoader("**/*.md", { excerpt: true }).load()
const template = await readFile(resolve(__dirname, "../../theme/components/OgImageTemplate.vue"), "utf-8")
const fonts: SatoriOptions["fonts"] = [
{
name: "Roboto",
data: await readFile(resolve(__fonts, "Roboto-Regular.ttf")),
weight: 400,
style: "normal",
},
{
name: "Roboto",
data: await readFile(resolve(__fonts, "Roboto-Medium.ttf")),
weight: 500,
style: "normal",
},
{
name: "Roboto",
data: await readFile(resolve(__fonts, "Roboto-Bold.ttf")),
weight: 700,
style: "normal",
},
]
const filteredPages = pages.filter((p) => p.frontmatter.image === undefined)
for (const page of filteredPages) {
await generateImage({
page,
template,
outDir: config.outDir,
fonts,
})
}
}
export default generateOgImages
interface GenerateImagesOptions {
page: ContentData
template: string
outDir: string
fonts: SatoriOptions["fonts"]
}
function getDir(url: string) {
if (url.startsWith("/manuals/faq/")) {
return "FAQ"
} else if (url.startsWith("/manuals/guides/")) {
return "Guide"
} else if (url.startsWith("/news/") && url !== "/news/") {
return "News"
} else if (url.startsWith("/sandbox/")) {
return "Sandbox"
} else if (url !== "/manuals/faq/" && url !== "/manuals/guides/" && url !== "/news/" && url !== "/sandbox/") {
return "Other"
}
return undefined
}
async function generateImage({ page, template, outDir, fonts }: GenerateImagesOptions) {
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 svg = await satoriVue(options, template)
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")
await mkdir(outputFolder, { recursive: true })
return await writeFile(outputFile, render.asPng())
}

@ -0,0 +1,130 @@
import type { Theme } from '../theme/types'
import type { DocsPageData } from '../theme/plugins/component'
import { type HeadConfig, defineConfigWithTheme } from 'vitepress'
import { config as ru, searchLocale as searchLocaleRu } from './ru'
import { config as root, searchLocale as searchLocaleEn } from './en'
import { config as ua, searchLocale as searchLocaleUa } from './ua'
import { addPlugins } from '../theme/plugins/markdown'
import { components, prepareData } from '../theme/plugins/component'
import { slugify } from 'transliteration'
import { fileURLToPath, URL } from 'node:url'
import { telegram } from '../../website/icons'
import { normalize } from 'vitepress/dist/client/shared'
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 = 'http://86.57.183.214:4173'
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',
markdown: {
theme: {
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') // Ограничение количества символов
return encodeURIComponent(slugify(str, { lowercase: true }))
}
},
config (md) {
addPlugins(md)
md.use(tabsMarkdownPlugin)
md.use(shortcode_plugin, shortcodes)
},
},
head: [
['link', { rel: 'preconnect', href: 'https://fonts.googleapis.com' }],
['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }],
['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' }],
['link', { rel: 'icon', href: '/favicon.ico?v=2', sizes: 'any' }],
['link', { rel: 'icon', href: '/icon.svg?v=2', type: 'image/svg+xml' }],
['link', { rel: 'apple-touch-icon', href: '/apple-touch-icon.png?v=2' }],
['link', { rel: 'manifest', href: '/site.webmanifest' }],
],
themeConfig: {
titleSeparator: SITE_TITLE_SEPARATOR,
i18nRouting: true,
logo: {
light: '/logo.svg',
dark: '/logo-dark.svg',
},
socialLinks: [
{
icon: { svg: telegram },
link: 'https://t.me/kotatsuapp',
},
{ icon: 'github', link: 'https://github.com/KotatsuApp/Kotatsu' },
],
search: {
provider: 'local',
},
components,
},
locales: {
...root,
...ru,
...ua,
},
transformPageData(pageData, { siteConfig }) {
return prepareData(pageData, siteConfig)
},
transformHead: async (context) => generateMeta(context, SITE_HOST),
vite: {
resolve: {
alias: [
'VPSidebar',
'VPNavBarTranslations',
'VPNavScreenTranslations',
'VPNavBar',
'VPNavBarMenu',
'VPNavScreenMenu'
].map(componentName => ({
find: new RegExp(`^.*\/${componentName}\.vue$`),
replacement: fileURLToPath(
new URL(`../theme/components/${componentName.replace(/^VP/, '')}.vue`, import.meta.url)
)
})),
},
},
sitemap: {
hostname: SITE_HOST,
},
buildEnd: async (context) => {
generateOgImages(context)
}
})

@ -0,0 +1,111 @@
import type { DefaultTheme, LocaleConfig } from 'vitepress'
import type { Theme } from '../theme/types'
import { components } from '../theme/plugins/component'
import 'dotenv/config'
export const SITE_NAME = 'Kotatsu Website'
export const META_DESCRIPTION = 'Простая и удобная читалка манги с открытым исходным кодом от сообщества и для сообщества, где вы можете найти и читать вашу любимую мангу проще как никогда.'
export const searchLocale: Record<string, Partial<Omit<DefaultTheme.LocalSearchOptions, 'locales'>>> = {
ru: {
translations: {
button: {
buttonText: 'Поиск',
buttonAriaLabel: 'Поиск'
},
modal: {
resetButtonTitle: 'Сбросить',
backButtonTitle: 'Закрыть',
noResultsText: 'Нет результатов по запросу',
footer: {
selectText: 'для выбора',
selectKeyAriaLabel: 'enter',
navigateText: 'для навигации',
navigateUpKeyAriaLabel: 'стрелка вверх',
navigateDownKeyAriaLabel: 'стрелка вниз',
closeText: 'закрыть',
closeKeyAriaLabel: 'escape'
}
}
}
},
}
export const config: LocaleConfig<Theme.Config> = {
ru: {
label: 'Русский',
lang: 'ru',
description: META_DESCRIPTION,
head: [
['meta', { property: 'og:site_name', content: SITE_NAME }],
],
themeConfig: {
nav: getNav(),
sidebar: {
},
outline: {
label: 'На этой странице',
level: 'deep',
},
returnToTopLabel: 'Наверх',
sidebarMenuLabel: 'Меню',
darkModeSwitchLabel: 'Тема',
langMenuLabel: 'Изменить язык',
teamSectionTitle: 'Команда',
lastUpdated: {
text: 'Последнее обновление',
},
docFooter: {
prev: 'Предыдущая страница',
next: 'Следующая страница',
},
editLink: {
pattern: 'https://github.com/KotatsuApp/kotatsuapp.github.io/edit/master/website/:path',
text: 'Предложить изменения на этой странице',
},
footer: {
message: "<a href=\"https://www.gnu.org/licenses/gpl-3.0.en.html\" target=\"_blank\">Лицензия GPL-3.0</a> <span class=\"divider\">|</span> <a href=\"/privacy/\">Политика конфиденциальности</a>",
copyright: `Copyright © 2020 - ${new Date().getFullYear()} Kotatsu Developers`,
},
components: components.filter(component => component.path.startsWith('ru/components/')),
},
}
}
function getNav(): DefaultTheme.NavItem[] {
return [
{
text: 'v{app_version}',
activeMatch: "^/*?(download|changelogs)/*?$",
items: [
{
text: "Скачать",
link: "/ru/download/",
},
{
text: "Изменения в версиях",
link: "/ru/changelogs/",
},
],
},
{
text: "Инструкции",
link: "/ru/manuals/guides/getting-started",
activeMatch: "/ru/manuals/",
},
{
text: "Новости",
link: "/ru/news/",
activeMatch: "/ru/news/",
},
]
}

@ -0,0 +1,89 @@
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>'
}
interface Navigation {
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" },
// 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: "Manage", dependsOn: "main_explore" },
// 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: "Manage 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" },
}
function generateNavigationHtml(navKey: string) {
const navData = navigationMappings[navKey]
if (!navData) {
return "<strong style='color:var(--vp-c-danger-1)'>Unsupported Navigation!</strong>"
}
const { name, icon, dependsOn } = navData
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}`
}
return html
}
const shortcodes = {
nav: {
render({ to }) {
return generateNavigationHtml(to)
},
},
}
export default shortcodes

@ -0,0 +1,109 @@
import type { DefaultTheme, LocaleConfig } from 'vitepress'
import type { Theme } from '../theme/types'
import { components } from '../theme/plugins/component'
export const SITE_NAME = 'Kotatsu Website'
export const META_DESCRIPTION = 'Проста і зручна читалка манги з відкритим вихідним кодом від спільноти і для спільноти, де ви можете знайти і читати вашу улюблену мангу простіше як ніколи.'
export const searchLocale: Record<string, Partial<Omit<DefaultTheme.LocalSearchOptions, 'locales'>>> = {
ru: {
translations: {
button: {
buttonText: 'Пошук',
buttonAriaLabel: 'Пошук'
},
modal: {
resetButtonTitle: 'Скинути',
backButtonTitle: 'Закрити',
noResultsText: 'Немає результатів за запитом',
footer: {
selectText: 'для вибору',
selectKeyAriaLabel: 'enter',
navigateText: 'для навігації',
navigateUpKeyAriaLabel: 'стрілка вгору',
navigateDownKeyAriaLabel: 'стрілка донизу',
closeText: 'закрити',
closeKeyAriaLabel: 'escape'
}
}
}
},
}
export const config: LocaleConfig<Theme.Config> = {
ua: {
label: 'Українська',
lang: 'ua',
description: META_DESCRIPTION,
head: [
['meta', { property: 'og:site_name', content: SITE_NAME }],
],
themeConfig: {
nav: getNav(),
sidebar: {
},
outline: {
label: 'На цій сторінці',
level: 'deep',
},
returnToTopLabel: 'Наверх',
sidebarMenuLabel: 'Меню',
darkModeSwitchLabel: 'Тема',
langMenuLabel: 'Змінити мову',
lastUpdated: {
text: 'Останнє оновлення',
},
docFooter: {
prev: 'Попередня сторінка',
next: 'Наступна сторінка',
},
editLink: {
pattern: 'https://github.com/KotatsuApp/kotatsuapp.github.io/edit/master/website/:path',
text: 'Запропонувати зміни на цій сторінці',
},
footer: {
message: "<a href=\"https://www.gnu.org/licenses/gpl-3.0.en.html\" target=\"_blank\">Ліцензія GPL-3.0</a> <span class=\"divider\">|</span> <a href=\"/privacy/\">Політика конфіденційності</a>",
copyright: `Copyright © 2020 - ${new Date().getFullYear()} Kotatsu Developers`,
},
components: components.filter(component => component.path.startsWith('ru/components/')),
}
}
}
function getNav(): DefaultTheme.NavItem[] {
return [
{
text: 'v{app_version}',
activeMatch: "^/*?(download|changelogs)/*?$",
items: [
{
text: "Завантажити",
link: "/ua/download/",
},
{
text: "Зміни у версіях",
link: "/ua/changelogs/",
},
],
},
{
text: "Інструкції",
link: "/ua/manuals/guides/getting-started",
activeMatch: "/ua/manuals/",
},
{
text: "Новини",
link: "/ua/news/",
activeMatch: "/ua/news/",
},
]
}

Binary file not shown.

@ -0,0 +1,106 @@
<script setup lang="ts">
import { Ref } from 'vue'
import { useData, withBase, useRoute } from 'vitepress'
import type { DocsPageData } from '../plugins/component'
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
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"
>
<li
v-for="(item, idx) in page.breadcrumbs"
:key="idx"
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>
<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"
>
</template>
<meta
itemprop="position"
:content="idx.toString()"
>
</li>
</ol>
</template>
<style scoped>
.Breadcrumbs {
display: flex;
white-space: nowrap;
column-gap: 10px;
color: var(--vp-c-text-2);
margin-bottom: 10px;
font-size: .9rem;
}
.item {
position: relative;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
@media (max-width: 767px) {
.item:last-child {
display: none;
}
}
.item + .item::before {
content: '/';
font-weight: normal;
margin-right: 8px;
}
.link {
font-weight: 500;
color: var(--vp-c-text-1);
transition: color 0.25s;
}
.link:hover {
color: var(--vp-c-brand-1);
}
.name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

@ -0,0 +1,136 @@
<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'
const props = defineProps<{
tag?: string
theme?: 'brand' | 'alt' | 'sponsor'
text: string
href?: string
target?: string
}>()
const classes = computed(() => [
props.theme ?? 'brand'
])
const isExternal = computed(() => props.href && EXTERNAL_URL_RE.test(props.href))
const component = computed(() => {
if (props.tag) {
return props.tag
}
return props.href ? 'a' : 'button'
})
</script>
<template>
<component
:is="component"
class="Button"
:class="classes"
:href="href ? normalizeLink(href) : undefined"
:target="target ?? (isExternal ? '_blank' : undefined)"
:rel="isExternal ? 'noreferrer' : undefined"
:aria-label="text"
>
{{ text }}
</component>
</template>
<style scoped>
.Button {
position: relative;
display: inline-block;
border: 4px solid transparent;
border-radius: 8px;
padding: 8px 18px;
font-size: 16px;
font-weight: 500;
transition: all 0.25s, color 0.25s;
}
.Button.alt {
border: 4px solid var(--vp-c-gray-1);
}
.Button.alt:hover {
background-color: var(--vp-c-gray-1);
color: var(--vp-c-black);
}
.Button.alt:active {
border-color: var(--vp-c-black);
background-color: var(--vp-c-black);
color: var(--vp-c-white);
}
.Button.cta:hover {
background-color: var(--vp-c-gray-1);
color: var(--vp-c-black);
}
.Button.cta:before {
content: '';
position: absolute;
display: block;
width: 25px;
height: 25px;
top: calc(50% - 12.5px);
left: 12px;
}
.Button.cta.question {
padding-left: 45px;
}
.Button.cta.question::before {
content: '';
display: inline-block;
width: 25px;
height: 25px;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="dodgerblue" class="w-6 h-6"><path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm11.378-3.917c-.89-.777-2.366-.777-3.255 0a.75.75 0 01-.988-1.129c1.454-1.272 3.776-1.272 5.23 0 1.513 1.324 1.513 3.518 0 4.842a3.75 3.75 0 01-.837.552c-.676.328-1.028.774-1.028 1.152v.75a.75.75 0 01-1.5 0v-.75c0-1.279 1.06-2.107 1.875-2.502.182-.088.351-.199.503-.331.83-.727.83-1.857 0-2.584zM12 18a.75.75 0 100-1.5.75.75 0 000 1.5z" clip-rule="evenodd" /></svg>');
background-size: 25px;
background-repeat: no-repeat;
background-position: bottom;
margin-right: 0.5em;
}
.Button.brand {
border-color: var(--vp-button-brand-bg);
color: var(--vp-button-brand-active-text);
background-color: var(--vp-button-brand-bg);
}
.Button.brand:hover {
border-color: var(--vp-button-brand-hover-bg);
color: var(--vp-button-brand-active-text);
background-color: var(--vp-button-brand-hover-bg);
}
.Button.brand:active {
border-color: var(--vp-button-brand-active-bg);
color: var(--vp-button-brand-active-text);
background-color: var(--vp-button-brand-active-bg);
}
.Button.sponsor {
border-color: var(--vp-button-sponsor-border);
color: var(--vp-button-sponsor-text);
background-color: var(--vp-button-sponsor-bg);
}
.Button.sponsor:hover {
border-color: var(--vp-button-sponsor-hover-border);
color: var(--vp-button-sponsor-hover-text);
background-color: var(--vp-button-sponsor-hover-bg);
}
.Button.sponsor:active {
border-color: var(--vp-button-sponsor-active-border);
color: var(--vp-button-sponsor-active-text);
background-color: var(--vp-button-sponsor-active-bg);
}
</style>

@ -0,0 +1,110 @@
<script setup lang="ts">
import { computed, toRefs } from "vue"
import MarkdownIt from "markdown-it"
import { type AppRelease, data as release } from "../data/release.data"
const props = defineProps<{ type: keyof AppRelease }>()
const { type } = toRefs(props)
const md = new MarkdownIt()
const changelog = computed(() => {
const flavoredString = (release[type.value].body ?? "")
.replace(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g, "[@$2](https://github.com/$2)")
.replace("https://github.com/KotatsuApp/Kotatsu/releases", "/changelogs/")
.replace("##", "-")
return md.render(flavoredString)
})
</script>
<template>
<div class="changelog">
<header>
<h2>Changelog</h2>
</header>
<div v-html="changelog" />
</div>
<div class="fullChangelog">
<p>
View the full release
<a href="https://github.com/KotatsuApp/Kotatsu/releases/latest" target="_blank" rel="noopener">
here
</a>
</p>
</div>
</template>
<style lang="stylus">
.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
header {
display: flex
justify-content: center
align-items: baseline
margin: 0 0 1rem
}
svg {
font-size: 1.2em
margin-right: 0.5rem
vertical-align: middle
}
h2 {
font-size: 1.5rem
margin: 0
padding: 0
color: var(--vp-c-text-1)
border: none
}
div > p {
margin: 0 0 1rem
color: var(--vp-c-text-2)
font-size: 0.9rem
}
table {
border-radius: 8px
border-collapse: collapse
border: 1px solid var(--vp-c-divider)
tr,
th,
td {
border: none
width: 100%
}
tbody tr {
border-top: 1px solid var(--vp-c-divider)
}
tr > td {
&:first-child {
color: var(--vp-c-text-2)
}
&:last-child {
font-family: var(--vp-font-family-mono)
font-size: var(--vp-code-font-size)
}
}
}
}
.fullChangelog {
margin: 0 0 1rem
color: var(--vp-c-text-2)
font-size: 0.9rem
}
</style>

@ -0,0 +1,64 @@
<script setup lang="ts">
import MarkdownIt from "markdown-it"
import { data as changelogs } from "../data/changelogs.data"
import Contributors from "./Contributors.vue"
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")
.trim()
return md.render(flavoredString)
}
const dateFormatter = new Intl.DateTimeFormat("en", {
dateStyle: "medium",
})
</script>
<template>
<div
v-for="(release, index) of changelogs"
:key="release.tag_name"
>
<h2 :id="index === 0 ? 'latest' : release.tag_name">
<a
:href="release.html_url"
target="_blank"
>
{{ release.tag_name.substring(1) }}
</a>
<Badge v-if="index === 0" type="tip" text="Latest" />
<a
class="header-anchor"
:href="index === 0 ? '#latest' : `#${release.tag_name}`"
:aria-label="`Permalink to &quot;${release.tag_name}&quot;`"
/>
</h2>
<time :datetime="release.published_at!">
{{ dateFormatter.format(new Date(release.published_at!)) }}
</time>
<div v-html="renderMarkdown(release.body)" />
</div>
</template>
<style lang="stylus" scoped>
h2 {
margin-bottom: 0
display: flex
align-items: center
gap: 0.5rem
}
time {
font-size: 0.875rem
color: var(--vp-c-text-2)
}
</style>

@ -0,0 +1,108 @@
<script setup lang="ts">
import { computed, ref, toRefs } from "vue"
const props = defineProps<{ body: string; author: string; tag: string }>()
const { body, author, tag } = toRefs(props)
function isHigherThan(tagName: string, reference: string) {
return reference.localeCompare(tagName, undefined, { numeric: true, sensitivity: "base" }) >= 0
}
const notMentioned = computed(() => {
return isHigherThan("v0.1", tag.value) ? ["Koitharu"] : []
})
const nonExistent = ref<string[]>([])
const contributors = computed(() => {
const list = [...body.value.matchAll(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g)]
.map((match) => match[2])
const uncredited = author.value.includes("[bot]")
? notMentioned.value
: [author.value, ...notMentioned.value]
return [...new Set([...uncredited, ...list])].filter((user) => !nonExistent.value.includes(user))
})
const listFormatter = new Intl.ListFormat("en", {
style: "long",
type: "conjunction",
})
const contributorsText = computed(() => {
if (contributors.value.length <= 3) {
return listFormatter.format(contributors.value)
}
return listFormatter.format([
...contributors.value.slice(0, 2),
`${contributors.value.length - 2} other contributors`,
])
})
function addToNonExistent(user: string) {
if (!nonExistent.value.includes(user)) {
nonExistent.value.push(user)
}
}
</script>
<template>
<div v-if="contributors.length > 0" class="contributors">
<h3>Contributors</h3>
<ul>
<li
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="avatar"
@error="addToNonExistent(contributor)"
>
</a>
</li>
</ul>
<div class="names">
{{ contributorsText }}
</div>
</div>
</template>
<style lang="stylus" scoped>
.contributors {
ul {
display: flex
align-items: center
flex-wrap: wrap
gap: 0.5rem
list-style-type: none
padding-left: 0
li + li {
margin-top: 0
}
}
.avatar {
width: 32px
height: 32px
border-radius: 50%
box-shadow: var(--vp-shadow-1)
border: 1px solid var(--vp-c-divider)
}
.names {
font-size: 0.875rem
color: var(--vp-c-text-2)
}
}
</style>

@ -0,0 +1,159 @@
<script setup lang="ts">
/// <reference types="@types/gtag.js" />
import { computed, onMounted, ref } 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)),
},
}))
const isAndroid = ref(true)
onMounted(() => {
isAndroid.value = !!navigator.userAgent.match(/android/i)
})
function handleAnalytics() {
window.gtag?.("event", "Download", {
event_category: "App",
event_label: "Stable",
version: release.stable.tag_name
})
}
</script>
<template>
<div>
<div v-if="!isAndroid" class="custom-block danger">
<p class="custom-block-title">
Unsupported operating system
</p>
<p>
<strong>Kotatsu</strong> is an <strong>Android app</strong> only.
Use an <strong>Android device</strong> to download and install the app.
</p>
</div>
<div v-if="!isAndroid" class="custom-block warning">
<p class="custom-block-title">
Caution
</p>
<p>
Any app for any operating systems other than Android called
<strong>Kotatsu</strong> (except kotatsu-dl) is not affiliated with this project.
</p>
<blockquote>
For more information, read the
<a href="/manuals/faq/general">General FAQ</a>.
</blockquote>
</div>
<div class="download-buttons">
<a
class="download-button primary"
:download="downloadInformation.stable.asset?.name"
:href="downloadInformation.stable.asset?.browser_download_url"
@click="handleAnalytics()"
>
<span class="text">Stable</span>
<span class="version">{{ downloadInformation.stable.tagName }}</span>
</a>
<a
class="download-button secondary"
href="https://f-droid.org/packages/org.koitharu.kotatsu/"
>
<span class="text">Get it on F-Droid</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;
}
.download-button {
position: relative;
display: inline-block;
border: 4px solid transparent;
border-radius: 8px;
justify-content: center;
padding: 8px 18px;
font-size: 16px;
font-weight: 500;
transition: all 0.25s, color 0.25s;
}
.download-button:hover {
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);
}
.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);
}
.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);
}
.download-button.secondary {
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);
}
.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);
}
svg {
display: inline-block;
vertical-align: middle;
margin-right: 0.5em;
font-size: 1.25em;
}
.text {
margin-right: 10px;
}
.version {
font-size: 0.8em;
}
.version-disclaimer {
display: block;
text-align: center;
margin: 0.75em auto;
font-size: 0.75rem;
}
</style>

@ -0,0 +1,60 @@
<script setup lang="ts">
defineProps<{
icon?: string
title: string
details?: string
}>()
</script>
<template>
<div class="Feature">
<article class="box">
<article class="iconBox">
<h3 class="icon" v-html="icon"></h3>
</article>
<h2 class="title" v-html="title"></h2>
<p v-if="details" class="details" v-html="details"></p>
</article>
</div>
</template>
<style scoped>
.Feature {
display: block;
border: 1px solid var(--vp-c-bg-soft);
border-radius: 12px;
height: 100%;
background-color: var(--vp-c-bg-soft);
transition: border-color 0.25s, background-color 0.25s;
}
.box {
display: flex;
flex-direction: column;
padding: 24px;
height: 100%;
}
.title {
line-height: 24px;
font-size: 16px;
font-weight: 600;
}
.details {
flex-grow: 1;
padding-top: 8px;
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.details:deep(a) {
color: var(--vp-c-brand-1);
}
.details:deep(a:hover) {
text-decoration: underline;
}
</style>

@ -0,0 +1,112 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { DefaultTheme } from 'vitepress/theme-without-fonts'
import Feature from './Feature.vue'
export interface Feature {
icon?: DefaultTheme.FeatureIcon
title: string
details: string
link?: string
linkText?: string
}
const props = defineProps<{
features: Feature[]
}>()
const grid = computed(() => {
const length = props.features.length
if (!length) {
return
} else if (length === 2) {
return 'grid-2'
} else if (length === 3) {
return 'grid-3'
} else if (length % 3 === 0) {
return 'grid-6'
} else 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
:icon="feature.icon"
:title="feature.title"
:details="feature.details"
:link="feature.link"
:link-text="feature.linkText"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.Features {
position: relative;
padding: 0 24px;
}
.container {
margin: 0 auto;
max-width: 1152px;
}
.items {
display: flex;
flex-wrap: wrap;
margin: -12px;
}
.item {
padding: 12px;
width: 100%;
}
@media (min-width: 640px) {
.item.grid-2,
.item.grid-4,
.item.grid-6 {
width: calc(100% / 2);
}
}
@media (min-width: 768px) {
.item.grid-2,
.item.grid-4 {
width: calc(100% / 2);
}
.item.grid-3,
.item.grid-6 {
width: calc(100% / 3);
}
}
@media (min-width: 960px) {
.item.grid-4 {
width: calc(100% / 4);
}
}
@media (min-width: 1200px) {
.Features {
padding: 0;
}
}
</style>

@ -0,0 +1,24 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import HomeHero from './HomeHero.vue'
const { frontmatter: fm } = useData()
</script>
<template>
<template v-if="fm.main">
<div class="container">
<HomeHero :data="fm.main" />
</div>
</template>
</template>
<style scoped>
.container {
display: flex;
flex-direction: column;
margin: 0 auto;
max-width: 1340px;
}
</style>

@ -0,0 +1,143 @@
<script setup lang="ts">
import Button from './Button.vue'
export interface HeroAction {
theme?: 'brand' | 'alt'
text: string
link: string
}
export interface Data {
title: string
text: string
tagline: string
actions: HeroAction[]
}
defineProps<{
data: Data
}>()
</script>
<template>
<section id="hero">
<h1 class="title">
<span class="accent">{{ data.title }}</span>
</h1>
<h2 class="text">
{{ data.text }}
</h2>
<p class="description">
{{ data.tagline }}
</p>
<p 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"
/>
</p>
</p>
</section>
</template>
<style scoped>
section {
padding: 42px 32px;
}
#hero {
padding: 96px;
text-align: left;
}
.title {
font-size: 76px;
line-height: 1.25;
font-weight: 900;
letter-spacing: -1.5px;
max-width: 1340px;
margin: 0px auto;
}
html:not(.dark) .accent,
.dark .title {
background: -webkit-linear-gradient(315deg, var(--vp-c-brand-1) 10%, var(--vp-c-brand-3) 50%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.text {
font-size: 54px;
line-height: 1.25;
font-weight: 900;
letter-spacing: -1.5px;
max-width: 1340px;
margin: 0px auto;
}
.description {
max-width: 1340px;
line-height: 1.5;
color: var(--vt-c-text-2);
transition: color 0.5s;
font-size: 22px;
margin: 24px auto 40px;
}
.actions {
display: flex;
flex-wrap: wrap;
justify-content: left;
margin: -6px;
}
.action {
flex-shrink: 0;
padding: 6px;
}
@media (max-width: 1340px) {
.tagline {
font-size: 64px;
letter-spacing: -0.5px;
}
.description {
font-size: 18px;
margin-bottom: 48px;
}
}
@media (max-width: 768px) {
.tagline {
font-size: 48px;
letter-spacing: -0.5px;
}
}
@media (max-width: 576px) {
#hero {
padding: 56px 32px;
}
.description {
font-size: 16px;
margin: 18px 0 30px;
}
#highlights h3 {
margin-bottom: 0.6em;
}
#highlights .vt-box {
padding: 20px 36px;
}
}
@media (max-width: 370px) {
.tagline {
font-size: 36px;
}
}
</style>

@ -0,0 +1,31 @@
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme-without-fonts'
import Home from './Home.vue'
import TranslatorWidget from './TranslatorWidget.vue'
import ScreenAuthorWidget from './ScreenAuthorWidget.vue'
import Breadcrumbs from './Breadcrumbs.vue'
import NotFound from './NotFound.vue'
const { Layout } = DefaultTheme
</script>
<template>
<Layout>
<template #home-hero-before>
<Home />
</template>
<template #aside-outline-after>
<TranslatorWidget />
</template>
<template #doc-before>
<Breadcrumbs />
</template>
<template #doc-footer-before>
<ScreenAuthorWidget />
</template>
<template #not-found>
<NotFound />
</template>
</Layout>
</template>

@ -0,0 +1,45 @@
<script setup lang="ts">
import { DefaultTheme, useRoute, withBase } from 'vitepress'
import List from './List.vue'
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
const route = useRoute()
defineProps<{
items: DefaultTheme.SidebarItem[]
}>()
</script>
<template>
<ul>
<li
v-for="(item, key) in items"
:key="key"
class="item"
:class="{ 'has-link': item.link }"
>
<VPLink
v-if="item.link"
:href="item.link"
:class="{ active: withBase(item.link) === route.path }"
>
{{ item.text }}
</VPLink>
<span v-else>
{{ item.text }}
</span>
<List v-if="item.items" :items="item.items" />
</li>
</ul>
</template>
<style scoped>
.link {
color: var(--vp-c-text-1);
text-decoration: none;
}
.active {
color: var(--vp-c-brand-1);
}
</style>

@ -0,0 +1,232 @@
<script lang="ts" setup>
import { useWindowScroll } from '@vueuse/core'
import { ref, watchPostEffect } from 'vue'
import { useData, inBrowser } from 'vitepress'
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
import VPNavBarTitle from 'vitepress/dist/client/theme-default/components/VPNavBarTitle.vue'
import VPNavBarSearch from 'vitepress/dist/client/theme-default/components/VPNavBarSearch.vue'
import VPNavBarMenu from 'vitepress/dist/client/theme-default/components/VPNavBarMenu.vue'
import VPNavBarTranslations from 'vitepress/dist/client/theme-default/components/VPNavBarTranslations.vue'
import VPNavBarAppearance from 'vitepress/dist/client/theme-default/components/VPNavBarAppearance.vue'
import VPNavBarSocialLinks from 'vitepress/dist/client/theme-default/components/VPNavBarSocialLinks.vue'
import VPNavBarExtra from 'vitepress/dist/client/theme-default/components/VPNavBarExtra.vue'
import VPNavBarHamburger from 'vitepress/dist/client/theme-default/components/VPNavBarHamburger.vue'
defineProps<{
isScreenOpen: boolean
}>()
defineEmits<{
(e: 'toggle-screen'): void
}>()
const { y } = useWindowScroll({ window: inBrowser && window })
const { hasSidebar } = useSidebar()
const { frontmatter } = useData()
const classes = ref<Record<string, boolean>>({})
watchPostEffect(() => {
classes.value = {
'has-sidebar': hasSidebar.value,
top: frontmatter.value?.layout === 'home' && y.value === 0,
}
})
</script>
<template>
<div class="VPNavBar" :class="classes">
<div class="container">
<div class="title">
<VPNavBarTitle>
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
</VPNavBarTitle>
</div>
<div class="content">
<div class="curtain" />
<div class="content-body">
<slot name="nav-bar-content-before" />
<VPNavBarSearch class="search" />
<VPNavBarMenu class="menu" />
<VPNavBarTranslations class="translations" />
<VPNavBarAppearance class="appearance" />
<VPNavBarSocialLinks class="social-links" />
<VPNavBarExtra class="extra" />
<slot name="nav-bar-content-after" />
<VPNavBarHamburger class="hamburger" :active="isScreenOpen" @click="$emit('toggle-screen')" />
</div>
</div>
</div>
</div>
</template>
<style scoped>
.VPNavBar {
position: relative;
border-bottom: 1px solid transparent;
padding: 0 8px 0 24px;
height: var(--vp-nav-height);
pointer-events: none;
white-space: nowrap;
}
@media (min-width: 768px) {
.VPNavBar {
padding: 0 32px;
}
}
@media (min-width: 960px) {
/* .VPNavBar.has-sidebar {
padding: 0;
} */
.VPNavBar:not(.has-sidebar):not(.top) {
border-bottom-color: var(--vp-c-gutter);
background-color: var(--vp-nav-bg-color);
}
}
.container {
display: flex;
justify-content: space-between;
margin: 0 auto;
max-width: calc(var(--vp-layout-max-width) - 64px);
height: var(--vp-nav-height);
pointer-events: none;
}
.container > .title,
.container > .content {
pointer-events: none;
}
.container :deep(*) {
pointer-events: auto;
}
/* @media (min-width: 960px) {
.VPNavBar.has-sidebar .container {
max-width: 100%;
}
} */
.title {
flex-shrink: 0;
height: calc(var(--vp-nav-height) - 1px);
transition: background-color 0.5s;
}
/* @media (min-width: 960px) {
.VPNavBar.has-sidebar .title {
position: absolute;
top: 0;
left: 0;
z-index: 2;
padding: 0 32px;
width: var(--vp-sidebar-width);
height: var(--vp-nav-height);
background-color: transparent;
}
} */
/* @media (min-width: 1440px) {
.VPNavBar.has-sidebar .title {
padding-left: max(32px, calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));
width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
}
} */
.content {
flex-grow: 1;
}
/* @media (min-width: 960px) {
.VPNavBar.has-sidebar .content {
position: relative;
z-index: 1;
padding-right: 32px;
padding-left: var(--vp-sidebar-width);
}
} */
/* @media (min-width: 1440px) {
.VPNavBar.has-sidebar .content {
padding-right: calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);
padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
}
} */
.content-body {
display: flex;
justify-content: flex-end;
align-items: center;
height: calc(var(--vp-nav-height) - 1px);
transition: background-color 0.5s;
}
@media (min-width: 960px) {
.VPNavBar:not(.top) .content-body {
position: relative;
background-color: var(--vp-nav-bg-color);
}
}
@media (max-width: 767px) {
.content-body {
column-gap: 0.5rem;
}
}
.menu + .translations::before,
.menu + .appearance::before,
.menu + .social-links::before,
.translations + .appearance::before,
.appearance + .social-links::before {
margin-right: 8px;
margin-left: 8px;
width: 1px;
height: 24px;
background-color: var(--vp-c-divider);
content: "";
}
.menu + .appearance::before,
.translations + .appearance::before {
margin-right: 16px;
}
.appearance + .social-links::before {
margin-left: 16px;
}
.social-links {
margin-right: -8px;
}
/* @media (min-width: 960px) {
.VPNavBar.has-sidebar .curtain {
position: absolute;
right: 0;
bottom: -31px;
width: calc(100% - var(--vp-sidebar-width));
height: 32px;
}
.VPNavBar.has-sidebar .curtain::before {
display: block;
width: 100%;
height: 32px;
background: linear-gradient(var(--vp-c-bg), transparent 70%);
content: "";
}
} */
/* @media (min-width: 1440px) {
.VPNavBar.has-sidebar .curtain {
width: calc(100% - ((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)));
}
} */
</style>

@ -0,0 +1,64 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue"
import { useData } from "vitepress"
import type { DefaultTheme } from 'vitepress'
import VPNavBarMenuLink from "vitepress/dist/client/theme-default/components/VPNavBarMenuLink.vue"
import VPNavBarMenuGroup from "vitepress/dist/client/theme-default/components/VPNavBarMenuGroup.vue"
import { data as release } from "../data/release.data"
const { theme } = useData<DefaultTheme.Config>()
// Used to avoid hydration issues.
const replace = ref(false)
onMounted(() => {
replace.value = true
})
/**
* Workaround to use the release data directly while the sidebar
* and navbar doesn't support using the VitePress data loading.
*/
const nav = computed(() => {
if (!replace.value) {
return theme.value.nav
}
return theme.value.nav?.map((item) => {
if (!item.text.includes("{app_version}")) {
return item
}
const appVersion = release.stable.tag_name.substring(1)
return {
...item,
text: item.text.replace("{app_version}", appVersion),
} satisfies DefaultTheme.NavItem
})
})
</script>
<template>
<nav v-if="nav" aria-labelledby="main-nav-aria-label" class="VPNavBarMenu">
<span id="main-nav-aria-label" class="visually-hidden">Main navigation</span>
<template v-for="item in nav" :key="item.text">
<VPNavBarMenuLink v-if="'link' in item" :item="item" />
<VPNavBarMenuGroup v-else :item="item" />
</template>
</nav>
</template>
<style scoped>
.VPNavBarMenu {
display: none
}
@media (min-width: 768px) {
.VPNavBarMenu {
display: flex
}
}
</style>

@ -0,0 +1,48 @@
<script setup lang="ts">
import VPIconLanguages from 'vitepress/dist/client/theme-default/components/icons/VPIconLanguages.vue'
import VPFlyout from 'vitepress/dist/client/theme-default/components/VPFlyout.vue'
import VPMenuLink from 'vitepress/dist/client/theme-default/components/VPMenuLink.vue'
import { useData } from 'vitepress'
import { useLangs } from '../composables/langs'
const { theme } = useData()
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
</script>
<template>
<VPFlyout
v-if="localeLinks.length && currentLang.label"
class="VPNavBarTranslations"
:icon="VPIconLanguages"
:label="theme.langMenuLabel || 'Change language'"
>
<div class="items">
<p class="title">{{ currentLang.label }}</p>
<template v-for="locale in localeLinks" :key="locale.link">
<VPMenuLink :item="locale" />
</template>
</div>
</VPFlyout>
</template>
<style scoped>
.VPNavBarTranslations {
display: none;
}
@media (min-width: 1280px) {
.VPNavBarTranslations {
display: flex;
align-items: center;
}
}
.title {
padding: 0 24px 0 12px;
line-height: 32px;
font-size: 14px;
font-weight: 700;
color: var(--vp-c-text-1);
}
</style>

@ -0,0 +1,57 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue"
import { type DefaultTheme, useData } from "vitepress"
import VPNavScreenMenuLink from "vitepress/dist/client/theme-default/components/VPNavScreenMenuLink.vue"
import VPNavScreenMenuGroup from "vitepress/dist/client/theme-default/components/VPNavScreenMenuGroup.vue"
import { data as release } from "../data/release.data"
const { theme } = useData<DefaultTheme.Config>()
// Used to avoid hydration issues.
const replace = ref(false)
onMounted(() => {
replace.value = true
})
/**
* Workaround to use the release data directly while the sidebar
* and navbar doesn't support using the VitePress data loading.
*/
const nav = computed(() => {
if (!replace.value) {
return theme.value.nav
}
return theme.value.nav?.map((item) => {
if (!item.text.includes("{app_version}")) {
return item
}
const appVersion = release.stable.tag_name.substring(1)
return {
...item,
text: item.text.replace("{app_version}", appVersion),
} satisfies DefaultTheme.NavItem
})
})
</script>
<template>
<nav v-if="nav" class="VPNavScreenMenu">
<template v-for="item in nav" :key="item.text">
<VPNavScreenMenuLink
v-if="'link' in item"
:item="item"
/>
<VPNavScreenMenuGroup
v-else
:text="item.text || ''"
:items="item.items"
/>
</template>
</nav>
</template>

@ -0,0 +1,77 @@
<script setup lang="ts">
import { ref } from 'vue'
import VPIconChevronDown from 'vitepress/dist/client/theme-default/components/icons/VPIconChevronDown.vue'
import VPIconLanguages from 'vitepress/dist/client/theme-default/components/icons/VPIconLanguages.vue'
import { useLangs } from '../composables/langs'
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
const isOpen = ref(false)
function toggle() {
isOpen.value = !isOpen.value
}
</script>
<template>
<div
v-if="localeLinks.length && currentLang.label"
class="VPNavScreenTranslations"
:class="{ open: isOpen }"
>
<button class="title" @click="toggle">
<VPIconLanguages class="icon lang" />
{{ currentLang.label }}
<VPIconChevronDown class="icon chevron" />
</button>
<ul class="list">
<li v-for="locale in localeLinks" :key="locale.link" class="item">
<VPLink class="link" :href="locale.link">{{ locale.text }}</VPLink>
</li>
</ul>
</div>
</template>
<style scoped>
.VPNavScreenTranslations {
height: 24px;
overflow: hidden;
}
.VPNavScreenTranslations.open {
height: auto;
}
.title {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-1);
}
.icon {
width: 16px;
height: 16px;
fill: currentColor;
}
.icon.lang {
margin-right: 8px;
}
.icon.chevron {
margin-left: 4px;
}
.list {
padding: 4px 0 0 24px;
}
.link {
line-height: 32px;
font-size: 13px;
color: var(--vp-c-text-1);
}
</style>

@ -0,0 +1,84 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useData } from 'vitepress'
import { useLangs } from 'vitepress/dist/client/theme-default/composables/langs'
import Button from './Button.vue'
const { site, theme } = useData()
const { localeLinks } = useLangs({ removeCurrent: false })
const root = ref<string>('/')
onMounted(() => {
const path = window.location.pathname
.replace(site.value.base, '')
.replace(/(^.*?\/).*$/, '/$1')
if (localeLinks.value.length) {
root.value =
localeLinks.value.find(({ link }) => link.startsWith(path))?.link ||
localeLinks.value[0].link
}
})
</script>
<template>
<div class="NotFound">
<p class="code">{{ theme.notFound?.code ?? 'Four-Oh-Four' }}</p>
<h1 class="title">{{ theme.notFound?.title ?? 'Page not found' }}</h1>
<div class="divider" />
<blockquote class="quote">
<template v-if="theme.notFound?.quote">
{{ theme.notFound?.quote }}
</template>
<template v-else>
<p>Похоже, что вы перешли по неверной или устаревшей ссылке.</p>
<p>Информация, которую вы искали, где-то здесь. Вы можете воспользоваться поиском.</p>
<br>
<p>Этот сайт автоматически генерируется из файлов, расположенных на GitHub, поэтому адреса могут иногда меняться.</p>
</template>
</blockquote>
<div class="action">
<Button
class="link"
:href="root"
theme="alt"
:text="theme.notFound?.linkText ?? 'Вернуться на главную'"
/>
</div>
</div>
</template>
<style scoped>
.NotFound {
padding: 64px 24px 96px;
text-align: center;
}
@media (min-width: 768px) {
.NotFound {
padding: 96px 32px 168px;
}
}
.code {
line-height: 64px;
font-size: 64px;
font-weight: 600;
}
.title {
padding-top: 12px;
letter-spacing: 2px;
line-height: 20px;
font-size: 20px;
font-weight: 700;
}
.quote {
margin: 0 auto;
max-width: 512px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.action {
padding-top: 20px;
}
</style>

@ -0,0 +1,42 @@
<script setup lang="ts">
defineProps<{ title: string; description?: string; dir?: string }>()
</script>
<template>
<div
tw="w-full h-full bg-white flex"
style="background-color: #0058c9;"
>
<div tw="p-14 w-full min-h-0 grow flex flex-1 flex-col justify-between">
<div tw="w-full flex justify-between items-center text-4xl font-medium">
<div tw="flex items-center">
<svg width="64" height="64" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet" fill="#FFFFFF"><path d="M 145.3 101.9 C 127 108.5 111.1 133.3 103.4 167.2 C 100.6 179.6 98 200.4 98 210.5 L 98 217.5 L 89.7 230.5 C 66.6 266.5 45.4 304 25.5 344 C 7.8 379.4 6 383.7 6 390 C 6 392.8 6.7 396.4 7.4 397.9 C 9.7 402.2 15.3 406.6 21.5 408.9 L 27.2 411.1 L 258.7 411 C 483.8 411 490.2 410.9 493.8 409.1 C 505.4 403.1 509.1 390.7 503.1 377.8 C 480.4 328.4 449.6 272.3 421.5 229 L 414.7 218.5 L 413.9 206.5 C 410.8 160.5 401.1 132.3 381.2 111.7 C 372.6 102.9 369.3 100.9 363.2 100.8 C 354.7 100.8 349 104.7 336.3 119.3 C 323.8 133.7 321.2 137.1 312.5 150 C 308.2 156.3 302.9 163.6 300.7 166.2 L 296.7 171 L 255.1 171 L 213.5 170.9 L 205.1 158 C 189 133.2 162.6 103.1 155.1 101 C 150.7 99.8 151.5 99.7 145.3 101.9 Z M 171.8 242.6 C 175 244.8 175.8 246.8 176.9 256 C 178.3 266.9 179.6 268.4 186.7 267.5 C 193.1 266.7 198.3 268.8 201.2 273.4 C 206.9 282.6 197.1 301.9 183.2 308.7 C 178.2 311.2 176.4 311.5 168 311.5 C 159.9 311.5 157.8 311.1 154 309.1 C 142.9 303.3 136.1 294.8 133.3 283.6 C 131.9 277.7 131.8 275.9 132.9 270.7 C 134.4 263.2 137 258.2 141.7 252.9 C 150 243.7 165.7 238.3 171.8 242.6 Z M 348.8 243.5 C 351.9 246.1 352.1 246.9 353.6 258.9 C 354.5 266.2 356.7 268.4 362.6 267.5 C 368.3 266.7 373.2 268.3 376.5 272.1 C 381.9 278.6 378.7 291.1 369.1 301.3 C 361.4 309.5 356.6 311.4 344.5 311.5 C 335.3 311.5 334.1 311.3 329.3 308.6 C 322.1 304.6 315.6 298 312.3 291.2 C 310.1 286.7 309.4 283.8 309.1 277.3 C 308.8 269.8 309.1 268.4 311.8 262.7 C 316.8 251.9 324.4 245.2 334.9 242.4 C 342.1 240.4 345.5 240.7 348.8 243.5 Z" /></svg>
<div tw="text-white ml-8 font-bold">
Kotatsu
</div>
</div>
</div>
<div tw="text-7xl font-bold text-white" v-html="title" />
<div tw="w-full flex flex-col items-start justify-end">
<div v-if="description" tw="mt-8 text-3xl text-white" v-html="description" />
</div>
</div>
<div v-if="dir" tw="w-98 h-full flex flex-none">
<svg v-if="dir === 'FAQ'" height="600" width="600" viewBox="0 0 960 960" fill="#0b37aa" xmlns="http://www.w3.org/2000/svg"><path d="M 700 580 L 220 580 L 60 740 L 60 100 C 60 77.91 77.91 60 100 60 L 700 60 C 722.09 60 740 77.91 740 100 L 740 540 C 740 562.09 722.09 580 700 580 M 900 340 L 900 900 L 740 740 L 300 740 C 277.91 740 260 722.09 260 700 L 260 660 L 820 660 L 820 300 L 860 300 C 882.09 300 900 317.91 900 340 M 307.6 140 C 272.8 140 244.8 148 223.2 163.6 C 202.4 180 192 202.8 192.4 234.4 L 192.8 235.6 L 270 235.6 C 270.4 223.6 274 214.4 281.2 208 C 288.4 202 297.2 198.8 307.6 198.8 C 320 198.8 330.4 202.8 337.6 210 C 344.8 217.6 348 228 348 240 C 348 252.8 345.2 263.6 338.8 272.8 C 333.2 282 324.8 290 314.4 296.4 C 294 310 280 322 272.4 332.8 C 264.4 343.2 260 360 260 380 L 340 380 C 340 367.6 341.6 357.6 345.2 350.4 C 348.8 343.2 355.6 336 365.6 329.6 C 383.6 320 398.4 308.4 410 292.4 C 421.6 276.4 427.6 260 427.6 240 C 427.6 209.6 416.8 185.2 395.2 167.2 C 374 149.2 344.8 140 307.6 140 M 260 420 L 260 500 L 340 500 L 340 420 L 260 420 M 500 500 L 580 500 L 580 420 L 500 420 L 500 500 M 500 140 L 500 380 L 580 380 L 580 140 L 500 140 Z" /></svg>
<svg v-else-if="dir === 'Guide'" height="600" width="600" viewBox="0 0 960 960" fill="#0b37aa" xmlns="http://www.w3.org/2000/svg"><path d="M 480 261.82 C 600.5 261.82 698.18 359.5 698.18 480 C 698.18 560.73 654.18 631.27 589.09 669.09 L 589.09 734.54 C 589.09 754.62 572.81 770.9 552.73 770.9 L 407.27 770.9 C 387.19 770.9 370.91 754.62 370.91 734.54 L 370.91 669.09 C 305.82 631.27 261.82 560.73 261.82 480 C 261.82 359.5 359.5 261.82 480 261.82 M 552.73 807.27 L 552.73 843.63 C 552.73 863.71 536.45 879.99 516.37 879.99 L 443.64 879.99 C 423.56 879.99 407.28 863.71 407.28 843.63 L 407.28 807.27 L 552.73 807.27 M 770.91 443.64 L 880 443.64 L 880 516.37 L 770.91 516.37 L 770.91 443.64 M 80 443.64 L 189.09 443.64 L 189.09 516.37 L 80 516.37 L 80 443.64 M 516.36 80 L 516.36 189.09 L 443.63 189.09 L 443.63 80 L 516.36 80 M 222.55 170.91 L 300 248.73 L 248.36 300 L 170.91 222.91 L 222.55 170.91 M 660 248.36 L 737.09 170.91 L 789.09 222.91 L 711.64 300 L 660 248.36 Z" /></svg>
<svg v-else-if="dir === 'News'" height="600" width="600" viewBox="0 0 960 960" fill="#0b37aa" xmlns="http://www.w3.org/2000/svg"><path d="M157.5-122.5q-31 0-53-22t-22-53v-614q0-6.5 5.5-9t10.5 2.5l35.5 35.5q5.5 5.5 13 5.25t13-5.75l40-40q5.5-5.5 13-5.75t13 5.25l41 41q5.5 5.5 13 5.5t13-5.5l41-41q5.5-5.5 13-5.25t13 5.75l40 40q5.5 5.5 13 5.75t13-5.25l41-41q5.5-5.5 13-5.5t13 5.5l41 41q5.5 5.5 13 5.25t13-5.75l40-40q5.5-5.5 13-5.75t13 5.25l41 41q5.5 5.5 13 5.5t13-5.5l41-41q5.5-5.5 13-5.25t13 5.75l40 40q5.5 5.5 13 5.75t13-5.25l35.5-35.5q5-5 10.5-2.5t5.5 9v614q0 31-22 53t-53 22h-645Zm0-75h285v-245h-285v245Zm360 0h285v-85h-285v85Zm0-160h285v-85h-285v85Zm-360-160h645v-125h-645v125Z" /></svg>
<svg v-else-if="dir === 'Sandbox'" height="600" width="600" viewBox="0 0 960 960" fill="#0b37aa" xmlns="http://www.w3.org/2000/svg"><path d="M 220 80 C 142.4 80 80 142.4 80 220 L 80 740 C 80 817.6 142.4 880 220 880 L 640 880 L 880 640 L 880 220 C 880 142.4 817.6 80 740 80 L 220 80 M 230 160 L 730 160 C 768.66 160 800 191.34 800 230 L 800 600 L 740 600 C 662.4 600 600 662.4 600 740 L 600 800 L 230 800 C 191.34 800 160 768.66 160 730 L 160 230 C 160 191.34 191.34 160 230 160 M 577.6 270.8 C 571.2 270.8 564.8 271.6 558.8 273.2 C 521.2 283.6 500 322 509.6 360 C 511.6 366 514.4 372 518 377.6 L 647.2 342.4 C 647.2 335.6 646.4 328.8 644.8 322 C 636.4 292 608.8 270.8 577.6 270.8 M 326.8 340 C 320 340 314 340 308 342 C 270.8 352.4 248.8 390.8 258.8 428 C 260 434.4 263.6 440 267.2 446.4 L 396.4 411.2 C 396.4 404.4 395.6 397.6 394 391.2 C 385.6 360 358 340 326.8 340 M 668.8 450.4 L 303.6 550.8 C 356.4 612 440 637.6 518 616.4 C 596 594.8 654.4 530 668.8 450.4 Z" /></svg>
<svg v-else-if="dir === 'Other'" height="600" width="600" viewBox="0 0 960 960" fill="#0b37aa" xmlns="http://www.w3.org/2000/svg">
<path d="M 500.37 828.06 C 551.94 827.86 603.32 821.92 653.57 810.35 L 663.79 807.89 C 662.43 812.48 661.75 817.23 661.76 822.02 C 662.06 823.99 662.8 825.88 663.92 827.54 C 665.04 829.2 666.52 830.58 668.24 831.6 C 670.92 832.4 675.62 829.09 678.35 826.25 C 680.68 823.16 682.67 819.82 684.29 816.3 C 686.16 813.41 688.41 810.95 690.93 807.52 C 695.19 813.26 699.74 818.78 704.58 824.05 C 705.78 825.13 707.23 825.9 708.79 826.3 C 710.36 826.7 712 826.72 713.57 826.35 C 715.6 825.28 715.92 820.62 716.41 817.41 C 716.62 815.65 716.18 813.89 715.18 812.43 C 703.46 795.63 711.43 785.67 725.19 774.28 C 771.64 717.99 781.75 680.21 779.34 617.87 C 776.5 543.54 777.36 470.39 745.74 401.85 C 735.68 380.07 731.99 355.24 726.26 331.59 C 725.31 324.71 725.45 317.72 726.69 310.88 C 731.4 262.72 729.26 216.17 731.51 167.79 C 731.41 144.75 727.39 121.9 719.63 100.21 C 711.07 75.06 665.74 52.48 646.86 71.26 C 626.63 91.43 620.69 131.35 609.4 157.52 C 595.17 189.63 592.28 222.16 574.73 255.12 C 551.45 254.09 528.13 255.54 505.17 259.45 C 504.39 233.35 501.35 207.37 496.07 181.81 C 487.56 141.94 476.7 102.61 449.25 70.56 C 447.27 68.21 445.4 65.74 443.26 63.6 C 423.35 43.48 403.02 45.3 393.01 71.57 C 381.41 101.72 372.73 132.92 367.11 164.73 C 360.15 205.08 357.85 246.28 353.3 287.06 C 352.99 291.79 351.74 296.41 349.61 300.65 C 331.43 333.75 320.9 370.5 318.79 408.21 C 317.09 436.1 312.41 463.73 304.82 490.62 C 288.77 544.13 297.11 597.32 311.29 649.81 C 322.07 692.78 340.72 733.39 366.3 769.57 C 375.97 784.41 387.63 797.84 400.97 809.49 C 408.3 815.27 419.91 815.54 419.91 817.3 C 419.91 819.06 414.93 820.67 402.09 828 C 394.92 832.12 392.73 833.94 392.67 836.29 C 392.62 838.64 397.54 839.77 400.11 841.16 C 400.55 841.4 401.05 841.52 401.55 841.52 C 402.05 841.52 402.55 841.4 402.99 841.16 C 409.02 838.77 414.58 835.35 419.42 831.05 C 423.34 828.33 426.67 824.86 429.21 820.83 C 425.65 827.97 422.97 835.51 421.24 843.3 C 420.7 846.4 430.93 847.58 433.17 843.3 C 436.11 837.95 433.6 829.44 442.37 825.05 C 442.37 825.05 434.5 843.4 440.23 848.54 C 441.68 849.58 443.42 850.15 445.21 850.15 C 447 850.15 448.74 849.59 450.19 848.54 C 455.97 843.99 450.19 833.4 453.94 827.14 C 457.69 820.88 468.48 823.94 500.37 828.06 Z M 586.36 276.15 C 588.55 271.98 590.75 268.28 592.51 264.38 C 605.41 235.86 608.03 207.66 620.28 175.66 C 634.78 137.93 636.33 107.22 657.74 83.51 C 669.19 70.93 703.6 92.29 707.88 108.45 C 713.21 128.9 715.61 149.99 715 171.11 C 713.5 211.19 720.35 251.38 714.04 290.97 C 707.14 330.31 713.09 370.85 731 406.55 C 745.53 434.17 755.07 464.13 759.2 495.06 C 767.71 561.09 774.77 621.13 761.61 684.49 C 748.02 727.83 734.32 764.76 673.96 787.23 C 603.33 813.56 529.16 819.02 454.19 809.33 C 440.23 807.86 426.74 803.47 414.59 796.44 C 402.44 789.41 391.91 779.91 383.67 768.55 C 353.75 727.42 332.6 680.57 321.54 630.92 C 310.2 584.1 306.61 537.06 319.61 489.54 C 325.58 469.06 329.62 448.06 331.65 426.83 C 335.13 383.17 342.35 340.95 365.2 302.74 C 367.41 298.45 368.45 293.65 368.2 288.83 C 368.31 241.14 374.08 193.63 385.38 147.29 C 390.71 123.88 397.59 100.86 405.98 78.37 C 412.4 61.46 422.03 60.12 432.74 74.36 C 444.95 89.67 455.18 106.47 463.19 124.34 C 481.01 167.3 489.99 213.42 489.57 259.94 C 489.57 267.54 491.18 273.42 500.7 271.28 C 529.13 264.76 558.85 266.44 586.36 276.15 Z"/>
<path d="M 617.29 348.5 C 600.41 351.64 585.44 361.35 575.7 375.5 C 565.96 389.65 562.23 407.1 565.33 423.99 C 568.43 440.88 578.12 455.87 592.25 465.63 C 606.38 475.39 623.82 479.15 640.72 476.08 C 657.61 473.01 672.62 463.34 682.4 449.23 C 692.19 435.12 695.98 417.69 692.94 400.78 C 690.9 389.55 685.93 379.04 678.54 370.34 C 671.15 361.63 661.59 355.03 650.84 351.2 C 640.1 347.36 628.52 346.43 617.29 348.5 Z M 620.82 365.03 C 633.27 363.35 645.91 366.48 656.12 373.79 C 666.34 381.09 673.39 392.04 675.83 404.36 C 679.95 428.76 661.76 455.03 637.89 459.1 C 627.94 460.94 617.67 459.57 608.54 455.2 C 599.42 450.83 591.92 443.67 587.12 434.77 C 582.32 425.87 580.47 415.67 581.83 405.64 C 583.19 395.62 587.71 386.29 594.71 378.99 C 601.72 371.69 610.86 366.81 620.82 365.03 Z M 427.54 450.01 C 443.01 447.11 456.71 438.19 465.62 425.22 C 474.53 412.25 477.94 396.25 475.1 380.77 C 472.26 365.29 463.39 351.56 450.45 342.6 C 437.51 333.64 421.53 330.17 406.04 332.96 C 390.55 335.75 376.78 344.57 367.78 357.48 C 358.77 370.39 355.25 386.35 357.98 401.85 C 359.82 412.2 364.37 421.88 371.16 429.91 C 377.95 437.93 386.74 444.02 396.65 447.55 C 406.54 451.08 417.21 451.92 427.54 450.01 Z M 375.42 398.8 C 373.81 387.89 376.42 376.76 382.69 367.7 C 388.96 358.63 398.46 352.28 409.24 349.95 C 418.13 348.06 427.41 349.09 435.66 352.9 C 443.92 356.7 450.72 363.09 455.05 371.09 C 459.37 379.08 461 388.27 459.68 397.27 C 458.36 406.27 454.15 414.59 447.7 421 C 441.26 427.41 432.9 431.56 423.9 432.84 C 412.99 434.51 401.85 431.9 392.82 425.56 C 383.8 419.21 377.56 409.62 375.42 398.8 Z"/>
<path d="M 630.4 445.78 C 636.82 444.23 643.19 442.3 643.19 430.64 C 643.42 426.72 642.47 422.82 640.46 419.44 C 638.45 416.06 635.47 413.37 631.91 411.7 C 628.35 410.04 624.38 409.47 620.5 410.09 C 610.07 411.8 603.48 426.14 605.73 435.99 C 607.71 444.6 615.2 448.56 630.4 445.78 Z M 415.77 420.36 C 420.4 419.17 424.4 416.23 426.91 412.16 C 429.43 408.09 430.27 403.21 429.26 398.53 C 427.92 389.7 417.54 383.33 405.72 385.9 C 402.53 386.65 399.61 388.27 397.28 390.57 C 394.95 392.87 393.3 395.77 392.5 398.94 C 391.7 402.12 391.8 405.45 392.77 408.58 C 394.85 417.74 401.8 423.09 415.77 420.36 Z"/>
<path d="M 44.08 395.21 L 44.08 721.2 C 43.72 725.38 44.46 729.59 46.22 733.4 C 48.36 736.42 51.13 738.92 54.35 740.73 C 120.86 784.13 148.74 809.97 215.26 853.37 C 222.11 857.81 239.29 873.44 247.37 874.77 C 279.48 880.12 290.66 886.49 298.58 887.51 C 386.98 898.96 473.88 901.26 563.08 901.53 C 563.08 783.81 565.43 669.29 565.49 551.41 C 565.69 549.56 565.32 547.69 564.42 546.06 C 563.44 545.03 562.25 544.24 560.93 543.73 C 559.61 543.22 558.18 543.01 556.77 543.12 L 314.63 528.24 C 297.74 527.88 280.94 525.52 264.6 521.23 C 251.56 516.83 239.01 511.11 227.14 504.16 C 165.38 470.21 106.05 432 49.59 389.81" fill="#0058c9"/>
<path d="M 34.72 395.21 L 34.72 679.14 C 34.72 695.89 30.87 717.08 36.27 732.92 C 39.96 743.62 49.06 748.28 58.1 754.32 C 88.92 774.98 118.73 797.13 148.75 818.53 C 176.36 838.6 204.45 861.61 233.94 878.57 C 250.64 888.2 276.75 892.43 296.07 896.34 C 382.01 913.68 475.87 910.31 563.09 910.68 C 565.56 910.65 567.93 909.66 569.68 907.91 C 571.43 906.16 572.43 903.79 572.45 901.32 C 572.45 819.02 573.68 736.77 574.38 654.47 C 574.38 633.87 574.7 613.27 574.75 592.67 C 574.75 578.86 579.08 559.12 574 546.01 C 565.81 524.98 527.39 531.78 509.41 530.65 L 388.32 523.21 C 346.96 520.7 298.48 524.07 259.36 509.3 C 222.38 495.33 187.33 470.88 154.05 449.9 C 119.95 428.5 86.77 405.68 54.52 381.46 C 44.83 374.29 35.47 390.56 45.05 397.51 C 103.46 441.72 165.26 481.26 229.88 515.77 C 290.88 547.88 368.69 540.55 435.68 544.67 L 493.63 548.26 C 508.93 549.17 540.77 545.64 551.21 556.45 C 561.64 567.26 556.03 597.87 555.92 613.17 C 555.92 632.01 555.92 650.63 555.49 669.73 C 554.79 746.89 553.78 824 553.72 901.16 L 563.08 891.8 C 481.58 891.48 399.93 889.71 318.96 880.24 C 296.11 877.62 263.47 872.86 243.4 862.26 C 217.18 848.45 191.92 826.46 167.84 809.18 C 142.05 790.72 116.79 771.72 90.68 753.53 C 71.04 739.88 56.38 735.34 53.22 710.35 C 50.81 690.02 53.22 667.54 53.22 647.26 L 53.22 394.84 C 53.22 392.36 52.23 389.98 50.47 388.22 C 48.71 386.46 46.33 385.47 43.85 385.47 C 41.37 385.47 38.99 386.46 37.23 388.22 C 35.47 389.98 34.48 392.36 34.48 394.84 L 34.72 395.21 Z"/>
<path d="M 248.55 548.36 C 248.12 643.02 237.85 737.31 237.85 831.97 C 237.85 834.45 238.84 836.83 240.6 838.59 C 242.36 840.35 244.74 841.34 247.22 841.34 C 249.7 841.34 252.08 840.35 253.84 838.59 C 255.6 836.83 256.59 834.45 256.59 831.97 C 256.59 737.31 266.86 643.02 267.29 548.36 C 267.29 545.88 266.3 543.5 264.54 541.74 C 262.78 539.98 260.4 538.99 257.92 538.99 C 255.44 538.99 253.06 539.98 251.3 541.74 C 249.54 543.5 248.55 545.88 248.55 548.36 Z M 295.48 776.96 L 343.27 779.37 L 338.72 773.43 C 336.83 781.98 333.14 790.04 327.92 797.07 C 322.69 804.1 316.03 809.94 308.38 814.21 C 303.03 817.1 307.79 824.91 313.09 822.34 C 321.82 817.45 329.42 810.78 335.39 802.76 C 341.37 794.74 345.58 785.55 347.76 775.79 C 348.01 774.86 347.97 773.89 347.65 772.98 C 347.32 772.08 346.72 771.3 345.94 770.75 C 345.15 770.2 344.22 769.9 343.26 769.9 L 295.47 767.49 C 289.42 767.22 289.48 776.59 295.47 776.85 L 295.47 776.96 Z"/>
<path d="M 290.34 807.03 C 303.5 816.93 316.19 827.26 324.11 842.03 C 326.95 847.38 334.81 842.62 332.19 837.32 C 323.52 821.27 309.5 809.82 295.05 798.95 C 290.23 795.36 285.58 803.45 290.34 807.03 Z M 368.95 779.15 C 375.46 786.2 383.3 791.89 392.01 795.9 C 397.36 798.2 402.28 790.12 396.72 787.77 C 388.62 784.19 381.34 779 375.32 772.52 C 371.09 768.24 364.62 774.82 368.68 779.16 L 368.95 779.16 Z M 421.76 775.78 C 421.96 788.76 419.97 801.67 415.87 813.99 C 408.54 833.74 391.84 835.88 373.06 835.07 C 367.01 835.07 367.07 844.22 373.06 844.43 C 389.43 845.13 409.61 844.11 419.4 828.86 C 429.19 813.61 430.9 793.38 431.28 775.78 C 431.27 774.53 430.78 773.34 429.89 772.46 C 429.01 771.58 427.81 771.08 426.57 771.08 C 425.32 771.08 424.13 771.57 423.25 772.46 C 422.37 773.34 421.87 774.53 421.86 775.78 L 421.76 775.78 Z M 469.92 770.43 C 469.92 783.7 472.81 796.92 472.38 810.24 C 471.79 826.83 461.68 835.87 446.64 840.85 C 440.97 842.72 443.38 851.55 449.1 849.84 C 463.17 845.13 475.27 837.59 480.3 822.55 C 485.65 806.5 479.55 786.91 479.34 770.43 C 479.34 769.18 478.84 767.98 477.96 767.1 C 477.08 766.22 475.88 765.72 474.63 765.72 C 473.38 765.72 472.18 766.22 471.3 767.1 C 470.42 767.98 469.92 769.18 469.92 770.43 Z"/>
<path d="M 447.87 796.54 L 498.44 796.54 C 502.67 796.54 505.29 795.42 507.86 799.38 C 509.41 801.84 507.86 809.6 507.86 812.33 C 507.86 820.84 511.18 846.42 495.07 841.28 C 489.34 839.41 486.88 848.45 492.61 850.32 C 522.15 859.74 517.44 815.16 517.12 799.38 C 517.12 786.75 513 787.18 500.75 787.18 L 447.88 787.18 C 446.63 787.18 445.43 787.68 444.55 788.56 C 443.67 789.44 443.17 790.64 443.17 791.89 C 443.17 793.14 443.67 794.34 444.55 795.22 C 445.43 796.1 446.63 796.6 447.88 796.6 L 447.88 796.54 Z M 511.5 778.88 C 514.62 780.83 517.47 783.18 519.95 785.89 C 524.07 790.33 530.65 783.7 526.59 779.25 C 523.53 775.99 520.06 773.14 516.26 770.8 C 515.2 770.3 513.99 770.21 512.86 770.55 C 511.74 770.9 510.78 771.65 510.19 772.66 C 509.59 773.67 509.39 774.87 509.64 776.02 C 509.88 777.18 510.55 778.2 511.5 778.88 Z M 521.02 769.84 C 524.64 772 527.55 775.16 529.42 778.94 C 532.26 784.29 540.12 779.53 537.55 774.18 C 534.85 769.03 530.78 764.73 525.78 761.77 C 524.72 761.27 523.51 761.18 522.38 761.52 C 521.26 761.87 520.3 762.62 519.71 763.63 C 519.11 764.64 518.91 765.84 519.16 766.99 C 519.41 768.14 520.07 769.16 521.02 769.84 Z M 501.39 647.35 C 503.42 646.39 501.82 641.3 501.82 639.06 C 501.82 603.53 501.23 565.16 501.39 529.63 L 442.53 524.28 C 443.94 564.17 442.35 604.12 437.77 643.77 C 448.29 636.75 458.36 629.06 467.9 620.76 C 469.68 619.29 471.98 618.58 474.28 618.79 C 476.59 619 478.72 620.12 480.2 621.89 L 501.39 647.35 Z"/>
<path d="M 505.88 648.58 C 509.95 633.38 506.52 613.91 506.42 598.17 C 506.42 575.32 505.99 552.47 506.1 529.62 C 506.05 528.39 505.53 527.22 504.66 526.35 C 503.79 525.48 502.62 524.96 501.39 524.91 L 442.53 519.56 C 441.29 519.58 440.1 520.07 439.22 520.96 C 438.34 521.84 437.84 523.03 437.82 524.27 C 439.09 564.16 437.51 604.09 433.11 643.76 C 433.13 644.86 433.52 645.92 434.24 646.76 C 434.96 647.59 435.94 648.16 437.02 648.34 C 438.11 648.53 439.22 648.33 440.17 647.78 C 452.1 639.75 467.73 621.02 480.78 629.85 C 487.36 634.18 492.98 644.51 498.06 650.67 C 501.91 655.33 508.76 648.64 504.7 644.03 C 497.96 635.95 491.22 623.75 482.55 617.86 C 466.02 606.46 447.61 631.51 435.41 639.69 L 442.47 643.76 C 446.43 611.65 452.1 574.2 447.45 542.41 C 447.45 541.02 447.08 525.71 443.65 529.03 C 444.35 528.39 448.14 529.73 449.27 529.57 C 456.88 529.41 464.5 530.12 471.96 531.66 C 479.01 531.72 486.04 532.34 492.99 533.53 C 503.69 536.37 498.34 533.53 496.68 547.6 C 495.96 562.91 496.06 578.26 497 593.57 C 497 609.62 500.96 631.03 497 646.12 C 496.86 647.23 497.12 648.36 497.74 649.29 C 498.36 650.22 499.3 650.9 500.37 651.2 C 501.45 651.5 502.61 651.4 503.62 650.92 C 504.63 650.44 505.43 649.61 505.88 648.58 Z"/>
</svg>
</div>
</div>
</template>

@ -0,0 +1,30 @@
<script setup lang="ts">
import { computed, onMounted, ref, toRefs } from "vue"
import moment from "moment"
import { type AppRelease, data as release } from "../data/release.data"
const props = defineProps<{ type: keyof AppRelease }>()
const { type } = toRefs(props)
const momentInfo = computed(() => ({
relative: moment(release[type.value].published_at).fromNow(),
exact: moment(release[type.value].published_at).format("dddd, MMMM Do YYYY [at] HH:mm"),
iso: release[type.value].published_at ?? undefined,
}))
// Mimic the <ClientOnly /> behavior to show custom text while rendering.
const show = ref(false)
onMounted(() => {
show.value = true
})
</script>
<template>
<time v-if="show" :datetime="momentInfo.iso" :title="momentInfo.exact">
{{ momentInfo.relative }}
</time>
<time v-else :datetime="momentInfo.iso">
{{ momentInfo.exact }}
</time>
</template>

@ -0,0 +1,102 @@
<script setup lang="ts">
import { type Ref, computed } from 'vue'
import { type PageData, useData } from 'vitepress'
import type { DocsPageData } from '../plugins/component'
import { type Translator, translators } from '../../../website/translators'
import { VPImage } from 'vitepress/theme-without-fonts'
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
export interface Data {
page: Ref<DocsPageData>
frontmatter: Ref<PageData['frontmatter']>
lang: Ref<string>
}
const { page, frontmatter, lang }: Data = useData()
const translatorLabel = computed<string>(() => lang.value === 'ru' ? 'Переводчик' : 'Translator')
const translator = computed<Translator>(() => {
const data = page.value?.component?.translator || translators[frontmatter.value.translator]
if (!data) {
return
}
return {
...data,
name: typeof data.name === 'string' ? data.name : data.name[lang.value]
}
})
</script>
<template>
<article
v-if="translator"
class="ScreenAuthorWidget"
>
<VPLink
:href="translator.github"
:no-icon="true"
class="body"
>
<VPImage
v-if="translator.avatar"
class="avatar"
:image="translator.avatar"
/>
<div class="info">
<span class="label">{{ translatorLabel }}:</span>
<span class="name">{{ translator.name }}</span>
</div>
</VPLink>
</article>
</template>
<style scoped>
.ScreenAuthorWidget {
margin-bottom: 20px;
}
@media (min-width: 1280px) {
.ScreenAuthorWidget {
display: none;
}
}
.body {
display: flex;
align-items: center;
}
.info {
display: flex;
font-size: 13px;
gap: 5px;
}
:deep(.avatar) {
width: 25px;
height: 25px;
border-radius: 50%;
margin-right: 5px;
}
:deep(.name) {
font-weight: bold;
line-height: normal;
}
:deep(.label) {
line-height: normal;
}
:deep(.link) {
transition: color 0.25s;
}
:deep(.link):hover {
color: var(--vp-c-brand-1);
}
</style>

@ -0,0 +1,178 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useData } from 'vitepress'
import { createTranslate } from 'vitepress/dist/client/theme-default/support/translation'
import { v4 as uuidv4 } from 'uuid'
const uid = uuidv4()
const { theme } = useData()
const $t = createTranslate(theme.value.search?.options)
withDefaults(defineProps<{
modelValue: string
placeholder: string
backButton?: boolean
}>(), {
backButton: true
})
/* Search input focus */
const searchInput = ref<HTMLInputElement>()
function focusSearchInput() {
searchInput.value?.focus()
// searchInput.value?.select()
}
onMounted(() => {
focusSearchInput()
})
function onSearchBarClick(event: PointerEvent) {
if (event.pointerType === 'mouse') {
focusSearchInput()
}
}
</script>
<template>
<form class="search-bar" @pointerup="onSearchBarClick($event)" @submit.prevent="">
<label :title="placeholder" id="localsearch-label" :for="uid">
<svg
class="search-icon"
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21l-4.35-4.35" />
</g>
</svg>
</label>
<div
v-if="backButton"
class="search-actions before"
>
<button
class="back-button"
:title="$t('modal.backButtonTitle')"
@click="$emit('close')"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 12H5m7 7l-7-7l7-7"
/>
</svg>
</button>
</div>
<input
ref="searchInput"
:id="uid"
:value="modelValue"
:placeholder="placeholder"
class="search-input"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
<div class="search-actions">
<button
v-if="modelValue"
class="clear-button"
:title="$t('modal.resetButtonTitle')"
@click="$emit('update:modelValue', '')"
>
<svg width="24" height="24" viewBox="0 0 20 20">
<path d="M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z" stroke="currentColor" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</button>
</div>
</form>
</template>
<style scoped>
.search-bar {
border: 1px solid var(--vp-c-gray-soft);
border-radius: 8px;
display: flex;
align-items: center;
padding: 6px 8px;
height: 56px;
cursor: text;
background-color: var(--vp-c-bg-elv);
box-shadow: var(--vp-shadow-1);
}
@media (max-width: 767px) {
.search-bar {
padding: 0 8px;
}
}
.search-bar:focus-within {
border-color: var(--vp-c-border);
}
.search-icon {
margin: 8px;
}
@media (max-width: 767px) {
.search-icon {
display: none;
}
}
.search-input {
padding: 6px 12px;
font-size: inherit;
width: 100%;
}
@media (max-width: 767px) {
.search-input {
padding: 6px 4px;
}
}
.search-actions {
display: flex;
gap: 4px;
}
@media (any-pointer: coarse) {
.search-actions {
gap: 8px;
}
}
@media (min-width: 769px) {
.search-actions.before {
display: none;
}
}
.search-actions button {
padding: 8px;
}
.search-actions button:hover {
color: var(--vp-c-brand-1);
}
</style>

@ -0,0 +1,186 @@
<script lang="ts" setup>
import { useScrollLock } from '@vueuse/core'
import { ref, watchPostEffect, watch, computed } from 'vue'
import { DefaultTheme, useRoute, useData, inBrowser } from 'vitepress'
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
import VPSidebarItem from 'vitepress/dist/client/theme-default/components/VPSidebarItem.vue'
const { sidebar: flatSidebar, sidebarGroups, hasSidebar } = useSidebar()
const route = useRoute()
const sidebar = computed<DefaultTheme.SidebarItem[]>(() => {
return route.path.includes('/components/') ? flatSidebar.value : sidebarGroups.value
})
const props = defineProps<{
open: boolean
}>()
// a11y: focus Nav element when menu has opened
const navEl = ref<HTMLElement | null>(null)
const isLocked = useScrollLock(inBrowser ? document.body : null)
watch(
[props, navEl],
() => {
if (props.open) {
isLocked.value = true
navEl.value?.focus()
} else isLocked.value = false
},
{ immediate: true, flush: 'post' }
)
const { lang } = useData()
const activeLinkEl = ref<HTMLElement | null>(null)
const activeGroupEl = ref<HTMLElement | null>(null)
watch(() => route.path, () => {
if (!route.path.includes('/components/')) {
return
}
if (!navEl.value) {
return
}
activeLinkEl.value = navEl.value.querySelector<HTMLElement>(`a[href="${route.path}"]`)
activeGroupEl.value = activeLinkEl.value.closest<HTMLElement>('.VPSidebarItem.level-0')
const offset = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--vp-nav-height')) * 2
if (!isInViewport(activeGroupEl.value, offset)) {
navEl.value.scrollTo({
top: activeGroupEl.value.offsetTop - offset
})
}
}, { flush: 'post' })
// tmp fix vitepress bug
watch(lang, () => activeGroupEl.value && activeGroupEl.value.classList.remove('collapsed'), { flush: 'post' })
function isInViewport(el: HTMLElement, offset: number) {
const { top, bottom } = el.getBoundingClientRect()
const { innerHeight } = window
return top >= offset && bottom <= offset + innerHeight
};
</script>
<template>
<aside
v-if="hasSidebar"
class="VPSidebar"
:class="{ open }"
ref="navEl"
@click.stop
>
<div class="curtain" />
<nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1">
<span class="visually-hidden" id="sidebar-aria-label">
Sidebar Navigation
</span>
<slot name="sidebar-nav-before" />
<div
v-for="item in sidebar"
:key="item.text"
class="group"
>
<VPSidebarItem :item="item" :depth="0" :class="{ landing: !item.items }" />
</div>
<slot name="sidebar-nav-after" />
</nav>
</aside>
</template>
<style scoped>
.VPSidebar {
position: fixed;
top: var(--vp-layout-top-height, 0px);
bottom: 0;
left: 0;
z-index: var(--vp-z-index-sidebar);
padding: 32px 32px 96px;
width: calc(100vw - 64px);
max-width: 320px;
background-color: var(--vp-sidebar-bg-color);
opacity: 0;
box-shadow: var(--vp-shadow-3);
overflow-x: hidden;
overflow-y: auto;
transform: translateX(-100%);
transition: opacity 0.5s, transform 0.25s ease;
overscroll-behavior: contain;
}
.VPSidebar.open {
opacity: 1;
visibility: visible;
transform: translateX(0);
transition: opacity 0.25s,
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
}
.dark .VPSidebar {
box-shadow: var(--vp-shadow-1);
}
@media (min-width: 960px) {
.VPSidebar {
z-index: 1;
padding-top: var(--vp-nav-height);
padding-bottom: 128px;
width: var(--vp-sidebar-width);
max-width: 100%;
background-color: var(--vp-sidebar-bg-color);
opacity: 1;
visibility: visible;
box-shadow: none;
transform: translateX(0);
}
}
@media (min-width: 1440px) {
.VPSidebar {
padding-left: max(32px, calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));
width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
}
}
@media (min-width: 960px) {
.curtain {
position: sticky;
top: -64px;
left: 0;
z-index: 1;
margin-top: calc(var(--vp-nav-height) * -1);
margin-right: -32px;
margin-left: -32px;
height: var(--vp-nav-height);
background-color: var(--vp-sidebar-bg-color);
}
}
.nav {
outline: 0;
}
.group + .group {
border-top: 1px solid var(--vp-c-divider);
padding-top: 10px;
}
@media (min-width: 960px) {
.group {
padding-top: 10px;
width: calc(var(--vp-sidebar-width) - 64px);
}
}
.landing :deep(.VPLink .text) {
font-weight: normal;
}
</style>

@ -0,0 +1,95 @@
<script setup lang="ts">
import { type Ref, computed } from 'vue'
import { type PageData, useData } from 'vitepress'
import type { DocsPageData } from '../plugins/component'
import { type Translator, translators } from '../../../website/translators'
import { VPImage } from 'vitepress/theme-without-fonts'
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
export interface Data {
page: Ref<DocsPageData>
frontmatter: Ref<PageData['frontmatter']>
lang: Ref<string>
}
const { page, frontmatter, lang }: Data = useData()
const translatorLabel = computed<string>(() => lang.value === 'en' ? 'Translator' : 'Переводчик')
const translator = computed<Translator>(() => {
const data = page.value?.component?.translator || translators[frontmatter.value.author]
if (!data) {
return
}
return {
...data,
name: typeof data.name === 'string' ? data.name : data.name[lang.value]
}
})
</script>
<template>
<article
v-if="translator"
class="TranslatorWidget"
>
<VPLink
:href="translator.github"
:no-icon="true"
class="body"
>
<VPImage
v-if="translator.avatar"
class="avatar"
:image="translator.avatar"
/>
<div class="info">
<div class="name">{{ translator.name }}</div>
<div class="label">{{ translatorLabel }}</div>
</div>
</VPLink>
</article>
</template>
<style scoped>
.TranslatorWidget {
background-color: var(--vp-c-bg-soft);
border-radius: var(--vp-border-radius);
margin-top: 20px;
padding: 20px;
}
.body {
display: flex;
align-items: center;
}
:deep(.avatar) {
width: 35px;
height: 35px;
border-radius: 50%;
margin-right: 12px;
}
:deep(.name) {
font-size: 13px;
font-weight: bold;
line-height: normal;
}
:deep(.label) {
font-size: 14px;
line-height: normal;
}
:deep(.link) {
transition: color 0.25s;
}
:deep(.link):hover {
color: var(--vp-c-brand-1);
}
</style>

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress'
import TreeItem from './TreeItem.vue'
defineProps<{
items: DefaultTheme.SidebarItem[]
}>()
</script>
<template>
<ul v-if="items" class="list">
<TreeItem
v-for="(item, index) in items"
:key="index"
:item="item"
/>
</ul>
</template>
<style scoped>
.list {
padding-left: 0;
list-style: none;
}
</style>

@ -0,0 +1,130 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { DefaultTheme } from 'vitepress'
import Tree from './Tree.vue'
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
const props = defineProps<{
item: DefaultTheme.SidebarItem
}>()
const isOpen = ref<boolean>(false)
const isFolder = computed<boolean>(() => {
return !!(props.item.items && props.item.items.length)
})
const toggle = function () {
if (isFolder.value) {
isOpen.value = !isOpen.value
}
}
</script>
<template>
<li
class="item"
:class="{
folder: isFolder,
opened: isOpen,
'is-link': !!props.item.link
}"
>
<span
class="title"
@click="toggle"
>
<div
v-if="isFolder"
class="icon"
>
<svg
viewBox="0 -0.5 17 17"
version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
>
<path d="M6.113,15.495 C5.531,16.076 4.01,16.395 4.01,14.494 L4.01,1.506 C4.01,-0.333 5.531,-0.076 6.113,0.506 L12.557,6.948 C13.137,7.529 13.137,8.47 12.557,9.052 L6.113,15.495 L6.113,15.495 Z"></path>
</svg>
</div>
<VPLink :href="props.item.link">
{{ props.item.text }}
</VPLink>
</span>
<Tree
v-show="isOpen"
v-if="isFolder"
:items="props.item.items"
class="list"
/>
</li>
</template>
<style scoped>
.item {
position: relative;
margin-left: 5px;
}
.item:not(.folder) {
border-left: 1px dashed var(--vp-c-text-3);
}
.item:not(.folder)::before {
content: '';
position: absolute;
top: calc(50% - .5px);
left: 3px;
width: 12px;
height: 0;
border-top: 1px dashed var(--vp-c-text-3);
}
.title {
position: relative;
padding-left: 20px;
}
.folder .title {
cursor: pointer;
}
.icon {
position: absolute;
top: 0;
left: 0;
width: 20px;
height: 100%;
display: flex;
align-items: center;
}
.icon svg {
position: relative;
margin-left: -3px;
width: 10px;
height: 10px;
fill: var(--vp-c-text-3);
transition: fill .25s;
}
.item:not(.is-link) > .title:hover > .icon svg,
.icon:hover svg {
fill: var(--vp-c-brand-1);
}
.opened > .title > .icon svg {
transform: rotate(90deg);
}
:deep(.link) {
color: var(--vp-c-text-1);
text-decoration: none;
}
:deep(.link:hover) {
color: var(--vp-c-brand-1);
text-decoration: none;
}
.item :deep(.list) {
padding-left: 20px;
}
</style>

@ -0,0 +1,114 @@
import { Ref, computed } from 'vue'
import type { DefaultTheme, SiteData } from 'vitepress'
import type { DocsPageData } from '../plugins/component'
import { useData } from 'vitepress'
import { ensureStartingSlash } from '../utils'
import { getFlatSideBarLinks } from 'vitepress/dist/client/theme-default/support/sidebar'
export function useLangs({
removeCurrent = true,
correspondingLink = false
} = {}) {
interface Data {
site: Ref<SiteData>
localeIndex: Ref<string>
page: Ref<DocsPageData>
theme: Ref<DefaultTheme.Config>
}
const { site, localeIndex, page, theme }: Data = useData()
const currentLang = computed(() => ({
label: site.value.locales[localeIndex.value]?.label,
link:
site.value.locales[localeIndex.value]?.link ||
(localeIndex.value === 'root' ? '/' : `/${localeIndex.value}/`)
}))
const localeLinks = computed(() =>
Object.entries(site.value.locales).flatMap(([key, value]) => {
if (removeCurrent && currentLang.value.label === value.label) {
return []
}
const text = value.label
const rootLink = value.link || (key === 'root' ? '/' : `/${key}/`)
let link = normalizeLink(
rootLink,
theme.value.i18nRouting !== false && correspondingLink,
page.value.relativePath.slice(currentLang.value.link.length - 1),
!site.value.cleanUrls
)
if (
link === '/' ||
Object.entries(site.value.locales).some(([key]) => link.replace(/^\//, '').replace(/\/$/, '') === key)
) {
return {
text,
link
}
}
const { themeConfig } = site.value.locales[key]
const { nav, sidebar } = themeConfig
for (const item of nav) {
if (Object.prototype.hasOwnProperty.call(item, 'link') && item.link === link) {
return {
text,
link
}
}
}
for (const data of Object.values(sidebar)) {
const flatSidebar = getFlatSideBarLinks(data)
for (const item of flatSidebar) {
if (item.link === link) {
return {
text,
link
}
}
}
}
if (!page.value.component) {
return {
text: value.label,
link: rootLink,
}
}
const component = themeConfig.components.find(component => component.title === page.value.component.title)
if (!component) {
return {
text: value.label,
link: rootLink,
}
}
return {
text,
link: component.link
}
}
))
return { localeLinks, currentLang }
}
function normalizeLink(
link: string,
addPath: boolean,
path: string,
addExt: boolean
): string {
return addPath
? link.replace(/\/$/, '') +
ensureStartingSlash(path
.replace(/(^|\/)?index.md$/, '$1')
.replace(/\.md$/, addExt ? '.html' : ''))
: link
}

@ -0,0 +1,66 @@
import { computed } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import type { DocsPageData } from '../plugins/component'
import { useData } from 'vitepress'
import { isActive } from 'vitepress/dist/client/shared'
import { getFlatSideBarLinks } from 'vitepress/dist/client/theme-default/support/sidebar'
export function usePrevNext(): ComputedRef {
interface Data {
theme: Ref<any>
page: Ref<DocsPageData>
frontmatter: Ref<DocsPageData['frontmatter']>
}
const { page, theme, frontmatter }: Data = useData()
return computed(() => {
const component = page.value.component
if (!component) {
return ''
}
const candidates = getFlatSideBarLinks([component])
const index = candidates.findIndex((link) => isActive(page.value.relativePath, link.link))
const hidePrev =
(theme.value.docFooter?.prev === false && !frontmatter.value.prev) ||
frontmatter.value.prev === false
const hideNext =
(theme.value.docFooter?.next === false && !frontmatter.value.next) ||
frontmatter.value.next === false
return {
prev: hidePrev
? undefined
: {
text: (typeof frontmatter.value.prev === 'string'
? frontmatter.value.prev
: typeof frontmatter.value.prev === 'object'
? frontmatter.value.prev.text
: undefined) ??
candidates[index - 1]?.docFooterText ??
candidates[index - 1]?.text,
link: (typeof frontmatter.value.prev === 'object'
? frontmatter.value.prev.link
: undefined) ?? candidates[index - 1]?.link
},
next: hideNext
? undefined
: {
text: (typeof frontmatter.value.next === 'string'
? frontmatter.value.next
: typeof frontmatter.value.next === 'object'
? frontmatter.value.next.text
: undefined) ??
candidates[index + 1]?.docFooterText ??
candidates[index + 1]?.text,
link: (typeof frontmatter.value.next === 'object'
? frontmatter.value.next.link
: undefined) ?? candidates[index + 1]?.link
},
}
})
}

@ -0,0 +1,71 @@
import 'glightbox/dist/css/glightbox.css'
import { scrollbarWidth } from '@xobotyi/scrollbar-width'
import {
watch,
type App,
} from 'vue'
import type { Router } from 'vitepress'
export const glightboxSymbol = Symbol('glightbox')
export async function createZoom(app: App, router: Router) {
if (import.meta.env.SSR) {
return
}
const GLightbox = await import('glightbox')
const lightbox = GLightbox.default({
openEffect: 'fade',
closeEffect: 'fade',
zoomable: true,
skin: 'white',
svg: {
close: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17,17.8c-0.2,0-0.4-0.1-0.6-0.2L12,13.1l-4.4,4.4c-0.3,0.3-0.8,0.3-1.1,0c-0.3-0.3-0.3-0.8,0-1.1l4.4-4.4L6.4,7.6 c-0.3-0.3-0.3-0.8,0-1.1c0.3-0.3,0.8-0.3,1.1,0l4.4,4.4l4.4-4.4c0.3-0.3,0.8-0.3,1.1,0c0.2,0.2,0.2,0.4,0.2,0.6s-0.1,0.4-0.2,0.6 L13.1,12l4.4,4.4c0.3,0.3,0.3,0.8,0,1.1C17.4,17.7,17.2,17.8,17,17.8z"/></svg>',
next: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9,18.8c-0.2,0-0.4-0.1-0.5-0.2c-0.3-0.3-0.3-0.8,0-1.1l5.5-5.5L8.5,6.5c-0.3-0.3-0.3-0.8,0-1.1s0.8-0.3,1.1,0l6,6 c0.3,0.3,0.3,0.8,0,1.1l-6,6C9.4,18.7,9.2,18.8,9,18.8z"/></svg>',
prev: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15,18.8c-0.2,0-0.4-0.1-0.5-0.2l-6-6c-0.3-0.3-0.3-0.8,0-1.1l6-6c0.3-0.3,0.8-0.3,1.1,0s0.3,0.8,0,1.1L10.1,12l5.5,5.5 c0.3,0.3,0.3,0.8,0,1.1C15.4,18.7,15.2,18.8,15,18.8z"/></svg>',
},
})
lightbox.reload = () => {
const elements = Array.from(document.querySelectorAll('.vp-doc img'))
.map((element, index) => {
const target = element.parentElement instanceof HTMLAnchorElement ? element.parentElement : element
target.addEventListener('click', (e) => {
e.preventDefault()
lightbox.openAt(index)
})
const href = target.getAttribute(target instanceof HTMLAnchorElement ? 'href' : 'src')
const title = element.getAttribute('alt')
return {
href,
title,
type: 'image',
}
})
lightbox.setElements(elements)
}
const scrollWidth = scrollbarWidth()
lightbox.on('open', () => updateFixedElements(scrollWidth))
lightbox.on('close', () => updateFixedElements())
app.provide(glightboxSymbol, lightbox)
watch(
() => router.route.data.relativePath,
() => setTimeout(() => {
lightbox.reload()
}),
{ immediate: true }
)
}
function updateFixedElements(scrollWidth?: number) {
document.documentElement.style.setProperty('--scrollbar-width', scrollWidth ? scrollWidth + 'px' : '')
}

@ -0,0 +1,22 @@
import { defineLoader } from "vitepress"
import { Octokit } from "@octokit/rest"
import type { GetResponseDataTypeFromEndpointMethod } from "@octokit/types"
const octokit = new Octokit()
type GitHubReleaseList = GetResponseDataTypeFromEndpointMethod<typeof octokit.repos.listReleases>
declare const data: GitHubReleaseList
export { data }
export default defineLoader({
async load(): Promise<GitHubReleaseList> {
const releases = await octokit.paginate(octokit.repos.listReleases, {
owner: "KotatsuApp",
repo: "Kotatsu",
per_page: 100,
})
return releases
},
})

@ -0,0 +1,25 @@
import { defineLoader } from "vitepress"
import { Octokit } from "@octokit/rest"
import type { GetResponseDataTypeFromEndpointMethod } from "@octokit/types"
const octokit = new Octokit()
type GitHubRelease = GetResponseDataTypeFromEndpointMethod<typeof octokit.repos.getLatestRelease>
export interface AppRelease {
stable: GitHubRelease
}
declare const data: AppRelease
export { data }
export default defineLoader({
async load(): Promise<AppRelease> {
const { data: stable } = await octokit.repos.getLatestRelease({
owner: "KotatsuApp",
repo: "Kotatsu",
})
return { stable }
},
})

@ -0,0 +1,33 @@
import { type Router, inBrowser } from 'vitepress'
import { type App, watch, defineComponent } from 'vue'
import { createZoom } from './composables/zoom'
import { enhanceAppWithTabs } from "vitepress-plugin-tabs/client"
import analytics from "./plugins/analytics"
import DefaultTheme from 'vitepress/theme-without-fonts'
import Layout from './components/Layout.vue'
import './styles/global.css'
import './styles/glightbox.css'
export default {
extends: DefaultTheme,
Layout: Layout,
enhanceApp({ app, router }: { app: App, router: Router }) {
enhanceAppWithTabs(app)
createZoom(app, router)
analytics({ id: "G-X37JGMJE4R" })
if (
import.meta.env.PROD &&
inBrowser
) {
watch(
() => router.route.data.relativePath,
(_path, oldPath) => {
if (!oldPath) { // Skip initial change
return
}
}
)
}
},
}

@ -0,0 +1,37 @@
// Code based on vitepress-plugin-google-analytics.
// Customized as the plugin did not consider the script loading time.
// https://github.com/ZhongxuYang/vitepress-plugin-google-analytics
function mountGoogleAnalytics(id: string) {
if (("dataLayer" in window && window.gtag) || window.location.hostname === "localhost") {
return
}
const analyticsScript = document.createElement("script")
analyticsScript.addEventListener("load", () => {
// @ts-expect-error Missing types
window.dataLayer = window.dataLayer || []
function gtag(..._args: any[]) {
// @ts-expect-error Missing types
// eslint-disable-next-line prefer-rest-params
window.dataLayer.push(arguments)
}
gtag("js", new Date())
gtag("config", id)
window.gtag = gtag
})
analyticsScript.src = `https://www.googletagmanager.com/gtag/js?id=${id}`
document.body.appendChild(analyticsScript)
}
export default function ({ id }: { id: string }) {
// eslint-disable-next-line n/prefer-global/process
if (process.env.NODE_ENV === "production" && id && typeof window !== "undefined") {
mountGoogleAnalytics(id)
}
}

@ -0,0 +1,109 @@
import type { DefaultTheme, PageData, SiteConfig } from 'vitepress'
import { normalize } from 'vitepress/dist/client/shared'
import { ensureStartingSlash, getTranslator } from '../utils'
import { readFileSync } from 'fs'
import { basename } from 'path'
import fg from 'fast-glob'
import matter from 'gray-matter'
import { generateSidebarItem, getTitleFromContent } from './sidebar'
import type { Translator } from '../../../website/translators'
import { findPath } from '../utils'
export interface ComponentData {
path: string
link: string
title: string
titleLower: string
description?: string
text?: string
translator?: Translator
logo?: string
dependencies?: Array<string>
categories?: Array<string>
repository?: string
items?: DefaultTheme.SidebarItem[]
}
export interface DocsPageData extends PageData {
component?: ComponentData
breadcrumbs?: DefaultTheme.SidebarItem[]
}
export const components: ComponentData[] = fg
.sync([
'website/manuals/faq/explore/*.md',
'!website/manuals/faq/explore/index.md',
])
.map(file => {
const content = readFileSync(file, 'utf-8')
const { data } = matter(content)
const {
title = getTitleFromContent(content) || basename(file),
translator,
logo,
categories = [],
dependencies = [],
items,
repository,
description,
} = data
const filePath = file.substring(file.indexOf('/') + 1)
const component: ComponentData = {
path: filePath,
link: ensureStartingSlash(normalize(filePath)),
repository,
title,
titleLower: title.toLowerCase(),
text: title,
description,
logo,
dependencies: Array.isArray(dependencies) ? dependencies : Array(dependencies),
categories: Array.isArray(categories) ? categories : Array(categories),
}
component.translator = getTranslator(translator)
if (items) {
component.items = generateSidebarItem(items, component.link)
}
return component
})
.sort((a, b) => (a.text && b.text) ? a.text.localeCompare(b.text) : 0)
export default class DocsComponent {
static prepareData(
pageData: DocsPageData,
siteConfig: SiteConfig,
): DocsPageData {
const component = components.find(component => pageData.relativePath.startsWith(component.path.replace(/index\.md$/, '')))
pageData.component = component
pageData.breadcrumbs = findPath(pageData, siteConfig.userConfig)
pageData.title = !pageData.frontmatter.title && pageData.breadcrumbs.length
? pageData.breadcrumbs.map(item => item.text).reverse().join(siteConfig.userConfig.themeConfig.titleSeparator)
: pageData.title
if (
component
&& !pageData.description
&& pageData.component.description
) {
pageData.description = pageData.component.description
}
return pageData
}
}
export { DocsComponent }
export const { prepareData } = DocsComponent

@ -0,0 +1,280 @@
import type MarkdownIt from 'markdown-it'
import type { RenderRule } from 'markdown-it/lib/renderer'
import type StateBlock from 'markdown-it/lib/rules_block/state_block'
import { isSpace } from 'markdown-it/lib/common/utils'
import container from 'markdown-it-container'
import kbd from 'markdown-it-kbd'
export const addPlugins = (md: MarkdownIt) => {
md.use(...createContainer('info', 'Информация', md))
.use(...createContainer('tip', 'Подсказка', md))
.use(...createContainer('warning', 'Внимание', md))
.use(...createContainer('danger', 'Осторожно', md))
.use(...createContainer('details', 'Подробнее', md))
.use(kbd)
md.block.ruler.at('table', table)
}
type ContainerArgs = [typeof container, string, { render: RenderRule }]
function createContainer(
klass: string,
defaultTitle: string,
md: MarkdownIt
): ContainerArgs {
return [
container,
klass,
{
render(tokens, idx, _options, env) {
const token = tokens[idx]
const info = token.info.trim().slice(klass.length).trim()
const attrs = md.renderer.renderAttrs(token)
if (token.nesting === 1) {
const title = md.renderInline(info || defaultTitle, {
references: env.references
})
if (klass === 'details')
return `<details class="${klass} custom-block"${attrs}><summary>${title}</summary>\n`
return `<div class="${klass} custom-block"${attrs}><p class="custom-block-title">${title}</p>\n`
} else return klass === 'details' ? `</details>\n` : `</div>\n`
}
}
]
}
// from https://github.com/markdown-it/markdown-it/blob/2b6cac25823af011ff3bc7628bc9b06e483c5a08/lib/rules_block/table.js
// GFM table, non-standard
function table(
state: StateBlock,
startLine: number,
endLine: number,
silent: any
) {
var ch, lineText, pos, i, l, nextLine, headers, columns, columnCount, token,
aligns, t, tableLines, tbodyLines, oldParentType, terminate,
terminatorRules, firstCh, secondCh;
// should have at least two lines
if (startLine + 2 > endLine) { return false; }
nextLine = startLine + 1;
if (state.sCount[nextLine] < state.blkIndent) { return false; }
// if it's indented more than 3 spaces, it should be a code block
if (state.sCount[nextLine] - state.blkIndent >= 4) { return false; }
// first character of the second line should be '|', '-', ':',
// and no other characters are allowed but spaces;
// basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp
pos = state.bMarks[nextLine] + state.tShift[nextLine];
if (pos >= state.eMarks[nextLine]) { return false; }
firstCh = state.src.charCodeAt(pos++);
if (firstCh !== 0x7C/* | */ && firstCh !== 0x2D/* - */ && firstCh !== 0x3A/* : */) { return false; }
if (pos >= state.eMarks[nextLine]) { return false; }
secondCh = state.src.charCodeAt(pos++);
if (secondCh !== 0x7C/* | */ && secondCh !== 0x2D/* - */ && secondCh !== 0x3A/* : */ && !isSpace(secondCh)) {
return false;
}
// if first character is '-', then second character must not be a space
// (due to parsing ambiguity with list)
if (firstCh === 0x2D/* - */ && isSpace(secondCh)) { return false; }
while (pos < state.eMarks[nextLine]) {
ch = state.src.charCodeAt(pos);
if (ch !== 0x7C/* | */ && ch !== 0x2D/* - */ && ch !== 0x3A/* : */ && !isSpace(ch)) { return false; }
pos++;
}
lineText = getLine(state, startLine + 1);
columns = lineText.split('|');
aligns = [];
for (i = 0; i < columns.length; i++) {
t = columns[i].trim();
if (!t) {
// allow empty columns before and after table, but not in between columns;
// e.g. allow ` |---| `, disallow ` ---||--- `
if (i === 0 || i === columns.length - 1) {
continue;
} else {
return false;
}
}
if (!/^:?-+:?$/.test(t)) { return false; }
if (t.charCodeAt(t.length - 1) === 0x3A/* : */) {
aligns.push(t.charCodeAt(0) === 0x3A/* : */ ? 'center' : 'right');
} else if (t.charCodeAt(0) === 0x3A/* : */) {
aligns.push('left');
} else {
aligns.push('');
}
}
lineText = getLine(state, startLine).trim();
if (lineText.indexOf('|') === -1) { return false; }
if (state.sCount[startLine] - state.blkIndent >= 4) { return false; }
columns = escapedSplit(lineText);
if (columns.length && columns[0] === '') columns.shift();
if (columns.length && columns[columns.length - 1] === '') columns.pop();
// header row will define an amount of columns in the entire table,
// and align row should be exactly the same (the rest of the rows can differ)
columnCount = columns.length;
headers = [...columns];
if (columnCount === 0 || columnCount !== aligns.length) { return false; }
if (silent) { return true; }
oldParentType = state.parentType;
// @ts-expect-error
state.parentType = 'table';
// use 'blockquote' lists for termination because it's
// the most similar to tables
terminatorRules = state.md.block.ruler.getRules('blockquote');
token = state.push('table_open', 'table', 1);
token.map = tableLines = [ startLine, 0 ];
token = state.push('thead_open', 'thead', 1);
token.map = [ startLine, startLine + 1 ];
token = state.push('tr_open', 'tr', 1);
token.map = [ startLine, startLine + 1 ];
for (i = 0; i < columns.length; i++) {
token = state.push('th_open', 'th', 1);
if (aligns[i]) {
token.attrs = [ [ 'style', 'text-align:' + aligns[i] ] ];
}
token = state.push('inline', '', 0);
token.content = columns[i].trim();
token.children = [];
token = state.push('th_close', 'th', -1);
}
token = state.push('tr_close', 'tr', -1);
token = state.push('thead_close', 'thead', -1);
for (nextLine = startLine + 2; nextLine < endLine; nextLine++) {
if (state.sCount[nextLine] < state.blkIndent) { break; }
terminate = false;
for (i = 0, l = terminatorRules.length; i < l; i++) {
if (terminatorRules[i](state, nextLine, endLine, true)) {
terminate = true;
break;
}
}
if (terminate) { break; }
lineText = getLine(state, nextLine).trim();
if (!lineText) { break; }
if (state.sCount[nextLine] - state.blkIndent >= 4) { break; }
columns = escapedSplit(lineText);
if (columns.length && columns[0] === '') columns.shift();
if (columns.length && columns[columns.length - 1] === '') columns.pop();
if (nextLine === startLine + 2) {
token = state.push('tbody_open', 'tbody', 1);
token.map = tbodyLines = [ startLine + 2, 0 ];
}
token = state.push('tr_open', 'tr', 1);
token.map = [ nextLine, nextLine + 1 ];
for (i = 0; i < columnCount; i++) {
token = state.push('td_open', 'td', 1);
const attrs = [];
if (aligns[i]) {
token.attrs = attrs.push([ 'style', 'text-align:' + aligns[i] ]);
}
attrs.push(['data-label', headers[i].trim()]);
if (attrs.length) {
token.attrs = attrs;
}
token = state.push('inline', '', 0);
token.content = columns[i] ? columns[i].trim() : '';
token.children = [];
token = state.push('td_close', 'td', -1);
}
token = state.push('tr_close', 'tr', -1);
}
if (tbodyLines) {
token = state.push('tbody_close', 'tbody', -1);
tbodyLines[1] = nextLine;
}
token = state.push('table_close', 'table', -1);
tableLines[1] = nextLine;
state.parentType = oldParentType;
state.line = nextLine;
return true;
}
function getLine(
state: StateBlock,
line: number
): string {
var pos = state.bMarks[line] + state.tShift[line],
max = state.eMarks[line];
return state.src.slice(pos, max)
}
function escapedSplit(str: string): string[] {
var result = [],
pos = 0,
max = str.length,
ch,
isEscaped = false,
lastPos = 0,
current = '';
ch = str.charCodeAt(pos);
while (pos < max) {
if (ch === 0x7c/* | */) {
if (!isEscaped) {
// pipe separating cells, '|'
result.push(current + str.substring(lastPos, pos));
current = '';
lastPos = pos + 1;
} else {
// escaped pipe, '\|'
current += str.substring(lastPos, pos - 1);
lastPos = pos;
}
}
isEscaped = (ch === 0x5c/* \ */);
pos++;
ch = str.charCodeAt(pos);
}
result.push(current + str.substring(lastPos));
return result;
}

@ -0,0 +1,97 @@
import type { DefaultTheme } from 'vitepress'
import { normalize } from 'vitepress/dist/client/shared'
import faqCategories from '../../../website/faq/categories.json'
import { readFileSync } from 'fs'
import { join, basename } from 'path'
import fg from 'fast-glob'
import matter from 'gray-matter'
declare interface Options {
root: string | Array<string>
ignore?: Array<string>
collapsed?: boolean
}
export default class Sidebar {
static generateSidebar(
options: Options
): DefaultTheme.SidebarItem[] {
const entries = fg.sync(options.root, options)
return entries
.map(path => Sidebar.getData(path, options))
.sort((a, b) => (a.text && b.text) ? a.text.localeCompare(b.text) : 0)
}
static generateSidebarItem(
items: DefaultTheme.SidebarItem[],
path: string,
): DefaultTheme.SidebarItem[] {
items = items.map(({ text, link, items }) => {
const item: DefaultTheme.SidebarItem = { text }
if (link) {
item.link = join(path, link).replace(/\\/g, '/')
}
if (items) {
item.collapsed = true
item.items = Sidebar.generateSidebarItem(items, path)
}
return item
})
return items
}
static getData(
path: string,
options?: Partial<Options>
): DefaultTheme.SidebarItem {
const src = readFileSync(path, 'utf-8')
const { data } = matter(src)
const {
title = Sidebar.getTitleFromContent(src) || basename(path),
items,
} = data
const link = normalize(path.replace(/^website/, ''))
const output: DefaultTheme.SidebarItem = {
text: title,
link,
}
if (items) {
output.collapsed = options.collapsed === null || options.collapsed === undefined || options.collapsed
output.items = Sidebar.generateSidebarItem(items, link)
}
return output
}
static getTitleFromContent(
content: string,
): string | undefined {
const lines = content.split('\n')
for (let i = 0, len = lines.length; i < len; i += 1) {
let str = lines[i].toString().replace('\r', '')
if (str.indexOf('# ') !== -1) {
str = str.replace('# ', '')
return str
}
}
return
}
}
export { Sidebar }
export const {
generateSidebar,
generateSidebarItem,
getTitleFromContent
} = Sidebar

@ -0,0 +1,93 @@
:root {
--vp-c-overlay: rgba(255 255 255 / .8);
}
.dark {
--vp-c-overlay: rgba(0 0 0 / .8);
}
.vp-doc img {
cursor: zoom-in;
}
.glightbox-open.glightbox-mobile .VPLocalNav {
position: fixed;
}
.glightbox-white {
color: var(--vp-c-text-1);
}
.glightbox-white .goverlay {
background: var(--vp-c-overlay);
}
.glightbox-white .gloader {
border: 2px solid var(--vp-c-text-1);
border-right-color: var(--vp-c-bg);
}
.glightbox-white .gslide-description {
background: none;
padding-block: 20px;
}
.glightbox-white .gclose {
top: 20px;
right: 20px;
position: absolute;
}
.glightbox-white .gbtn:not(.disabled):hover {
background-color: var(--vp-c-gray-3);
}
.glightbox-white .gnext,
.glightbox-white .gprev {
position: absolute;
top: -100%;
}
.glightbox-white .gprev {
left: 30px;
}
.glightbox-white .gnext {
right: 30px;
}
.glightbox-white .gbtn {
width: 40px;
height: 40px;
border: 1px solid var(--vp-c-gray-soft);
border-radius: 50%;
box-shadow: var(--vp-shadow-1);
background-color: var(--vp-c-bg);
}
.glightbox-white .gbtn svg {
fill: currentColor;
}
@media (min-width: 767px) {
.glightbox-white .gclose {
top: 30px;
right: 30px;
}
.glightbox-white .gbtn {
width: 50px;
height: 50px;
}
.glightbox-white .gprev,
.glightbox-white .gnext {
top: 45%;
}
}
.glightbox-mobile .glightbox-container .gslide-title,
.glightbox-mobile .glightbox-container .gslide-description {
background: none;
color: var(--vp-c-text-1);
}

@ -0,0 +1,468 @@
:root {
--vp-font-family-base: 'Montserrat', sans-serif;
--vp-shadow-6: 0px 4px 15px rgba(47, 63, 147, 0.08);
--vp-border: var(--vp-border-width) solid var(--vp-c-bg-soft);
--vp-border-width: 4px;
--vp-border-radius: 20px;
--vp-c-gray-1: #f7f7f7;
--vp-c-blue-1: #0059C8;
--vp-c-blue-2: #0152b6;
--vp-c-blue-3: #0764d4;
--vp-c-blue-soft: #005ac80e;
--vp-c-brand-1: var(--vp-c-blue-1);
--vp-c-brand-2: var(--vp-c-blue-2);
--vp-c-brand-3: var(--vp-c-blue-3);
--vp-c-brand-soft: var(--vp-c-blue-soft);
--vp-c-sponsor-1: #eab11f;
--vp-c-sponsor-2: #e0a615;
--vp-c-sponsor-3: #ecb732;
--vp-c-sponsor-soft: rgba(236, 183, 50, 0.14);
--vp-button-sponsor-border: var(--vp-c-sponsor-3);
--vp-button-sponsor-text: var(--vp-c-black);
--vp-button-sponsor-hover-border: var(--vp-c-sponsor-3);
--vp-button-sponsor-hover-text: var(--vp-c-black);
--vp-button-sponsor-hover-bg: var(--vp-c-sponsor-3);
--vp-button-sponsor-active-border: var(--vp-c-sponsor-2);
--vp-button-sponsor-active-text: var(--vp-c-black);
--vp-button-sponsor-active-bg: var(--vp-c-sponsor-2);
--vp-code-color: var(--vp-c-blue-3);
--vp-custom-block-info-border: var(--vp-c-blue-soft);
--vp-custom-block-info-bg: var(--vp-c-blue-soft);
--vp-custom-block-info-text: var(--vp-c-neutral);
--vp-custom-block-tip-border: var(--vp-c-blue-soft);
--vp-custom-block-tip-bg: var(--vp-c-blue-soft);
--vp-custom-block-tip-text: var(--vp-c-neutral);
--vp-custom-block-warning-border: var(--vp-c-yellow-soft);
--vp-custom-block-warning-bg: var(--vp-c-yellow-soft);
--vp-custom-block-warning-text: var(--vp-c-neutral);
--vp-custom-block-danger-border: var(--vp-c-danger-soft);
--vp-custom-block-danger-bg: var(--vp-c-danger-soft);
--vp-custom-block-danger-text: var(--vp-c-neutral);
--vp-custom-block-details-border: var(--vp-c-divider);
--vp-custom-block-details-bg: var(--vp-c-bg-soft);
--vp-local-search-highlight-bg: transparent;
--vp-local-search-highlight-text: var(--vp-c-blue-1);
--vp-local-search-bg: var(--vp-c-bg-soft);
--vp-local-search-result-bg: var(--vp-c-white);
--vp-local-search-result-shadow: 0 1px 3px 0 #d4d9e1;
--vp-local-search-footer-shadow: 0 -1px 0 0 #e0e3e8, 0 -3px 6px 0 rgba(69, 98, 155, .12);
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, var(--vp-c-brand) 30%, var(--vp-c-brand-dark));
--vp-home-hero-image-background-image: linear-gradient(-45deg, var(--vp-c-brand-light) 50%, var(--vp-c-brand-lighter) 50%);
--vp-home-hero-image-filter: blur(40px);
}
[lang|="ru"] {
--vp-code-copy-copied-text-content: 'Скопировано';
}
.dark {
--vp-c-blue-1: #7da1e9;
--vp-c-blue-2: #6382c2;
--vp-c-blue-3: #86a2da;
--vp-local-search-result-bg: var(--vp-c-bg-soft-mute);
--vp-local-search-result-shadow: none;
--vp-local-search-footer-shadow: none;
--vp-home-hero-image-background-image: linear-gradient(-45deg, var(--vp-c-brand-darker) 25%, var(--vp-c-brand-darkest) 25%);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(72px);
}
}
main figure {
margin: 2rem 0;
transition: transform var(--vp-tt);
}
main img {
margin-left: auto;
margin-right: auto;
margin-top: 24px;
margin-bottom: 24px;
border-radius: var(--vp-border-radius);
box-shadow: 2px 2px 8px 4px var(--vp-c-bg-alt);
}
figcaption {
text-align: center;
margin-top: 1.25rem;
font-size: 0.875rem;
color: var(--vp-c-text-2);
}
main figure > a .external-link-icon {
display: none
}
figure figcaption {
color: inherit
}
.custom-block.tip figure img {
--vp-c-bg-alt: var(--vp-custom-block-tip-bg)
}
main :where(h1, h2, h3, h4, h5, h6) + figure {
margin-top: 1.5rem;
}
.Layout {
margin-right: var(--scrollbar-width);
}
.VPNav {
background-color: var(--vp-c-bg);
border-radius: 0 0 var(--vp-border-radius) var(--vp-border-radius);
box-shadow: var(--vp-shadow-6);
padding-right: var(--scrollbar-width);
}
.VPLocalNav {
border-top: none !important;
}
.VPNavBar.has-sidebar {
border-bottom: none;
}
.VPNavBarTitle .title {
font-size: 0;
border: none;
}
@media (min-width: 1440px) {
.VPSidebar {
left: calc((var(--scrollbar-width) / 2) * -1) !important;
}
}
.VPSidebarItem.level-0 {
padding-bottom: 10px !important;
}
.VPDocAsideOutline {
border: var(--vp-border);
border-radius: var(--vp-border-radius);
padding: 20px 20px 20px 0;
}
.VPDocAsideOutline .content {
padding-left: 20px;
border-left: none;
}
.VPDocAsideOutline .outline-marker {
width: 4px;
left: -4px;
}
.VPDoc .content-container {
max-width: unset !important;
}
.pager .pager-link {
border: 1px solid var(--vp-c-gray-soft);
box-shadow: var(--vp-shadow-1);
}
.pager .pager-link .title {
color: var(--vp-c-text-1);
}
.curtain {
display: none;
}
kbd {
border: 1px solid var(--vp-c-border);
box-shadow: var(--vp-c-gray-soft) 0px 1px 1px 0px, var(--vp-c-neutral-inverse) 0px 1px 0px 0px inset;
background-color: var(--vp-c-gray-soft);
border-radius: 3px;
display: inline-block;
margin: 0 .1em;
overflow-wrap: break-word;
padding: .1em .6em;
text-shadow: 0 1px 0 var(--vp-c-white);
}
.vp-doc p {
line-height: 26px;
}
.vp-doc li + li {
margin-top: .25em;
}
.vp-doc table {
line-height: 1.5;
}
.vp-doc ol {
counter-reset: li;
list-style: none;
position: relative;
}
.vp-doc ol > li {
padding-left: 1.25rem;
}
.vp-doc ol > li::before {
width: calc(1.5rem - 2px);
height: calc(1.5rem - 2px);
font-size: calc(1rem - 2px);
margin: 1px 0 0 .5rem;
content: counter(li);
counter-increment: li;
position: absolute;
left: 0;
color: var(--vp-c-text-1);
font-weight: 500;
text-align: center;
border-radius: 50%;
background-color: var(--vp-sidebar-bg-color);
}
.custom-block {
border-radius: var(--vp-border-radius);
border-width: var(--vp-border-width);
}
.custom-block.info a,
.custom-block.info code {
color: var(--vp-c-blue-1);
}
.custom-block.info a:hover {
color: var(--vp-c-blue-2);
}
.navigation {
color: var(--vp-c-brand-2);
font-weight: 600;
}
.navigation:hover {
color: var(--vp-c-brand-1);
cursor: default;
}
.navigation svg,
span.name {
vertical-align: middle;
position: relative;
bottom: 1px;
}
.navigation svg {
fill: currentColor;
height: 1em;
width: 1em;
display: inline-block;
margin-right: 4px;
}
.navigation.direction {
fill: currentColor;
height: 1em;
width: 1em;
display: inline-block;
}
.plugin-tabs {
border-radius: var(--vp-border-radius);
}
.plugin-tabs--content {
padding: 2rem !important;
}
.plugin-tabs--tab {
position: relative;
padding: 0 12px;
line-height: 54px;
border-bottom: 2px solid transparent;
color: var(--vp-plugin-tabs-tab-text-color);
font-size: 14px;
font-weight: 500;
white-space: nowrap;
transition: color 0.50s;
}
@media (max-width: 1023px) {
.vp-doc table {
border: 0;
}
.vp-doc table tbody {
display: block;
}
.vp-doc table thead {
border: none;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.vp-doc table tr {
border-bottom: 1px solid var(--vp-c-divider);
display: block;
margin-bottom: 1rem;
}
.vp-doc table tr:nth-child(2n) {
background-color: inherit;
}
.vp-doc table td {
border-top: none;
display: block;
overflow-x: auto;
}
.vp-doc table td::before {
display: block;
content: attr(data-label) ':';
color: var(--vp-c-text-2);
}
.vp-doc table td:empty {
display: none;
}
.vp-doc table td:last-child {
border-bottom: 0;
}
}
@media (min-width: 768px) {
.DocSearch-Button {
display: flex;
justify-content: space-between;
background-color: var(--vp-c-bg-elv);
border: 1px solid var(--vp-c-gray-soft);
box-shadow: var(--vp-shadow-1);
}
.DocSearch-Button:hover {
background-color: var(--vp-c-bg-elv);
border: 1px solid var(--vp-c-gray-2);
box-shadow: var(--vp-shadow-1);
}
}
@media (min-width: 1024px) {
.DocSearch-Button {
min-width: 200px;
}
}
.screenshots {
display: flex;
margin: 0 auto 0;
max-width: 1152px;
}
.shot {
flex: 33.33%;
padding: 12px;
margin: 12px auto 0;
}
/* Parsers lib layout */
.dl {
color: var(--vp-c-text-1);
padding: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 1rem;
max-width: 1152px;
margin: 0 auto 0;
}
.dl h1 {
font-size: 3rem;
font-weight: 600;
margin: 0.5rem;
}
.dl .btn {
position: relative;
display: inline-block;
border: 4px solid transparent;
border-radius: 8px;
padding: 8px 18px;
font-size: 16px;
font-weight: 500;
transition: all 0.25s, color 0.25s;
}
/* Parsers lib layout */
.parsers {
background-color: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
padding: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 1rem;
max-width: 1152px;
margin: 0 auto 0;
border-radius: var(--vp-border-radius);
}
.parsers h1 {
font-size: 2.2rem;
font-weight: 600;
margin: 0.5rem;
}
.parsers .btn {
position: relative;
display: inline-block;
border: 4px solid transparent;
border-radius: 8px;
padding: 8px 18px;
font-size: 16px;
font-weight: 500;
transition: all 0.25s, color 0.25s;
}
.divider {
margin: 24px auto 18px;
width: 64px;
height: 1px;
background-color: var(--vp-c-divider);
}

@ -0,0 +1,23 @@
import { DefaultTheme } from 'vitepress'
import type { ComponentData } from '../plugins/component'
export namespace Theme {
export interface Sponsor {
message?: string
linkText?: string
link?: string
}
export interface Config extends DefaultTheme.Config {
titleSeparator?: string
components?: Array<ComponentData>
teamSectionTitle?: string
sponsorLink?: string
sponsor?: Sponsor
}
export interface TeamMember extends Omit<DefaultTheme.TeamMember, 'name'> {
name: Record<string, string>
}
}

@ -0,0 +1,111 @@
import type { DefaultTheme, UserConfig } from 'vitepress'
import type { ComponentData, DocsPageData } from './plugins/component'
import { normalize } from 'vitepress/dist/client/shared'
import { type Translator, translators } from '../../website/translators'
import { Theme } from './types'
export function findPath(
pageData: DocsPageData,
config: UserConfig,
): DefaultTheme.SidebarItem[] {
let searchable = normalize(pageData.relativePath)
const localeLinks = Object.entries(config.locales).flatMap(([key]) => ({ key, link: key === 'root' ? '/' : `${key}/` }))
if (localeLinks.some(({ link }) => link === searchable)) return []
const locale = localeLinks.find(locale => locale.link.startsWith(searchable.replace(/(^.*?\/).*$/, '$1'))) || localeLinks[0]
searchable = ensureStartingSlash(searchable)
const localeConfig: Theme.Config = config.locales[locale.key].themeConfig
const root = localeConfig.nav.find(item => {
if (!('link' in item)) {
return false
}
const tmp = locale.link === '/' ? item.link.replace(/(^\/.*?\/).*$/, '$1') : item.link.replace(/(^\/.*?\/)(.*?\/).*$/, '$1$2')
return searchable.startsWith(tmp)
})
const path: DefaultTheme.SidebarItem[] = []
if (root && ('link' in root)) {
path.push({ text: root.text, link: root.link })
}
let tree: DefaultTheme.SidebarItem | ComponentData
if (pageData.component) {
const { title, link, items } = pageData.component
tree = pageData.component
path.push({ text: title, link, items })
} else {
const sidebar = Object.entries(localeConfig.sidebar).find(([link]) => searchable.startsWith(link))
if (sidebar) tree = sidebar[1]
}
if (!tree) return path
const keyExists = (tree) => {
if (!tree || (typeof tree !== 'object' && !Array.isArray(tree.items) && !Array.isArray(tree))) {
return false
}
else if (tree.hasOwnProperty('link') && ensureStartingSlash(tree.link) === searchable) {
return true
}
else if (Array.isArray(tree)) {
for (let i = 0; i < tree.length; i++) {
const item = tree[i]
path.push(item)
const result = keyExists(item)
if (result) {
return result
}
path.pop()
}
}
else if (Array.isArray(tree.items)) {
for (let i = 0; i < tree.items.length; i++) {
const item = tree.items[i]
path.push(item)
const result = keyExists(item)
if (result) {
return result
}
path.pop()
}
}
return false
}
keyExists(tree)
return path
}
export function ellipsis(
string: string = '',
length: number = 0,
etc: string = '...'
): string {
if (string.length <= length) {
return string
}
return string.substring(0, length) + (string.length > length ? etc : '')
}
export function ensureStartingSlash(path: string): string {
return /^\//.test(path) ? path : `/${path}`
}
export function getTranslator(translator: string): Translator | undefined {
if (!translator) {
return
}
if (!Object.prototype.hasOwnProperty.call(translators, translator)) {
return
}
return translators[translator]
}

@ -0,0 +1 @@
node_modules

@ -0,0 +1,70 @@
{
"name": "kotatsu-website",
"version": "1.0.0",
"repository": "https://github.com/KotatsuApp/kotatsuapp.github.io.git",
"type": "module",
"contributors": [
{
"name": "Zakhar Timoshenko",
"email": "xtimms@live.com"
},
{
"name": "Koitharu",
"email": "nvasya95@gmail.com"
}
],
"scripts": {
"preinstall": "npx only-allow pnpm",
"test": "pnpm lint && pnpm build && pnpm preview",
"dev": "vitepress dev",
"build": "vitepress build",
"preview": "vitepress preview",
"lint": "markdownlint **/*.md --disable MD045"
},
"devDependencies": {
"@docsearch/css": "^3.3.3",
"@docsearch/js": "^3.3.3",
"@modix/smarty-tmlanguage": "^1.1.0",
"@resvg/resvg-js": "2.4.1",
"@types/gtag.js": "0.0.14",
"@types/markdown-it": "^12.2.3",
"@types/node": "^18.16.13",
"dotenv": "^16.3.1",
"fast-glob": "^3.2.12",
"gray-matter": "^4.0.3",
"markdown-it": "^13.0.1",
"markdown-it-container": "^3.0.0",
"markdown-it-kbd": "^2.2.2",
"markdown-it-shortcode-tag": "1.1.0",
"markdownlint": "^0.29.0",
"markdownlint-cli": "^0.34.0",
"modx-tmlanguage": "^1.2.0",
"plop": "^3.1.2",
"stylus": "0.60.0",
"transliteration": "^2.3.5",
"vue3-carousel": "0.3.1",
"vitepress": "^1.0.0-rc.20",
"vitepress-plugin-tabs": "0.4.1",
"vue": "^3.3.4",
"x-satori": "0.1.5"
},
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"@algolia/client-search"
]
}
},
"dependencies": {
"@octokit/rest": "20.0.2",
"@octokit/types": "12.0.0",
"@vueuse/core": "^10.1.2",
"@xobotyi/scrollbar-width": "^1.9.5",
"glightbox": "^3.2.0",
"moment": "2.29.4",
"uuid": "^9.0.0"
},
"engines": {
"node": ">=18"
}
}

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"types": [
"vite/client",
"@types/gtag.js"
],
"paths": {
"@/*": ["./.vitepress/*"]
}
},
"include": [
".vitepress/theme/**/*",
"node_modules/vitepress/dist/client/theme-default/components/**/*.vue"
]
}

@ -0,0 +1,8 @@
---
title: Account
description: On this page you can log in to your Kotatsu Sync Service account.
lastUpdated: false
editLink: false
prev: false
next: false
---

@ -0,0 +1,18 @@
---
title: Changelogs
description: Changelogs of all Kotatsu stable releases.
lastUpdated: false
editLink: false
prev: false
next: false
---
<script setup>
import ChangelogsList from "@theme/components/ChangelogsList.vue";
</script>
# Changelogs
Changelogs of all Kotatsu stable releases, which are also available [on GitHub](https://github.com/KotatsuApp/Kotatsu/releases).
<ChangelogsList />

@ -0,0 +1,31 @@
---
title: Contribute
description: Find out how to help translate or build the app and parsers.
lastUpdated: false
editLink: false
prev: false
next: false
---
# Contribute
Find out how to help translate or build the app and parsers.
## Code
Know how to code and want to improve something or you generally want to support the creation of the app?
[![KotatsuApp/Kotatsu - GitHub](https://gh-card.dev/repos/KotatsuApp/Kotatsu.svg)](https://github.com/KotatsuApp/Kotatsu)
[![KotatsuApp/kotatsu-parsers - GitHub](https://gh-card.dev/repos/KotatsuApp/kotatsu-parsers.svg)](https://github.com/KotatsuApp/kotatsu-parsers)
[![KotatsuApp/kotatsuapp.github.io - GitHub](https://gh-card.dev/repos/KotatsuApp/kotatsuapp.github.io.svg)](https://github.com/KotatsuApp/kotatsuapp.github.io)
## Translation
![Graph of Weblate Translations](https://hosted.weblate.org/widgets/kotatsu/-/strings/open-graph.png)
Want to help translate the app to your language? You can easily help by utilizing a service we use called **Weblate**.
### Helpful links
* [Translators guide](https://docs.weblate.org/en/latest/user/translating.html)
* [Secondary languages](https://docs.weblate.org/en/latest/user/profile.html#secondary-languages)
* [Subscriptions](https://docs.weblate.org/en/latest/user/profile.html#subscriptions)
* [Glossary](https://docs.weblate.org/en/latest/user/translating.html#glossary)

@ -0,0 +1,22 @@
---
title: Download
description: Download page that allows users to access and install the latest version of the app.
lastUpdated: false
editLink: false
prev: false
next: false
---
<script setup>
import DownloadButtons from "@theme/components/DownloadButtons.vue";
import ReleaseDate from "@theme/components/ReleaseDate.vue";
import Changelog from "@theme/components/Changelog.vue";
</script>
# Download
The latest stable version of **Kotatsu** was released **<ReleaseDate type="stable" />**.
<DownloadButtons />
<Changelog type="stable" />

@ -0,0 +1,8 @@
const telegram = `<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="12" fill="#039be5"></circle>
<path fill="#fff" d="m5.491 11.74 11.57-4.461c.537-.194 1.006.131.832.943l.001-.001-1.97 9.281c-.146.658-.537.818-1.084.508l-3-2.211-1.447 1.394c-.16.16-.295.295-.605.295l.213-3.053 5.56-5.023c.242-.213-.054-.333-.373-.121l-6.871 4.326-2.962-.924c-.643-.204-.657-.643.136-.953z"></path>
</svg>`
export {
telegram,
}

@ -0,0 +1,52 @@
---
title: Kotatsu
layout: home
main:
title: Kotatsu
text: Manga Reader
tagline: 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.
actions:
- theme: brand
text: Get started
link: /manuals/guides/getting-started
- theme: alt
text: Download
link: /download/
features:
- icon: <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24" fill="var(--vp-c-brand-1)"><path d="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z" /></svg>
title: Huge count of sources
details: Supports over a 500+ sources
- icon: <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24" fill="var(--vp-c-brand-1)"><path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z" /></svg>
title: Supports tracking
details: Supports MyAnimeList, Anilist and Shikimori
- icon: <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24" fill="var(--vp-c-brand-1)"><path d="M13.5 20C13.81 20.75 14.26 21.42 14.82 22H6C4.89 22 4 21.11 4 20V4C4 2.9 4.89 2 6 2H18C19.11 2 20 2.9 20 4V11.03C19.84 11 19.67 11 19.5 11C19 11 18.5 11.07 18 11.18V4H13V12L10.5 9.75L8 12V4H6V20H13.5M19 20C17.62 20 16.5 18.88 16.5 17.5C16.5 17.1 16.59 16.72 16.76 16.38L15.67 15.29C15.25 15.92 15 16.68 15 17.5C15 19.71 16.79 21.5 19 21.5V23L21.25 20.75L19 18.5V20M19 13.5V12L16.75 14.25L19 16.5V15C20.38 15 21.5 16.12 21.5 17.5C21.5 17.9 21.41 18.28 21.24 18.62L22.33 19.71C22.75 19.08 23 18.32 23 17.5C23 15.29 21.21 13.5 19 13.5Z" /></svg>
title: Synchronization
details: Easily sync series between your devices.
---
<div class="divider" />
<div class="screenshots">
<div class="shot">
<img src="/img/phone_1.png" alt="Home" style="width:100%">
</div>
<div class="shot">
<img src="/img/phone_2.png" alt="Explore" style="width:100%">
</div>
<div class="shot">
<img src="/img/phone_3.png" alt="Details" style="width:100%">
</div>
</div>
<div class="divider" />
<div class="dl">
<h1>kotatsu-dl</h1>
<p>Cross-platform desktop application for downloading manga from various services. Allows you to search for manga and download it to the CBZ comic archives.</p>
<a href="https://github.com/KotatsuApp/kotatsu-dl" target="_blank" class="btn">View on GitHub</a>
<img src="/img/kotatsu-dl.png">
</div>
<div class="divider" />
<div class="parsers">
<h1><svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 24 24" fill="var(--vp-c-brand-1)"><path d="M5.59 3.41L7 4.82L3.82 8L7 11.18L5.59 12.6L1 8L5.59 3.41M11.41 3.41L16 8L11.41 12.6L10 11.18L13.18 8L10 4.82L11.41 3.41M22 6V18C22 19.11 21.11 20 20 20H4C2.9 20 2 19.11 2 18V14H4V18H20V6H17.03V4H20C21.11 4 22 4.89 22 6Z" /></svg></h1>
<h1>Kotatsu parsers library</h1>
<p>A Java/Kotlin library that allows you to scrape manga from various online services and create your own applications for reading or downloading manga. It supports Android but does not depend on it, so it can be also used in pure Kotlin/JVM projects.</p>
<a href="https://github.com/KotatsuApp/kotatsu-parsers#readme" target="_blank" class="btn">Read more</a>
</div>

@ -0,0 +1,24 @@
---
title: Hello here
description: You might need to have a Kotatsu application installed to follow this link.
search: false
sidebar: true
sitemap:
exclude: true
head:
- - meta
- name: robots
content: noindex, nofollow
---
<script setup>
import DownloadButtons from "@theme/components/DownloadButtons.vue";
</script>
# Hello here
You might need to have a Kotatsu application installed to follow this link.
Kotatsu is a free and open source manga reader for Android platform. It supports a lot of online catalogues on different languages with filters and search, offline reading from local storage, favourites, bookmarks, new chapters notifications and more features.
<DownloadButtons />

@ -0,0 +1,4 @@
---
title: Bookmarks
description: Frequently Asked Questions about Bookmarks.
---

@ -0,0 +1,19 @@
---
title: Downloads
description: Frequently Asked Question about Downloads.
---
# Downloads
Frequently Asked Question about Downloads.
## Why did my downloads stop midway?
Downloads stopping midway may be related to network connection issues or source problems.
**Kotatsu** will provide notifications regarding encountered errors during download attempts.
## How do I manage what's downloading?
Navigate to <nav to="explore_downloads"> to interact with queued downloads.
You can cancel item by clicking the **Cancel** button.
## Can I use both internal storage and external SD card storage?
No, you must choose a single location. Internal storage performs better than external SD cards.

@ -0,0 +1,8 @@
---
title: Explore
description: Help center of Explore section.
lastUpdated: false
editLink: false
prev: false
next: false
---

@ -0,0 +1,4 @@
---
title: Local storage
description: Frequently Asked Questions about Local storage.
---

@ -0,0 +1,4 @@
---
title: Random
description: Frequently Asked Questions about Random.
---

@ -0,0 +1,37 @@
---
title: Sources
description: Frequently Asked Questions about Sources.
---
# Sources
Frequently Asked Questions about Sources.
## What are some recommended sources?
**Kotatsu** does not endorse or recommend any source, and there is no best source.
Instead, we encourage users to spend some time trying out a few sources themselves and discover what sources work best for them.
What works well for somebody else might not work well for you.
::: info Disclaimer
**Kotatsu** isn't responsible for slow, down, missing chapters, or subpar image quality of sources as it doesn't host content.
:::
## What is a scanlator source?
Non-officially licensed series are translated by scanlators, often found on their websites or [MangaDex](https://mangadex.org/).
Learn more in this [Wikipedia article](https://en.wikipedia.org/wiki/Scanlation).
## How do I request new sources?
[Open an issue](https://github.com/KotatsuApp/kotatsu-parsers/issues) on **GitHub** if not already there.
Check the removed [parsers list](https://github.com/KotatsuApp/kotatsu-parsers/issues/39) first.
## Why was a source removed?
Extensions can be removed due to several reasons:
* Frequent website changes.
* Image scrambling.
* Scanlator removal request.
* Paywall implementation.
* Reverse engineering needs.
* Site shutdown.
Find the list of removed sources [here](https://github.com/KotatsuApp/kotatsu-parsers/issues/39), excluding offline sites.

@ -0,0 +1,4 @@
---
title: Suggestions
description: Frequently Asked Questions about Suggestions.
---

@ -0,0 +1,15 @@
---
title: Feed
titleTemplate: Frequently Asked Questions
description: Frequently Asked Questions about the Feed.
---
# Feed
Frequently Asked Questions about the Feed.
## Why aren't series updates working?
Some Android skins (e.g., **MIUI**) aggressively save battery, potentially shutting down apps in the background.
Whitelist **Kotatsu** from your battery saver by going to <nav to="checking"> and tapping **Disable battery optimization**.
If unsuccessful, refer to [Don't Kill My App](https://dontkillmyapp.com/) for how to disable specific battery-saving options for your device.

@ -0,0 +1,49 @@
---
title: General
description: Frequently Asked Questions
---
# General
Frequently Asked Questions
## Why isn't Kotatsu on the Google Play Store?
**Kotatsu** won't be on the **Google Play Store**. **Google** might take down the app due to certain content, which the developers wishes to avoid.
To report **Kotatsu** copycats on the **Google Play Store**, fill out [this form](https://support.google.com/googleplay/android-developer/contact/takedown) following the steps below.
:::details Steps to report Kotatsu copycats
1. Go to the page -> three dots menu -> Flag as inappropriate -> Other objection.
1. After choosing "Other objection", you may choose to put down any or all of the following:
The app infringes on the Google Play Developer Policy by
Encouraging Infringement of Copyright
> We dont allow apps that induce or encourage copyright infringement. Before you publish your app, look for ways your app may be encouraging copyright infringement and get legal advice if necessary.
Trademark Infringement
> We dont allow apps that infringe on others trademarks. A trademark is a word, symbol, or combination that identifies the source of a good or service. Once acquired, a trademark gives the owner exclusive rights to the trademark usage with respect to certain goods or services.
>
> Trademark infringement is an improper or unauthorized use of an identical or similar trademark in a way that is likely to cause confusion as to the source of that product. If your app uses another partys trademarks in a way that is likely to cause confusion, your app may be suspended.
Misleading Claims, e.g. "MangaTop - Best Manga Reader"
> We dont allow apps that contain false or misleading information or claims, including in the description, title, icon, and screenshots. Here are some examples of common violations: Apps that misrepresent or do not accurately and clearly describe their functionality:
> - Developer or app names that misrepresent their current status or performance on Play. (E.g. “Editors Choice,” “Number 1 App,” “Top Paid”)...
> - Apps that are improperly categorized.
:::
## Is Kotatsu available for iOS/iPadOS?
There is no iOS or iPadOS version and neither are there plans for one.
Porting is difficult due to the separate codebases of iOS and Android apps.
Any app proclaiming to be "**Kotatsu for iOS**" is not by us and should be treated as a scam.
## Can I read light novels?
**Kotatsu** can't read light novels; it's an image parser, not a text parser.
## Can I stream anime?
**Kotatsu** isn't designed for anime streaming.
Projects using the **Kotatsu** name for anime streaming aren't affiliated with the main project.

@ -0,0 +1,18 @@
---
title: Reader
titleTemplate: Frequently Asked Questions
description: Frequently Asked Questions about the Reader.
---
# Reader
Frequently Asked Questions about the Reader.
## Why didn't the page load?
Besides network-related problems, **Kotatsu** may occasionally fail to recognize certain images.
To address this, simply exit and re-enter the reader or click **Try again** button, often resolving the issue.
## Can I see two pages at once?
Not currently. Creating an effective dual-page reader that accommodates scanlator page inconsistencies and other complexities poses challenges. This feature may be added in the future.
## What do all the settings do?
For detailed instructions, please consult the guides section on the website here.

@ -0,0 +1,14 @@
---
title: Settings
description: Frequently Asked Questions about various app settings.
---
# Settings
Frequently Asked Questions about various app settings.
## Why is taking screenshots blocked?
Turn off **Screenshot policy** in <nav to="reader">.
## What is DNS over HTTPS?
**DNS over HTTPS** (in <nav to="network">) offers secure DNS resolution through HTTPS, preventing attacks.
Learn more [here](https://www.cloudflare.com/learning/dns/dns-over-tls/). This may help bypass some basic website blocking.

@ -0,0 +1,29 @@
---
title: Backups
description: Backups helps you prevent losing your library if something happens.
---
# Backups
Backups in **Kotatsu** are compatible between different versions of the app.
::: tip How to create a backup
1. Go to <nav to="data">.
1. Select **Create data backup** and choose a location to save it.
<img src="/manuals/guides/backups/backup.png" alt="Backup and Restore" width="500"/>
:::
## General backup details
### What's included in a backup?
- **Reading history**
- **Favourites**
- **Categories of favourites**
- **App settings**
### What's not included in a backup?
- **Downloaded** chapters
## Restoring a backup
Restoring a backup can be done through the **Restore from backup** setting.

@ -0,0 +1,34 @@
---
title: Categories
description: Organize your favorite series effortlessly with categories.
---
# Categories
Organize your favorite series effortlessly with categories.
To manage your categories, navigate to <nav to="favourites_manage_categories">.
- You can name and sort categories as you prefer (e.g., by `Source`, `Genre`, `Reading Status`).
- Add series to multiple categories and control update options through category settings.
## Content
Categories would be useless without any content in them.
Below are some tips for using them.
:::: tabs
== Add entries
### Add series to a category
1. Long press the series you want to add.
1. Press the **heart** button.
1. Select which category or categories you want it in.
== Remove entries
### Remove series from a category
1. Long press the series that you want to remove.
1. Press the **heart** button.
1. Deselect the category or categories you want to remove it from.
::::

@ -0,0 +1,63 @@
---
title: Getting started
description: Essential information to help you get set up with Kotatsu.
---
<script setup lang="ts">
import { data as release } from "@theme/data/release.data"
</script>
# Getting started
Essential information to help you get set up with Kotatsu.
## Installation guide
### Downloading Kotatsu
1. Visit our [download](/download/) page to get the latest version of **Kotatsu**.
1. After the download is complete, open the `kotatsu-{{ release.stable.tag_name }}-release.apk` file.
1. Proceed with the installation process.
### Selecting sources depending on languages
At the first launch, a dialog with a list of source languages available in the application will appear. By default, the languages that are on the Android system and multilingual sources will be enabled.
<img src="/manuals/guides/getting-started/welcome.png" alt="Welcome screen" width="500"/>
::: tip
You can organize the list of sources in <nav to="explore_manage_sources">
:::
### Adding series to your favourites
Here's how you can add series to your favourites:
1. Select the source you'd like to browse.
1. Once you've found the series that you want to add, tap on it for more details.
1. Press the heart button on toolbar, choose category or create new one, and the series will be added to your favourites.
<img src="/manuals/guides/getting-started/add-to-favourites.png" alt="Adding to favourites" width="500"/>
## Additional setup
### Series search options
If you want to search for series across all your sources, you can use the Global Search feature.
Follow these steps:
1. Go to the main section.
1. Use the search bar in the toolbar to find series from all available sources.
### Trouble finding a specific series?
If you encounter difficulties while searching for a specific series, consider the following points:
* Double-check your spelling and try again, as some sources might use **Japanese romanized** titles instead of **English** ones.
> Example: **Boku no Hero Academia** instead of **My Hero Academia**.
* Some sources may use different spellings or wordings for titles.
> Example: **Bungo Stray Dogs** instead of **Bungou Stray Dogs**
> Example: **3-gatsu no Lion** instead of **Sangatsu no Lion**.

@ -0,0 +1,36 @@
---
title: Tracking
description: Tracking helps track your library with different online services.
---
<script setup lang="ts">
import { data as release } from "@theme/data/release.data"
</script>
# Tracking
Tracking helps you automatically send read chapters to supported trackers, so you can keep track of what and when you read it online.
## Services
Kotatsu (version {{ release.stable.tag_name }}) supports the following services: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Shikimori](https://shikimori.one/).
- Tracking is manual for each entry.
- Reading the last page of a chapter marks it as read.
- Offline progress syncs when online.
- Completion status auto-updates.
- Tracking is one-way: **Kotatsu -> Tracker**.
## General questions
### How do I log in to trackers?
1. Go to <nav to="services">.
1. Tap the desired tracker to begin login.
### How do I set up tracking for each series?
1. Open the series.
1. Tap <nav to="details_tracking">.
1. Find a series and click **Done**.
::: tip
You can also change the search query if there is no match.
:::

@ -0,0 +1,74 @@
---
title: Common issues
description: Facing issues with a source or the app? Here's how to tackle common challenges.
---
# Common issues
Facing issues with a source or the app?
Here's how to tackle common challenges.
## Basic issues
### Slow loading
Sources being slow could stem from site slowness, your internet, or source-imposed rate limits/IP bans.
## Advanced errors
### `Unable to resolve host` / `Connection failed` / `Failed to connect to` / `timeout` / `connection reset`
These errors indicate connection issues. Possible causes include:
* Weak internet connection.
* App lacks internet access.
* Your ISP has blocked the site.
* The site is down.
Try these solutions:
* Enable **DNS over HTTPS**.
* Change network (Wi-Fi, mobile data, VPN).
* Reboot router.
### `java.security.cert.CertPathValidatorException` / `Chain validation failed`
Validation issue with source's certificate.
Try these solutions:
* Check expired certificate, use SSL checker.
* Set correct device date and time.
* Change network (Wi-Fi, mobile data, VPN).
* Enable **Ignore SSL errors** in <nav to="network"/>
* Reboot device.
## HTTP errors
Encountering HTTP errors? Here's what they mean and how to address them.
### `HTTP Error: 403` - Forbidden
Possible reasons for this error:
* The selected source has Cloudflare protection. Check the [Cloudflare guide](/manuals/guides/troubleshooting/#cloudflare) for solutions.
* The source might be down, removed the series, or banned your IP.
> Open WebView to confirm.
### `HTTP Error: 404` - Not Found
This error likely indicates a down source or removed series.
* Use **WebView** to verify.
> Consider switching to a different source for the series.
### `HTTP Error: 429` - Too Many Requests
This error suggests the source temporarily banned your IP due to fast downloads/reads.
### `HTTP Error: 5xx`
Errors like `500`, `502`, etc., indicate server-side issues on the source's end.
[Check the source in WebView](/manuals/guides/troubleshooting/#accessing-websites-via-webview) to confirm if it's down.
### `HTTP Error: 1006`
This error means a temporary IP ban by the source.
### `HTTP Error: 1020`
This error points to violating a firewall rule set by the site owner.
The owner might raise Cloudflare protection or block IPs from outside their country.
::: warning
For unlisted errors or if instructions don't help, refer to [Diagnosis](/manuals/guides/troubleshooting/diagnosis).
:::

@ -0,0 +1,63 @@
---
title: Diagnosis
description: Facing issues with a source or the app? Follow these steps to troubleshoot and find solutions.
---
# Diagnosis
Facing issues with a source or the app?
Follow these steps to troubleshoot and find solutions.
## Primary diagnosis
1. **Update app**: Go to <nav to="about"> and tap **Check for updates**.
1. **Manual series refresh**: Go to <nav to="main_feed"> and drag down to manually refresh problematic series.
1. **Test other series**: Try different series from the same source.
1. **Update WebView**: Ensure your WebView is current.
1. **Public WebView**: Attempt opening series in public WebView. Solve for CAPTCHA or Cloudflare protection if needed.
1. **Change connection**: Switch networks (Wi-Fi, mobile data, VPN) and confirm IP change.
1. **Collaborative check**: Get others to replicate the error.
1. **Source status**: Verify the source's status in a browser.
1. **Retry button**: Look for a retry button on the series page.
1. **Advanced settings**: Under <nav to="data">, try these options:
- Clear thumbnails cache
- Clear page cache
- Clear network cache
- Clear cookies
- Under <nav to="network">, try to enable DNS over HTTPS
1. **Download issues**: Delete the queue and retry downloads.
1. **Restart Kotatsu**: Force close and reopen the app.
If any of these solutions help, go to [Personalized Issue](#personalized-issue).
If it is not just you, go to [Widespread Issue](#widespread-issue).
If none of these solutions help, try asking in our [Telegram group](https://t.me/kotatsuapp).
State your app version and the source, series, and chapter with the problem.
::: tip An app update may fix your issue
Wait or check for an app update if you have not already.
There are no ETAs for updates.
:::
## Personalized issue
If you're the only one facing a problem, you might be encountering an IP ban, or other countermeasures set by website owners against programs like **Kotatsu**.
**To minimize future issues:**
- Avoid using downloads with the source.
- Reduce the number of series in your favourites/history from that source.
::: warning
These are general guidelines as each site has its specific undisclosed limits and triggers.
:::
## Widespread issue
When everyone experiences a problem, it could be with the extension or app:
1. Check open issues [for the app](https://github.com/KotatsuApp/Kotatsu/issues) and/or [**parsers**](https://github.com/KotatsuApp/kotatsu-parsers/issues).
1. Check closed issues ([app](https://github.com/KotatsuApp/Kotatsu/issues?q=is%3Aissue+is%3Aclosed)/[parsers](https://github.com/KotatsuApp/kotatsu-parsers/issues?q=is%3Aissue+is%3Aclosed)) in case it's resolved but not yet released.
1. If not found, create a new issue.
::: warning
If the site itself is problematic, patience is the only solution until it becomes functional again.
:::

@ -0,0 +1,51 @@
---
title: Troubleshooting
description: Facing source or app issues? Here's how to troubleshoot.
---
# Troubleshooting
Facing source or app issues? Here's how to troubleshoot.
Be sure to check the [Frequently Asked Questions]() for how to address common issues too.
## WebView
### Clearing cookies and WebView data
This resets your WebView to a clean state, including any login states.
1. Navigate to <nav to="data">.
1. Tap **Clear network cache**.
1. Tap **Clear cookies**.
### WebView update
To update WebView, you need to find what WebView implementation is used on your device.
Typical default implementation depends on the Android version as follows:
::: tabs
== Android 10 and above
[Android System WebView](https://play.google.com/store/apps/details?id=com.google.android.webview)
== Android 7 - 9
[Google Chrome](https://play.google.com/store/apps/details?id=com.android.chrome)
== Android 6 and below
[Android System WebView](https://play.google.com/store/apps/details?id=com.google.android.webview)
:::
::: tip **Android 7** and above
Newer Android users can check/change WebView in [Developer Options](https://developer.android.com/studio/debug/dev-options).
:::
::: warning Caution with Non-Standard WebView
Using non-standard WebView (like Firefox) might cause **Kotatsu** to malfunction or crash.
It's best to use the standard [Android System WebView](https://play.google.com/store/apps/details?id=com.google.android.webview) or [Google Chrome](https://play.google.com/store/apps/details?id=com.android.chrome).
:::
## General
### Obtaining crash/error logs
For crash investigations, navigate to <nav to="about"> and tap **Share logs**.
### Obtaining more logs
To diagnose abnormal app behavior, record device logs using a [Logcat Reader](https://play.google.com/store/apps/details?id=com.dp.logcatapp).

@ -0,0 +1,45 @@
---
title: Privacy policy
description: Privacy Policy that explains how Kotatsu collects, uses, and protects users' personal information.
lastUpdated: false
editLink: false
---
# Privacy policy
Kotatsu is an Open Source app.
This SERVICE is provided at no cost and is intended for use as is.
This page details our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our Service.
If you choose to use our Service, then you agree to the collection and use of information in relation to this policy.
The Personal Information that we collect is used for providing and improving the Service.
We will not use or share your information with anyone except as described in this Privacy Policy.
## Log Data
In a case of an error in the app, the Service collects data and information called Log Data.
This Log Data may include information including your device name, operating system version, the configuration of the app when utilizing our Service, the time and date of your use of the Service, and other statistics.
Log Data can be sent at the discretion of the user.
## External Links
This Service contains links to other sites.
If you click on a third-party link, you will be directed to that site.
Note that these external sites are not operated by us.
Therefore, we strongly advise you to review the Privacy Policy of these websites.
We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
This includes the use of external tracking services (e.g. MyAnimeList).
## Changes to This Privacy Policy
We may periodically update our Privacy Policy.
Thus, you are advised to review this page periodically for any changes.
The current iteration of this policy is effective as of October 2, 2023.
## Contact Us
If you have any questions or suggestions about this Privacy Policy, do not hesitate to reach out to us on [our Telegram group](https://t.me/kotatsuapp).

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
id="vector">
<path
id="path"
d="M 145.3 101.9 C 127 108.5 111.1 133.3 103.4 167.2 C 100.6 179.6 98 200.4 98 210.5 L 98 217.5 L 89.7 230.5 C 66.6 266.5 45.4 304 25.5 344 C 7.8 379.4 6 383.7 6 390 C 6 392.8 6.7 396.4 7.4 397.9 C 9.7 402.2 15.3 406.6 21.5 408.9 L 27.2 411.1 L 258.7 411 C 483.8 411 490.2 410.9 493.8 409.1 C 505.4 403.1 509.1 390.7 503.1 377.8 C 480.4 328.4 449.6 272.3 421.5 229 L 414.7 218.5 L 413.9 206.5 C 410.8 160.5 401.1 132.3 381.2 111.7 C 372.6 102.9 369.3 100.9 363.2 100.8 C 354.7 100.8 349 104.7 336.3 119.3 C 323.8 133.7 321.2 137.1 312.5 150 C 308.2 156.3 302.9 163.6 300.7 166.2 L 296.7 171 L 255.1 171 L 213.5 170.9 L 205.1 158 C 189 133.2 162.6 103.1 155.1 101 C 150.7 99.8 151.5 99.7 145.3 101.9 Z M 171.8 242.6 C 175 244.8 175.8 246.8 176.9 256 C 178.3 266.9 179.6 268.4 186.7 267.5 C 193.1 266.7 198.3 268.8 201.2 273.4 C 206.9 282.6 197.1 301.9 183.2 308.7 C 178.2 311.2 176.4 311.5 168 311.5 C 159.9 311.5 157.8 311.1 154 309.1 C 142.9 303.3 136.1 294.8 133.3 283.6 C 131.9 277.7 131.8 275.9 132.9 270.7 C 134.4 263.2 137 258.2 141.7 252.9 C 150 243.7 165.7 238.3 171.8 242.6 Z M 348.8 243.5 C 351.9 246.1 352.1 246.9 353.6 258.9 C 354.5 266.2 356.7 268.4 362.6 267.5 C 368.3 266.7 373.2 268.3 376.5 272.1 C 381.9 278.6 378.7 291.1 369.1 301.3 C 361.4 309.5 356.6 311.4 344.5 311.5 C 335.3 311.5 334.1 311.3 329.3 308.6 C 322.1 304.6 315.6 298 312.3 291.2 C 310.1 286.7 309.4 283.8 309.1 277.3 C 308.8 269.8 309.1 268.4 311.8 262.7 C 316.8 251.9 324.4 245.2 334.9 242.4 C 342.1 240.4 345.5 240.7 348.8 243.5 Z"
fill="#ABC7FF"
stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
id="vector">
<path
id="path"
d="M 145.3 101.9 C 127 108.5 111.1 133.3 103.4 167.2 C 100.6 179.6 98 200.4 98 210.5 L 98 217.5 L 89.7 230.5 C 66.6 266.5 45.4 304 25.5 344 C 7.8 379.4 6 383.7 6 390 C 6 392.8 6.7 396.4 7.4 397.9 C 9.7 402.2 15.3 406.6 21.5 408.9 L 27.2 411.1 L 258.7 411 C 483.8 411 490.2 410.9 493.8 409.1 C 505.4 403.1 509.1 390.7 503.1 377.8 C 480.4 328.4 449.6 272.3 421.5 229 L 414.7 218.5 L 413.9 206.5 C 410.8 160.5 401.1 132.3 381.2 111.7 C 372.6 102.9 369.3 100.9 363.2 100.8 C 354.7 100.8 349 104.7 336.3 119.3 C 323.8 133.7 321.2 137.1 312.5 150 C 308.2 156.3 302.9 163.6 300.7 166.2 L 296.7 171 L 255.1 171 L 213.5 170.9 L 205.1 158 C 189 133.2 162.6 103.1 155.1 101 C 150.7 99.8 151.5 99.7 145.3 101.9 Z M 171.8 242.6 C 175 244.8 175.8 246.8 176.9 256 C 178.3 266.9 179.6 268.4 186.7 267.5 C 193.1 266.7 198.3 268.8 201.2 273.4 C 206.9 282.6 197.1 301.9 183.2 308.7 C 178.2 311.2 176.4 311.5 168 311.5 C 159.9 311.5 157.8 311.1 154 309.1 C 142.9 303.3 136.1 294.8 133.3 283.6 C 131.9 277.7 131.8 275.9 132.9 270.7 C 134.4 263.2 137 258.2 141.7 252.9 C 150 243.7 165.7 238.3 171.8 242.6 Z M 348.8 243.5 C 351.9 246.1 352.1 246.9 353.6 258.9 C 354.5 266.2 356.7 268.4 362.6 267.5 C 368.3 266.7 373.2 268.3 376.5 272.1 C 381.9 278.6 378.7 291.1 369.1 301.3 C 361.4 309.5 356.6 311.4 344.5 311.5 C 335.3 311.5 334.1 311.3 329.3 308.6 C 322.1 304.6 315.6 298 312.3 291.2 C 310.1 286.7 309.4 283.8 309.1 277.3 C 308.8 269.8 309.1 268.4 311.8 262.7 C 316.8 251.9 324.4 245.2 334.9 242.4 C 342.1 240.4 345.5 240.7 348.8 243.5 Z"
fill="#0059c8"
stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

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

Loading…
Cancel
Save