mirror of
https://github.com/fmhy/edit.git
synced 2025-10-13 08:11:07 +11:00
hello world, again
This commit is contained in:
commit
f479740dd1
133 changed files with 32895 additions and 0 deletions
131
docs/.vitepress/config.mts
Normal file
131
docs/.vitepress/config.mts
Normal file
|
@ -0,0 +1,131 @@
|
|||
import consola from 'consola'
|
||||
import { basename } from 'pathe'
|
||||
import UnoCSS from 'unocss/vite'
|
||||
import { defineConfig } from 'vitepress'
|
||||
import {
|
||||
commitRef,
|
||||
feedback,
|
||||
meta,
|
||||
search,
|
||||
sidebar,
|
||||
socialLinks
|
||||
} from './constants'
|
||||
import { generateFeed, generateImages, generateMeta } from './hooks'
|
||||
import { defs, emojiRender, movePlugin } from './markdown/emoji'
|
||||
import { toggleStarredPlugin } from './markdown/toggleStarred'
|
||||
import { transformer } from './transformer'
|
||||
|
||||
// @unocss-include
|
||||
|
||||
const baseUrl = process.env.GITHUB_ACTIONS ? '/FMHYedit' : '/'
|
||||
export default defineConfig({
|
||||
title: 'FMHY',
|
||||
description: meta.description,
|
||||
titleTemplate: ':title • freemediaheckyeah',
|
||||
lang: 'en-US',
|
||||
lastUpdated: false,
|
||||
cleanUrls: true,
|
||||
appearance: 'dark',
|
||||
base: baseUrl,
|
||||
srcExclude: ['README.md', 'single-page'],
|
||||
ignoreDeadLinks: true,
|
||||
sitemap: {
|
||||
hostname: meta.hostname
|
||||
},
|
||||
head: [
|
||||
['meta', { name: 'theme-color', content: '#7bc5e4' }],
|
||||
['meta', { name: 'og:type', content: 'website' }],
|
||||
['meta', { name: 'og:locale', content: 'en' }],
|
||||
['link', { rel: 'icon', href: '/test.png' }],
|
||||
// PWA
|
||||
['link', { rel: 'icon', href: '/test.png', type: 'image/svg+xml' }],
|
||||
['link', { rel: 'alternate icon', href: '/test.png' }],
|
||||
['link', { rel: 'mask-icon', href: '/test.png', color: '#7bc5e4' }],
|
||||
// prettier-ignore
|
||||
["meta", { name: "keywords", content: meta.keywords.join(" ") }],
|
||||
['link', { rel: 'apple-touch-icon', href: '/test.png', sizes: '192x192' }]
|
||||
],
|
||||
transformHead: async (context) => generateMeta(context, meta.hostname),
|
||||
buildEnd: async (context) => {
|
||||
generateImages(context)
|
||||
.then(() => generateFeed(context))
|
||||
.finally(() => consola.success('Success!'))
|
||||
},
|
||||
vite: {
|
||||
optimizeDeps: { exclude: ['workbox-window'] },
|
||||
plugins: [
|
||||
UnoCSS({
|
||||
configFile: '../unocss.config.ts'
|
||||
}),
|
||||
transformer(),
|
||||
{
|
||||
name: 'custom:adjust-order',
|
||||
configResolved(c) {
|
||||
movePlugin(
|
||||
c.plugins as any,
|
||||
'vitepress',
|
||||
'before',
|
||||
'unocss:transformers:pre'
|
||||
)
|
||||
movePlugin(
|
||||
c.plugins as any,
|
||||
'custom:transform-content',
|
||||
'before',
|
||||
'vitepress'
|
||||
)
|
||||
}
|
||||
}
|
||||
],
|
||||
build: {
|
||||
// Shut the fuck up
|
||||
chunkSizeWarningLimit: Number.POSITIVE_INFINITY
|
||||
}
|
||||
},
|
||||
markdown: {
|
||||
emoji: { defs },
|
||||
config(md) {
|
||||
md.use(emojiRender)
|
||||
md.use(toggleStarredPlugin)
|
||||
}
|
||||
},
|
||||
themeConfig: {
|
||||
search,
|
||||
footer: {
|
||||
message: `${feedback} (rev: ${commitRef})`,
|
||||
copyright: `© ${new Date().getFullYear()}, <a href="https://github.com/nbats">nbats</a>, <a href="https://github.com/taskylizard">taskylizard</a> and contributors.`
|
||||
},
|
||||
editLink: {
|
||||
pattern: 'https://github.com/fmhy/FMHYEdit/edit/main/docs/:path',
|
||||
text: '📝 Edit this page'
|
||||
},
|
||||
outline: 'deep',
|
||||
logo: '/fmhy.ico',
|
||||
nav: [
|
||||
{ text: '📚 Beginners Guide', link: '/beginners-guide' },
|
||||
{ text: '🔖 Glossary', link: 'https://rentry.org/The-Piracy-Glossary' },
|
||||
{ text: '📑 Guides', link: 'https://rentry.co/fmhy-guides' },
|
||||
{
|
||||
text: '💾 Backups',
|
||||
link: 'https://github.com/fmhy/FMHY/wiki/Backups'
|
||||
},
|
||||
{
|
||||
text: '🪅 Ecosystem',
|
||||
items: [
|
||||
{ text: '🌐 Search', link: '/posts/search' },
|
||||
{ text: '📰 Posts', link: '/posts' },
|
||||
{ text: '💬 Feedback', link: '/feedback' },
|
||||
{ text: '🏞 Wallpapers', link: '/other/wallpapers' },
|
||||
{ text: '📋 snowbin', link: 'https://pastes.fmhy.net' },
|
||||
{ text: '🔍 SearXNG', link: 'https://searx.fmhy.net/' },
|
||||
{ text: '🔍 Whoogle', link: 'https://whoogle.fmhy.net/' },
|
||||
{
|
||||
text: '🔗 Bookmarks',
|
||||
link: 'https://github.com/Rust1667/make-fmhy-bookmarks'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
sidebar,
|
||||
socialLinks
|
||||
}
|
||||
})
|
229
docs/.vitepress/constants.ts
Normal file
229
docs/.vitepress/constants.ts
Normal file
|
@ -0,0 +1,229 @@
|
|||
import type { DefaultTheme } from 'vitepress'
|
||||
import { transform, transformGuide } from './transformer'
|
||||
// @unocss-include
|
||||
|
||||
export const meta = {
|
||||
name: 'freemediaheckyeah',
|
||||
description: 'The largest collection of free stuff on the internet!',
|
||||
hostname: 'https://fmhy.net',
|
||||
keywords: ['stream', 'movies', 'gaming', 'reading', 'anime']
|
||||
}
|
||||
|
||||
export const commitRef =
|
||||
process.env.CF_PAGES && process.env.CF_PAGES_COMMIT_SHA
|
||||
? `<a href="https://github.com/fmhy/FMHYEdit/commit/${process.env.CF_PAGES_COMMIT_SHA
|
||||
}">${process.env.CF_PAGES_COMMIT_SHA.slice(0, 8)}</a>`
|
||||
: 'dev'
|
||||
|
||||
export const feedback = `<a href="/feedback" class="feedback-footer">Made with ❤</a>`
|
||||
|
||||
export const search: DefaultTheme.Config['search'] = {
|
||||
options: {
|
||||
_render(src, env, md) {
|
||||
let contents = src
|
||||
// I do this as env.frontmatter is not available until I call `md.render`
|
||||
if (contents.includes('Beginners Guide'))
|
||||
contents = transformGuide(contents)
|
||||
contents = transform(contents)
|
||||
const html = md.render(contents, env)
|
||||
return html
|
||||
},
|
||||
miniSearch: {
|
||||
options: {
|
||||
tokenize: (text) => text.split(/[\n\r #%*,=/:;?[\]{}()&]+/u), // simplified charset: removed [-_.@] and non-english chars (diacritics etc.)
|
||||
processTerm: (term, fieldName) => {
|
||||
// biome-ignore lint/style/noParameterAssign: h
|
||||
term = term
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^\.+/, '')
|
||||
.replace(/\.+$/, '')
|
||||
const stopWords = [
|
||||
'frontmatter',
|
||||
'$frontmatter.synopsis',
|
||||
'and',
|
||||
'about',
|
||||
'but',
|
||||
'now',
|
||||
'the',
|
||||
'with',
|
||||
'you'
|
||||
]
|
||||
if (term.length < 2 || stopWords.includes(term)) return false
|
||||
|
||||
if (fieldName === 'text') {
|
||||
const parts = term.split('.')
|
||||
if (parts.length > 1) {
|
||||
const newTerms = [term, ...parts]
|
||||
.filter((t) => t.length >= 2)
|
||||
.filter((t) => !stopWords.includes(t))
|
||||
return newTerms
|
||||
}
|
||||
}
|
||||
return term
|
||||
}
|
||||
},
|
||||
searchOptions: {
|
||||
combineWith: 'AND',
|
||||
fuzzy: true,
|
||||
// @ts-ignore
|
||||
boostDocument: (
|
||||
documentId,
|
||||
term,
|
||||
storedFields: Record<string, string | string[]>
|
||||
) => {
|
||||
const titles = (storedFields?.titles as string[])
|
||||
.filter((t) => Boolean(t))
|
||||
.map((t) => t.toLowerCase())
|
||||
// Downrank posts
|
||||
if (documentId.match(/\/posts/)) return -5
|
||||
// Downrank /other
|
||||
if (documentId.match(/\/other/)) return -5
|
||||
|
||||
// Uprate if term appears in titles. Add bonus for higher levels (i.e. lower index)
|
||||
const titleIndex =
|
||||
titles
|
||||
.map((t, i) => (t?.includes(term) ? i : -1))
|
||||
.find((i) => i >= 0) ?? -1
|
||||
if (titleIndex >= 0) return 10000 - titleIndex
|
||||
|
||||
return 1
|
||||
}
|
||||
}
|
||||
},
|
||||
detailedView: true
|
||||
},
|
||||
provider: 'local'
|
||||
}
|
||||
|
||||
export const socialLinks: DefaultTheme.SocialLink[] = [
|
||||
{ icon: 'github', link: 'https://github.com/fmhy/FMHYEdit' },
|
||||
{ icon: 'discord', link: 'https://discord.gg/Stz6y6NgNg' },
|
||||
{
|
||||
icon: 'reddit',
|
||||
link: 'https://reddit.com/r/FREEMEDIAHECKYEAH'
|
||||
}
|
||||
]
|
||||
|
||||
export const sidebar: DefaultTheme.Sidebar | DefaultTheme.NavItemWithLink[] = [
|
||||
{
|
||||
text: '<span class="i-twemoji:name-badge"></span> Adblocking / Privacy',
|
||||
link: '/adblockvpnguide'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:robot"></span> Artificial Intelligence',
|
||||
link: '/ai'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:television"></span> Movies / TV / Anime',
|
||||
link: '/videopiracyguide'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:musical-note"></span> Music / Podcasts / Radio',
|
||||
link: '/audiopiracyguide'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:video-game"></span> Gaming / Emulation',
|
||||
link: '/gamingpiracyguide'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:green-book"></span> Books / Comics / Manga',
|
||||
link: '/readingpiracyguide'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:floppy-disk"></span> Downloading',
|
||||
link: '/downloadpiracyguide'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:cyclone"></span> Torrenting',
|
||||
link: '/torrentpiracyguide'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:brain"></span> Educational',
|
||||
link: '/edupiracyguide'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:mobile-phone"></span> Android / iOS',
|
||||
link: '/android-iosguide'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:penguin"></span> Linux / MacOS',
|
||||
link: '/linuxguide'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:globe-showing-asia-australia"></span> Non-English',
|
||||
link: '/non-english'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:file-folder"></span> Miscellaneous',
|
||||
link: '/miscguide'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:wrench"></span> Tools',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{
|
||||
text: '<span class="i-twemoji:laptop"></span> System Tools',
|
||||
link: '/system-tools'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:card-file-box"></span> File Tools',
|
||||
link: '/file-tools'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:paperclip"></span> Internet Tools',
|
||||
link: '/internet-tools'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:left-speech-bubble"></span> Social Media Tools',
|
||||
link: '/social-media-tools'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:memo"></span> Text Tools',
|
||||
link: '/text-tools'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:alien-monster"></span> Gaming Tools',
|
||||
link: '/gaming-tools'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:camera"></span> Image Tools',
|
||||
link: '/img-tools'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:videocassette"></span> Video Tools',
|
||||
link: '/video-tools'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:speaker-high-volume"></span> Audio Tools',
|
||||
link: '/audiopiracyguide#audio-tools'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:red-apple"></span> Educational Tools',
|
||||
link: '/edupiracyguide#educational-tools'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:man-technologist"></span> Developer Tools',
|
||||
link: '/devtools'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:plus"></span> More',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: '<span class="i-twemoji:no-one-under-eighteen"></span> NSFW',
|
||||
link: '/nsfwpiracy'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:warning"></span> Unsafe Sites',
|
||||
link: '/unsafesites'
|
||||
},
|
||||
{
|
||||
text: '<span class="i-twemoji:package"></span> Storage',
|
||||
link: '/storage'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
BIN
docs/.vitepress/fonts/Inter-Bold.otf
Normal file
BIN
docs/.vitepress/fonts/Inter-Bold.otf
Normal file
Binary file not shown.
BIN
docs/.vitepress/fonts/Inter-Medium.otf
Normal file
BIN
docs/.vitepress/fonts/Inter-Medium.otf
Normal file
Binary file not shown.
BIN
docs/.vitepress/fonts/Inter-Regular.otf
Normal file
BIN
docs/.vitepress/fonts/Inter-Regular.otf
Normal file
Binary file not shown.
BIN
docs/.vitepress/fonts/Inter-SemiBold.otf
Normal file
BIN
docs/.vitepress/fonts/Inter-SemiBold.otf
Normal file
Binary file not shown.
43
docs/.vitepress/hooks/Template.vue
Normal file
43
docs/.vitepress/hooks/Template.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{ title: string; description?: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
tw="w-full h-full bg-black flex flex-col"
|
||||
style="
|
||||
background-image: linear-gradient(
|
||||
43deg,
|
||||
#b794f4 2%,
|
||||
#b18df2 7.5%,
|
||||
#ab87ef 13%,
|
||||
#9f7aea 24%,
|
||||
#8c6ee2 32%,
|
||||
#7864d8 40%,
|
||||
#4c51bf 56%,
|
||||
#4949ae 60.5%,
|
||||
#46419b 65%,
|
||||
#3c366b 74%,
|
||||
#2f315d 80.5%,
|
||||
#272d47 87%,
|
||||
#1a202c 100%
|
||||
);
|
||||
"
|
||||
>
|
||||
<span
|
||||
tw="p-10 w-full min-h-0 grow flex flex-col items-center justify-between"
|
||||
>
|
||||
<span tw="w-full flex justify-between items-center text-5xl font-medium">
|
||||
<span tw="flex items-center">
|
||||
<span tw="text-zinc-100 ml-2 mt-1 font-semibold">
|
||||
freemediaheckyeah
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span tw="w-full pr-56 flex flex-col items-start justify-end">
|
||||
<span style="color: #f3f4f6" tw="text-6xl font-bold" v-html="title" />
|
||||
<span style="color: #c0caf5" tw="mt-2 text-4xl" v-html="description" />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
8
docs/.vitepress/hooks/index.ts
Normal file
8
docs/.vitepress/hooks/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Barrel generated using @taskylizard/tasker.
|
||||
*/
|
||||
|
||||
export * from './meta'
|
||||
export * from './opengraph'
|
||||
export * from './rss'
|
||||
export * from './satoriConfig'
|
100
docs/.vitepress/hooks/meta.ts
Normal file
100
docs/.vitepress/hooks/meta.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import type { HeadConfig, TransformContext } from 'vitepress'
|
||||
|
||||
export function generateMeta(context: TransformContext, hostname: string) {
|
||||
const head: HeadConfig[] = []
|
||||
const { pageData } = context
|
||||
|
||||
const url = `${hostname}/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, '$2')}`
|
||||
|
||||
head.push(
|
||||
['link', { rel: 'canonical', href: url }],
|
||||
['meta', { property: 'og:url', content: url }],
|
||||
['meta', { name: 'twitter:url', content: url }],
|
||||
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||
['meta', { property: 'og:title', content: pageData.frontmatter.title }],
|
||||
['meta', { name: 'twitter:title', content: pageData.frontmatter.title }]
|
||||
)
|
||||
if (pageData.frontmatter.description) {
|
||||
head.push(
|
||||
[
|
||||
'meta',
|
||||
{
|
||||
property: 'og:description',
|
||||
content: pageData.frontmatter.description
|
||||
}
|
||||
],
|
||||
[
|
||||
'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`
|
||||
.replaceAll('//', '/')
|
||||
.replace(/^\//, '')
|
||||
|
||||
head.push(
|
||||
['meta', { property: 'og:image', content: `${hostname}/${imageUrl}` }],
|
||||
['meta', { property: 'og:image:width', content: '1200' }],
|
||||
['meta', { property: 'og:image:height', content: '628' }],
|
||||
['meta', { property: 'og:image:type', content: 'image/png' }],
|
||||
[
|
||||
'meta',
|
||||
{ property: 'og:image:alt', content: pageData.frontmatter.title }
|
||||
],
|
||||
['meta', { name: 'twitter:image', content: `${hostname}/${imageUrl}` }],
|
||||
['meta', { name: 'twitter:image:width', content: '1200' }],
|
||||
['meta', { name: 'twitter:image:height', content: '628' }],
|
||||
[
|
||||
'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
|
||||
}
|
96
docs/.vitepress/hooks/opengraph.ts
Normal file
96
docs/.vitepress/hooks/opengraph.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { createContentLoader } from 'vitepress'
|
||||
import type { ContentData, SiteConfig } from 'vitepress'
|
||||
import { type SatoriOptions, satoriVue } from 'x-satori/vue'
|
||||
import { renderAsync } from '@resvg/resvg-js'
|
||||
import consola from 'consola'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const __fonts = resolve(__dirname, '../fonts')
|
||||
|
||||
export async function generateImages(config: SiteConfig): Promise<void> {
|
||||
const pages = await createContentLoader('**/*.md', { excerpt: true }).load()
|
||||
const template = await readFile(resolve(__dirname, './Template.vue'), 'utf-8')
|
||||
|
||||
const fonts: SatoriOptions['fonts'] = [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: await readFile(resolve(__fonts, 'Inter-Regular.otf')),
|
||||
weight: 400,
|
||||
style: 'normal'
|
||||
},
|
||||
{
|
||||
name: 'Inter',
|
||||
data: await readFile(resolve(__fonts, 'Inter-Medium.otf')),
|
||||
weight: 500,
|
||||
style: 'normal'
|
||||
},
|
||||
{
|
||||
name: 'Inter',
|
||||
data: await readFile(resolve(__fonts, 'Inter-SemiBold.otf')),
|
||||
weight: 600,
|
||||
style: 'normal'
|
||||
},
|
||||
{
|
||||
name: 'Inter',
|
||||
data: await readFile(resolve(__fonts, 'Inter-Bold.otf')),
|
||||
weight: 700,
|
||||
style: 'normal'
|
||||
}
|
||||
]
|
||||
|
||||
for (const page of pages) {
|
||||
await generateImage({
|
||||
page,
|
||||
template,
|
||||
outDir: config.outDir,
|
||||
fonts
|
||||
})
|
||||
}
|
||||
return consola.info('Generated opengraph images.')
|
||||
}
|
||||
|
||||
interface GenerateImagesOptions {
|
||||
page: ContentData
|
||||
template: string
|
||||
outDir: string
|
||||
fonts: SatoriOptions['fonts']
|
||||
}
|
||||
|
||||
async function generateImage({
|
||||
page,
|
||||
template,
|
||||
outDir,
|
||||
fonts
|
||||
}: GenerateImagesOptions): Promise<void> {
|
||||
const { frontmatter, url } = page
|
||||
|
||||
const options: SatoriOptions = {
|
||||
width: 1200,
|
||||
height: 628,
|
||||
fonts,
|
||||
props: {
|
||||
title:
|
||||
frontmatter.layout === 'home'
|
||||
? (frontmatter.hero.name ?? frontmatter.title)
|
||||
: frontmatter.title,
|
||||
description:
|
||||
frontmatter.layout === 'home'
|
||||
? (frontmatter.hero.tagline ?? frontmatter.description)
|
||||
: frontmatter.description
|
||||
}
|
||||
}
|
||||
|
||||
const svg = await satoriVue(options, template)
|
||||
|
||||
const render = await renderAsync(svg)
|
||||
|
||||
const outputFolder = resolve(outDir, url.slice(1), '__og_image__')
|
||||
const outputFile = resolve(outputFolder, 'og.png')
|
||||
|
||||
await mkdir(outputFolder, { recursive: true })
|
||||
|
||||
await writeFile(outputFile, render.asPng())
|
||||
}
|
49
docs/.vitepress/hooks/rss.ts
Normal file
49
docs/.vitepress/hooks/rss.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import path from 'node:path'
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { Feed } from 'feed'
|
||||
import {
|
||||
createContentLoader,
|
||||
type ContentData,
|
||||
type SiteConfig
|
||||
} from 'vitepress'
|
||||
import consola from 'consola'
|
||||
import { meta } from '../constants'
|
||||
|
||||
export async function generateFeed(config: SiteConfig): Promise<void> {
|
||||
const feed: Feed = new Feed({
|
||||
id: meta.hostname,
|
||||
link: meta.hostname,
|
||||
title: 'FMHY blog',
|
||||
description: meta.description,
|
||||
language: 'en-US',
|
||||
image: 'https://github.com/fmhy.png',
|
||||
favicon: `${meta.hostname}/favicon.ico`,
|
||||
copyright: 'Copyright (c) 2023-present FMHY'
|
||||
})
|
||||
|
||||
const posts: ContentData[] = await createContentLoader('posts/*.md', {
|
||||
excerpt: true,
|
||||
render: true,
|
||||
transform: (rawData) => {
|
||||
return rawData.sort((a, b) => {
|
||||
return (
|
||||
Number(new Date(b.frontmatter.date)) -
|
||||
Number(new Date(a.frontmatter.date))
|
||||
)
|
||||
})
|
||||
}
|
||||
}).load()
|
||||
|
||||
for (const { url, frontmatter, html } of posts) {
|
||||
feed.addItem({
|
||||
title: frontmatter.title as string,
|
||||
id: `${meta.hostname}${url.replace(/\/\d+\./, '/')}`,
|
||||
link: `${meta.hostname}${url.replace(/\/\d+\./, '/')}`,
|
||||
date: frontmatter.date,
|
||||
content: html?.replaceAll('​', '')
|
||||
})
|
||||
}
|
||||
|
||||
writeFileSync(path.join(config.outDir, 'feed.rss'), feed.rss2())
|
||||
return consola.info('Generated rss feed.')
|
||||
}
|
47
docs/.vitepress/hooks/satoriConfig.ts
Normal file
47
docs/.vitepress/hooks/satoriConfig.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { SatoriOptions } from 'x-satori/vue'
|
||||
import { defineSatoriConfig } from 'x-satori/vue'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const __fonts = resolve(__dirname, '../fonts')
|
||||
|
||||
const fonts: SatoriOptions['fonts'] = [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: await readFile(resolve(__fonts, 'Inter-Regular.otf')),
|
||||
weight: 400,
|
||||
style: 'normal'
|
||||
},
|
||||
{
|
||||
name: 'Inter',
|
||||
data: await readFile(resolve(__fonts, 'Inter-Medium.otf')),
|
||||
weight: 500,
|
||||
style: 'normal'
|
||||
},
|
||||
{
|
||||
name: 'Inter',
|
||||
data: await readFile(resolve(__fonts, 'Inter-SemiBold.otf')),
|
||||
weight: 600,
|
||||
style: 'normal'
|
||||
},
|
||||
{
|
||||
name: 'Inter',
|
||||
data: await readFile(resolve(__fonts, 'Inter-Bold.otf')),
|
||||
weight: 700,
|
||||
style: 'normal'
|
||||
}
|
||||
]
|
||||
|
||||
export default defineSatoriConfig({
|
||||
width: 1200,
|
||||
height: 628,
|
||||
fonts,
|
||||
props: {
|
||||
title: 'Title',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.',
|
||||
dir: '/j'
|
||||
}
|
||||
})
|
29
docs/.vitepress/markdown/base64.ts
Normal file
29
docs/.vitepress/markdown/base64.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import type { MarkdownRenderer } from 'vitepress'
|
||||
|
||||
// FIXME: tasky: possibly write less horror jank?
|
||||
export function base64DecodePlugin(md: MarkdownRenderer) {
|
||||
const decode = (str: string): string =>
|
||||
Buffer.from(str, 'base64').toString('binary')
|
||||
// Save the original rule for backticks
|
||||
const defaultRender =
|
||||
md.renderer.rules.code_inline ||
|
||||
function (tokens, idx, options, _env, self) {
|
||||
return self.renderToken(tokens, idx, options)
|
||||
}
|
||||
|
||||
md.renderer.rules.code_inline = function (tokens, idx, options, env, self) {
|
||||
// @ts-expect-error shut the fuck up already I HATE THIS
|
||||
if (
|
||||
!env.frontmatter.title ||
|
||||
(env.frontmatter.title && !env.frontmatter.title === 'base64')
|
||||
) {
|
||||
return defaultRender(tokens, idx, options, env, self)
|
||||
}
|
||||
const token = tokens[idx]
|
||||
const content = token.content
|
||||
|
||||
return `<button class='base64' onclick="(function(btn){ const codeEl = btn.querySelector('code'); navigator.clipboard.writeText('${decode(
|
||||
content
|
||||
)}').then(() => { const originalText = codeEl.textContent; codeEl.textContent = 'Copied'; setTimeout(() => codeEl.textContent = originalText, 3000); }).catch(console.error); })(this)"><code>${content}</code></button>`
|
||||
}
|
||||
}
|
42
docs/.vitepress/markdown/emoji.ts
Normal file
42
docs/.vitepress/markdown/emoji.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { icons as twemoji } from '@iconify-json/twemoji'
|
||||
import type { MarkdownRenderer } from 'vitepress'
|
||||
|
||||
export const defs = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(twemoji.icons).map(([key]) => {
|
||||
return [key, '']
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export function emojiRender(md: MarkdownRenderer) {
|
||||
md.renderer.rules.emoji = (tokens, idx) => {
|
||||
if (tokens[idx].markup.startsWith('star')) {
|
||||
return `<span class="i-twemoji-${tokens[idx].markup} starred"></span>`
|
||||
}
|
||||
return `<span class="i-twemoji-${tokens[idx].markup}"></span>`
|
||||
}
|
||||
}
|
||||
|
||||
export function movePlugin(
|
||||
plugins: { name: string }[],
|
||||
pluginAName: string,
|
||||
order: 'before' | 'after',
|
||||
pluginBName: string
|
||||
) {
|
||||
const pluginBIndex = plugins.findIndex((p) => p.name === pluginBName)
|
||||
if (pluginBIndex === -1) return
|
||||
|
||||
const pluginAIndex = plugins.findIndex((p) => p.name === pluginAName)
|
||||
if (pluginAIndex === -1) return
|
||||
|
||||
if (order === 'before' && pluginAIndex > pluginBIndex) {
|
||||
const pluginA = plugins.splice(pluginAIndex, 1)[0]
|
||||
plugins.splice(pluginBIndex, 0, pluginA)
|
||||
}
|
||||
|
||||
if (order === 'after' && pluginAIndex < pluginBIndex) {
|
||||
const pluginA = plugins.splice(pluginAIndex, 1)[0]
|
||||
plugins.splice(pluginBIndex, 0, pluginA)
|
||||
}
|
||||
}
|
17
docs/.vitepress/markdown/toggleStarred.ts
Normal file
17
docs/.vitepress/markdown/toggleStarred.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { MarkdownRenderer } from 'vitepress'
|
||||
|
||||
const excluded = ['Beginners Guide']
|
||||
|
||||
export function toggleStarredPlugin(md: MarkdownRenderer) {
|
||||
md.renderer.rules.list_item_open = (tokens, index, options, env, self) => {
|
||||
const contentToken = tokens[index + 2]
|
||||
if (
|
||||
!excluded.includes(env.frontmatter.title) &&
|
||||
contentToken &&
|
||||
contentToken.content.startsWith(':star:')
|
||||
) {
|
||||
return `<li class="starred">`
|
||||
}
|
||||
return self.renderToken(tokens, index, options)
|
||||
}
|
||||
}
|
83
docs/.vitepress/theme/Layout.vue
Normal file
83
docs/.vitepress/theme/Layout.vue
Normal file
|
@ -0,0 +1,83 @@
|
|||
<script setup lang="ts">
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import { useData } from 'vitepress'
|
||||
import { nextTick, provide } from 'vue'
|
||||
import Sidebar from './components/SidebarCard.vue'
|
||||
import Announcement from './components/Announcement.vue'
|
||||
|
||||
const { isDark } = useData()
|
||||
|
||||
const enableTransitions = () =>
|
||||
'startViewTransition' in document &&
|
||||
window.matchMedia('(prefers-reduced-motion: no-preference)').matches
|
||||
|
||||
provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
|
||||
if (!enableTransitions()) {
|
||||
isDark.value = !isDark.value
|
||||
return
|
||||
}
|
||||
|
||||
const clipPath = [
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${Math.hypot(
|
||||
Math.max(x, innerWidth - x),
|
||||
Math.max(y, innerHeight - y)
|
||||
)}px at ${x}px ${y}px)`
|
||||
]
|
||||
|
||||
// @ts-expect-error
|
||||
await document.startViewTransition(async () => {
|
||||
isDark.value = !isDark.value
|
||||
await nextTick()
|
||||
}).ready
|
||||
|
||||
document.documentElement.animate(
|
||||
{ clipPath: isDark.value ? clipPath.reverse() : clipPath },
|
||||
{
|
||||
duration: 300,
|
||||
easing: 'ease-in',
|
||||
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const { Layout } = DefaultTheme
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<template #sidebar-nav-after>
|
||||
<Sidebar />
|
||||
</template>
|
||||
<template #home-hero-prelink>
|
||||
<Announcement />
|
||||
</template>
|
||||
<Content />
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
.dark::view-transition-new(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
::view-transition-new(root),
|
||||
.dark::view-transition-old(root) {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.VPSwitchAppearance {
|
||||
width: 22px !important;
|
||||
}
|
||||
|
||||
.VPSwitchAppearance .check {
|
||||
transform: none !important;
|
||||
}
|
||||
</style>
|
24
docs/.vitepress/theme/PostLayout.vue
Normal file
24
docs/.vitepress/theme/PostLayout.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
import Authors from './components/Authors.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
authors: string[]
|
||||
}>()
|
||||
|
||||
const formatDate = (raw: string): string => {
|
||||
const date = new Date(raw)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const { frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ frontmatter.title }}</h1>
|
||||
<div>{{ frontmatter.description }} • {{ formatDate(frontmatter.date) }}</div>
|
||||
<Authors :authors="props.authors" />
|
||||
</template>
|
67
docs/.vitepress/theme/Posts.vue
Normal file
67
docs/.vitepress/theme/Posts.vue
Normal file
|
@ -0,0 +1,67 @@
|
|||
<!-- eslint-disable vue/require-v-for-key -->
|
||||
<script setup lang="ts">
|
||||
import { data as posts } from './posts.data'
|
||||
|
||||
const formatDate = (raw: string): string => {
|
||||
const date = new Date(raw)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<section>
|
||||
<h1 class="flex items-center gap-2">Posts</h1>
|
||||
<p>Everything from Monthly Updates to fmhy updates.</p>
|
||||
|
||||
We also have a
|
||||
<a href="/feed.rss" target="_blank" title="RSS feed">
|
||||
<div class="i-carbon-rss vertical-top" />
|
||||
RSS feed.
|
||||
</a>
|
||||
</section>
|
||||
<template v-for="year in Object.keys(posts).reverse()" :key="year">
|
||||
<h2>{{ year }}</h2>
|
||||
<ul>
|
||||
<li v-for="post of posts[year]" :key="post.url">
|
||||
<article>
|
||||
<a :href="post.url" class="border-none">{{ post.title }}</a>
|
||||
-
|
||||
<dl class="m-0 inline">
|
||||
<dt class="sr-only">Published on</dt>
|
||||
<dd class="m-0 inline">
|
||||
<time :datetime="post.date" class="font-bold">
|
||||
{{ formatDate(post.date) }}
|
||||
</time>
|
||||
</dd>
|
||||
</dl>
|
||||
</article>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPBadge {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
margin-left: 2px;
|
||||
padding: 0 10px;
|
||||
line-height: 22px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transform: translateY(-2px);
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
padding-right: 10px;
|
||||
vertical-align: middle;
|
||||
color: var(--vp-badge-tip-text);
|
||||
background-color: transparent;
|
||||
border-color: var(--vp-custom-block-tip-outline);
|
||||
}
|
||||
</style>
|
15
docs/.vitepress/theme/components/Announcement.vue
Normal file
15
docs/.vitepress/theme/components/Announcement.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
v-if="frontmatter.hero.prelink"
|
||||
:href="frontmatter.hero.prelink.link"
|
||||
class="inline-flex items-center rounded-lg bg-[var(--vp-c-default-soft)] px-4 py-1 text-sm font-semibold mb-3"
|
||||
>
|
||||
{{ frontmatter.hero.prelink.title }}
|
||||
</a>
|
||||
</template>
|
49
docs/.vitepress/theme/components/Authors.vue
Normal file
49
docs/.vitepress/theme/components/Authors.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
authors: string[]
|
||||
}>()
|
||||
|
||||
interface Author {
|
||||
name: string
|
||||
github: string
|
||||
}
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: 'nbats',
|
||||
github: 'https://github.com/nbats'
|
||||
},
|
||||
{
|
||||
name: 'Kai',
|
||||
github: 'https://github.com/Kai-FMHY'
|
||||
},
|
||||
{
|
||||
name: 'taskylizard',
|
||||
github: 'https://github.com/taskylizard'
|
||||
},
|
||||
{
|
||||
name: 'zinklog',
|
||||
github: 'https://github.com/zinklog2'
|
||||
},
|
||||
{
|
||||
name: 'Q',
|
||||
github: 'https://github.com/qiracy'
|
||||
}
|
||||
] satisfies Author[]
|
||||
|
||||
const authors = computed(() =>
|
||||
data.filter((author) => props.authors.includes(author.name))
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-4 pt-2">
|
||||
<div v-for="(c, index) of authors" class="flex gap-2 items-center">
|
||||
<img :src="`${c.github}.png`" class="w-8 h-8 rounded-full" />
|
||||
<a :href="c.github">{{ c.name }}</a>
|
||||
<span v-if="index < authors.length - 1">•</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
16
docs/.vitepress/theme/components/CardField.vue
Normal file
16
docs/.vitepress/theme/components/CardField.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
icon: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center mb-[8px] g-[12px]">
|
||||
<span class="flex items-center">
|
||||
<div class="text-2xl" :class="icon" />
|
||||
<div class="ml-2 text-sm text-[var(--vp-c-text-2)]">
|
||||
<slot />
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
327
docs/.vitepress/theme/components/Feedback.vue
Normal file
327
docs/.vitepress/theme/components/Feedback.vue
Normal file
|
@ -0,0 +1,327 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import {
|
||||
TransitionRoot,
|
||||
TransitionChild,
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
Listbox,
|
||||
ListboxLabel,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption
|
||||
} from '@headlessui/vue'
|
||||
import { useRouter } from 'vitepress'
|
||||
import {
|
||||
type FeedbackType,
|
||||
getFeedbackOption,
|
||||
feedbackOptions
|
||||
} from '../../types/Feedback'
|
||||
|
||||
const loading = ref<boolean>(false)
|
||||
const error = ref<unknown>(null)
|
||||
const success = ref<boolean>(false)
|
||||
|
||||
const router = useRouter()
|
||||
const feedback = reactive<FeedbackType>({ message: '' })
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '💡 Suggestion',
|
||||
value: 'suggestion'
|
||||
},
|
||||
{
|
||||
label: '❤️ Appreciation',
|
||||
value: 'appreciate'
|
||||
},
|
||||
{ label: '🐞 Bug', value: 'bug' },
|
||||
{ label: '📂 Other', value: 'other' }
|
||||
]
|
||||
const selectedOption = ref(options[0])
|
||||
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
|
||||
const body: FeedbackType = {
|
||||
message: feedback.message,
|
||||
type: selectedOption.value.value,
|
||||
page: router.route.path
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.fmhy.net/feedback', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.error) {
|
||||
error.value = data.error
|
||||
return
|
||||
}
|
||||
if (data.status === 'ok') {
|
||||
success.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
error.value = error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
function closeModal() {
|
||||
isOpen.value = false
|
||||
}
|
||||
function openModal() {
|
||||
isOpen.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="p-[4px 8px] text-xl i-carbon:user-favorite-alt-filled"
|
||||
@click="openModal"
|
||||
/>
|
||||
|
||||
<Teleport to="body">
|
||||
<TransitionRoot appear :show="isOpen" as="template">
|
||||
<Dialog as="div" class="relative z-10" @close="closeModal">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div class="fixed inset-0 bg-black/25" />
|
||||
</TransitionChild>
|
||||
|
||||
<div class="fixed inset-0 overflow-y-auto">
|
||||
<div
|
||||
class="flex min-h-full items-center justify-center p-4 text-center"
|
||||
>
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0 scale-95"
|
||||
enter-to="opacity-100 scale-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100 scale-100"
|
||||
leave-to="opacity-0 scale-95"
|
||||
>
|
||||
<DialogPanel
|
||||
class="w-full max-w-md transform overflow-hidden rounded-2xl bg-bg p-6 text-left align-middle shadow-xl transition-all"
|
||||
>
|
||||
<DialogTitle
|
||||
as="h3"
|
||||
class="text-lg font-medium leading-6 text-text"
|
||||
>
|
||||
Feedback
|
||||
</DialogTitle>
|
||||
|
||||
<div class="mt-4 top-16 w-72" v-if="!success">
|
||||
<Listbox v-model="selectedOption">
|
||||
<div class="relative mt-1">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-lg bg-bg-alt text-text py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm"
|
||||
>
|
||||
<span class="block truncate">
|
||||
{{ selectedOption.label }}
|
||||
</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
<div
|
||||
class="i-heroicons-solid:chevron-up-down h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<transition
|
||||
leave-active-class="transition duration-100 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<ListboxOptions
|
||||
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bg-alt py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm"
|
||||
>
|
||||
<ListboxOption
|
||||
v-slot="{ active, selected }"
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option"
|
||||
as="template"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
active ? 'text-primary' : 'text-gray-500',
|
||||
'relative cursor-default select-none py-2 pl-10 pr-4'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-medium' : 'font-normal',
|
||||
'block truncate'
|
||||
]"
|
||||
>
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="selected"
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary"
|
||||
>
|
||||
<div
|
||||
class="i-heroicons-solid:check h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
|
||||
<div class="mt-2">
|
||||
<div>
|
||||
<label class="field-label">Message</label>
|
||||
<textarea
|
||||
v-model="feedback.message"
|
||||
class="mt-2 h-32"
|
||||
placeholder="What a lovely wiki!"
|
||||
rows="5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 mb-2">
|
||||
If you want a reply to your feedback, feel free to mention a
|
||||
contact in the message or join our
|
||||
<a
|
||||
class="text-primary font-semibold text-underline"
|
||||
href="https://discord.gg/Stz6y6NgNg"
|
||||
>
|
||||
Discord.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<details
|
||||
v-if="selectedOption.value === 'suggestion'"
|
||||
class="text-sm text-gray-400"
|
||||
>
|
||||
<summary class="mb-2">Submission Guidelines</summary>
|
||||
<strong>🕹️ Emulators</strong>
|
||||
<p>
|
||||
They're already on the
|
||||
<a
|
||||
class="text-primary font-bold text-underline"
|
||||
href="https://emulation.gametechwiki.com/index.php/Main_Page"
|
||||
>
|
||||
Game Tech Wiki.
|
||||
</a>
|
||||
</p>
|
||||
<strong>🔻 Leeches</strong>
|
||||
<p>
|
||||
They're already on the
|
||||
<a
|
||||
class="text-primary font-bold text-underline"
|
||||
href="https://filehostlist.miraheze.org/wiki/Free_Premium_Leeches"
|
||||
>
|
||||
File Hosting Wiki.
|
||||
</a>
|
||||
</p>
|
||||
<strong>🐧 Distros</strong>
|
||||
<p>
|
||||
They're already on
|
||||
<a
|
||||
class="text-primary font-bold text-underline"
|
||||
href="https://distrowatch.com/"
|
||||
>
|
||||
DistroWatch.
|
||||
</a>
|
||||
</p>
|
||||
<strong>🎲 Mining / Betting Sites</strong>
|
||||
<p>
|
||||
Don't post anything related to betting, mining, BINs, CCs,
|
||||
etc.
|
||||
</p>
|
||||
<strong>🎮 Multiplayer Game Hacks</strong>
|
||||
<p>
|
||||
Don't post any hacks/exploits that give unfair advantages
|
||||
in multiplayer games.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<div class="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex justify-center rounded-md border border-transparent bg-blue-500 px-4 py-2 text-sm font-medium text-blue-100 hover:bg-blue-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:bg-blue-400"
|
||||
:disabled="
|
||||
feedback.message.length < 5 ||
|
||||
feedback.message.length > 1000
|
||||
"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 inline-flex justify-center rounded-md border border-transparent bg-red-500 px-4 py-2 text-sm font-medium text-red-100 hover:bg-red-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
|
||||
@click="closeModal()"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="error">
|
||||
<div class="text-sm font-medium leading-6 text-text">
|
||||
Error!
|
||||
</div>
|
||||
<details>{{ error }}</details>
|
||||
</div>
|
||||
<div v-else>
|
||||
<TransitionRoot
|
||||
enter="transition-opacity duration-75"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
>
|
||||
Thanks!
|
||||
</TransitionRoot>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
textarea,
|
||||
input {
|
||||
font-family: var(--vp-font-family-base);
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--vp-c-text-2) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
47
docs/.vitepress/theme/components/InputField.vue
Normal file
47
docs/.vitepress/theme/components/InputField.vue
Normal file
|
@ -0,0 +1,47 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
id: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-field">
|
||||
<div v-if="label" class="input-label">
|
||||
<label :for="id" class="pane-label">
|
||||
{{ label }}
|
||||
</label>
|
||||
<div class="display-value">
|
||||
<slot name="display" />
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pane-label {
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vt-c-text-1);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-field:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.display-value {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
60
docs/.vitepress/theme/components/SidebarCard.vue
Normal file
60
docs/.vitepress/theme/components/SidebarCard.vue
Normal file
|
@ -0,0 +1,60 @@
|
|||
<script setup lang="ts">
|
||||
import Field from './CardField.vue'
|
||||
import Feedback from './Feedback.vue'
|
||||
import InputField from './InputField.vue'
|
||||
import ToggleStarred from './ToggleStarred.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">Emoji Legend</div>
|
||||
</div>
|
||||
<Field icon="i-twemoji-globe-with-meridians">Indexes</Field>
|
||||
<Field icon="i-twemoji-repeat-button">Storage Links</Field>
|
||||
<Field icon="i-twemoji-star">Recommendations</Field>
|
||||
<div class="card-header">
|
||||
<div class="card-title">Options</div>
|
||||
</div>
|
||||
<InputField id="feedback" label="Feedback">
|
||||
<template #display>
|
||||
<Feedback />
|
||||
</template>
|
||||
</InputField>
|
||||
<InputField id="toggle-starred" label="Toggle Starred">
|
||||
<template #display>
|
||||
<ToggleStarred />
|
||||
</template>
|
||||
</InputField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 32px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px 24px 24px;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
border: 1px solid transparent;
|
||||
transition: border-color 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
</style>
|
52
docs/.vitepress/theme/components/Switch.vue
Normal file
52
docs/.vitepress/theme/components/Switch.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Switch } from '@headlessui/vue'
|
||||
|
||||
const enabled = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch v-model="enabled" class="switch" :class="{ enabled }">
|
||||
<span class="thumb" />
|
||||
</Switch>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.switch {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--vp-input-border-color);
|
||||
background-color: var(--vp-input-switch-bg-color);
|
||||
transition:
|
||||
border-color 0.25s,
|
||||
background-color 0.4s ease;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.switch.enabled {
|
||||
background-color: var(--vp-c-brand);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.switch:hover {
|
||||
border-color: var(--vp-input-hover-border-color);
|
||||
}
|
||||
|
||||
.thumb {
|
||||
display: inline-block;
|
||||
background-color: #fff;
|
||||
transition: transform 0.25s;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
|
||||
.switch.enabled .thumb {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
</style>
|
16
docs/.vitepress/theme/components/ToggleStarred.vue
Normal file
16
docs/.vitepress/theme/components/ToggleStarred.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
import Switch from './Switch.vue'
|
||||
|
||||
const toggleStarred = () =>
|
||||
document.documentElement.classList.toggle('starred-only')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch @click="toggleStarred()" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.starred-only li:not(.starred) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
29
docs/.vitepress/theme/components/WallpaperCard.vue
Normal file
29
docs/.vitepress/theme/components/WallpaperCard.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string
|
||||
description: string
|
||||
mobile: string
|
||||
desktop: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-lg border bg-bg-elv text-text shadow-sm w-full max-w-md">
|
||||
<div class="flex flex-col space-y-1.5 p-6 pb-4">
|
||||
<h4 class="whitespace-nowrap text-2xl font-semibold leading-none tracking-tight">
|
||||
{{ title }}
|
||||
</h4>
|
||||
<p class="text-sm text-text-2 font-medium leading-none tracking-tight">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<img :src="desktop" :alt="title" class="rounded-md object-cover aspect-[3/2]" width="1080" height="1920" />
|
||||
</div>
|
||||
<div class="items-center p-6 flex gap-2">
|
||||
<a :href="mobile" target="_blank">Mobile</a>
|
||||
<span class="text-text-2">•</span>
|
||||
<a :href="desktop" target="_blank">Desktop</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
23
docs/.vitepress/theme/composables/nprogress.ts
Normal file
23
docs/.vitepress/theme/composables/nprogress.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import nprogress, { type NProgress } from 'nprogress'
|
||||
import type { EnhanceAppContext } from 'vitepress'
|
||||
|
||||
export function loadProgress(router: EnhanceAppContext['router']): NProgress {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
setTimeout(() => {
|
||||
nprogress.configure({ showSpinner: false })
|
||||
|
||||
const cacheBeforeRouteChange = router.onBeforeRouteChange
|
||||
const cacheAfterRouteChange = router.onAfterRouteChanged
|
||||
router.onBeforeRouteChange = (to) => {
|
||||
nprogress.start()
|
||||
cacheBeforeRouteChange?.(to)
|
||||
}
|
||||
router.onAfterRouteChanged = (to) => {
|
||||
nprogress.done()
|
||||
cacheAfterRouteChange?.(to)
|
||||
}
|
||||
})
|
||||
|
||||
return nprogress
|
||||
}
|
16
docs/.vitepress/theme/index.ts
Normal file
16
docs/.vitepress/theme/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import type { Theme } from 'vitepress'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import Layout from './Layout.vue'
|
||||
import Post from './PostLayout.vue'
|
||||
import { loadProgress } from './composables/nprogress'
|
||||
import './style.css'
|
||||
import 'uno.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout,
|
||||
enhanceApp({ router, app }) {
|
||||
app.component('Post', Post)
|
||||
loadProgress(router)
|
||||
}
|
||||
} satisfies Theme
|
30
docs/.vitepress/theme/posts.data.ts
Normal file
30
docs/.vitepress/theme/posts.data.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { createContentLoader, type ContentData } from 'vitepress'
|
||||
import { groupBy } from '../utils'
|
||||
|
||||
interface Post {
|
||||
title: string
|
||||
url: string
|
||||
date: string
|
||||
}
|
||||
|
||||
type Dictionary = ReturnType<typeof transformRawPosts>
|
||||
|
||||
declare const data: Dictionary
|
||||
export { data }
|
||||
|
||||
function transformRawPosts(rawPosts: ContentData[]): Record<string, Post[]> {
|
||||
const posts: Post[] = rawPosts
|
||||
.map(({ url, frontmatter }) => ({
|
||||
title: frontmatter.title,
|
||||
url,
|
||||
date: (frontmatter.date as Date).toISOString().slice(0, 10)
|
||||
}))
|
||||
.sort((a, b) => b.date.localeCompare(a.date))
|
||||
|
||||
return groupBy(posts, (post) => post.date.slice(0, 4))
|
||||
}
|
||||
|
||||
export default createContentLoader('posts/*.md', {
|
||||
includeSrc: true,
|
||||
transform: (raw) => transformRawPosts(raw)
|
||||
})
|
279
docs/.vitepress/theme/style.css
Normal file
279
docs/.vitepress/theme/style.css
Normal file
|
@ -0,0 +1,279 @@
|
|||
:root {
|
||||
/* Colors: Brand */
|
||||
--vp-c-brand-1: theme('colors.swarm.400');
|
||||
--vp-c-brand-2: theme('colors.swarm.500');
|
||||
--vp-c-brand-3: theme('colors.swarm.700');
|
||||
--vp-c-brand-soft: theme('colors.swarm.300');
|
||||
|
||||
/* Colors: Button */
|
||||
--vp-button-brand-bg: var(--vp-c-brand-1);
|
||||
--vp-button-brand-border: var(--vp-c-brand-soft);
|
||||
--vp-button-brand-text: rgba(42, 40, 47);
|
||||
--vp-button-brand-hover-border: var(--vp-c-brand-soft);
|
||||
--vp-button-brand-hover-text: rgba(42, 40, 47);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-soft);
|
||||
--vp-button-brand-active-border: var(--vp-c-brand-soft);
|
||||
--vp-button-brand-active-text: rgba(42, 40, 47);
|
||||
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
|
||||
--vp-button-alt-bg: #484848;
|
||||
--vp-button-alt-text: #f0eeee;
|
||||
--vp-button-alt-hover-bg: #484848;
|
||||
--vp-button-alt-hover-text: #f0eeee;
|
||||
|
||||
--vp-c-bg-elv: rgba(255, 255, 255, 0.7);
|
||||
--vp-c-bg-mark: rgb(232, 232, 232);
|
||||
|
||||
/* Colors: Custom Block */
|
||||
--vp-custom-block-info-bg: theme('colors.swarm.100');
|
||||
--vp-custom-block-info-border: theme('colors.swarm.800');
|
||||
--vp-custom-block-info-text: theme('colors.swarm.800');
|
||||
--vp-custom-block-info-text-deep: theme('colors.swarm.900');
|
||||
--vp-custom-block-tip-bg: theme('colors.meadow.100');
|
||||
--vp-custom-block-tip-border: theme('colors.meadow.800');
|
||||
--vp-custom-block-tip-text: theme('colors.meadow.800');
|
||||
--vp-custom-block-tip-text-deep: theme('colors.meadow.900');
|
||||
--vp-custom-block-warning-bg: rgba(253, 224, 71, 0.1);
|
||||
--vp-custom-block-warning-border: rgba(202, 138, 4, 1);
|
||||
--vp-custom-block-warning-text: rgba(234, 179, 8, 1);
|
||||
--vp-custom-block-warning-text-deep: rgba(250, 204, 21, 1);
|
||||
--vp-custom-block-danger-bg: theme('colors.carnation.100');
|
||||
--vp-custom-block-danger-border: theme('colors.carnation.800');
|
||||
--vp-custom-block-danger-text: theme('colors.carnation.800');
|
||||
--vp-custom-block-danger-text-deep: theme('colors.carnation.900');
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Colors: Background */
|
||||
--vp-c-bg: rgb(26, 26, 26);
|
||||
--vp-c-bg-alt: rgb(23, 23, 23);
|
||||
--vp-c-bg-elv: rgba(23, 23, 23, 0.8);
|
||||
|
||||
/* Colors: Custom Block */
|
||||
--vp-custom-block-info-bg: theme('colors.swarm.950');
|
||||
--vp-custom-block-info-border: theme('colors.swarm.600');
|
||||
--vp-custom-block-info-text: theme('colors.swarm.500');
|
||||
--vp-custom-block-info-text-deep: theme('colors.swarm.600');
|
||||
--vp-custom-block-tip-bg: theme('colors.meadow.950');
|
||||
--vp-custom-block-tip-border: theme('colors.meadow.600');
|
||||
--vp-custom-block-tip-text: theme('colors.meadow.500');
|
||||
--vp-custom-block-tip-text-deep: theme('colors.meadow.600');
|
||||
--vp-custom-block-warning-bg: rgba(253, 224, 71, 0.1);
|
||||
--vp-custom-block-warning-border: rgba(202, 138, 4, 1);
|
||||
--vp-custom-block-warning-text: rgba(234, 179, 8, 1);
|
||||
--vp-custom-block-warning-text-deep: rgba(250, 204, 21, 1);
|
||||
--vp-custom-block-danger-bg: theme('colors.carnation.950');
|
||||
--vp-custom-block-danger-border: theme('colors.carnation.600');
|
||||
--vp-custom-block-danger-text: theme('colors.carnation.400');
|
||||
--vp-custom-block-danger-text-deep: theme('colors.carnation.500');
|
||||
}
|
||||
|
||||
.vp-doc a {
|
||||
color: var(--vp-c-brand-1);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
text-decoration-style: solid;
|
||||
text-decoration-color: transparent;
|
||||
-webkit-text-decoration-color: transparent;
|
||||
transition: text-decoration-color 0.25s;
|
||||
}
|
||||
|
||||
.vp-doc a:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
text-decoration-color: var(--vp-c-brand-1);
|
||||
-webkit-text-decoration-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.vp-doc .custom-block a {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
text-decoration-style: solid;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #0f2c47;
|
||||
}
|
||||
|
||||
.VPFooter a {
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-underline-offset: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.VPFooter a:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-underline-offset: 5px;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
.VPSidebar::-webkit-scrollbar {
|
||||
block-size: 4px;
|
||||
border-end-end-radius: 14px;
|
||||
border-start-end-radius: 14px;
|
||||
inline-size: 4px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
*/
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
#c4b5fd 30%,
|
||||
#7bc5e4
|
||||
);
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
-45deg,
|
||||
#c4b5fd 50%,
|
||||
#47caff 50%
|
||||
);
|
||||
--vp-home-hero-image-filter: blur(44px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(68px);
|
||||
}
|
||||
}
|
||||
|
||||
.base64 {
|
||||
min-width: 100%;
|
||||
width: 0px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Make clicks pass-through */
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: var(--vp-c-brand-1);
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/* Fancy blur effect */
|
||||
#nprogress .peg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
box-shadow:
|
||||
0 0 10px var(--vp-c-brand-1),
|
||||
0 0 5px var(--vp-c-brand-1);
|
||||
opacity: 1;
|
||||
|
||||
-webkit-transform: rotate(3deg) translate(0px, -4px);
|
||||
-ms-transform: rotate(3deg) translate(0px, -4px);
|
||||
transform: rotate(3deg) translate(0px, -4px);
|
||||
}
|
||||
|
||||
#nprogress .spinner {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
#nprogress .spinner-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
|
||||
border: solid 2px transparent;
|
||||
border-top-color: var(--vp-c-brand);
|
||||
border-left-color: var(--vp-c-brand);
|
||||
border-radius: 50%;
|
||||
|
||||
-webkit-animation: nprogress-spinner 400ms linear infinite;
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent #nprogress .spinner,
|
||||
.nprogress-custom-parent #nprogress .bar {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@-webkit-keyframes nprogress-spinner {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes nprogress-spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#VPContent strong > a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info.custom-block {
|
||||
--icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWluZm8iPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0iTTEyIDE2di00Ii8+PHBhdGggZD0iTTEyIDhoLjAxIi8+PC9zdmc+');
|
||||
}
|
||||
|
||||
.note.custom-block {
|
||||
--icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWluZm8iPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0iTTEyIDE2di00Ii8+PHBhdGggZD0iTTEyIDhoLjAxIi8+PC9zdmc+');
|
||||
}
|
||||
|
||||
.tip.custom-block {
|
||||
--icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWxpZ2h0YnVsYiI+PHBhdGggZD0iTTE1IDE0Yy4yLTEgLjctMS43IDEuNS0yLjUgMS0uOSAxLjUtMi4yIDEuNS0zLjVBNiA2IDAgMCAwIDYgOGMwIDEgLjIgMi4yIDEuNSAzLjUuNy43IDEuMyAxLjUgMS41IDIuNSIvPjxwYXRoIGQ9Ik05IDE4aDYiLz48cGF0aCBkPSJNMTAgMjJoNCIvPjwvc3ZnPg==');
|
||||
}
|
||||
|
||||
.warning.custom-block {
|
||||
--icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWFsZXJ0LXRyaWFuZ2xlIj48cGF0aCBkPSJtMjEuNzMgMTgtOC0xNGEyIDIgMCAwIDAtMy40OCAwbC04IDE0QTIgMiAwIDAgMCA0IDIxaDE2YTIgMiAwIDAgMCAxLjczLTNaIi8+PHBhdGggZD0iTTEyIDl2NCIvPjxwYXRoIGQ9Ik0xMiAxN2guMDEiLz48L3N2Zz4=');
|
||||
}
|
||||
|
||||
.danger.custom-block {
|
||||
--icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNrdWxsIj48Y2lyY2xlIGN4PSI5IiBjeT0iMTIiIHI9IjEiLz48Y2lyY2xlIGN4PSIxNSIgY3k9IjEyIiByPSIxIi8+PHBhdGggZD0iTTggMjB2Mmg4di0yIi8+PHBhdGggZD0ibTEyLjUgMTctLjUtMS0uNSAxaDF6Ii8+PHBhdGggZD0iTTE2IDIwYTIgMiAwIDAgMCAxLjU2LTMuMjUgOCA4IDAgMSAwLTExLjEyIDBBMiAyIDAgMCAwIDggMjAiLz48L3N2Zz4=');
|
||||
}
|
||||
|
||||
.custom-block-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.custom-block-title::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
-webkit-mask: var(--icon) no-repeat;
|
||||
mask: var(--icon) no-repeat;
|
||||
-webkit-mask-size: 100% 100%;
|
||||
mask-size: 100% 100%;
|
||||
background-color: currentColor;
|
||||
color: inherit;
|
||||
}
|
403
docs/.vitepress/transformer.ts
Normal file
403
docs/.vitepress/transformer.ts
Normal file
|
@ -0,0 +1,403 @@
|
|||
import { basename } from 'pathe'
|
||||
import type { Plugin } from 'vitepress'
|
||||
|
||||
interface Header {
|
||||
[key: string]: { title: string; description: string }
|
||||
}
|
||||
|
||||
const headers: Header = {
|
||||
'adblockvpnguide.md': {
|
||||
title: 'Adblocking / Privacy',
|
||||
description: "Adblocking, Privacy, VPN's, Proxies, Antivirus"
|
||||
},
|
||||
'ai.md': {
|
||||
title: 'Artificial Intelligence',
|
||||
description: 'Chat Bots, Text Generators, Image Generators, ChatGPT Tools'
|
||||
},
|
||||
'android-iosguide.md': {
|
||||
title: 'Android / iOS',
|
||||
description: 'Apps, Jailbreaking, Android Emulators'
|
||||
},
|
||||
'audiopiracyguide.md': {
|
||||
title: 'Music / Podcasts / Radio',
|
||||
description: 'Stream Audio, Download Audio, Torrent Audio'
|
||||
},
|
||||
'beginners-guide.md': {
|
||||
title: 'Beginners Guide',
|
||||
description: 'A Guide for Beginners to Piracy'
|
||||
},
|
||||
'downloadpiracyguide.md': {
|
||||
title: 'Downloading',
|
||||
description: 'Download Sites, Software Sites, Open Directories'
|
||||
},
|
||||
'edupiracyguide.md': {
|
||||
title: 'Educational',
|
||||
description: 'Courses, Documentaries, Learning Resources'
|
||||
},
|
||||
'gamingpiracyguide.md': {
|
||||
title: 'Gaming / Emulation',
|
||||
description: 'Download Games, ROMs, Gaming Tools'
|
||||
},
|
||||
'linuxguide.md': {
|
||||
title: 'Linux / MacOS',
|
||||
description: 'Apps, Software Sites, Gaming'
|
||||
},
|
||||
'miscguide.md': {
|
||||
title: 'Miscellaneous',
|
||||
description: 'Extensions, Indexes, News, Health, Food, Fun'
|
||||
},
|
||||
'nsfwpiracy.md': {
|
||||
title: 'NSFW',
|
||||
description: 'NSFW Indexes, Streaming, Downloading'
|
||||
},
|
||||
'non-english.md': {
|
||||
title: 'Non-English',
|
||||
description: 'International Piracy Sites'
|
||||
},
|
||||
'readingpiracyguide.md': {
|
||||
title: 'Books / Comics / Manga',
|
||||
description: 'Books, Comics, Magazines, Newspapers'
|
||||
},
|
||||
'gaming-tools.md': {
|
||||
title: 'Gaming Tools',
|
||||
description: 'Gaming Optimization, Game Launchers, Multiplayer'
|
||||
},
|
||||
'devtools.md': {
|
||||
title: 'Developer Tools',
|
||||
description: 'Git, Hosting, App Dev, Software Dev'
|
||||
},
|
||||
'img-tools.md': {
|
||||
title: 'Image Tools',
|
||||
description: 'Image Editors, Generators, Compress'
|
||||
},
|
||||
'audio-tools.md': {
|
||||
title: 'Audio Tools',
|
||||
description: 'Audio Players, Audio Editors, Audio Downloaders'
|
||||
},
|
||||
'system-tools.md': {
|
||||
title: 'System Tools',
|
||||
description: 'System Tools, Hardware Tools, Windows ISOs, Customization'
|
||||
},
|
||||
'file-tools.md': {
|
||||
title: 'File Tools',
|
||||
description: 'Download Managers, File Hosting, File Archivers'
|
||||
},
|
||||
'video-tools.md': {
|
||||
title: 'Video Tools',
|
||||
description: 'Video Players, Video Editors, Live Streaming, Animation'
|
||||
},
|
||||
'text-tools.md': {
|
||||
title: 'Text Tools',
|
||||
description: 'Text Editors, Pastebins, Fonts, Translators'
|
||||
},
|
||||
'internet-tools.md': {
|
||||
title: 'Internet Tools',
|
||||
description: 'Browsers, Extensions, Search Engines'
|
||||
},
|
||||
'social-media-tools.md': {
|
||||
title: 'Social Media Tools',
|
||||
description: 'Discord Tools, Reddit Tools, YouTube Tools'
|
||||
},
|
||||
'storage.md': {
|
||||
title: 'Storage',
|
||||
description: 'Sections too big to fit on main pages'
|
||||
},
|
||||
'torrentpiracyguide.md': {
|
||||
title: 'Torrenting',
|
||||
description: 'Torrent Clients, Torrent Sites, Trackers'
|
||||
},
|
||||
'videopiracyguide.md': {
|
||||
title: 'Movies / TV / Anime',
|
||||
description: 'Stream Videos, Download Videos, Torrent Videos'
|
||||
},
|
||||
'base64.md': {
|
||||
title: 'Base64',
|
||||
description: 'Base64 storage'
|
||||
},
|
||||
'unsafesites.md': {
|
||||
title: 'Unsafe Sites',
|
||||
description: 'Unsafe/harmful sites to avoid.'
|
||||
}
|
||||
}
|
||||
|
||||
const excluded = ['readme.md', 'single-page', 'feedback.md', 'index.md']
|
||||
|
||||
export function transformer(): Plugin {
|
||||
return {
|
||||
name: 'custom:transform-content',
|
||||
enforce: 'pre',
|
||||
transform(code, id) {
|
||||
const _id = basename(id)
|
||||
|
||||
if (
|
||||
id.endsWith('.md') &&
|
||||
!excluded.includes(_id) &&
|
||||
// check if it's a post
|
||||
!id.includes('posts') &&
|
||||
!id.includes('other')
|
||||
) {
|
||||
const header = getHeader(_id)
|
||||
const contents = transform(code)
|
||||
|
||||
if (_id === 'beginners-guide.md') {
|
||||
const _contents = transformGuide(contents)
|
||||
return header + _contents
|
||||
}
|
||||
if (_id === 'storage.md') return header + contents
|
||||
|
||||
return header + transformLinks(contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getHeader(id: string) {
|
||||
const title =
|
||||
'<div class="space-y-2 not-prose"><h1 class="text-4xl font-extrabold tracking-tight text-primary underline lg:text-5xl lg:leading-[3.5rem]">'
|
||||
|
||||
const description = '<p class="text-black dark:text-text-2">'
|
||||
|
||||
const data = headers[id]
|
||||
let header = '---\n'
|
||||
header += `title: "${data.title}"\n`
|
||||
header += `description: ${data.description}\n`
|
||||
header += '---\n'
|
||||
header += `${title}${data.title}</h1>\n`
|
||||
header += `${description}${data.description}</p></div>\n\n`
|
||||
return header
|
||||
}
|
||||
|
||||
export function transformGuide(text: string): string {
|
||||
const _text = text
|
||||
.replace(/\[TOC\]\n/gm, '')
|
||||
.replace(/\*\*Table of Contents\*\*\n\[TOC2\]\n/gm, '')
|
||||
.replace(/# -> \*\*\*Beginners Guide to Piracy\*\*\* <-\n/gm, '')
|
||||
.replace(/!!!note\s(.+?)\n/gm, '\n:::info\n$1\n:::\n')
|
||||
.replace(/!!!info\s(.+?)\n/gm, '\n:::info\n$1\n:::\n')
|
||||
.replace(/!!!warning\s(.+?)\n/gm, ':::warning\n$1\n:::\n')
|
||||
.replace(/>\s(.+?)\n/gm, '> $1\n\n')
|
||||
.replace(/\*\*\[\^ Back to Top\]\(#beginners-guide-to-piracy\)\*\*/gm, '')
|
||||
.replace(/!!!\s(.+?)\n/gm, ':::info\n$1\n:::\n')
|
||||
.replace(/\n\*\*\[/gm, '\n* **[')
|
||||
.replace(/>(.*)\n\n(.*)/gm, ':::details $1\n$2\n:::')
|
||||
return _text
|
||||
}
|
||||
|
||||
function replaceUnderscore(text: string): string {
|
||||
const pattern = /\/#[\w\-]+(?:_[\w]+)*/g
|
||||
const matches = text.match(pattern) || []
|
||||
let _text = text
|
||||
for (const match of matches) {
|
||||
const replacement = match.replace(/_/g, '-')
|
||||
_text = _text.replace(match, replacement)
|
||||
}
|
||||
return _text
|
||||
}
|
||||
|
||||
export function transform(text: string): string {
|
||||
let _text = text
|
||||
// Transform reddit index links
|
||||
.replace(
|
||||
/\*\*\[◄◄ Back to Wiki Index\]\(https:\/\/www\.reddit\.com\/r\/FREEMEDIAHECKYEAH\/wiki\/index\)\*\*\n/gm,
|
||||
''
|
||||
)
|
||||
.replace(
|
||||
/\*\*\[◄◄ Back to Wiki Index\]\(https:\/\/www\.reddit\.com\/r\/FREEMEDIAHECKYEAH\/wiki\/tools-index\)\*\*\n/gm,
|
||||
''
|
||||
)
|
||||
.replace(
|
||||
/\*\*\[Table of Contents\]\(https?:\/\/.*?ibb\.co.*\)\*\* - For mobile users\n/gm,
|
||||
''
|
||||
)
|
||||
// Remove extra lines
|
||||
.replace(/\*\*\*\n\*\*\*\n\*\*\*\n\*\*\*\n\n\n\*\*\*\n\*\*\*\n\n/gm, '')
|
||||
.replace(/\*\*\*\n\*\*\*\n\*\*\*\n\*\*\*\n\n\n\*\*\*\n\*\*\* \n\n/gm, '')
|
||||
.replace(/\*\*\*\n\*\*\*\n\*\*\*\n\n\n\*\*\*\n\*\*\*\n\n/gm, '')
|
||||
.replace(/\*\*\*\n\*\*\*\n\*\*\*\n\*\*\*\n\n\n\*\*\*\n\n/gm, '')
|
||||
.replace(/\*\*\*\n\*\*\*\n\n\n\*\*\*\n\n/gm, '')
|
||||
// Transform reddit links
|
||||
.replace(/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/ai/g, '/ai')
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/adblock-vpn-privacy/g,
|
||||
'/adblockvpnguide'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/android/g,
|
||||
'/android-iosguide'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/games/g,
|
||||
'/gamingpiracyguide'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/reading/g,
|
||||
'/readingpiracyguide'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/download/g,
|
||||
'/downloadpiracyguide'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/torrent/g,
|
||||
'/torrentpiracyguide'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/edu/g,
|
||||
'/edupiracyguide'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/system-tools/g,
|
||||
'/system-tools'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/file-tools/g,
|
||||
'/file-tools'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/internet-tools/g,
|
||||
'/internet-tools'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/social-media/g,
|
||||
'/social-media-tools'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/text-tools/g,
|
||||
'/text-tools'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/video-tools/g,
|
||||
'/video-tools'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/audio-tools/g,
|
||||
'/audio-tools'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/game-tools/g,
|
||||
'/gaming-tools'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/video/g,
|
||||
'/videopiracyguide'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/audio/g,
|
||||
'/audiopiracyguide'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/linux/g,
|
||||
'/linuxguide'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/non-eng/g,
|
||||
'/non-english'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/misc/g,
|
||||
'/miscguide'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/storage/g,
|
||||
'/storage'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/dev-tools/g,
|
||||
'/devtools'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/www.reddit.com\/r\/FREEMEDIAHECKYEAH\/wiki\/img-tools/g,
|
||||
'/img-tools'
|
||||
)
|
||||
.replace(
|
||||
/https:\/\/github.com\/nbats\/FMHYedit\/blob\/main\/base64.md#/g,
|
||||
'/base64/#'
|
||||
)
|
||||
// Remove extra characters
|
||||
.replace(/\/#wiki_/g, '/#')
|
||||
.replace(/#wiki_/g, '/#')
|
||||
.replace(/.25BA_/g, '')
|
||||
.replace(/.25B7_/g, '')
|
||||
.replace(/_.2F_/g, '-')
|
||||
|
||||
_text = replaceUnderscore(_text)
|
||||
.replace(/\/#(\d)/g, '/#_$1') // Prefix headings starting with numbers
|
||||
.replace(/#(\d)/g, '#_$1') // Prefix headings starting with numbers
|
||||
.replace(/\/#/g, '#')
|
||||
.replace(/\*\*\*\n\n/gm, '')
|
||||
.replace(/\*\*\*\n/gm, '')
|
||||
.replace(/# ►/g, '##')
|
||||
.replace(/## ▷/g, '###')
|
||||
.replace(/####/g, '###')
|
||||
// Replace emojis
|
||||
.replace(/⭐/g, ':star:')
|
||||
.replace(/🌐/g, ':globe-with-meridians: ')
|
||||
.replace(/↪/g, ':repeat-button: ')
|
||||
// Replace note/warning/tip
|
||||
.replace(/^\*\*Note\*\* - (.+)$/gm, ':::tip\n$1\n:::')
|
||||
.replace(/^\* \*\*Note\*\* - (.+)$/gm, ':::tip\n$1\n:::')
|
||||
.replace(/^Note - (.+)$/gm, ':::tip\n$1\n:::')
|
||||
.replace(/^\*\*Warning\*\* - (.+)$/gm, ':::warning\n$1\n:::')
|
||||
.replace(/^\* \*\*Warning\*\* - (.+)$/gm, ':::warning\n$1\n:::')
|
||||
.replace(/^\*\s([^*])/gm, '- $1')
|
||||
// Replace links
|
||||
.replace(
|
||||
/\/storage\/#encode--decode_urls/g,
|
||||
'/storage/#encode--decode-urls'
|
||||
)
|
||||
.replace(/\/base64\/#do-k-ument/g, '/base64/#do_k_ument')
|
||||
.replace(/\/devtools\/#machine-learning2/g, '/devtools/#machine-learning-1')
|
||||
.replace(/\/linuxguide#software-sites2/g, '/linuxguide#software-sites-1')
|
||||
.replace(/\/linuxguide#software_sites/g, '/linuxguide#software-sites')
|
||||
.replace(/\/non-english#reading10/g, '/non-english#reading-9')
|
||||
.replace(
|
||||
/\/storage#satellite-.26amp.3B_street_view_maps/g,
|
||||
'/storage#satellite-street-view-maps'
|
||||
)
|
||||
.replace(
|
||||
/(.+?) site or extension\.\n/gm,
|
||||
'Click on the texts to copy them decoded.\n'
|
||||
)
|
||||
|
||||
return _text
|
||||
}
|
||||
|
||||
function transformLinks(text: string): string {
|
||||
const _text = text
|
||||
// Transform Discord links to icons
|
||||
.replace(
|
||||
/\[Discord\]\(([^\)]*?)\)/gm,
|
||||
'<a target="_blank" href="$1"><div alt="Discord" class="i-carbon:logo-discord" /></a>'
|
||||
)
|
||||
// Transform GitHub links to icons
|
||||
.replace(
|
||||
/\[GitHub\]\(([^\)]*?)\)/gm,
|
||||
'<a target="_blank" href="$1"><div alt="GitHub" class="i-carbon:logo-github mb-1" /></a>'
|
||||
)
|
||||
// Fallback
|
||||
.replace(
|
||||
/\[Github\]\(([^\)]*?)\)/gm,
|
||||
'<a target="_blank" href="$1"><div alt="GitHub" class="i-carbon:logo-github mb-1" /></a>'
|
||||
)
|
||||
// Transform GitLab links to icons
|
||||
.replace(
|
||||
/\[GitLab\]\(([^\)]*?)\)/gm,
|
||||
'<a target="_blank" href="$1"><div alt="GitLab" class="i-carbon:logo-gitlab" /></a>'
|
||||
)
|
||||
// Fallback
|
||||
.replace(
|
||||
/\[Gitlab\]\(([^\)]*?)\)/gm,
|
||||
'<a target="_blank" href="$1"><div alt="GitLab" class="i-carbon:logo-gitlab" /></a>'
|
||||
)
|
||||
// Transform Telegram links to icons
|
||||
.replace(
|
||||
/\[Telegram\]\(([^\)]*?)\)/gm,
|
||||
'<a target="_blank" href="$1"><div alt="Telegram" class="i-mdi:telegram" /></a>'
|
||||
)
|
||||
// Transform Subreddit links to icons
|
||||
.replace(
|
||||
/\[Subreddit\]\(([^\)]*?)\)/gm,
|
||||
'<a target="_blank" href="$1"><div alt="Telegram" class="i-mdi:reddit" /></a>'
|
||||
)
|
||||
return _text
|
||||
}
|
29
docs/.vitepress/types/Feedback.ts
Normal file
29
docs/.vitepress/types/Feedback.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import z from 'zod'
|
||||
|
||||
export const FeedbackSchema = z.object({
|
||||
message: z.string().min(5).max(1000),
|
||||
type: z.enum(['bug', 'suggestion', 'appreciate', 'other']),
|
||||
page: z.string().optional()
|
||||
})
|
||||
|
||||
export const feedbackOptions = [
|
||||
{ label: '🐞 Bug', value: 'bug' },
|
||||
{
|
||||
label: '💡 Suggestion',
|
||||
value: 'suggestion'
|
||||
},
|
||||
{ label: '📂 Other', value: 'other' },
|
||||
{
|
||||
label: '❤️ Appreciation',
|
||||
value: 'appreciate'
|
||||
}
|
||||
]
|
||||
|
||||
export function getFeedbackOption(value: string): {
|
||||
label: string
|
||||
value: string
|
||||
} {
|
||||
return feedbackOptions.find((option) => option.value === value)
|
||||
}
|
||||
|
||||
export type FeedbackType = z.infer<typeof FeedbackSchema>
|
13
docs/.vitepress/utils.ts
Normal file
13
docs/.vitepress/utils.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export function groupBy<T, K extends keyof any>(
|
||||
arr: T[],
|
||||
key: (i: T) => K
|
||||
): Record<K, T[]> {
|
||||
return arr.reduce(
|
||||
(groups, item) => {
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
|
||||
;(groups[key(item)] ||= []).push(item)
|
||||
return groups
|
||||
},
|
||||
{} as Record<K, T[]>
|
||||
)
|
||||
}
|
5
docs/.vitepress/vue-shim.d.ts
vendored
Normal file
5
docs/.vitepress/vue-shim.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/* eslint-disable ts/consistent-type-imports */
|
||||
declare module '*.vue' {
|
||||
const component: import('vue').Component
|
||||
export default component
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue