commit bd2ba8dd3f9a13c695553e3ca776bb1fc74768d5 Author: Zakhar Timoshenko Date: Mon Oct 9 03:43:17 2023 +0300 Initial commit diff --git a/.github/workflow/deploy.yml b/.github/workflow/deploy.yml new file mode 100644 index 0000000..36e04cd --- /dev/null +++ b/.github/workflow/deploy.yml @@ -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 }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bcb1064 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..07f894b --- /dev/null +++ b/.markdownlint.jsonc @@ -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": "" + } +} \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..937f800 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-import-method=clone-or-copy \ No newline at end of file diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts new file mode 100644 index 0000000..da04440 --- /dev/null +++ b/.vitepress/config/en.ts @@ -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>> = { + 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 = { + 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: "GPL-3.0 Licensed | Privacy policy", + 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/" + } + ] +} \ No newline at end of file diff --git a/.vitepress/config/hooks/generateMeta.ts b/.vitepress/config/hooks/generateMeta.ts new file mode 100644 index 0000000..6cecaed --- /dev/null +++ b/.vitepress/config/hooks/generateMeta.ts @@ -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 \ No newline at end of file diff --git a/.vitepress/config/hooks/generateOgImages.ts b/.vitepress/config/hooks/generateOgImages.ts new file mode 100644 index 0000000..3767c4f --- /dev/null +++ b/.vitepress/config/hooks/generateOgImages.ts @@ -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()) +} \ No newline at end of file diff --git a/.vitepress/config/index.ts b/.vitepress/config/index.ts new file mode 100644 index 0000000..d5b17fa --- /dev/null +++ b/.vitepress/config/index.ts @@ -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({ + 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) + } +}) \ No newline at end of file diff --git a/.vitepress/config/ru.ts b/.vitepress/config/ru.ts new file mode 100644 index 0000000..7b301aa --- /dev/null +++ b/.vitepress/config/ru.ts @@ -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>> = { + ru: { + translations: { + button: { + buttonText: 'Поиск', + buttonAriaLabel: 'Поиск' + }, + modal: { + resetButtonTitle: 'Сбросить', + backButtonTitle: 'Закрыть', + noResultsText: 'Нет результатов по запросу', + footer: { + selectText: 'для выбора', + selectKeyAriaLabel: 'enter', + navigateText: 'для навигации', + navigateUpKeyAriaLabel: 'стрелка вверх', + navigateDownKeyAriaLabel: 'стрелка вниз', + closeText: 'закрыть', + closeKeyAriaLabel: 'escape' + } + } + } + }, +} + +export const config: LocaleConfig = { + 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: "Лицензия GPL-3.0 | Политика конфиденциальности", + 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/", + }, + ] +} \ No newline at end of file diff --git a/.vitepress/config/shortcodes.ts b/.vitepress/config/shortcodes.ts new file mode 100644 index 0000000..ca79d3b --- /dev/null +++ b/.vitepress/config/shortcodes.ts @@ -0,0 +1,89 @@ +const iconMappings = { + about: 'information-outline', + bookmark: 'bookmark-outline', + bookPage: '', + compassOutline: '', + dataPrivacy: '', + dice: 'dice-5-outline', + dotsHorizontal: '', + downloadOutline: '', + favourite: 'heart-outline', + feed: 'rss', + history: '', + local: '', + paletteOutline: '', + sources: '', + services: '', + threeDots: 'dots-vertical', + web: '' +} + +interface Navigation { + name: string + icon?: string + dependsOn?: string +} + +const navigationMappings: Record = { + // 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 "Unsupported Navigation!" + } + + const { name, icon, dependsOn } = navData + + const iconHtml = icon ?? "" + let html = `${iconHtml}${name}` + + if (dependsOn) { + html = `${generateNavigationHtml(dependsOn)} arrow-right-thin ${html}` + } + + return html +} + +const shortcodes = { + nav: { + render({ to }) { + return generateNavigationHtml(to) + }, + }, +} + +export default shortcodes \ No newline at end of file diff --git a/.vitepress/config/ua.ts b/.vitepress/config/ua.ts new file mode 100644 index 0000000..a8cee5f --- /dev/null +++ b/.vitepress/config/ua.ts @@ -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>> = { + ru: { + translations: { + button: { + buttonText: 'Пошук', + buttonAriaLabel: 'Пошук' + }, + modal: { + resetButtonTitle: 'Скинути', + backButtonTitle: 'Закрити', + noResultsText: 'Немає результатів за запитом', + footer: { + selectText: 'для вибору', + selectKeyAriaLabel: 'enter', + navigateText: 'для навігації', + navigateUpKeyAriaLabel: 'стрілка вгору', + navigateDownKeyAriaLabel: 'стрілка донизу', + closeText: 'закрити', + closeKeyAriaLabel: 'escape' + } + } + } + }, + } + +export const config: LocaleConfig = { + 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: "Ліцензія GPL-3.0 | Політика конфіденційності", + 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/", + }, + ] + } \ No newline at end of file diff --git a/.vitepress/fonts/Montserrat-Bold.ttf b/.vitepress/fonts/Montserrat-Bold.ttf new file mode 100644 index 0000000..0927b81 Binary files /dev/null and b/.vitepress/fonts/Montserrat-Bold.ttf differ diff --git a/.vitepress/fonts/Montserrat-Medium.ttf b/.vitepress/fonts/Montserrat-Medium.ttf new file mode 100644 index 0000000..4012225 Binary files /dev/null and b/.vitepress/fonts/Montserrat-Medium.ttf differ diff --git a/.vitepress/fonts/Montserrat-Regular.ttf b/.vitepress/fonts/Montserrat-Regular.ttf new file mode 100644 index 0000000..f4a266d Binary files /dev/null and b/.vitepress/fonts/Montserrat-Regular.ttf differ diff --git a/.vitepress/fonts/Montserrat-SemiBold.ttf b/.vitepress/fonts/Montserrat-SemiBold.ttf new file mode 100644 index 0000000..189ce9d Binary files /dev/null and b/.vitepress/fonts/Montserrat-SemiBold.ttf differ diff --git a/.vitepress/fonts/Roboto-Bold.ttf b/.vitepress/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..43da14d Binary files /dev/null and b/.vitepress/fonts/Roboto-Bold.ttf differ diff --git a/.vitepress/fonts/Roboto-Medium.ttf b/.vitepress/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..ac0f908 Binary files /dev/null and b/.vitepress/fonts/Roboto-Medium.ttf differ diff --git a/.vitepress/fonts/Roboto-Regular.ttf b/.vitepress/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..ddf4bfa Binary files /dev/null and b/.vitepress/fonts/Roboto-Regular.ttf differ diff --git a/.vitepress/theme/components/Breadcrumbs.vue b/.vitepress/theme/components/Breadcrumbs.vue new file mode 100644 index 0000000..7f90e2f --- /dev/null +++ b/.vitepress/theme/components/Breadcrumbs.vue @@ -0,0 +1,106 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/Button.vue b/.vitepress/theme/components/Button.vue new file mode 100644 index 0000000..5ef7e03 --- /dev/null +++ b/.vitepress/theme/components/Button.vue @@ -0,0 +1,136 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/Changelog.vue b/.vitepress/theme/components/Changelog.vue new file mode 100644 index 0000000..9606b31 --- /dev/null +++ b/.vitepress/theme/components/Changelog.vue @@ -0,0 +1,110 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/ChangelogsList.vue b/.vitepress/theme/components/ChangelogsList.vue new file mode 100644 index 0000000..76e6f9c --- /dev/null +++ b/.vitepress/theme/components/ChangelogsList.vue @@ -0,0 +1,64 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/Contributors.vue b/.vitepress/theme/components/Contributors.vue new file mode 100644 index 0000000..7d704d0 --- /dev/null +++ b/.vitepress/theme/components/Contributors.vue @@ -0,0 +1,108 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/DownloadButtons.vue b/.vitepress/theme/components/DownloadButtons.vue new file mode 100644 index 0000000..4797205 --- /dev/null +++ b/.vitepress/theme/components/DownloadButtons.vue @@ -0,0 +1,159 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/Feature.vue b/.vitepress/theme/components/Feature.vue new file mode 100644 index 0000000..75ebc4f --- /dev/null +++ b/.vitepress/theme/components/Feature.vue @@ -0,0 +1,60 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/Features.vue b/.vitepress/theme/components/Features.vue new file mode 100644 index 0000000..a8c73cb --- /dev/null +++ b/.vitepress/theme/components/Features.vue @@ -0,0 +1,112 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/Home.vue b/.vitepress/theme/components/Home.vue new file mode 100644 index 0000000..3b41bdb --- /dev/null +++ b/.vitepress/theme/components/Home.vue @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/HomeHero.vue b/.vitepress/theme/components/HomeHero.vue new file mode 100644 index 0000000..eaea1b5 --- /dev/null +++ b/.vitepress/theme/components/HomeHero.vue @@ -0,0 +1,143 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/Layout.vue b/.vitepress/theme/components/Layout.vue new file mode 100644 index 0000000..6f1f1a4 --- /dev/null +++ b/.vitepress/theme/components/Layout.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/.vitepress/theme/components/List.vue b/.vitepress/theme/components/List.vue new file mode 100644 index 0000000..02b4bdf --- /dev/null +++ b/.vitepress/theme/components/List.vue @@ -0,0 +1,45 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/NavBar.vue b/.vitepress/theme/components/NavBar.vue new file mode 100644 index 0000000..810dc64 --- /dev/null +++ b/.vitepress/theme/components/NavBar.vue @@ -0,0 +1,232 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/NavBarMenu.vue b/.vitepress/theme/components/NavBarMenu.vue new file mode 100644 index 0000000..b0cec09 --- /dev/null +++ b/.vitepress/theme/components/NavBarMenu.vue @@ -0,0 +1,64 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/NavBarTranslations.vue b/.vitepress/theme/components/NavBarTranslations.vue new file mode 100644 index 0000000..ec202c3 --- /dev/null +++ b/.vitepress/theme/components/NavBarTranslations.vue @@ -0,0 +1,48 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/NavScreenMenu.vue b/.vitepress/theme/components/NavScreenMenu.vue new file mode 100644 index 0000000..280ef53 --- /dev/null +++ b/.vitepress/theme/components/NavScreenMenu.vue @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/.vitepress/theme/components/NavScreenTranslations.vue b/.vitepress/theme/components/NavScreenTranslations.vue new file mode 100644 index 0000000..5ea139b --- /dev/null +++ b/.vitepress/theme/components/NavScreenTranslations.vue @@ -0,0 +1,77 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/NotFound.vue b/.vitepress/theme/components/NotFound.vue new file mode 100644 index 0000000..c7f42a7 --- /dev/null +++ b/.vitepress/theme/components/NotFound.vue @@ -0,0 +1,84 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/OgImageTemplate.vue b/.vitepress/theme/components/OgImageTemplate.vue new file mode 100644 index 0000000..04f5fe1 --- /dev/null +++ b/.vitepress/theme/components/OgImageTemplate.vue @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/.vitepress/theme/components/ReleaseDate.vue b/.vitepress/theme/components/ReleaseDate.vue new file mode 100644 index 0000000..5565bdd --- /dev/null +++ b/.vitepress/theme/components/ReleaseDate.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/.vitepress/theme/components/ScreenAuthorWidget.vue b/.vitepress/theme/components/ScreenAuthorWidget.vue new file mode 100644 index 0000000..e21f99b --- /dev/null +++ b/.vitepress/theme/components/ScreenAuthorWidget.vue @@ -0,0 +1,102 @@ + + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/SearchBar.vue b/.vitepress/theme/components/SearchBar.vue new file mode 100644 index 0000000..b92ac4d --- /dev/null +++ b/.vitepress/theme/components/SearchBar.vue @@ -0,0 +1,178 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/Sidebar.vue b/.vitepress/theme/components/Sidebar.vue new file mode 100644 index 0000000..9df173f --- /dev/null +++ b/.vitepress/theme/components/Sidebar.vue @@ -0,0 +1,186 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/TranslatorWidget.vue b/.vitepress/theme/components/TranslatorWidget.vue new file mode 100644 index 0000000..4ea00de --- /dev/null +++ b/.vitepress/theme/components/TranslatorWidget.vue @@ -0,0 +1,95 @@ + + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/Tree.vue b/.vitepress/theme/components/Tree.vue new file mode 100644 index 0000000..7d6fc4e --- /dev/null +++ b/.vitepress/theme/components/Tree.vue @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/components/TreeItem.vue b/.vitepress/theme/components/TreeItem.vue new file mode 100644 index 0000000..e3a2b8a --- /dev/null +++ b/.vitepress/theme/components/TreeItem.vue @@ -0,0 +1,130 @@ + + + + + \ No newline at end of file diff --git a/.vitepress/theme/composables/langs.ts b/.vitepress/theme/composables/langs.ts new file mode 100644 index 0000000..2224a80 --- /dev/null +++ b/.vitepress/theme/composables/langs.ts @@ -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 + localeIndex: Ref + page: Ref + theme: Ref + } + + 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 +} \ No newline at end of file diff --git a/.vitepress/theme/composables/prev-next.ts b/.vitepress/theme/composables/prev-next.ts new file mode 100644 index 0000000..6e7ce8c --- /dev/null +++ b/.vitepress/theme/composables/prev-next.ts @@ -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 + page: Ref + frontmatter: Ref + } + + 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 + }, + } + }) +} \ No newline at end of file diff --git a/.vitepress/theme/composables/zoom.ts b/.vitepress/theme/composables/zoom.ts new file mode 100644 index 0000000..da838fb --- /dev/null +++ b/.vitepress/theme/composables/zoom.ts @@ -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: '', + next: '', + prev: '', + }, + }) + + 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' : '') +} \ No newline at end of file diff --git a/.vitepress/theme/data/changelogs.data.ts b/.vitepress/theme/data/changelogs.data.ts new file mode 100644 index 0000000..3817512 --- /dev/null +++ b/.vitepress/theme/data/changelogs.data.ts @@ -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 + +declare const data: GitHubReleaseList +export { data } + +export default defineLoader({ + async load(): Promise { + const releases = await octokit.paginate(octokit.repos.listReleases, { + owner: "KotatsuApp", + repo: "Kotatsu", + per_page: 100, + }) + + return releases + }, +}) \ No newline at end of file diff --git a/.vitepress/theme/data/release.data.ts b/.vitepress/theme/data/release.data.ts new file mode 100644 index 0000000..db339e4 --- /dev/null +++ b/.vitepress/theme/data/release.data.ts @@ -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 + +export interface AppRelease { + stable: GitHubRelease +} + +declare const data: AppRelease +export { data } + +export default defineLoader({ + async load(): Promise { + const { data: stable } = await octokit.repos.getLatestRelease({ + owner: "KotatsuApp", + repo: "Kotatsu", + }) + + return { stable } + }, +}) \ No newline at end of file diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts new file mode 100644 index 0000000..9113c1e --- /dev/null +++ b/.vitepress/theme/index.ts @@ -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 + } + } + ) + } + }, +} \ No newline at end of file diff --git a/.vitepress/theme/plugins/analytics.ts b/.vitepress/theme/plugins/analytics.ts new file mode 100644 index 0000000..db7f3ee --- /dev/null +++ b/.vitepress/theme/plugins/analytics.ts @@ -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) + } +} \ No newline at end of file diff --git a/.vitepress/theme/plugins/component.ts b/.vitepress/theme/plugins/component.ts new file mode 100644 index 0000000..2e72adb --- /dev/null +++ b/.vitepress/theme/plugins/component.ts @@ -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 + categories?: Array + + 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 \ No newline at end of file diff --git a/.vitepress/theme/plugins/markdown.ts b/.vitepress/theme/plugins/markdown.ts new file mode 100644 index 0000000..759f6d3 --- /dev/null +++ b/.vitepress/theme/plugins/markdown.ts @@ -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 `
${title}\n` + return `

${title}

\n` + } else return klass === 'details' ? `
\n` : `\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; +} \ No newline at end of file diff --git a/.vitepress/theme/plugins/sidebar.ts b/.vitepress/theme/plugins/sidebar.ts new file mode 100644 index 0000000..8fed9e2 --- /dev/null +++ b/.vitepress/theme/plugins/sidebar.ts @@ -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 + ignore?: Array + 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 + ): 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 \ No newline at end of file diff --git a/.vitepress/theme/styles/glightbox.css b/.vitepress/theme/styles/glightbox.css new file mode 100644 index 0000000..3c07cf1 --- /dev/null +++ b/.vitepress/theme/styles/glightbox.css @@ -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); + } \ No newline at end of file diff --git a/.vitepress/theme/styles/global.css b/.vitepress/theme/styles/global.css new file mode 100644 index 0000000..f81cefc --- /dev/null +++ b/.vitepress/theme/styles/global.css @@ -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); + } \ No newline at end of file diff --git a/.vitepress/theme/types/index.ts b/.vitepress/theme/types/index.ts new file mode 100644 index 0000000..4ae9c6a --- /dev/null +++ b/.vitepress/theme/types/index.ts @@ -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 + teamSectionTitle?: string + + sponsorLink?: string + sponsor?: Sponsor + } + + export interface TeamMember extends Omit { + name: Record + } +} \ No newline at end of file diff --git a/.vitepress/theme/utils.ts b/.vitepress/theme/utils.ts new file mode 100644 index 0000000..9986cf6 --- /dev/null +++ b/.vitepress/theme/utils.ts @@ -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] +} \ No newline at end of file diff --git a/markdownlintignore b/markdownlintignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/markdownlintignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b1b391b --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..adc57e7 --- /dev/null +++ b/tsconfig.json @@ -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" + ] +} \ No newline at end of file diff --git a/website/account/index.md b/website/account/index.md new file mode 100644 index 0000000..3869483 --- /dev/null +++ b/website/account/index.md @@ -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 +--- \ No newline at end of file diff --git a/website/changelogs/index.md b/website/changelogs/index.md new file mode 100644 index 0000000..a9f433b --- /dev/null +++ b/website/changelogs/index.md @@ -0,0 +1,18 @@ +--- +title: Changelogs +description: Changelogs of all Kotatsu stable releases. +lastUpdated: false +editLink: false +prev: false +next: false +--- + + + +# Changelogs + +Changelogs of all Kotatsu stable releases, which are also available [on GitHub](https://github.com/KotatsuApp/Kotatsu/releases). + + \ No newline at end of file diff --git a/website/contribute/index.md b/website/contribute/index.md new file mode 100644 index 0000000..d1f3158 --- /dev/null +++ b/website/contribute/index.md @@ -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) \ No newline at end of file diff --git a/website/download/index.md b/website/download/index.md new file mode 100644 index 0000000..8144111 --- /dev/null +++ b/website/download/index.md @@ -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 +--- + + + +# Download + +The latest stable version of **Kotatsu** was released ****. + + + + \ No newline at end of file diff --git a/website/icons.ts b/website/icons.ts new file mode 100644 index 0000000..bed7c48 --- /dev/null +++ b/website/icons.ts @@ -0,0 +1,8 @@ +const telegram = ` + + +` + +export { + telegram, +} \ No newline at end of file diff --git a/website/index.md b/website/index.md new file mode 100644 index 0000000..4c9c991 --- /dev/null +++ b/website/index.md @@ -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: + title: Huge count of sources + details: Supports over a 500+ sources + - icon: + title: Supports tracking + details: Supports MyAnimeList, Anilist and Shikimori + - icon: + title: Synchronization + details: Easily sync series between your devices. +--- +
+
+
+ Home +
+
+ Explore +
+
+ Details +
+
+
+
+

kotatsu-dl

+

Cross-platform desktop application for downloading manga from various services. Allows you to search for manga and download it to the CBZ comic archives.

+ View on GitHub + +
+
+
+

+

Kotatsu parsers library

+

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.

+ Read more +
\ No newline at end of file diff --git a/website/manga/index.md b/website/manga/index.md new file mode 100644 index 0000000..f9f66a2 --- /dev/null +++ b/website/manga/index.md @@ -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 +--- + + + +# 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. + + \ No newline at end of file diff --git a/website/manuals/faq/explore/bookmarks.md b/website/manuals/faq/explore/bookmarks.md new file mode 100644 index 0000000..6d43a6b --- /dev/null +++ b/website/manuals/faq/explore/bookmarks.md @@ -0,0 +1,4 @@ +--- +title: Bookmarks +description: Frequently Asked Questions about Bookmarks. +--- \ No newline at end of file diff --git a/website/manuals/faq/explore/downloads.md b/website/manuals/faq/explore/downloads.md new file mode 100644 index 0000000..5dee847 --- /dev/null +++ b/website/manuals/faq/explore/downloads.md @@ -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