26 |
27 |
28 |
29 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
{
43 | console.log('sidebar && onToggle(): ', sidebar)
44 | sidebar && onToggle()
45 | }}
46 | />
47 |
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/packages/v2/src/assets/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v2/src/assets/avatar.jpg
--------------------------------------------------------------------------------
/packages/v2/src/assets/avatar.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v2/src/assets/avatar.webp
--------------------------------------------------------------------------------
/packages/v2/src/assets/blog.svg:
--------------------------------------------------------------------------------
1 |
2 |
14 | blog
15 |
19 |
20 |
--------------------------------------------------------------------------------
/packages/v2/src/assets/folder.svg:
--------------------------------------------------------------------------------
1 |
3 | Folder
4 |
5 |
--------------------------------------------------------------------------------
/packages/v2/src/assets/github.svg:
--------------------------------------------------------------------------------
1 |
GitHub
2 |
--------------------------------------------------------------------------------
/packages/v2/src/assets/joplin-vscode-plugin-cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v2/src/assets/joplin-vscode-plugin-cover.png
--------------------------------------------------------------------------------
/packages/v2/src/assets/joplin-vscode-plugin-cover.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v2/src/assets/joplin-vscode-plugin-cover.webp
--------------------------------------------------------------------------------
/packages/v2/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
14 |
16 |
19 |
23 |
24 |
33 |
34 |
53 | Logo
55 |
59 |
63 |
79 | 璃
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/packages/v2/src/assets/open.svg:
--------------------------------------------------------------------------------
1 |
External Link
2 |
--------------------------------------------------------------------------------
/packages/v2/src/assets/pixiv.svg:
--------------------------------------------------------------------------------
1 |
3 | Pixiv
4 |
7 |
--------------------------------------------------------------------------------
/packages/v2/src/assets/telegram.svg:
--------------------------------------------------------------------------------
1 |
Telegram
2 |
--------------------------------------------------------------------------------
/packages/v2/src/assets/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/v2/src/components/LeftLinks.module.css:
--------------------------------------------------------------------------------
1 | @media (max-width: 768px) {
2 | .LeftLinks {
3 | display: none;
4 | }
5 | }
6 |
7 | .LeftLinks {
8 | position: fixed;
9 | left: 0;
10 | bottom: 0;
11 | margin-left: 40px;
12 | }
13 |
14 | .LeftLinks:after {
15 | content: '';
16 | display: block;
17 | width: 1px;
18 | height: 90px;
19 | margin: 30px auto 0;
20 | background-color: var(--light-slate);
21 | }
22 |
23 | .LeftLinks ul {
24 | list-style: none;
25 | margin: 0;
26 | padding: 0;
27 | }
28 |
--------------------------------------------------------------------------------
/packages/v2/src/components/LeftLinks.tsx:
--------------------------------------------------------------------------------
1 | import css from './LeftLinks.module.css'
2 | import blog from '../assets/blog.svg?raw'
3 | import telegram from '../assets/telegram.svg?raw'
4 | import github from '../assets/github.svg?raw'
5 | import twitter from '../assets/twitter.svg?raw'
6 | import pixiv from '../assets/pixiv.svg?raw'
7 | import { LinkIcon, LinkIconItem } from './LinkIcon'
8 | import { useInView } from '../hooks/useInView'
9 | import transition from '../components/TransitionGroup.module.css'
10 | import classNames from 'classnames'
11 |
12 | const links: LinkIconItem[] = [
13 | { title: 'github', link: 'https://github.com/rxliuli', icon: github },
14 | { title: 'twitter', link: 'https://twitter.com/rxliuli', icon: twitter },
15 | { title: 'telegram', link: 'https://t.me/rxliuli', icon: telegram },
16 | { title: 'blog', link: 'https://blog.rxliuli.com/', icon: blog },
17 | { title: 'pixiv', link: 'https://www.pixiv.net/users/16247572', icon: pixiv },
18 | ]
19 |
20 | export const LeftLinks = () => {
21 | const { ref, inView } = useInView({ timeout: 1000 })
22 | return (
23 |
33 |
34 | {links.map((item) => (
35 |
36 |
37 |
38 | ))}
39 |
40 |
41 | )
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/packages/v2/src/components/LinkButton.module.css:
--------------------------------------------------------------------------------
1 | .LinkButton {
2 | color: var(--green);
3 | text-decoration: none;
4 | display: inline-block;
5 | margin-top: 50px;
6 | padding: 20px 28px;
7 | border: solid 1px var(--green);
8 | border-radius: 5px;
9 | transition: all 0.3s;
10 | }
11 |
12 | .LinkButton:hover {
13 | background-color: #133040;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/v2/src/components/LinkButton.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionalComponent, JSX } from 'preact'
2 | import css from './LinkButton.module.css'
3 |
4 | export const LinkButton: FunctionalComponent<{ href: string; style?: JSX.CSSProperties }> = (props) => {
5 | return (
6 |
7 | {props.children}
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/packages/v2/src/components/LinkIcon.module.css:
--------------------------------------------------------------------------------
1 | .LinkIcon {
2 | display: block;
3 | padding: 10px;
4 | transition: all 0.3s;
5 | color: var(--light-slate);
6 | }
7 |
8 | .LinkIcon:hover {
9 | color: var(--green);
10 | transform: translateY(-4px);
11 | }
12 |
13 | .LinkIcon svg {
14 | width: 20px;
15 | height: 20px;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/v2/src/components/LinkIcon.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionalComponent } from 'preact'
2 | import css from './LinkIcon.module.css'
3 |
4 | export interface LinkIconItem {
5 | title: string
6 | link: string
7 | icon: string
8 | }
9 |
10 | export const LinkIcon: FunctionalComponent<{ item: LinkIconItem }> = ({ item }) => {
11 | return
12 | }
13 |
--------------------------------------------------------------------------------
/packages/v2/src/components/RightMail.module.css:
--------------------------------------------------------------------------------
1 | .RightMail {
2 | position: fixed;
3 | right: 0;
4 | bottom: 0;
5 | margin-right: 40px;
6 | }
7 |
8 | .RightMail:after {
9 | content: '';
10 | display: block;
11 | width: 1px;
12 | height: 90px;
13 | margin: 30px auto 0;
14 | background-color: var(--light-slate);
15 | }
16 |
17 | .RightMail a {
18 | text-decoration: none;
19 | transition: all 0.3s;
20 | color: var(--light-slate);
21 | writing-mode: vertical-lr;
22 | }
23 |
24 | .RightMail a:hover {
25 | color: var(--green);
26 | transform: translateY(-4px);
27 | }
28 |
29 | @media (max-width: 768px) {
30 | .RightMail {
31 | display: none;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/v2/src/components/RightMail.tsx:
--------------------------------------------------------------------------------
1 | import css from './RightMail.module.css'
2 | import transition from '../components/TransitionGroup.module.css'
3 | import classNames from 'classnames'
4 | import { useInView } from '../hooks/useInView'
5 |
6 | export const RightMail = () => {
7 | const { ref, inView } = useInView({ timeout: 1000 })
8 | return (
9 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/packages/v2/src/components/Tabs.module.css:
--------------------------------------------------------------------------------
1 | .tab {
2 | display: grid;
3 | grid-template-columns: auto 1fr;
4 | gap: 40px;
5 | }
6 |
7 | .tab > nav {
8 | position: relative;
9 | }
10 |
11 | .tab > nav > ul,
12 | .tab > ul {
13 | list-style: none;
14 | padding: 0;
15 | margin: 0;
16 | }
17 |
18 | .tab > nav button {
19 | border: none;
20 | background-color: transparent;
21 | color: var(--light-slate);
22 | display: block;
23 | width: 100%;
24 | height: 42px;
25 | text-align: left;
26 | padding: 0 20px;
27 | border-left: 2px solid #233554;
28 | transition: var(--transition);
29 | cursor: pointer;
30 | }
31 |
32 | .tab nav button:hover {
33 | color: var(--green);
34 | background-color: #112240;
35 | }
36 |
37 | .tab nav button.active {
38 | color: var(--green);
39 | border-left: 2px solid var(--green);
40 | }
41 |
42 | .bar {
43 | content: '';
44 | display: block;
45 | position: absolute;
46 | background-color: var(--green);
47 | transition: var(--transition);
48 | transition-property: left, top;
49 | }
50 |
51 | @media (max-width: 768px) {
52 | .tab {
53 | grid-template-rows: auto 1fr;
54 | grid-template-columns: 1fr;
55 | }
56 | .tab > nav > ul {
57 | width: 100%;
58 | overflow-x: auto;
59 | display: flex;
60 | }
61 | /* 使用权重更高的 css 强制设置避免 github pages 返回的 css 有奇怪的问题 */
62 | .tab > nav > ul button {
63 | border-left: none;
64 | border-bottom: 2px solid #233554;
65 | }
66 |
67 | .tab > nav > ul button.active {
68 | border-left: none;
69 | border-bottom: 2px solid var(--green);
70 | }
71 | .bar {
72 | bottom: 0;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/packages/v2/src/components/Tabs.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import { ComponentChildren, FunctionComponent, JSX, Key, VNode } from 'preact'
3 | import { useEffect, useRef, useState } from 'preact/hooks'
4 | import css from './Tabs.module.css'
5 |
6 | interface TabItemProps {
7 | title: string
8 | children: ComponentChildren
9 | }
10 |
11 | const TabItem: FunctionComponent
= (props: TabItemProps) => {
12 | return <>{props.children}>
13 | }
14 |
15 | export interface TabsProps {
16 | active: K
17 | onChange: (key: K) => void
18 | children: VNode | VNode[]
19 | }
20 |
21 | export function _Tabs(props: TabsProps) {
22 | const children = Array.isArray(props.children)
23 | ? props.children
24 | : [props.children]
25 | const $list = useRef(null)
26 | const [barStyle, setBarStyle] = useState()
27 | const aligh = useRef<'horizontal' | 'portrait'>(
28 | window.innerWidth > 768 ? 'portrait' : 'horizontal',
29 | )
30 |
31 | function calcStyle() {
32 | if (!$list.current) {
33 | return
34 | }
35 | const i = children.findIndex((item) => item.key === props.active)
36 | if (i === -1) {
37 | return
38 | }
39 | const $el = [...$list.current.children][i] as HTMLLIElement
40 | if (aligh.current === 'portrait') {
41 | console.log('$el.portrait: ', $el.offsetTop, $el.offsetHeight)
42 | setBarStyle({
43 | top: $el.offsetTop,
44 | width: 2,
45 | height: $el.offsetHeight,
46 | })
47 | } else if (aligh.current === 'horizontal') {
48 | console.log('$el.horizontal: ', $el.offsetLeft, $el.offsetWidth)
49 | setBarStyle({
50 | left: $el.offsetLeft,
51 | width: $el.offsetWidth,
52 | height: 2,
53 | })
54 | }
55 | }
56 |
57 | useEffect(calcStyle, [children, props.active, $list])
58 | useEffect(() => {
59 | const listener = () => {
60 | aligh.current = window.innerWidth > 768 ? 'portrait' : 'horizontal'
61 | calcStyle()
62 | }
63 | window.addEventListener('resize', listener)
64 | return () => window.removeEventListener('resize', listener)
65 | }, [props.active])
66 |
67 | return (
68 |
69 |
70 |
71 | {children.map((item) => (
72 | props.onChange(item.key)}>
73 |
78 | {item.props.title}
79 |
80 |
81 | ))}
82 |
83 |
84 |
85 |
86 | {children.map((item) => (
87 |
93 | {item}
94 |
95 | ))}
96 |
97 |
98 | )
99 | }
100 |
101 | export const Tabs = Object.assign(_Tabs, {
102 | Item: TabItem,
103 | })
104 |
--------------------------------------------------------------------------------
/packages/v2/src/components/TransitionGroup.module.css:
--------------------------------------------------------------------------------
1 | .fadeupEnter > * {
2 | --delay: 100ms;
3 | opacity: 0.01;
4 | transform: translateY(20px);
5 | transition: opacity 300ms ease-in, transform 300ms ease-in;
6 | }
7 |
8 | .fadeupEnterActive > * {
9 | opacity: 1;
10 | transform: translateY(0px);
11 | transition: opacity 300ms ease-in, transform 300ms ease-in;
12 | }
13 |
14 | .fadedownEnter > * {
15 | opacity: 0.01;
16 | transform: translateY(-20px);
17 | transition: opacity 300ms var(--easing), transform 300ms var(--easing);
18 | }
19 |
20 | .fadedownEnterActive > * {
21 | opacity: 1;
22 | transform: translateY(0px);
23 | transition: opacity 300ms var(--easing), transform 300ms var(--easing);
24 | }
25 |
26 | .fadeEnter {
27 | opacity: 0;
28 | }
29 | .fadeEnterActive {
30 | opacity: 1;
31 | transition: opacity 300ms var(--easing);
32 | }
33 |
--------------------------------------------------------------------------------
/packages/v2/src/components/TransitionGroup.tsx:
--------------------------------------------------------------------------------
1 | import { VNode } from 'preact';
2 | import { cloneElement } from 'preact/compat'
3 |
4 | export function TransitionGroup(props: { children: VNode | VNode[]; timeout?: number }) {
5 | const children = (Array.isArray(props.children) ? props.children : [props.children]) as unknown as VNode<{
6 | style?: JSX.CSSProperties
7 | children: VNode
8 | }>[]
9 | return (
10 | <>
11 | {children.map((item, i) =>
12 | cloneElement(
13 | item,
14 | {
15 | ...item.props,
16 | key: i,
17 | style: {
18 | transitionDelay: `${(props.timeout ?? 0) + i * 100}ms`,
19 | ...item.props.style,
20 | },
21 | },
22 | item.props.children,
23 | ),
24 | )}
25 | >
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/packages/v2/src/constants/__tests__/i18n.test.ts:
--------------------------------------------------------------------------------
1 | // @vitest-environment happy-dom
2 | import { it, expect } from 'vitest'
3 | import { createI18n, locales } from '../i18n'
4 |
5 | it('should be able to import i18n', () => {
6 | const { t } = createI18n(locales, 'en-US')
7 | expect(t('example')).eq('example')
8 | })
9 |
--------------------------------------------------------------------------------
/packages/v2/src/constants/i18n.ts:
--------------------------------------------------------------------------------
1 | import enUS from '../i18n/en-US.json'
2 | import zhCN from '../i18n/zh-CN.json'
3 | import jaJP from '../i18n/ja-JP.json'
4 | import { TranslateType } from '../i18n'
5 |
6 | export const locales = {
7 | 'en-US': enUS,
8 | 'zh-CN': zhCN,
9 | 'ja-JP': jaJP,
10 | } as const
11 |
12 | const DefaultLang = 'en-US'
13 | export function getLanguage(): keyof typeof locales {
14 | return 'zh-CN'
15 | const lang = localStorage.getItem('language') ?? navigator.language
16 | if (lang in locales) {
17 | return lang as keyof typeof locales
18 | }
19 | return DefaultLang
20 | }
21 |
22 | export function createI18n>>(
23 | locales: T,
24 | lang: keyof T,
25 | ) {
26 | return {
27 | get lang() {
28 | return getLanguage()
29 | },
30 | setLang(lang: keyof T) {
31 | localStorage.setItem('language', lang as string)
32 | },
33 | t(
34 | ...args: TranslateType[K]['params']
35 | ): string {
36 | const [key, params] = args as any
37 | const template = ((locales[lang] as any)[key] ??
38 | (locales[DefaultLang] as any)[key]) as string | undefined
39 | if (!template) {
40 | throw new Error(`Cannot find template for key: ${key}`)
41 | }
42 | if (!params) {
43 | return template
44 | }
45 | return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
46 | return params[key] !== undefined ? String(params[key]) : match
47 | })
48 | },
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/v2/src/constants/useI18n.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'preact'
2 | import { createI18n, getLanguage, locales } from './i18n'
3 | import { useContext, useEffect, useMemo, useState } from 'preact/hooks'
4 | import React from 'preact/compat'
5 |
6 | const LocaleContext = createContext(createI18n(locales, getLanguage()))
7 |
8 | export function useLocale() {
9 | const context = useContext(LocaleContext)
10 | if (!context) {
11 | throw new Error('useLocale must be used within a LocaleProvider')
12 | }
13 | return context
14 | }
15 |
16 | export const LocaleProvider: React.FC = (props) => {
17 | const [lang, setLang] = useState(getLanguage())
18 | const context = useMemo(() => {
19 | const r = createI18n(locales, lang)
20 | return {
21 | ...r,
22 | setLang(lang: keyof typeof locales) {
23 | setLang(lang)
24 | r.setLang(lang)
25 | },
26 | }
27 | }, [lang])
28 | useEffect(() => {
29 | // @ts-expect-error
30 | globalThis.context = context
31 | })
32 | return (
33 |
34 | {props.children}
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/packages/v2/src/hooks/useInView.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'preact/hooks'
2 |
3 | interface Options extends IntersectionObserverInit {
4 | timeout?: number
5 | }
6 |
7 | export function useInView(options?: Options) {
8 | const { timeout, ...other }: Options = { threshold: 0.5, ...options }
9 | const html = useRef(null)
10 | const [inView, setInView] = useState(false)
11 | useEffect(() => {
12 | let n: any
13 | const observer = new IntersectionObserver((entries) => {
14 | const intersectionRatio = entries[0].intersectionRatio
15 | // console.log('entries: ', entries, intersectionRatio, intersectionRatio > 0 && intersectionRatio <= 1)
16 | if (intersectionRatio > 0 && intersectionRatio <= 1) {
17 | if (timeout === undefined) {
18 | setInView(true)
19 | } else {
20 | n = setTimeout(() => setInView(true), timeout)
21 | }
22 | observer.unobserve(html.current!)
23 | }
24 | }, other)
25 | observer.observe(html.current!)
26 | return () => {
27 | observer.unobserve(html.current!)
28 | clearTimeout(n)
29 | }
30 | }, [timeout])
31 | return {
32 | ref: html,
33 | inView,
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/v2/src/hooks/useWindowScroll.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'preact/hooks'
2 |
3 | export function useWindowScroll() {
4 | const [dir, setDir] = useState<'up' | 'down'>()
5 | const [scrollY, setScrollY] = useState(0)
6 | useEffect(() => {
7 | const listener = () => {
8 | const dir = scrollY > window.scrollY ? 'up' : 'down'
9 | setDir(dir)
10 | setScrollY(window.scrollY)
11 | }
12 | window.addEventListener('scroll', listener)
13 | return () => window.removeEventListener('scroll', listener)
14 | }, [scrollY])
15 | return { dir, scrollY }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/v2/src/i18n/zh-CN.json:
--------------------------------------------------------------------------------
1 | {
2 | "about.title": "关于",
3 | "experience.title": "经历",
4 | "work.title": "作品",
5 | "concat.title": "联系",
6 | "navbar.source": "源代码",
7 | "home.hello": "你好,吾辈名为",
8 | "home.name": "琉璃",
9 | "home.title": "吾辈基于 web 技术创造各种东西",
10 | "home.desc": "现在是一个前端开发工程师,喜欢折腾有趣的技术、二次元、开源和分享,目前专注于",
11 | "home.point": "前端工程化",
12 | "home.example": "看看 joplin-utils",
13 | "about.desc1": "你好,吾辈是 rxliuli,中文名是琉璃。吾辈曾经是一个 Java CURD 研发工程师,后面感觉现代前端日新月异,有许多新奇的东西,所以在 2019 年正式转换为前端工作。",
14 | "about.desc2": "快进到今天,吾辈现在在一个做着类似于 webos 东西的公司,可以花更多的时间来做一些长线的事情,像是各种前端工程化的基建,以及基于 web 的应用平台研发。",
15 | "about.desc3": "虽然公司使用了 vue3+typescript,但吾辈更喜欢 react,所以社区项目仍然继续使用它开发。虽然公司使用了 vue3+typescript,但吾辈更喜欢 react,所以社区项目仍然继续使用它开发。",
16 | "about.recent": "以下是最近一直在使用的一些技术:",
17 | "about.picture": "我的照片",
18 | "experience.pinefield.name": "北京奇岱松",
19 | "experience.pinefield.jobTitle": "前端开发工程师",
20 | "experience.pinefield.item1": "前端工程化和基础建设,包括各种 cli、插件和 library 等",
21 | "experience.pinefield.item2": "webos 子应用系统的设计实现,协助其他人开发子应用,多线程模型、通信、系统 api",
22 | "experience.pinefield.item3": "实践和确定了众多技术的可行性,大型 monorepo、vite、esbuild、vue3、pnpm",
23 | "experience.ibingli.name": "广州秉理",
24 | "experience.ibingli.jobTitle": "前端开发工程师",
25 | "experience.ibingli.item1": "前端项目的基建和大型重构,保证项目在超过 4w 行代码、20 个模块时仍然保持可维护性",
26 | "experience.ibingli.item2": "lerna 的具体实践和落地,让前端工程演进为 monorepo 形式,支持了上面的大型前端项目",
27 | "experience.ibingli.item3": "项目规范的编写和推进,使用 `eslint/prettier/git hooks/code review` 的形式校验代码",
28 | "experience.ibingli.item4": "在前端日志方面做出探索,实现了在部署到医院内网之后前端可以通过日志查找问题",
29 | "experience.ibingli.item5": "领域限定硬件的调研/第三方服务的接入",
30 | "experience.zx-soft.name": "广州智晓",
31 | "experience.zx-soft.jobTitle": "Java 后端开发",
32 | "experience.zx-soft.item1": "学习并在后端团队内推广 vue 这种现代前端框架",
33 | "experience.zx-soft.item2": "使用 Java Spring Boot 开发各种后台管理系统",
34 | "experience.zx-soft.item3": "认识到了很好的同事,至今仍然在通过邮件继续联系",
35 | "experience.time.now": "至今",
36 | "work.primary.title": "特色项目",
37 | "work.other.title": "其他值得注意的项目",
38 | "work.other.viewList": "查看列表",
39 | "work.liuli-tools.desc": "一系列与开发者相关的模块,包括通用函数库、cli 以及 esbuild/vite 的插件,旨在解决开发中遇到的各种通用问题。",
40 | "work.new-project.desc": "这是一个 vscode 可视化创建项目的插件,尝试在 vscode 中提供类似于 jetbrains ide 的创建项目的面板。目前仅支持使用 vite/create-react-app/angular/svelte 创建项目,但支持自定义生成器。",
41 | "work.tsx.desc": "esbuild-kit/tsx 的 vscode 集成,可以运行任何 js/ts/jsx/tsx 代码,支持 esm/cjs 模块。",
42 | "work.vite-integrated.desc": "Vite 脚手架在 JetBrains IDE 中的集成,主要负责直接使用 IDE 的引导面板创建一个项目。",
43 | "work.react-router.desc": "封装 react-router 为集中式的 js 路由配置,组件仅暴露必要的 props,并且默认支持在 react 组件外使用路由。",
44 | "work.magia.desc": "一个复刻的可爱的动画网站(魔法少女小圆),源项目是 https://magia.cyris.moe/。",
45 | "work.saki.desc": "想知道基于 golang 编写 cli 能够提高多少性能,所以尝试使用 golang 编写了这个 cli 应用。",
46 | "work.mdbook.desc": "一个基于 pandoc 的 markdown => epub 的构建工具,支持使用配置而非命令行参数的形式指定章节文件(也是后面整理和发布小说 epub 版本的前提)",
47 | "work.tts.title": "魔法少女小圆 飞向星空",
48 | "work.tts.desc": "“丘比承诺说人类总有一天也能到达那遥远的星空。但它们很明智地没有说出来,人类将会在那里遇到什么。”—— 引言",
49 | "work.tts.topic1": "二次元",
50 | "work.tts.topic2": "同人",
51 | "work.tts.topic3": "硬科幻",
52 | "concat.next": "下一步是什么?",
53 | "concat.subtitle": "保持联系",
54 | "concat.desc": "虽然吾辈目前没有寻找任何新的机会,但吾辈的邮箱始终是打开的。无论您有任何问题或只是想打个招呼,不管是二次元还是开发者,吾辈都会尽力回复您!",
55 | "concat.hello": "打个招呼",
56 | "concat.footer": "由 bchiang7 设计,但吾辈重新实现了它。",
57 | "example": "示例"
58 | }
59 |
--------------------------------------------------------------------------------
/packages/v2/src/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | background-color: #0a192f;
6 | color: var(--light-slate);
7 | scroll-behavior: smooth;
8 | }
9 |
10 | * {
11 | box-sizing: border-box;
12 | }
13 |
14 | :root {
15 | --green: rgb(100, 255, 218);
16 | --light-slate: #8892b0;
17 | --lightest-slate: #ccd6f6;
18 | --transition: all 0.25s cubic-bezier(0.645, 0.045, 0.355, 1);
19 | --background: #0a192f;
20 | --easing: cubic-bezier(0.645, 0.045, 0.355, 1);
21 | }
22 |
23 | /* 滚动槽 */
24 | body::-webkit-scrollbar {
25 | width: 12px;
26 | }
27 | body::-webkit-scrollbar-track {
28 | background-color: var(--background);
29 | }
30 | /* 滚动条滑块 */
31 | body::-webkit-scrollbar-thumb {
32 | background: #495670;
33 | border: 3px solid var(--background);
34 | border-radius: 10px;
35 | /*-webkit-box-shadow: inset 0 0 10px #495670;*/
36 | }
37 |
--------------------------------------------------------------------------------
/packages/v2/src/main.tsx:
--------------------------------------------------------------------------------
1 | // if (import.meta.env.DEV) {
2 | // // @ts-ignore
3 | // await import('preact/debug')
4 | // }
5 |
6 | import { render } from 'preact'
7 | import { App } from './App'
8 | import './index.css'
9 |
10 | render( , document.getElementById('app')!)
11 |
--------------------------------------------------------------------------------
/packages/v2/src/preact.d.ts:
--------------------------------------------------------------------------------
1 | import JSX = preact.JSX
2 |
--------------------------------------------------------------------------------
/packages/v2/src/views/AboutView.module.css:
--------------------------------------------------------------------------------
1 | .AboutView {
2 | max-width: 900px;
3 | margin: auto;
4 | display: grid;
5 | grid-template-rows: auto 1fr;
6 | grid-template-columns: 1fr 1fr;
7 | grid-template-areas:
8 | 'header header'
9 | '. .';
10 | grid-column-gap: 90px;
11 | padding: 100px 0;
12 | }
13 |
14 | .header {
15 | grid-area: header;
16 | font-size: 32px;
17 | margin: 10px 0 40px;
18 | display: flex;
19 | align-items: center;
20 | color: var(--lightest-slate);
21 | white-space: nowrap;
22 | }
23 |
24 | .header span {
25 | font-size: 20px;
26 | color: var(--green);
27 | margin-right: 10px;
28 | }
29 | .header:after {
30 | content: '';
31 | display: block;
32 | width: 300px;
33 | height: 1px;
34 | margin-left: 20px;
35 | background: var(--lightest-slate);
36 | }
37 |
38 | .tab {
39 | font-size: 20px;
40 | }
41 |
42 | .skills {
43 | display: grid;
44 | grid-template-columns: 1fr 1fr;
45 | list-style: none;
46 | padding-left: 0;
47 | gap: 10px;
48 | }
49 |
50 | .skills li {
51 | display: flex;
52 | align-items: center;
53 | }
54 | .skills li:before {
55 | content: '▹';
56 | color: var(--green);
57 | font-size: 12px;
58 | display: inline-block;
59 | width: 20px;
60 | }
61 |
62 | .avatar {
63 | position: relative;
64 | width: 300px;
65 | height: 300px;
66 | }
67 |
68 | .avatar img {
69 | position: relative;
70 | border-radius: 5px;
71 | width: 100%;
72 | height: 100%;
73 | }
74 |
75 | .avatar:after {
76 | content: '';
77 | display: block;
78 | position: absolute;
79 | width: 100%;
80 | height: 100%;
81 | left: 0;
82 | top: 0;
83 | border-radius: 5px;
84 | background-color: var(--green);
85 | opacity: 0.5;
86 | transition: var(--transition);
87 | }
88 | .avatar:hover:after {
89 | opacity: 0;
90 | }
91 |
92 | .avatar:before {
93 | content: '';
94 | display: block;
95 | position: absolute;
96 | width: 100%;
97 | height: 100%;
98 | left: 20px;
99 | top: 20px;
100 | border: solid 2px var(--green);
101 | border-radius: 5px;
102 | transition: var(--transition);
103 | }
104 |
105 | .avatar:hover:before {
106 | transform: translate(-10px, -10px);
107 | }
108 |
109 | @media (max-width: 768px) {
110 | .AboutView {
111 | display: block;
112 | }
113 | .header:after {
114 | width: 100%;
115 | }
116 | .avatar {
117 | margin-top: 50px;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/packages/v2/src/views/AboutView.tsx:
--------------------------------------------------------------------------------
1 | import css from './AboutView.module.css'
2 | import avatar from '../assets/avatar.webp'
3 | import { FunctionalComponent, JSX } from 'preact'
4 | import classNames from 'classnames'
5 | import transition from '../components/TransitionGroup.module.css'
6 | import { useInView } from '../hooks/useInView'
7 | import { useLocale } from '../constants/useI18n'
8 |
9 | export const Header: FunctionalComponent<{
10 | order: string
11 | style?: JSX.CSSProperties
12 | }> = (props) => {
13 | return (
14 |
15 | {props.order}
16 | {props.children}
17 |
18 | )
19 | }
20 |
21 | const skillList = ['TypeScript', 'React', 'Vue3', 'Golang', 'DX']
22 |
23 | export const AboutView = () => {
24 | const { ref, inView } = useInView()
25 | const { t } = useLocale()
26 | return (
27 |
38 |
39 |
40 | {t('about.desc1')}
41 | {t('about.desc2')}
42 | {t('about.desc3')}
43 | {t('about.recent')}
44 |
45 | {skillList.map((item) => (
46 | {item}
47 | ))}
48 |
49 |
50 |
51 |
52 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/packages/v2/src/views/ConcatView.module.css:
--------------------------------------------------------------------------------
1 | .ConcatView {
2 | max-width: 600px;
3 | padding: 100px 0;
4 | margin: auto auto 100px;
5 | text-align: center;
6 | }
7 |
8 | .ConcatView > header > h2:first-child {
9 | font-size: 16px;
10 | margin: 10px 0 20px;
11 | color: var(--green);
12 | }
13 |
14 | .ConcatView > header > h2:last-child {
15 | font-size: 60px;
16 | color: var(--lightest-slate);
17 | margin: 0 auto 10px;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/v2/src/views/ConcatView.tsx:
--------------------------------------------------------------------------------
1 | import { LinkButton } from '../components/LinkButton'
2 | import { useInView } from '../hooks/useInView'
3 | import css from './ConcatView.module.css'
4 | import transition from '../components/TransitionGroup.module.css'
5 | import classNames from 'classnames'
6 | import { useLocale } from '../constants/useI18n'
7 |
8 | export const ConcatView = () => {
9 | const { ref, inView } = useInView()
10 | const { t } = useLocale()
11 | return (
12 |
23 |
24 |
25 | 04. {t('concat.next')}
26 |
27 | {t('concat.subtitle')}
28 |
29 |
30 |
31 | {t('concat.hello')}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/packages/v2/src/views/ExperienceView.module.css:
--------------------------------------------------------------------------------
1 | .ExperienceView {
2 | max-width: 700px;
3 | margin: auto;
4 | padding: 100px 0;
5 | }
6 |
7 | .content h3 {
8 | font-size: 22px;
9 | color: var(--lightest-slate);
10 | margin-bottom: 2px;
11 | margin-top: 0;
12 | }
13 |
14 | .content h3 a {
15 | color: var(--green);
16 | text-decoration: none;
17 | }
18 |
19 | .content p {
20 | margin-top: 0;
21 | }
22 |
23 | .content ul {
24 | list-style: none;
25 | padding-left: 0;
26 | }
27 |
28 | .content ul li {
29 | position: relative;
30 | padding-left: 30px;
31 | margin-bottom: 10px;
32 | }
33 |
34 | .content ul li:before {
35 | content: '▹';
36 | color: var(--green);
37 | position: absolute;
38 | left: 0;
39 | width: 20px;
40 | }
41 |
--------------------------------------------------------------------------------
/packages/v2/src/views/FooterView.module.css:
--------------------------------------------------------------------------------
1 | .FooterView {
2 | padding: 15px;
3 | text-align: center;
4 | }
5 |
6 | .FooterView a {
7 | text-decoration: none;
8 | color: var(--green);
9 | }
10 |
--------------------------------------------------------------------------------
/packages/v2/src/views/FooterView.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import { useInView } from '../hooks/useInView'
3 | import css from './FooterView.module.css'
4 | import transition from '../components/TransitionGroup.module.css'
5 | import { useLocale } from '../constants/useI18n'
6 |
7 | export const FooterView = () => {
8 | const { ref, inView } = useInView()
9 | const { t } = useLocale()
10 | return (
11 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/packages/v2/src/views/HomeView.module.css:
--------------------------------------------------------------------------------
1 | .HomeView {
2 | height: 100vh;
3 | display: flex;
4 | align-items: center;
5 | }
6 |
7 | .content {
8 | max-width: 1000px;
9 | margin: auto;
10 | transition-delay: 500ms;
11 | }
12 |
13 | .content > h1 {
14 | font-size: 16px;
15 | color: var(--green);
16 | }
17 |
18 | .content > h2 {
19 | color: #ccd6f6;
20 | font-size: clamp(40px, 8vw, 80px);
21 | margin: 0;
22 | }
23 |
24 | .content > h2:nth-of-type(2) {
25 | color: var(--light-slate);
26 | }
27 |
28 | .content a {
29 | color: var(--green);
30 | text-decoration: none;
31 | }
32 |
--------------------------------------------------------------------------------
/packages/v2/src/views/HomeView.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import { LinkButton } from '../components/LinkButton'
3 | import { TransitionGroup } from '../components/TransitionGroup'
4 | import { useInView } from '../hooks/useInView'
5 | import css from './HomeView.module.css'
6 | import transition from '../components/TransitionGroup.module.css'
7 | import { useWindowScroll } from '../hooks/useWindowScroll'
8 | import { useLocale } from '../constants/useI18n'
9 |
10 | export const HomeView = () => {
11 | const { ref, inView } = useInView({ threshold: 0.1 })
12 | const { dir } = useWindowScroll()
13 | const { t } = useLocale()
14 | return (
15 |
16 |
25 |
26 | {t('home.hello')}
27 | {t('home.name')}
28 | {t('home.title')}
29 |
30 | {t('home.desc')}{' '}
31 |
35 | {t('home.point')}
36 | {' '}
37 | 。
38 |
39 |
40 | {t('home.example')}
41 |
42 |
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/packages/v2/src/views/works/joplin-utils.en-US.md:
--------------------------------------------------------------------------------
1 | Community tools based on Joplin. The [joplin-vscode-plugin](https://marketplace.visualstudio.com/items?itemName=rxliuli.joplin-vscode-plugin) offers functionality to manage Joplin notes within VSCode, leveraging the powerful editor and its ecosystem of VSCode. [joplin-blog](https://www.npmjs.com/package/joplin-blog) publishes notes with specific tags as online websites, allowing users to choose between blog or wiki formats. There are also some developer-related toolsets, including [joplin-api](https://www.npmjs.com/package/joplin-api) and [joplin-plugin-cli](https://www.npmjs.com/package/joplin-plugin-cli).
--------------------------------------------------------------------------------
/packages/v2/src/views/works/joplin-utils.ja-JP.md:
--------------------------------------------------------------------------------
1 | Joplin の周辺コミュニティツールに基づいています。[joplin-vscode-plugin](https://marketplace.visualstudio.com/items?itemName=rxliuli.joplin-vscode-plugin) は、vscode内でJoplinのノートを管理する機能を提供しており、既存のvscodeの強力なエディタとそのエコシステムと組み合わせて使用されます。[joplin-blog](https://www.npmjs.com/package/joplin-blog) は、指定されたタグのノートをオンラインのウェブサイトとして公開し、blogまたはwikiの形式を選択することができます。開発者向けのツールセットもあり、[joplin-api](https://www.npmjs.com/package/joplin-api)/[joplin-plugin-cli](https://www.npmjs.com/package/joplin-plugin-cli) を含んでいます。
2 |
--------------------------------------------------------------------------------
/packages/v2/src/views/works/joplin-utils.zh-CN.md:
--------------------------------------------------------------------------------
1 | 基于 Joplin 的周边社区工具。[joplin-vscode-plugin](https://marketplace.visualstudio.com/items?itemName=rxliuli.joplin-vscode-plugin) 提供在 vscode 中管理 joplin 笔记的功能,结合 vscode 现有的强大编辑器及其生态。[joplin-blog](https://www.npmjs.com/package/joplin-blog) 将指定标签的笔记发布为在线网站,可以选择 blog 或 wiki 的形式。还有开发者相关的一些工具集,包括 [joplin-api](https://www.npmjs.com/package/joplin-api)/[joplin-plugin-cli](https://www.npmjs.com/package/joplin-plugin-cli)。
--------------------------------------------------------------------------------
/packages/v2/src/views/works/mami.en-US.md:
--------------------------------------------------------------------------------
1 | Mami is a conversion tool that can connect various markdown-based frameworks and tools. It can convert data from one tool to another, which is very helpful for cross-application migration and multi-platform publishing. Currently, it supports joplin, obsidian, hexo, hugo, and raw, with plans to support docsify and vuepress in the future.
--------------------------------------------------------------------------------
/packages/v2/src/views/works/mami.ja-JP.md:
--------------------------------------------------------------------------------
1 | mami は変換ツールであり、異なる markdown ベースのフレームワークやツールに接続することができます。一つのツールのデータを別のツールに変換することができ、アプリ間の移行や複数のプラットフォームでの公開に非常に役立ちます。現在、joplin/obsidian/hexo/hugo/raw/docsify/vuepress がサポートされています。
2 |
--------------------------------------------------------------------------------
/packages/v2/src/views/works/mami.zh-CN.md:
--------------------------------------------------------------------------------
1 | mami 是一个转换工具,可以连接不同的基于 markdown 框架和工具,能将一种工具的数据转换到另一种工具,这对于跨应用迁移以及多平台发布很有帮助,目前已支持 joplin/obsidian/hexo/hugo/raw/docsify/vuepress。
--------------------------------------------------------------------------------
/packages/v2/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.md' {
4 | export const html: string
5 | }
6 |
--------------------------------------------------------------------------------
/packages/v2/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "skipDefaultLibCheck": true,
9 | "esModuleInterop": false,
10 | "allowSyntheticDefaultImports": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "module": "ESNext",
14 | "moduleResolution": "Node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": true,
18 | "jsx": "preserve",
19 | "jsxFactory": "h",
20 | "jsxFragmentFactory": "Fragment"
21 | },
22 | "include": ["src"],
23 | "references": [{ "path": "./tsconfig.node.json" }]
24 | }
25 |
--------------------------------------------------------------------------------
/packages/v2/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "esModuleInterop": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/v2/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import preact from '@preact/preset-vite'
3 | import { visualizer } from 'rollup-plugin-visualizer'
4 | import { Mode, plugin } from 'vite-plugin-markdown'
5 | import MarkdownIt from 'markdown-it'
6 | import { i18nextDtsGen } from '@liuli-util/rollup-plugin-i18next-dts-gen'
7 |
8 | /**
9 | * @link https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
10 | * @param md
11 | */
12 | function linkTarget(md: MarkdownIt) {
13 | // Remember old renderer, if overridden, or proxy to default renderer
14 | var defaultRender =
15 | md.renderer.rules.link_open ||
16 | function (tokens, idx, options, env, self) {
17 | return self.renderToken(tokens, idx, options)
18 | }
19 |
20 | md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
21 | // If you are sure other plugins can't add `target` - drop check below
22 | var aIndex = tokens[idx].attrIndex('target')
23 |
24 | if (aIndex < 0) {
25 | tokens[idx].attrPush(['target', '_blank']) // add new attribute
26 | } else {
27 | tokens[idx].attrs[aIndex][1] = '_blank' // replace value of existing attr
28 | }
29 |
30 | // pass token to default renderer.
31 | return defaultRender(tokens, idx, options, env, self)
32 | }
33 | }
34 | const md = new MarkdownIt()
35 | linkTarget(md)
36 |
37 | export default defineConfig({
38 | base: '/v2/',
39 | plugins: [
40 | preact(),
41 | plugin({
42 | mode: [Mode.HTML],
43 | markdownIt: md,
44 | }),
45 | visualizer() as any,
46 | i18nextDtsGen({
47 | dirs: ['./src/i18n'],
48 | }),
49 | ],
50 | })
51 |
--------------------------------------------------------------------------------
/packages/v3/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/packages/v3/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 琉璃的个人主页 v3
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/packages/v3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "v3",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build",
8 | "preview": "vite preview",
9 | "deploy": "gh-pages -d dist --dotfiles -a"
10 | },
11 | "devDependencies": {
12 | "@preact/preset-vite": "^2.5.0",
13 | "autoprefixer": "^10.4.17",
14 | "clsx": "^2.1.0",
15 | "gh-pages": "^3.2.3",
16 | "postcss": "^8.4.33",
17 | "preact": "^10.19.3",
18 | "preact-iso": "^2.3.2",
19 | "preact-render-to-string": "^6.3.1",
20 | "tailwindcss": "^3.4.1",
21 | "typescript": "^5.3.3",
22 | "vite": "^5.0.12"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/v3/public/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v3/public/.nojekyll
--------------------------------------------------------------------------------
/packages/v3/public/CNAME:
--------------------------------------------------------------------------------
1 | rxliuli.com
--------------------------------------------------------------------------------
/packages/v3/public/ads.txt:
--------------------------------------------------------------------------------
1 | google.com, pub-9997051001110405, DIRECT, f08c47fec0942fa0
--------------------------------------------------------------------------------
/packages/v3/src/assets/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v3/src/assets/avatar.jpg
--------------------------------------------------------------------------------
/packages/v3/src/assets/banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v3/src/assets/banner.jpg
--------------------------------------------------------------------------------
/packages/v3/src/assets/blog.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/v3/src/assets/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/v3/src/assets/pixiv.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/v3/src/assets/telegram.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/v3/src/assets/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/v3/src/style.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .icon {
6 | transition: all 0.3s;
7 | }
8 |
9 | .icon:hover {
10 | color: var(--green);
11 | transform: translateY(-4px);
12 | }
13 |
14 | .icon svg {
15 | @apply w-6 h-6;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/v3/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
--------------------------------------------------------------------------------
/packages/v3/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "noEmit": true,
7 | "allowJs": true,
8 | "checkJs": true,
9 |
10 | /* Preact Config */
11 | "jsx": "react-jsx",
12 | "jsxImportSource": "preact",
13 | "skipLibCheck": true,
14 | "paths": {
15 | "react": ["./node_modules/preact/compat/"],
16 | "react-dom": ["./node_modules/preact/compat/"]
17 | }
18 | },
19 | "include": ["node_modules/vite/client.d.ts", "**/*"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/v3/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import preact from '@preact/preset-vite'
3 | import tailwindcss from 'tailwindcss'
4 | import autoprefixer from 'autoprefixer'
5 |
6 | export default defineConfig({
7 | plugins: [
8 | preact({
9 | prerender: {
10 | enabled: true,
11 | renderTarget: '#app',
12 | },
13 | }),
14 | ],
15 | css: {
16 | postcss: {
17 | plugins: [tailwindcss, autoprefixer],
18 | },
19 | },
20 | })
21 |
--------------------------------------------------------------------------------
/packages/v4/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | # Output
4 | .output
5 | .vercel
6 | /.svelte-kit
7 | /build
8 |
9 | # OS
10 | .DS_Store
11 | Thumbs.db
12 |
13 | # Env
14 | .env
15 | .env.*
16 | !.env.example
17 | !.env.test
18 |
19 | # Vite
20 | vite.config.js.timestamp-*
21 | vite.config.ts.timestamp-*
22 |
--------------------------------------------------------------------------------
/packages/v4/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/packages/v4/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "printWidth": 120,
4 | "semi": false,
5 | "singleQuote": true,
6 | "trailingComma": "all",
7 | "arrowParens": "always",
8 | "endOfLine": "crlf",
9 | "plugins": ["prettier-plugin-svelte"],
10 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/v4/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "i18n-ally.localesPaths": [
3 | "src/lib/i18n"
4 | ]
5 | }
--------------------------------------------------------------------------------
/packages/v4/README.md:
--------------------------------------------------------------------------------
1 | # create-svelte
2 |
3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
4 |
5 | ## Creating a project
6 |
7 | If you're seeing this, you've probably already done this step. Congrats!
8 |
9 | ```bash
10 | # create a new project in the current directory
11 | npm create svelte@latest
12 |
13 | # create a new project in my-app
14 | npm create svelte@latest my-app
15 | ```
16 |
17 | ## Developing
18 |
19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
20 |
21 | ```bash
22 | npm run dev
23 |
24 | # or start the server and open the app in a new browser tab
25 | npm run dev -- --open
26 | ```
27 |
28 | ## Building
29 |
30 | To create a production version of your app:
31 |
32 | ```bash
33 | npm run build
34 | ```
35 |
36 | You can preview the production build with `npm run preview`.
37 |
38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
39 |
--------------------------------------------------------------------------------
/packages/v4/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://shadcn-svelte.com/schema.json",
3 | "style": "default",
4 | "tailwind": {
5 | "config": "tailwind.config.ts",
6 | "css": "src/app.css",
7 | "baseColor": "slate"
8 | },
9 | "aliases": {
10 | "components": "$lib/components",
11 | "utils": "$lib/utils"
12 | },
13 | "typescript": true
14 | }
--------------------------------------------------------------------------------
/packages/v4/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js';
2 | import ts from 'typescript-eslint';
3 | import svelte from 'eslint-plugin-svelte';
4 | import prettier from 'eslint-config-prettier';
5 | import globals from 'globals';
6 |
7 | /** @type {import('eslint').Linter.Config[]} */
8 | export default [
9 | js.configs.recommended,
10 | ...ts.configs.recommended,
11 | ...svelte.configs['flat/recommended'],
12 | prettier,
13 | ...svelte.configs['flat/prettier'],
14 | {
15 | languageOptions: {
16 | globals: {
17 | ...globals.browser,
18 | ...globals.node
19 | }
20 | }
21 | },
22 | {
23 | files: ['**/*.svelte'],
24 | languageOptions: {
25 | parserOptions: {
26 | parser: ts.parser
27 | }
28 | }
29 | },
30 | {
31 | ignores: ['build/', '.svelte-kit/', 'dist/']
32 | }
33 | ];
34 |
--------------------------------------------------------------------------------
/packages/v4/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "v4",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "private": true,
6 | "scripts": {
7 | "dev": "vite dev",
8 | "build": "vite build",
9 | "preview": "vite preview",
10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
12 | "test": "vitest",
13 | "lint": "prettier --check . && eslint .",
14 | "format": "prettier --write ."
15 | },
16 | "devDependencies": {
17 | "@sveltejs/adapter-auto": "^3.0.0",
18 | "@sveltejs/adapter-static": "^3.0.4",
19 | "@sveltejs/kit": "^2.0.0",
20 | "@sveltejs/vite-plugin-svelte": "^3.0.0",
21 | "@tailwindcss/typography": "^0.5.14",
22 | "@types/eslint": "^9.6.0",
23 | "autoprefixer": "^10.4.20",
24 | "eslint": "^9.0.0",
25 | "eslint-config-prettier": "^9.1.0",
26 | "eslint-plugin-svelte": "^2.36.0",
27 | "globals": "^15.0.0",
28 | "prettier": "^3.1.1",
29 | "prettier-plugin-svelte": "^3.1.2",
30 | "prettier-plugin-tailwindcss": "^0.6.5",
31 | "svelte": "^4.2.7",
32 | "svelte-check": "^4.0.0",
33 | "tailwindcss": "^3.4.9",
34 | "typescript": "^5.0.0",
35 | "typescript-eslint": "^8.0.0",
36 | "vite": "^5.0.3",
37 | "vite-plugin-markdown": "^2.0.2",
38 | "vitest": "^2.0.0"
39 | },
40 | "dependencies": {
41 | "@fontsource/lxgw-wenkai": "^5.1.0",
42 | "bits-ui": "^0.21.13",
43 | "clsx": "^2.1.0",
44 | "mode-watcher": "^0.4.1",
45 | "svelte-radix": "^1.1.1",
46 | "sveltekit-i18n": "^2.4.2",
47 | "tailwind-merge": "^2.5.2",
48 | "tailwind-variants": "^0.2.1"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/v4/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/packages/v4/src/app.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 240 5.9% 10%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 240 5.9% 10%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | }
33 |
34 | .dark {
35 | --background: 240 10% 3.9%;
36 | --foreground: 0 0% 98%;
37 | --card: 240 10% 3.9%;
38 | --card-foreground: 0 0% 98%;
39 | --popover: 240 10% 3.9%;
40 | --popover-foreground: 0 0% 98%;
41 | --primary: 0 0% 98%;
42 | --primary-foreground: 240 5.9% 10%;
43 | --secondary: 240 3.7% 15.9%;
44 | --secondary-foreground: 0 0% 98%;
45 | --muted: 240 3.7% 15.9%;
46 | --muted-foreground: 240 5% 64.9%;
47 | --accent: 240 3.7% 15.9%;
48 | --accent-foreground: 0 0% 98%;
49 | --destructive: 0 62.8% 30.6%;
50 | --destructive-foreground: 0 0% 98%;
51 | --border: 240 3.7% 15.9%;
52 | --input: 240 3.7% 15.9%;
53 | --ring: 240 4.9% 83.9%;
54 | --chart-1: 220 70% 50%;
55 | --chart-2: 160 60% 45%;
56 | --chart-3: 30 80% 55%;
57 | --chart-4: 280 65% 60%;
58 | --chart-5: 340 75% 55%;
59 | }
60 | }
61 |
62 | @layer base {
63 | * {
64 | @apply border-border;
65 | }
66 | body {
67 | @apply bg-background text-foreground;
68 | font-family: 'LXGW WenKai';
69 | font-weight: normal;
70 | }
71 | .dark .prose {
72 | @apply prose-dark;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/packages/v4/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface PageState {}
9 | // interface Platform {}
10 | }
11 |
12 | declare module '*.md' {
13 | // "unknown" would be more detailed depends on how you structure frontmatter
14 | const attributes: Record
15 |
16 | // When "Mode.TOC" is requested
17 | const toc: { level: string; content: string }[]
18 |
19 | // When "Mode.HTML" is requested
20 | const html: string
21 |
22 | // When "Mode.RAW" is requested
23 | const raw: string
24 |
25 | // When "Mode.React" is requested. VFC could take a generic like React.VFC<{ MyComponent: TypeOfMyComponent }>
26 | import React from 'react'
27 | const ReactComponent: React.VFC
28 |
29 | // When "Mode.Vue" is requested
30 | import { ComponentOptions, Component } from 'vue'
31 | const VueComponent: ComponentOptions
32 | const VueComponentWith: (components: Record) => ComponentOptions
33 |
34 | // Modify below per your usage
35 | export { attributes, toc, html, ReactComponent, VueComponent, VueComponentWith }
36 | }
37 | }
38 |
39 | export {}
40 |
41 |
--------------------------------------------------------------------------------
/packages/v4/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | %sveltekit.head%
9 |
10 |
11 | %sveltekit.body%
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/v4/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 |
3 | describe('sum test', () => {
4 | it('adds 1 + 2 to equal 3', () => {
5 | expect(1 + 2).toBe(3);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/packages/v4/src/lib/components/logic/footer.svelte:
--------------------------------------------------------------------------------
1 |
46 |
47 |
67 |
--------------------------------------------------------------------------------
/packages/v4/src/lib/components/logic/navbar.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {$t('home.navbar.toggleTheme')}
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/packages/v4/src/lib/components/ui/button/button.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/packages/v4/src/lib/components/ui/button/index.ts:
--------------------------------------------------------------------------------
1 | import { type VariantProps, tv } from "tailwind-variants";
2 | import type { Button as ButtonPrimitive } from "bits-ui";
3 | import Root from "./button.svelte";
4 |
5 | const buttonVariants = tv({
6 | base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
7 | variants: {
8 | variant: {
9 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
10 | destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
11 | outline:
12 | "border-input bg-background hover:bg-accent hover:text-accent-foreground border",
13 | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
14 | ghost: "hover:bg-accent hover:text-accent-foreground",
15 | link: "text-primary underline-offset-4 hover:underline",
16 | },
17 | size: {
18 | default: "h-10 px-4 py-2",
19 | sm: "h-9 rounded-md px-3",
20 | lg: "h-11 rounded-md px-8",
21 | icon: "h-10 w-10",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | });
29 |
30 | type Variant = VariantProps["variant"];
31 | type Size = VariantProps["size"];
32 |
33 | type Props = ButtonPrimitive.Props & {
34 | variant?: Variant;
35 | size?: Size;
36 | };
37 |
38 | type Events = ButtonPrimitive.Events;
39 |
40 | export {
41 | Root,
42 | type Props,
43 | type Events,
44 | //
45 | Root as Button,
46 | type Props as ButtonProps,
47 | type Events as ButtonEvents,
48 | buttonVariants,
49 | };
50 |
--------------------------------------------------------------------------------
/packages/v4/src/lib/i18n/store.ts:
--------------------------------------------------------------------------------
1 | import { writable, derived } from 'svelte/store'
2 | import { translations, type Locale, type TranslationKey } from './translations'
3 |
4 | function createI18nStore() {
5 | const { subscribe, set } = writable('en-US')
6 |
7 | return {
8 | subscribe,
9 | setLocale: (locale: Locale) => set(locale),
10 | }
11 | }
12 |
13 | export const i18n = createI18nStore()
14 |
15 | export const t = derived(i18n, ($i18n) => (key: TranslationKey) => translations[$i18n][key] || key)
16 |
17 |
--------------------------------------------------------------------------------
/packages/v4/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | // place files you want to import through the `$lib` alias in this folder.
2 |
--------------------------------------------------------------------------------
/packages/v4/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 | import { cubicOut } from "svelte/easing";
4 | import type { TransitionConfig } from "svelte/transition";
5 |
6 | export function cn(...inputs: ClassValue[]) {
7 | return twMerge(clsx(inputs));
8 | }
9 |
10 | type FlyAndScaleParams = {
11 | y?: number;
12 | x?: number;
13 | start?: number;
14 | duration?: number;
15 | };
16 |
17 | export const flyAndScale = (
18 | node: Element,
19 | params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
20 | ): TransitionConfig => {
21 | const style = getComputedStyle(node);
22 | const transform = style.transform === "none" ? "" : style.transform;
23 |
24 | const scaleConversion = (
25 | valueA: number,
26 | scaleA: [number, number],
27 | scaleB: [number, number]
28 | ) => {
29 | const [minA, maxA] = scaleA;
30 | const [minB, maxB] = scaleB;
31 |
32 | const percentage = (valueA - minA) / (maxA - minA);
33 | const valueB = percentage * (maxB - minB) + minB;
34 |
35 | return valueB;
36 | };
37 |
38 | const styleToString = (
39 | style: Record
40 | ): string => {
41 | return Object.keys(style).reduce((str, key) => {
42 | if (style[key] === undefined) return str;
43 | return str + `${key}:${style[key]};`;
44 | }, "");
45 | };
46 |
47 | return {
48 | duration: params.duration ?? 200,
49 | delay: 0,
50 | css: (t) => {
51 | const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
52 | const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
53 | const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
54 |
55 | return styleToString({
56 | transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
57 | opacity: t
58 | });
59 | },
60 | easing: cubicOut
61 | };
62 | };
--------------------------------------------------------------------------------
/packages/v4/src/routes/+layout.server.ts:
--------------------------------------------------------------------------------
1 | import type { LayoutServerLoad } from './$types'
2 |
3 | export const load: LayoutServerLoad = async ({ request }) => {
4 | const acceptLanguage = request.headers.get('accept-language') || 'en-US'
5 | const language = acceptLanguage.split(',')[0].split('-')[0]
6 | const locale = language.includes('zh') ? 'zh-CN' : 'en-US'
7 |
8 | return { locale }
9 | }
10 |
11 | export const prerender = true
12 |
--------------------------------------------------------------------------------
/packages/v4/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/v4/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/packages/v4/src/routes/+page.ts:
--------------------------------------------------------------------------------
1 | import { error } from '@sveltejs/kit';
2 | import type { PageLoad } from './$types';
3 |
4 | export const load: PageLoad = () => {
5 | if (0 === 0) {
6 | return { title: 'Hello world!', content: 'Welcome to our blog. Lorem ipsum dolor sit amet...' };
7 | }
8 | error(404, 'Not found');
9 | };
10 |
--------------------------------------------------------------------------------
/packages/v4/src/routes/components/about.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
{$t('home.about.title')}
8 |
9 |
10 |
11 | {$t('home.about.section1')}
12 |
13 |
14 | {$t('home.about.section2')}
15 |
16 |
17 | {$t('home.about.section3')}
18 |
19 |
20 | {$t('home.about.section4')}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/packages/v4/src/routes/components/hero.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 | {$t('home.hero.title')}
9 |
10 |
11 | {$t('home.hero.description')}
12 |
13 |
14 | {$t('home.hero.button')}
15 |
16 |
17 |
--------------------------------------------------------------------------------
/packages/v4/src/routes/components/navbar.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 | {$t('home.navbar.about')}
8 | {$t('home.navbar.work')}
9 | {$t('home.navbar.life')}
10 | {$t('home.navbar.contact')}
11 |
12 |
--------------------------------------------------------------------------------
/packages/v4/src/routes/components/works.svelte:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
41 | {$t('home.footer.works.title')}
42 |
43 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/packages/v4/src/routes/ping/privacy/+page.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {@html html}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/packages/v4/src/routes/ping/privacy/privacy.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | > Last updated: 2025-05-01
4 |
5 | ## Introduction
6 |
7 | Welcome to our Android Ping Tool application. We are committed to protecting your privacy and ensuring the security of your personal information. This Privacy Policy explains how we collect, use, and protect your information when you use our Android application.
8 |
9 | ## Information We Collect
10 |
11 | - We do not collect any user data.
12 | - We do not track the use of the application.
13 | - We do not store any ping results or history.
14 | - We do not require any special permissions beyond basic network access for ping functionality.
15 |
16 | ## Information Sharing and Disclosure
17 |
18 | We do not share, sell, or distribute any personal information to third parties. The application operates entirely locally on your device.
19 |
20 | ## User Choices
21 |
22 | You have complete control over the application:
23 |
24 | - **Uninstall the App**: You can uninstall the application at any time through your device's settings.
25 | - **Network Access**: You can control the app's network access through your device's settings.
26 |
27 | ## Changes to This Privacy Policy
28 |
29 | We may update this Privacy Policy from time to time to reflect changes in our practices or legal requirements. We will notify you of any significant changes by posting the new Privacy Policy on our website and updating the "Last Updated" date at the top of this document.
30 |
31 | ## Contact Us
32 |
33 | If you have any questions or concerns about this Privacy Policy or our data practices, please contact us at:
34 |
35 | - **Email**: [rxliuli@gmail.com]
36 |
37 | ---
38 |
39 | By using our Android Ping Tool application, you acknowledge that you have read and understood this Privacy Policy and agree to its terms.
40 |
41 | Thank you for trusting us with your information.
42 |
43 |
--------------------------------------------------------------------------------
/packages/v4/src/routes/webstore/privacy/+page.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {@html html}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/packages/v4/src/routes/webstore/privacy/privacy.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | > Last updated: 2024-09-16
4 |
5 | ## Introduction
6 |
7 | Welcome to our Chrome extension. We are committed to protecting your privacy and ensuring the security of your personal information. This Privacy Policy explains how we collect, use, and protect your information when you use our Chrome extension.
8 |
9 | ## Information We Collect
10 |
11 | - We do not track the use of the extension at all.
12 | - We never send information or extension settings outside of your browser.
13 |
14 | ## Information Sharing and Disclosure
15 |
16 | We do not share, sell, or distribute your personal information to third parties. Your data is used solely within the context of the extension to provide the features and functionalities described above.
17 |
18 | ## User Choices
19 |
20 | You have control over the information we collect and how it is used. You can:
21 |
22 | - **Disable the Extension**: You can disable or uninstall the extension at any time through your browser's settings.
23 | - **Manage Permissions**: Adjust the permissions granted to the extension through your browser's extensions settings.
24 |
25 | ## Changes to This Privacy Policy
26 |
27 | We may update this Privacy Policy from time to time to reflect changes in our practices or legal requirements. We will notify you of any significant changes by posting the new Privacy Policy on our website and updating the "Last Updated" date at the top of this document.
28 |
29 | ## Contact Us
30 |
31 | If you have any questions or concerns about this Privacy Policy or our data practices, please contact us at:
32 |
33 | - **Email**: [rxliuli@gmail.com]
34 |
35 | ---
36 |
37 | By using our Chrome extension, you acknowledge that you have read and understood this Privacy Policy and agree to its terms.
38 |
39 | Thank you for trusting us with your information.
40 |
41 |
--------------------------------------------------------------------------------
/packages/v4/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v4/static/.nojekyll
--------------------------------------------------------------------------------
/packages/v4/static/CNAME:
--------------------------------------------------------------------------------
1 | rxliuli.com
--------------------------------------------------------------------------------
/packages/v4/static/ads.txt:
--------------------------------------------------------------------------------
1 | google.com, pub-9997051001110405, DIRECT, f08c47fec0942fa0
--------------------------------------------------------------------------------
/packages/v4/static/favicon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v4/static/favicon.jpg
--------------------------------------------------------------------------------
/packages/v4/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-static'
2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors
7 | // for more information about preprocessors
8 | preprocess: vitePreprocess(),
9 |
10 | kit: {
11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
14 | adapter: adapter({
15 | // default options are shown. On some platforms
16 | // these options are set automatically — see below
17 | pages: 'build',
18 | assets: 'build',
19 | fallback: undefined,
20 | precompress: false,
21 | strict: true,
22 | }),
23 | alias: {
24 | $lib: './src/lib',
25 | },
26 | },
27 | }
28 |
29 | export default config
30 |
31 |
--------------------------------------------------------------------------------
/packages/v4/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { fontFamily } from 'tailwindcss/defaultTheme'
2 | import type { Config } from 'tailwindcss'
3 | import typography from '@tailwindcss/typography'
4 |
5 | const config: Config = {
6 | darkMode: ['class'],
7 | content: ['./src/**/*.{html,js,svelte,ts}'],
8 | safelist: ['dark'],
9 | theme: {
10 | container: {
11 | center: true,
12 | padding: '2rem',
13 | screens: {
14 | '2xl': '1400px',
15 | },
16 | },
17 | extend: {
18 | colors: {
19 | border: 'hsl(var(--border) / )',
20 | input: 'hsl(var(--input) / )',
21 | ring: 'hsl(var(--ring) / )',
22 | background: 'hsl(var(--background) / )',
23 | foreground: 'hsl(var(--foreground) / )',
24 | primary: {
25 | DEFAULT: 'hsl(var(--primary) / )',
26 | foreground: 'hsl(var(--primary-foreground) / )',
27 | },
28 | secondary: {
29 | DEFAULT: 'hsl(var(--secondary) / )',
30 | foreground: 'hsl(var(--secondary-foreground) / )',
31 | },
32 | destructive: {
33 | DEFAULT: 'hsl(var(--destructive) / )',
34 | foreground: 'hsl(var(--destructive-foreground) / )',
35 | },
36 | muted: {
37 | DEFAULT: 'hsl(var(--muted) / )',
38 | foreground: 'hsl(var(--muted-foreground) / )',
39 | },
40 | accent: {
41 | DEFAULT: 'hsl(var(--accent) / )',
42 | foreground: 'hsl(var(--accent-foreground) / )',
43 | },
44 | popover: {
45 | DEFAULT: 'hsl(var(--popover) / )',
46 | foreground: 'hsl(var(--popover-foreground) / )',
47 | },
48 | card: {
49 | DEFAULT: 'hsl(var(--card) / )',
50 | foreground: 'hsl(var(--card-foreground) / )',
51 | },
52 | },
53 | borderRadius: {
54 | lg: 'var(--radius)',
55 | md: 'calc(var(--radius) - 2px)',
56 | sm: 'calc(var(--radius) - 4px)',
57 | },
58 | fontFamily: {
59 | sans: [...fontFamily.sans],
60 | },
61 | typography: (theme) => ({
62 | DEFAULT: {
63 | css: {
64 | color: theme('colors.gray.900'),
65 | // ... other default styles ...
66 | },
67 | },
68 | dark: {
69 | css: {
70 | color: theme('colors.gray.300'),
71 | a: {
72 | color: theme('colors.blue.400'),
73 | '&:hover': {
74 | color: theme('colors.blue.300'),
75 | },
76 | },
77 | h1: {
78 | color: theme('colors.gray.100'),
79 | },
80 | h2: {
81 | color: theme('colors.gray.100'),
82 | },
83 | strong: {
84 | color: theme('colors.gray.100'),
85 | },
86 | blockquote: {
87 | color: theme('colors.gray.100'),
88 | },
89 | },
90 | },
91 | }),
92 | },
93 | },
94 | plugins: [typography()],
95 | }
96 |
97 | export default config
98 |
99 |
--------------------------------------------------------------------------------
/packages/v4/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "bundler"
13 | }
14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
16 | //
17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
18 | // from the referenced tsconfig.json - TypeScript does not merge them in
19 | }
20 |
--------------------------------------------------------------------------------
/packages/v4/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite'
2 | import { defineConfig } from 'vitest/config'
3 | import { Mode, plugin } from 'vite-plugin-markdown'
4 |
5 | export default defineConfig({
6 | plugins: [sveltekit(), plugin({ mode: [Mode.HTML] })],
7 | test: {
8 | include: ['src/**/*.{test,spec}.{js,ts}'],
9 | },
10 | })
11 |
12 |
--------------------------------------------------------------------------------
/packages/v5/.cta.json:
--------------------------------------------------------------------------------
1 | {
2 | "framework": "react",
3 | "typescript": true,
4 | "projectName": "v5",
5 | "mode": "file-router",
6 | "tailwind": true,
7 | "packageManager": "pnpm",
8 | "toolchain": "none",
9 | "variableValues": {},
10 | "git": true,
11 | "version": 1,
12 | "existingAddOns": [
13 | "shadcn",
14 | "start"
15 | ]
16 | }
--------------------------------------------------------------------------------
/packages/v5/.cursorrules:
--------------------------------------------------------------------------------
1 | # shadcn instructions
2 |
3 | Use the latest version of Shadcn to install new components, like this command to add a button component:
4 |
5 | ```bash
6 | pnpx shadcn@latest add button
7 | ```
8 |
--------------------------------------------------------------------------------
/packages/v5/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 | .output
7 | .vinxi
8 | stats.html
9 | .temp
--------------------------------------------------------------------------------
/packages/v5/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.watcherExclude": {
3 | "**/routeTree.gen.ts": true
4 | },
5 | "search.exclude": {
6 | "**/routeTree.gen.ts": true
7 | },
8 | "files.readonlyInclude": {
9 | "**/routeTree.gen.ts": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/v5/app.config.timestamp_1748251366325.js:
--------------------------------------------------------------------------------
1 | // app.config.ts
2 | import { defineConfig } from "@tanstack/react-start/config";
3 | import viteTsConfigPaths from "vite-tsconfig-paths";
4 | import tailwindcss from "@tailwindcss/vite";
5 | import markdownPlugin from "unplugin-markdown/vite";
6 | var config = defineConfig({
7 | tsr: {
8 | appDirectory: "src"
9 | },
10 | vite: {
11 | plugins: [
12 | // this is the plugin that enables path aliases
13 | viteTsConfigPaths({
14 | projects: ["./tsconfig.json"]
15 | }),
16 | tailwindcss(),
17 | markdownPlugin()
18 | // analyzer({
19 | // analyzerMode: 'static',
20 | // fileName: 'stats.html',
21 | // }),
22 | ]
23 | },
24 | server: {
25 | prerender: {
26 | routes: ["/", "/ping/privacy", "/webstore/privacy"],
27 | crawlLinks: true
28 | }
29 | }
30 | });
31 | var app_config_default = config;
32 | export {
33 | app_config_default as default
34 | };
35 |
--------------------------------------------------------------------------------
/packages/v5/app.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@tanstack/react-start/config'
2 | import viteTsConfigPaths from 'vite-tsconfig-paths'
3 | import tailwindcss from '@tailwindcss/vite'
4 | import markdownPlugin from 'unplugin-markdown/vite'
5 | import type { Plugin } from 'vite'
6 |
7 | const config = defineConfig({
8 | tsr: {
9 | appDirectory: 'src',
10 | },
11 | vite: {
12 | plugins: [
13 | // this is the plugin that enables path aliases
14 | viteTsConfigPaths({
15 | projects: ['./tsconfig.json'],
16 | }),
17 | tailwindcss(),
18 | markdownPlugin() as Plugin,
19 | // analyzer({
20 | // analyzerMode: 'static',
21 | // fileName: 'stats.html',
22 | // }),
23 | ],
24 | },
25 | server: {
26 | prerender: {
27 | routes: ['/', '/ping/privacy', '/webstore/privacy'],
28 | crawlLinks: true,
29 | },
30 | },
31 | })
32 |
33 | export default config
34 |
35 |
--------------------------------------------------------------------------------
/packages/v5/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/styles.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/packages/v5/count.txt:
--------------------------------------------------------------------------------
1 | 5
--------------------------------------------------------------------------------
/packages/v5/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "v5",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "dev": "vinxi dev",
7 | "start": "vinxi start",
8 | "build": "vinxi build",
9 | "serve": "vite preview",
10 | "test": "vitest run",
11 | "generate-sitemap": "tsx scripts/generate-sitemap.ts"
12 | },
13 | "dependencies": {
14 | "@radix-ui/react-dropdown-menu": "^2.1.15",
15 | "@radix-ui/react-select": "^2.2.2",
16 | "@radix-ui/react-separator": "^1.1.7",
17 | "@radix-ui/react-slot": "^1.2.0",
18 | "@radix-ui/react-tabs": "^1.1.9",
19 | "@tailwindcss/vite": "^4.0.6",
20 | "@tanstack/react-router": "^1.114.3",
21 | "@tanstack/react-router-devtools": "^1.114.3",
22 | "@tanstack/react-router-with-query": "^1.114.3",
23 | "@tanstack/react-start": "^1.114.3",
24 | "@tanstack/router-plugin": "^1.114.3",
25 | "class-variance-authority": "^0.7.1",
26 | "clsx": "^2.1.1",
27 | "dayjs": "^1.11.13",
28 | "es-toolkit": "^1.37.0",
29 | "lucide-react": "^0.476.0",
30 | "react": "^19.0.0",
31 | "react-dom": "^19.0.0",
32 | "react-icons": "^5.5.0",
33 | "tailwind-merge": "^3.0.2",
34 | "tailwindcss": "^4.0.6",
35 | "tailwindcss-animate": "^1.0.7",
36 | "vinxi": "^0.5.3",
37 | "vite-tsconfig-paths": "^5.1.4"
38 | },
39 | "devDependencies": {
40 | "@tailwindcss/typography": "^0.5.16",
41 | "@testing-library/dom": "^10.4.0",
42 | "@testing-library/react": "^16.2.0",
43 | "@types/react": "^19.0.8",
44 | "@types/react-dom": "^19.0.3",
45 | "@vitejs/plugin-react": "^4.3.4",
46 | "rollup-plugin-visualizer": "^5.14.0",
47 | "tailwindcss-github-markdown": "^0.1.0",
48 | "tsx": "^4.19.4",
49 | "typescript": "^5.7.2",
50 | "unplugin-markdown": "^0.1.0",
51 | "vite": "^6.1.0",
52 | "vite-bundle-analyzer": "^0.20.1",
53 | "vitest": "^3.1.2",
54 | "web-vitals": "^4.2.4"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/packages/v5/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/favicon.png
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/bilibili-markdown.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/bilibili-markdown.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/clean-twitter.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/clean-twitter.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/cors-unblock.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/cors-unblock.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/idbport.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/idbport.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/joplin-vscode-plugin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/joplin-vscode-plugin.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/mass-block-twitter.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/mass-block-twitter.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/myunzip.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/myunzip.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/ping.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/ping.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/ponytab.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/ponytab.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/redirector.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/redirector.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/window-resizer-preferences.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/window-resizer-preferences.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/window-resizer-tray-menu.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/window-resizer-tray-menu.jpg
--------------------------------------------------------------------------------
/packages/v5/public/images/projects/window-resizer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/images/projects/window-resizer.jpg
--------------------------------------------------------------------------------
/packages/v5/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/logo.png
--------------------------------------------------------------------------------
/packages/v5/public/logo192.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/logo192.jpg
--------------------------------------------------------------------------------
/packages/v5/public/logo512.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/logo512.jpg
--------------------------------------------------------------------------------
/packages/v5/public/logo96.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rxliuli/site/b281f05e69b0d562c20e4c040be1e4d04ce90b72/packages/v5/public/logo96.jpg
--------------------------------------------------------------------------------
/packages/v5/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "TanStack App",
3 | "name": "Create TanStack App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.jpg",
12 | "type": "image/jpeg",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.jpg",
17 | "type": "image/jpeg",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/packages/v5/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Allow: /
4 |
5 | Sitemap: https://rxliuli.com/sitemap.xml
6 |
--------------------------------------------------------------------------------
/packages/v5/scripts/generate-sitemap.ts:
--------------------------------------------------------------------------------
1 | import { writeFileSync } from 'fs'
2 | import { join } from 'path'
3 |
4 | const BASE_URL = 'https://rxliuli.com'
5 |
6 | const routes = [
7 | '/',
8 | '/about',
9 | '/blog',
10 | '/projects',
11 | ]
12 |
13 | // Mock data for blog posts and projects
14 | const blogPosts = [
15 | { slug: 'extracting-large-zip-files-with-directory-structure-in-web-browsers' },
16 | { slug: 'convert-chrome-extension-to-safari' },
17 | ]
18 |
19 | const projects = [
20 | { slug: 'mass-block-twitter' },
21 | { slug: 'ping' },
22 | { slug: 'window-resizer' },
23 | { slug: 'cors-unblock' },
24 | { slug: 'redirector' },
25 | { slug: 'myunzip' },
26 | { slug: 'joplin-vscode-plugin' },
27 | { slug: 'clean-twitter' },
28 | { slug: 'bilibili-markdown' },
29 | ]
30 |
31 | const generateSitemap = () => {
32 | const projectSlugs = projects.map(project => `/project/${project.slug}`)
33 | const blogPostSlugs = blogPosts.map(post => `/blog/${post.slug}`)
34 |
35 | const allRoutes = [...routes, ...projectSlugs, ...blogPostSlugs]
36 |
37 | const sitemap = `
38 |
39 | ${allRoutes
40 | .map(
41 | (route) => `
42 |
43 | ${BASE_URL}${route}
44 | ${route === '/' ? 'daily' : 'weekly'}
45 | ${route === '/' ? '1.0' : '0.8'}
46 | `
47 | )
48 | .join('')}
49 | `
50 |
51 | writeFileSync(join(process.cwd(), 'public', 'sitemap.xml'), sitemap)
52 | console.log('Sitemap generated successfully!')
53 | }
54 |
55 | generateSitemap()
56 |
--------------------------------------------------------------------------------
/packages/v5/src/api.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createStartAPIHandler,
3 | defaultAPIFileRouteHandler,
4 | } from '@tanstack/react-start/api'
5 |
6 | export default createStartAPIHandler(defaultAPIFileRouteHandler)
7 |
--------------------------------------------------------------------------------
/packages/v5/src/client.tsx:
--------------------------------------------------------------------------------
1 | import { hydrateRoot } from 'react-dom/client'
2 | import { StartClient } from '@tanstack/react-start'
3 |
4 | import { createRouter } from './router'
5 |
6 | const router = createRouter()
7 |
8 | hydrateRoot(document, )
9 |
--------------------------------------------------------------------------------
/packages/v5/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from '@tanstack/react-router'
2 |
3 | export default function Header() {
4 | return (
5 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/packages/v5/src/components/MarkdownView.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import type { HTMLAttributes } from 'react'
3 |
4 | export function MarkdownView(props: HTMLAttributes) {
5 | return
6 | }
7 |
--------------------------------------------------------------------------------
/packages/v5/src/components/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from '@tanstack/react-router'
2 |
3 | export function NotFound() {
4 | return (
5 |
6 |
404
7 |
Page Not Found
8 |
9 | The page you're looking for doesn't exist or has been moved.
10 |
11 |
15 | Return Home
16 |
17 |
18 | )
19 | }
--------------------------------------------------------------------------------
/packages/v5/src/components/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { createContext, useContext, useEffect, useState } from "react"
4 |
5 | type Theme = "dark" | "light" | "system"
6 |
7 | type ThemeProviderProps = {
8 | children: React.ReactNode
9 | defaultTheme?: Theme
10 | storageKey?: string
11 | }
12 |
13 | type ThemeProviderState = {
14 | theme: Theme
15 | setTheme: (theme: Theme) => void
16 | }
17 |
18 | const initialState: ThemeProviderState = {
19 | theme: "system",
20 | setTheme: () => null,
21 | }
22 |
23 | const ThemeProviderContext = createContext(initialState)
24 |
25 | export function ThemeProvider({
26 | children,
27 | defaultTheme = "system",
28 | storageKey = "vite-ui-theme",
29 | ...props
30 | }: ThemeProviderProps) {
31 | const [theme, setTheme] = useState(defaultTheme)
32 | const [mounted, setMounted] = useState(false)
33 |
34 | useEffect(() => {
35 | setMounted(true)
36 | const stored = localStorage.getItem(storageKey) as Theme
37 | if (stored) {
38 | setTheme(stored)
39 | }
40 | }, [storageKey])
41 |
42 | useEffect(() => {
43 | if (!mounted) return
44 |
45 | const root = window.document.documentElement
46 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
47 | const currentTheme = theme === "system" ? systemTheme : theme
48 |
49 | root.setAttribute("data-theme", currentTheme)
50 | root.classList.remove("light", "dark")
51 | root.classList.add(currentTheme)
52 |
53 | // 监听系统主题变化
54 | const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
55 | const handleChange = () => {
56 | if (theme === "system") {
57 | const newSystemTheme = mediaQuery.matches ? "dark" : "light"
58 | root.setAttribute("data-theme", newSystemTheme)
59 | root.classList.remove("light", "dark")
60 | root.classList.add(newSystemTheme)
61 | }
62 | }
63 |
64 | mediaQuery.addEventListener("change", handleChange)
65 | return () => mediaQuery.removeEventListener("change", handleChange)
66 | }, [theme, mounted])
67 |
68 | const value = {
69 | theme,
70 | setTheme: (theme: Theme) => {
71 | localStorage.setItem(storageKey, theme)
72 | setTheme(theme)
73 | },
74 | }
75 |
76 | return (
77 |
78 | {children}
79 |
80 | )
81 | }
82 |
83 | export const useTheme = () => {
84 | const context = useContext(ThemeProviderContext)
85 |
86 | if (context === undefined)
87 | throw new Error("useTheme must be used within a ThemeProvider")
88 |
89 | return context
90 | }
--------------------------------------------------------------------------------
/packages/v5/src/components/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Moon, Sun } from 'lucide-react'
4 | import { useTheme } from '@/components/ThemeProvider'
5 |
6 | export function ThemeToggle() {
7 | const { theme, setTheme } = useTheme()
8 |
9 | return (
10 | setTheme(theme === 'dark' ? 'light' : 'dark')}
12 | className="inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
13 | aria-label="Toggle theme"
14 | >
15 |
16 |
17 |
18 | )
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/packages/v5/src/components/blog/BlogPostCard.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@tanstack/react-router";
2 | import type { BlogPost } from "@/types/blog";
3 | import { CalendarIcon, ClockIcon } from "lucide-react";
4 | import { Badge } from "@/components/ui/badge";
5 | import { formatDate } from "@/data/blog";
6 |
7 | interface BlogPostCardProps {
8 | post: BlogPost;
9 | }
10 |
11 | export function BlogPostCard({ post }: BlogPostCardProps) {
12 | return (
13 |
14 | {/* Date and reading time */}
15 |
16 |
17 |
18 | {formatDate(post.date)}
19 |
20 | {post.readingTime && (
21 |
22 |
23 | {post.readingTime}
24 |
25 | )}
26 |
27 |
28 | {/* Title and summary */}
29 |
30 |
31 |
36 | {post.title}
37 |
38 |
39 |
40 | {post.summary}
41 |
42 |
43 |
44 | {/* Tags */}
45 | {post.tags && post.tags.length > 0 && (
46 |
47 | {post.tags.map(tag => (
48 |
49 | {tag}
50 |
51 | ))}
52 |
53 | )}
54 |
55 | {/* Read more link */}
56 |
57 |
62 | Read more
63 |
64 |
65 |
66 | );
67 | }
--------------------------------------------------------------------------------
/packages/v5/src/components/blog/Outline.tsx:
--------------------------------------------------------------------------------
1 | import type { TocItem as OutlineItem } from '@/plugins/markdownToc'
2 | import { useEffect, useState } from 'react'
3 |
4 | interface OutlineProps {
5 | outline: OutlineItem[]
6 | }
7 |
8 | function OutlineItem({ item, activeId }: { item: OutlineItem; activeId: string }) {
9 | return (
10 |
11 | {
19 | e.preventDefault()
20 | document.getElementById(item.id)?.scrollIntoView({ behavior: 'smooth' })
21 | }}
22 | >
23 | {item.text}
24 |
25 | {item.children && (
26 |
27 | {item.children.map((child) => (
28 |
29 | ))}
30 |
31 | )}
32 |
33 | )
34 | }
35 |
36 | export function Outline(props: OutlineProps) {
37 | const [activeId, setActiveId] = useState('')
38 |
39 | // Track which heading is currently in view
40 | useEffect(() => {
41 | if (props.outline.length === 0) return
42 |
43 | const observer = new IntersectionObserver(
44 | (entries) => {
45 | entries.forEach((entry) => {
46 | if (entry.isIntersecting) {
47 | setActiveId(entry.target.id)
48 | }
49 | })
50 | },
51 | {
52 | rootMargin: '0px 0px -80% 0px',
53 | },
54 | )
55 |
56 | const observeHeadings = (items: OutlineItem[]) => {
57 | items.forEach((item) => {
58 | const element = document.getElementById(item.id)
59 | if (element) observer.observe(element)
60 | if (item.children) observeHeadings(item.children)
61 | })
62 | }
63 |
64 | observeHeadings(props.outline)
65 |
66 | return () => {
67 | const unobserveHeadings = (items: OutlineItem[]) => {
68 | items.forEach((item) => {
69 | const element = document.getElementById(item.id)
70 | if (element) observer.unobserve(element)
71 | if (item.children) unobserveHeadings(item.children)
72 | })
73 | }
74 | unobserveHeadings(props.outline)
75 | }
76 | }, [props.outline])
77 |
78 | if (props.outline.length === 0) return null
79 |
80 | return (
81 |
82 |
Outline
83 |
84 | {props.outline.map((item) => (
85 |
86 | ))}
87 |
88 |
89 | )
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/packages/v5/src/components/layout/footer.tsx:
--------------------------------------------------------------------------------
1 | export function Footer() {
2 | return (
3 |
22 | );
23 | }
--------------------------------------------------------------------------------
/packages/v5/src/components/layout/header.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@tanstack/react-router";
2 | import { ThemeToggle } from "../ThemeToggle";
3 |
4 | export function Header() {
5 | const navigation = [
6 | { name: "Home", href: "/" },
7 | { name: "Projects", href: "/project" },
8 | { name: "Blog", href: "/blog" },
9 | // { name: "About", href: "/about" }
10 | ];
11 |
12 | return (
13 |
14 |
15 |
16 | {/* Logo/Site Name */}
17 |
18 |
19 |
20 |
21 | {/* Navigation Links */}
22 |
23 | {navigation.map((item) => (
24 |
32 | {item.name}
33 |
34 | ))}
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
--------------------------------------------------------------------------------
/packages/v5/src/components/layout/root-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Header } from './header'
2 | import { Footer } from './footer'
3 | import { ThemeProvider } from '../ThemeProvider'
4 |
5 | interface RootLayoutProps {
6 | children: React.ReactNode
7 | }
8 |
9 | export function RootLayout({ children }: RootLayoutProps) {
10 | return (
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/packages/v5/src/components/project/project-icon.tsx:
--------------------------------------------------------------------------------
1 | import type { ProjectLink } from '@/types/project'
2 | import { BiLogoEdge } from 'react-icons/bi'
3 | import { SiDiscord, SiFirefox, SiGooglechrome, SiProducthunt, SiSafari } from 'react-icons/si'
4 |
5 | const IconMap: Record> = {
6 | firefox: SiFirefox,
7 | chrome: SiGooglechrome,
8 | edge: BiLogoEdge,
9 | safari: SiSafari,
10 | discord: SiDiscord,
11 | producthunt: SiProducthunt,
12 | }
13 |
14 | export function ProjectIcon({ icon }: { icon: ProjectLink['icon'] }) {
15 | const Icon = IconMap[icon]
16 | return
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/packages/v5/src/components/seo.tsx:
--------------------------------------------------------------------------------
1 | import type { AnyRouteMatch } from '@tanstack/react-router'
2 |
3 | interface SEOProps {
4 | title: string
5 | description: string
6 | image?: string
7 | type?: 'website' | 'article'
8 | publishedTime?: string
9 | modifiedTime?: string
10 | tags?: string[]
11 | }
12 |
13 | export function meta(props: SEOProps): AnyRouteMatch['meta'] {
14 | const fullTitle = `${props.title} | rxliuli`
15 |
16 | const image = props.image ?? 'https://rxliuli.com/logo.png'
17 | const r: AnyRouteMatch['meta'] = [
18 | { name: 'title', content: fullTitle },
19 | { name: 'description', content: props.description },
20 | { name: 'og:type', content: props.type },
21 | { name: 'og:title', content: fullTitle },
22 | { name: 'og:description', content: props.description },
23 | { name: 'og:image', content: image },
24 | { name: 'twitter:card', content: props.image ? 'summary_large_image' : 'summary' },
25 | { name: 'twitter:title', content: fullTitle },
26 | { name: 'twitter:description', content: props.description },
27 | { name: 'twitter:image', content: image },
28 | ]
29 | if (props.tags) {
30 | r.push({ name: 'article:tag', content: props.tags?.join(',') })
31 | }
32 | if (props.publishedTime) {
33 | r.push({ name: 'article:published_time', content: props.publishedTime })
34 | }
35 | if (props.modifiedTime) {
36 | r.push({ name: 'article:modified_time', content: props.modifiedTime })
37 | }
38 | return r
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/packages/v5/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const badgeVariants = cva(
8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14 | secondary:
15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 | destructive:
17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18 | outline:
19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | },
25 | }
26 | )
27 |
28 | function Badge({
29 | className,
30 | variant,
31 | asChild = false,
32 | ...props
33 | }: React.ComponentProps<"span"> &
34 | VariantProps & { asChild?: boolean }) {
35 | const Comp = asChild ? Slot : "span"
36 |
37 | return (
38 |
43 | )
44 | }
45 |
46 | export { Badge, badgeVariants }
47 |
--------------------------------------------------------------------------------
/packages/v5/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean
47 | }) {
48 | const Comp = asChild ? Slot : "button"
49 |
50 | return (
51 |
56 | )
57 | }
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/packages/v5/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
28 | )
29 | }
30 |
31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32 | return (
33 |
38 | )
39 | }
40 |
41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42 | return (
43 |
48 | )
49 | }
50 |
51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52 | return (
53 |
61 | )
62 | }
63 |
64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65 | return (
66 |
71 | )
72 | }
73 |
74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75 | return (
76 |
81 | )
82 | }
83 |
84 | export {
85 | Card,
86 | CardHeader,
87 | CardFooter,
88 | CardTitle,
89 | CardAction,
90 | CardDescription,
91 | CardContent,
92 | }
93 |
--------------------------------------------------------------------------------
/packages/v5/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | )
19 | }
20 |
21 | export { Input }
22 |
--------------------------------------------------------------------------------
/packages/v5/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function Separator({
7 | className,
8 | orientation = "horizontal",
9 | decorative = true,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
23 | )
24 | }
25 |
26 | export { Separator }
27 |
--------------------------------------------------------------------------------
/packages/v5/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | function Tabs({
7 | className,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | function TabsList({
20 | className,
21 | ...props
22 | }: React.ComponentProps) {
23 | return (
24 |
32 | )
33 | }
34 |
35 | function TabsTrigger({
36 | className,
37 | ...props
38 | }: React.ComponentProps) {
39 | return (
40 |
48 | )
49 | }
50 |
51 | function TabsContent({
52 | className,
53 | ...props
54 | }: React.ComponentProps) {
55 | return (
56 |
61 | )
62 | }
63 |
64 | export { Tabs, TabsList, TabsTrigger, TabsContent }
65 |
--------------------------------------------------------------------------------
/packages/v5/src/data/blog.ts:
--------------------------------------------------------------------------------
1 | import type { TocItem } from '@/plugins/markdownToc'
2 | import type { BlogPost } from '@/types/blog'
3 | import dayjs from 'dayjs'
4 |
5 | // 获取所有文章的元数据
6 | export function getAllBlogPosts(): Omit[] {
7 | const blogFiles = import.meta.glob('./blog/*.md', { query: '?frontmatter', eager: true, import: 'default' })
8 | const posts: Omit[] = []
9 |
10 | for (const path in blogFiles) {
11 | const data = blogFiles[path]
12 |
13 | posts.push({
14 | id: data.slug,
15 | title: data.title,
16 | slug: data.slug,
17 | date: data.date,
18 | summary: data.summary,
19 | tags: data.tags || [],
20 | status: data.status || 'published',
21 | coverImage: data.coverImage,
22 | author: data.author,
23 | })
24 | }
25 |
26 | // Sort by date (newest first)
27 | return posts
28 | .filter((post) => post.status === 'published')
29 | .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
30 | }
31 |
32 | // 获取单篇文章的完整内容
33 | export async function getBlogPostBySlug(slug: string): Promise {
34 | const blogHtmls = import.meta.glob('./blog/*.md', { query: '?html', import: 'default' })
35 | const blogTocs = import.meta.glob('./blog/*.md', { query: '?toc', import: 'default' })
36 | const posts = getAllBlogPosts()
37 | const post = posts.find((post) => post.slug === slug)
38 |
39 | if (!post) {
40 | return null
41 | }
42 |
43 | try {
44 | const [html, toc] = await Promise.all([blogHtmls[`./blog/${slug}.md`](), blogTocs[`./blog/${slug}.md`]()])
45 | const wordCount = html.trim().split(/\s+/).length
46 | const readingTime = `${Math.ceil(wordCount / 200)} min read`
47 |
48 | return {
49 | ...post,
50 | content: html,
51 | toc,
52 | readingTime,
53 | }
54 | } catch (error) {
55 | console.error(`Failed to load markdown content for blog post ${slug}:`, error)
56 | return post
57 | }
58 | }
59 |
60 | // 获取所有标签
61 | export function getAllTags(): string[] {
62 | const posts = getAllBlogPosts()
63 | const tags = new Set()
64 |
65 | posts.forEach((post) => {
66 | if (post.tags) {
67 | post.tags.forEach((tag) => tags.add(tag))
68 | }
69 | })
70 |
71 | return Array.from(tags)
72 | }
73 |
74 | // 获取特定标签的文章
75 | export function getPostsByTag(tag: string): Omit[] {
76 | return getAllBlogPosts().filter((post) => post.tags?.includes(tag))
77 | }
78 |
79 | // 获取精选文章
80 | export function getFeaturedPosts(limit = 3): Omit[] {
81 | return getAllBlogPosts().slice(0, limit)
82 | }
83 |
84 | // 格式化日期
85 | export function formatDate(date: string): string {
86 | return dayjs(date).format('YYYY-MM-DD')
87 | }
88 |
89 |
--------------------------------------------------------------------------------
/packages/v5/src/data/privacy/ping.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | > Last updated: 2025-05-01
4 |
5 | ## Introduction
6 |
7 | Welcome to our Android Ping Tool application. We are committed to protecting your privacy and ensuring the security of your personal information. This Privacy Policy explains how we collect, use, and protect your information when you use our Android application.
8 |
9 | ## Information We Collect
10 |
11 | - We do not collect any user data.
12 | - We do not track the use of the application.
13 | - We do not store any ping results or history.
14 | - We do not require any special permissions beyond basic network access for ping functionality.
15 |
16 | ## Information Sharing and Disclosure
17 |
18 | We do not share, sell, or distribute any personal information to third parties. The application operates entirely locally on your device.
19 |
20 | ## User Choices
21 |
22 | You have complete control over the application:
23 |
24 | - **Uninstall the App**: You can uninstall the application at any time through your device's settings.
25 | - **Network Access**: You can control the app's network access through your device's settings.
26 |
27 | ## Changes to This Privacy Policy
28 |
29 | We may update this Privacy Policy from time to time to reflect changes in our practices or legal requirements. We will notify you of any significant changes by posting the new Privacy Policy on our website and updating the "Last Updated" date at the top of this document.
30 |
31 | ## Contact Us
32 |
33 | If you have any questions or concerns about this Privacy Policy or our data practices, please contact us at:
34 |
35 | - **Email**: [rxliuli@gmail.com]
36 |
37 | ---
38 |
39 | By using our Android Ping Tool application, you acknowledge that you have read and understood this Privacy Policy and agree to its terms.
40 |
41 | Thank you for trusting us with your information.
42 |
--------------------------------------------------------------------------------
/packages/v5/src/data/privacy/webstore.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | > Last updated: 2024-09-16
4 |
5 | ## Introduction
6 |
7 | Welcome to our Chrome extension. We are committed to protecting your privacy and ensuring the security of your personal information. This Privacy Policy explains how we collect, use, and protect your information when you use our Chrome extension.
8 |
9 | ## Information We Collect
10 |
11 | - We do not track the use of the extension at all.
12 | - We never send information or extension settings outside of your browser.
13 |
14 | ## Information Sharing and Disclosure
15 |
16 | We do not share, sell, or distribute your personal information to third parties. Your data is used solely within the context of the extension to provide the features and functionalities described above.
17 |
18 | ## User Choices
19 |
20 | You have control over the information we collect and how it is used. You can:
21 |
22 | - **Disable the Extension**: You can disable or uninstall the extension at any time through your browser's settings.
23 | - **Manage Permissions**: Adjust the permissions granted to the extension through your browser's extensions settings.
24 |
25 | ## Changes to This Privacy Policy
26 |
27 | We may update this Privacy Policy from time to time to reflect changes in our practices or legal requirements. We will notify you of any significant changes by posting the new Privacy Policy on our website and updating the "Last Updated" date at the top of this document.
28 |
29 | ## Contact Us
30 |
31 | If you have any questions or concerns about this Privacy Policy or our data practices, please contact us at:
32 |
33 | - **Email**: [rxliuli@gmail.com]
34 |
35 | ---
36 |
37 | By using our Chrome extension, you acknowledge that you have read and understood this Privacy Policy and agree to its terms.
38 |
39 | Thank you for trusting us with your information.
40 |
--------------------------------------------------------------------------------
/packages/v5/src/data/projects/idbport.md:
--------------------------------------------------------------------------------
1 | # IDBPort
2 |
3 | **IDBPort** is a modern browser extension designed for effortless export and import of IndexedDB data. Say goodbye to export-only tools and welcome a complete data migration and backup solution!
4 |
5 | ## ✨ Features
6 |
7 | - **Effortless Export:**
8 | - Select IndexedDB databases for specific websites.
9 | - Export data into easy-to-read and process `.idb` files.
10 | - **Robust Import:**
11 | - Import data from `.idb` files into specified IndexedDB databases.
12 | - **User-Friendly Interface:**
13 | - Built with React and shadcn/ui for a beautiful, intuitive, and responsive experience.
14 | - Clear, step-by-step guidance, no expert knowledge required.
15 | - **Cross-Browser (Target):**
16 | - Built with WXT, aiming for easy packaging as an extension for major browsers like Chrome, Firefox, Edge, and Safari.
17 | - **Complex Data Type Support (Target):**
18 | - Correctly handles IndexedDB-supported types like `Date`, `Blob`, `File`, `ArrayBuffer` (serialization & deserialization).
19 |
20 | ## 🚀 Why IDBPort?
21 |
22 | Many existing IndexedDB tools only offer export functionality, making data migration, backup recovery, or syncing data across different environments incomplete. IDBPort aims to fill this gap by providing a comprehensive export _and_ import solution, empowering developers and power users to fully manage their local web application data.
23 |
24 | ## 🛠️ Installation
25 |
26 | Once released to browser extension stores, you can install it as follows:
27 |
28 | - **Chrome:** Go to the Chrome Web Store, search for "IDBPort," and install.
29 | - **Firefox:** Go to Firefox Add-ons, search for "IDBPort," and install.
30 | - **(Similar for other browsers)**
31 |
32 | ## 📖 How to Use
33 |
34 | 1. **Open the Extension:** Click the IDBPort icon in your browser's toolbar.
35 | 2. **Authorize (if needed):** The extension may require permission to access data on your current tab.
36 |
37 | ### Exporting Data
38 |
39 | 1. **Select Database:** The extension will attempt to list accessible IndexedDB databases on the current page. Choose the database you want to export from the dropdown. (Firefox requires manual input).
40 | 2. **Click "Export":** The data will be saved as an `.idb` file.
41 |
42 | ### Importing Data
43 |
44 | 1. Switch to the "Import" tab.
45 | 2. **Select File:** Click "Choose File" and select a previously exported `.idb` file.
46 | 3. Data will be automatically imported into the IndexedDB database.
47 |
48 | ## 💻 Tech Stack
49 |
50 | - **Framework:** [WXT](https://wxt.dev/)
51 | - **UI Library:** [React](https://react.dev/)
52 | - **CSS Framework:** [Tailwind CSS](https://tailwindcss.com/)
53 | - **Component Library:** [shadcn/ui](https://ui.shadcn.com/)
54 | - **Language:** TypeScript
55 |
56 | ## 🤝 Contributing
57 |
58 | Contributions of all kinds are welcome! Whether it's reporting bugs, suggesting new features, or submitting Pull Requests.
59 |
60 | 1. Fork this repository
61 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
62 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
63 | 4. Push to the branch (`git push origin feature/AmazingFeature`)
64 | 5. Open a Pull Request
65 |
66 | ## 📄 License
67 |
68 | This project is licensed under the [GPL-3.0 License](./LICENSE).
69 |
70 | ---
71 |
72 | **Disclaimer:** Please operate with caution on production environment data. It is recommended to back up important data before proceeding.
73 |
74 | ---
75 |
76 | **Like this project? Give it a ⭐!**
77 |
78 |
--------------------------------------------------------------------------------
/packages/v5/src/data/projects/mass-block-twitter.md:
--------------------------------------------------------------------------------
1 | # Mass Block Twitter
2 |
3 | ## Introduction
4 |
5 | One-Click Solution to Clean Up Twitter/X Spam.
6 |
7 | 
8 |
9 | Tired of spam accounts cluttering your Twitter feed? I built this simple yet powerful tool that lets you detect and block multiple spam accounts with just one click.
10 |
11 | ## Key Features
12 |
13 | - 🔍 Smart scan and batch blocking of suspicious accounts
14 | - 📋 Import/export block lists for sharing
15 | - 👥 Shared blocklist based on community reports
16 | - ⚡ One-click instant blocking
17 | - 🔒 Auto-hide heavily reported accounts
18 | - 🎯 Enhanced keyword filtering (across profile/username/tweets)
19 | - 🛡️ Auto-hide suspicious accounts (no avatar/bio/followers)
20 | - 🔐 100% open source
21 |
22 | Works on all major browsers, including mobile Kiwi browser.
23 |
24 | Give it a try and let me know what you think! 🙌
25 |
26 |
--------------------------------------------------------------------------------
/packages/v5/src/data/projects/myunzip.md:
--------------------------------------------------------------------------------
1 | # MyUnzip
2 |
3 | ## Features
4 |
5 | - Compress/decompress ZIP files of any size
6 | - Fully preserve directory structures
7 | - Provide an intuitive user interface
8 | - Process everything in-browser with no file uploads
9 |
10 | Video Demo:
11 |
12 | VIDEO
13 |
--------------------------------------------------------------------------------
/packages/v5/src/data/projects/ping.md:
--------------------------------------------------------------------------------
1 | # Ping
2 |
3 | Ping is a simple ping tool for testing network connectivity on Android devices.
4 |
5 | Video Demo:
6 |
7 | VIDEO
8 |
9 | > This app is still in the internal testing stage, if you want to use it, please DM me your gmail email on Twitter.
10 |
--------------------------------------------------------------------------------
/packages/v5/src/data/projects/ponytab.md:
--------------------------------------------------------------------------------
1 | # PonyTab
2 |
3 | [Chrome Web Store](https://chromewebstore.google.com/detail/gcahckghmpoodflilkignobggjmlcaih) [Firefox Add-ons](https://addons.mozilla.org/zh-CN/firefox/addon/ponytab/) [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/jilgmjcmkianchonfkadmomoieiglogo)
4 |
5 | A beautiful Browser extension that replaces your new tab page with stunning My Little Pony artwork by Sam Baneko.
6 |
7 | ## 🌟 Features
8 |
9 | - Displays a new MLP artwork each day
10 | - Clean and minimalist interface
11 | - Completely free and open-source
12 | - No ads or tracking
13 |
14 | ## 🎨 Artwork Credits
15 |
16 | All artwork featured in this extension is created by [Sam Baneko](https://spacecatsamba.com/). Used with permission.
17 |
18 | ## 🚀 Installation
19 |
20 | 1. Visit the Chrome Web Store
21 | 2. Click "Add to Chrome"
22 | 3. Open a new tab to see the beautiful MLP artwork!
23 |
24 | ## 📝 License
25 |
26 | This project is open source and available under the MIT License.
27 |
28 | ## 🌐 Links
29 |
30 | - [GitHub](https://github.com/rxliuli/ponytab)
31 | - [Artist's Website](https://spacecatsamba.com/)
32 |
--------------------------------------------------------------------------------
/packages/v5/src/data/projects/redirector.md:
--------------------------------------------------------------------------------
1 | # Redirector
2 |
3 | [Chrome](https://chromewebstore.google.com/detail/redirector/lioaeidejmlpffbndjhaameocfldlhin), [Safari](https://apps.apple.com/cn/app/url-redirector/id6743197230), [Edge](https://microsoftedge.microsoft.com/addons/detail/redirector/jhdjcofnjfeljeekjklhgfmfocfgibmm)
4 |
5 | ## Introduction
6 |
7 | Redirector is a powerful and flexible browser extension that allows users to customize URL redirection rules. Using regular expressions, users can create complex redirection rules applicable to various websites and URL patterns.
8 |
9 | ## Key Features
10 |
11 | - Create custom redirection rules using regular expressions
12 | - Support for multiple redirection rules
13 | - Real-time URL match testing
14 | - Intuitive user interface for easy addition, editing, and deletion of rules
15 | - Support for wildcards and complex URL patterns
16 |
17 | ## Installation
18 |
19 | Install from the Chrome Web Store.
20 |
21 | ## How to Use
22 |
23 | 1. Click on the Redirector icon in the browser toolbar to open the options page
24 | 2. In the "Add a redirect URL" section:
25 | - Enter the source URL pattern (using regular expression) in the left input box
26 | - Enter the target URL in the right input box (you can use $1, $2, etc., to reference capture groups in the regular expression)
27 | - Click the "Add" button to add a new rule
28 | 3. You can test your redirection rules in the "Test URL" section
29 | 4. Added rules will be displayed in the "Rules" list, where you can delete unnecessary rules at any time
30 |
31 | ## Example Rule
32 |
33 | Regular expression is a powerful tool for matching URL patterns. Here are some examples:
34 |
35 | - Redirect Google search to DuckDuckGo:
36 | - From: `^https://www.google.com/search\?q=(.*?)&.*$`
37 | - To: `https://duckduckgo.com/?q=$1`
38 |
39 | URLPattern is a more powerful and flexible tool for matching URL patterns. Here are some examples:
40 |
41 | - Redirect Google search to DuckDuckGo:
42 | - From: `^https://www.google.com/search?q=:id`
43 | - To: `https://duckduckgo.com/?q={{search.groups.id}}`
44 |
45 | Reference:
46 |
47 | ~~Glob pattern is a simple pattern matching tool. Here are some examples:~~
48 |
49 | - Redirect all YouTube videos to a specific page:
50 | - From: `https://youtu.be/*`
51 | - To: `https://example.com/watch?v=$1`
52 |
53 | ## Privacy
54 |
55 | This extension processes all redirection rules locally and does not collect or transmit any user data.
56 |
57 | ## Contributions
58 |
59 | Contributions are welcome! If you have any suggestions for improvements or have found a bug, please open an issue or submit a pull request.
60 |
61 | ## License
62 |
63 | [MIT License](./LICENSE)
64 |
65 | ## Contact
66 |
67 | If you have any questions or suggestions, please contact us at [rxliuli@gmail.com](mailto:rxliuli@gmail.com).
68 |
--------------------------------------------------------------------------------
/packages/v5/src/data/projects/window-resizer.md:
--------------------------------------------------------------------------------
1 | # WindowResizer
2 |
3 | A simple macOS menu bar utility to quickly resize the active window to your predefined dimensions.
4 |
5 | 
6 | _The menu bar interface for quick resizing._
7 |
8 | 
9 | _Manage your custom window size presets easily._
10 |
11 | ## ✨ Features
12 |
13 | - **Menu Bar Access:** Lives in your macOS menu bar for easy access.
14 | - **Active Window Resizing:** Instantly resizes the _currently active_ window.
15 | - **Custom Presets:** Define your own preferred window dimensions (width x height).
16 | - **Simple Management:** Add, edit, and delete presets through an intuitive preferences window.
17 |
18 | ## 🚀 Installation
19 |
20 | 1. Go to the [**Releases page**](https://github.com/rxliuli/window-resizer/releases).
21 | 2. Download the latest `.dmg` file (e.g., `WindowResizer-_version_.dmg`).
22 | 3. Open the `.dmg` file.
23 | 4. Drag `WindowResizer.app` to your `/Applications` folder.
24 | 5. Launch the app from your Applications folder. You might need to grant accessibility permissions if prompted (required for resizing other application windows).
25 |
26 | ## ⚙️ How to Use
27 |
28 | 1. **Launch the Application:** Start `WindowResizer`. Its icon will appear in your menu bar.
29 | 2. **Resize a Window:**
30 | - Make sure the window you want to resize is the _active_ (frontmost) window.
31 | - Click the application icon in the menu bar.
32 | - Select one of your predefined "Resize to WxH" options (e.g., "Resize to 1280x800").
33 | - The active window will instantly snap to that size.
34 | 3. **Manage Presets:**
35 | - Click the application icon in the menu bar.
36 | - Select "Preferences".
37 | - In the "Window Size Presets" window:
38 | - Click **+ Add Preset** to create a new size definition. Enter the desired Width and Height (in pixels) and save.
39 | - Click the **pencil icon (✎)** next to a preset to edit its dimensions.
40 | - Click the **trash can icon (🗑️)** next to a preset to delete it.
41 | - Changes are reflected immediately in the menu bar list.
42 | 4. **Quit:**
43 | - Click the application icon in the menu bar.
44 | - Select "Quit".
45 |
46 | ## 🛠️ Built With
47 |
48 | - **Electron:** For creating the cross-platform desktop application shell.
49 | - **React:** For the Preferences UI.
50 | - **Node.js:** For the underlying runtime.
51 |
52 | ## 🤝 Contributing
53 |
54 | Contributions are welcome! If you have suggestions or find bugs, please open an issue on the [GitHub Issues page](https://github.com/rxliuli/window-resizer/issues). If you'd like to contribute code, please fork the repository and submit a pull request.
55 |
56 | ## 📄 License
57 |
58 | This project is licensed under the [GPL-3.0 License](./LICENSE).
59 |
--------------------------------------------------------------------------------
/packages/v5/src/lib/ga.ts:
--------------------------------------------------------------------------------
1 | import type { AnyRouteMatch } from '@tanstack/react-router'
2 |
3 | export function ga(id: string): Exclude {
4 | const googleAnalyticsScript = `
5 | window.dataLayer = window.dataLayer || [];
6 | function gtag(){dataLayer.push(arguments);}
7 | gtag('js', new Date());
8 | gtag('config', '${id}');
9 | `
10 | return [
11 | {
12 | src: `https://www.googletagmanager.com/gtag/js?id=${id}`,
13 | async: true,
14 | },
15 | {
16 | children: googleAnalyticsScript,
17 | },
18 | ]
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/packages/v5/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/packages/v5/src/router.tsx:
--------------------------------------------------------------------------------
1 | import { createRouter as createTanstackRouter } from '@tanstack/react-router'
2 |
3 | // Import the generated route tree
4 | import { routeTree } from './routeTree.gen'
5 |
6 | import './styles.css'
7 |
8 | // Create a new router instance
9 | export const createRouter = () => {
10 | const router = createTanstackRouter({
11 | routeTree,
12 | scrollRestoration: true,
13 | defaultPreloadStaleTime: 0,
14 | })
15 |
16 | return router
17 | }
18 |
19 | // Register the router instance for type safety
20 | declare module '@tanstack/react-router' {
21 | interface Register {
22 | router: ReturnType
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/v5/src/routes/__root.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet, HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
2 | import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
3 | import { RootLayout } from '../components/layout/root-layout'
4 | import { NotFound } from '../components/NotFound'
5 |
6 | import appCss from '../styles.css?url'
7 | import { ga } from '@/lib/ga'
8 |
9 | export const Route = createRootRoute({
10 | notFoundComponent: NotFound,
11 | head: () => ({
12 | meta: [
13 | {
14 | charSet: 'utf-8',
15 | },
16 | {
17 | name: 'viewport',
18 | content: 'width=device-width, initial-scale=1',
19 | },
20 | {
21 | title: 'rxliuli - Personal Website',
22 | },
23 | {
24 | description:
25 | 'Personal website of rxliuli - I like to create interesting things, using programming and writing as tools.',
26 | },
27 | ],
28 | links: [
29 | {
30 | rel: 'stylesheet',
31 | href: appCss,
32 | },
33 | ],
34 | scripts: [...ga('G-G82PFVRCEF')],
35 | }),
36 |
37 | component: () => (
38 |
39 |
40 |
41 |
42 |
43 |
44 | ),
45 | })
46 |
47 | const themeScript = `
48 | (function() {
49 | const theme = (() => {
50 | if (typeof localStorage !== "undefined") {
51 | const stored = localStorage.getItem("vite-ui-theme");
52 | if (stored) return stored;
53 | }
54 | return "system";
55 | })();
56 |
57 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
58 | const currentTheme = theme === "system" ? systemTheme : theme;
59 |
60 | document.documentElement.classList.add(currentTheme);
61 | })();
62 | `
63 |
64 | function RootDocument({ children }: { children: React.ReactNode }) {
65 | return (
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | {children}
74 |
75 |
76 |
77 | )
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/packages/v5/src/routes/about.tsx:
--------------------------------------------------------------------------------
1 | import { MarkdownView } from '@/components/MarkdownView'
2 | import { createFileRoute } from '@tanstack/react-router'
3 |
4 | export const Route = createFileRoute('/about')({
5 | component: AboutPage,
6 | })
7 |
8 | export function AboutPage() {
9 | return (
10 |
11 |
About Me
12 |
13 | Detailed information about your background, expertise, and interests.
14 | {/* Add more content as needed */}
15 |
16 |
17 | )
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/packages/v5/src/routes/api.demo-names.ts:
--------------------------------------------------------------------------------
1 | import { createAPIFileRoute } from '@tanstack/react-start/api'
2 |
3 | export const APIRoute = createAPIFileRoute('/api/demo-names')({
4 | GET: async ({ request }) => {
5 | return new Response(JSON.stringify(['Alice', 'Bob', 'Charlie']), {
6 | headers: {
7 | 'Content-Type': 'application/json',
8 | },
9 | })
10 | },
11 | })
12 |
--------------------------------------------------------------------------------
/packages/v5/src/routes/demo.start.api-request.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | import { createFileRoute } from '@tanstack/react-router'
4 |
5 | function getNames() {
6 | return fetch('/api/demo-names').then((res) => res.json())
7 | }
8 |
9 | export const Route = createFileRoute('/demo/start/api-request')({
10 | component: Home,
11 | })
12 |
13 | function Home() {
14 | const [names, setNames] = useState>([])
15 | useEffect(() => {
16 | getNames().then(setNames)
17 | }, [])
18 |
19 | return (
20 |
21 |
{names.join(', ')}
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/packages/v5/src/routes/demo.start.server-funcs.tsx:
--------------------------------------------------------------------------------
1 | import * as fs from 'node:fs'
2 | import { createFileRoute, useRouter } from '@tanstack/react-router'
3 | import { createServerFn } from '@tanstack/react-start'
4 |
5 | const filePath = 'count.txt'
6 |
7 | async function readCount() {
8 | return parseInt(
9 | await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
10 | )
11 | }
12 |
13 | const getCount = createServerFn({
14 | method: 'GET',
15 | }).handler(() => {
16 | return readCount()
17 | })
18 |
19 | const updateCount = createServerFn({ method: 'POST' })
20 | .validator((d: number) => d)
21 | .handler(async ({ data }) => {
22 | const count = await readCount()
23 | await fs.promises.writeFile(filePath, `${count + data}`)
24 | })
25 |
26 | export const Route = createFileRoute('/demo/start/server-funcs')({
27 | component: Home,
28 | loader: async () => await getCount(),
29 | })
30 |
31 | function Home() {
32 | const router = useRouter()
33 | const state = Route.useLoaderData()
34 |
35 | return (
36 |
37 | {
40 | updateCount({ data: 1 }).then(() => {
41 | router.invalidate()
42 | })
43 | }}
44 | className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
45 | >
46 | Add 1 to {state}?
47 |
48 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/packages/v5/src/routes/ping.privacy.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from '@tanstack/react-router'
2 | import html from '@/data/privacy/ping.md?html'
3 | import { MarkdownView } from '@/components/MarkdownView'
4 |
5 | export const Route = createFileRoute('/ping/privacy')({
6 | component: PrivacyPage,
7 | })
8 |
9 | function PrivacyPage() {
10 | return (
11 |
12 |
13 |
14 | )
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/packages/v5/src/routes/project.index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { ProjectCard } from '@/components/project/project-card'
3 | import { getAllProjects, getAllProjectTypes } from '@/data/projects'
4 | import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
5 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
6 | import { createFileRoute } from '@tanstack/react-router'
7 | import { meta } from '@/components/seo'
8 |
9 | export const Route = createFileRoute('/project/')({
10 | component: ProjectPage,
11 | head: () => ({
12 | meta: meta({
13 | title: 'Projects',
14 | description: 'Projects I have worked on, from browser extensions to mobile apps.',
15 | }),
16 | }),
17 | })
18 |
19 | export function ProjectPage() {
20 | const projects = getAllProjects()
21 | const projectTypes = ['All', ...getAllProjectTypes()]
22 | const [activeType, setActiveType] = useState('All')
23 |
24 | const filteredProjects = (
25 | activeType === 'All' ? projects : projects.filter((project) => project.type === activeType)
26 | ).sort((a, b) => (b.featured ? 1 : -1) - (a.featured ? 1 : -1))
27 |
28 | return (
29 |
30 |
31 |
My Projects
32 |
33 | A collection of projects I've worked on, from browser extensions to mobile apps.
34 |
35 |
36 |
37 | {/* 移动端下拉,md及以上Tabs */}
38 |
39 |
40 |
41 |
42 |
43 |
44 | {projectTypes.map((type) => (
45 |
46 | {type}
47 |
48 | ))}
49 |
50 |
51 |
52 |
53 |
54 |
55 | {projectTypes.map((type) => (
56 |
57 | {type}
58 |
59 | ))}
60 |
61 |
62 |
63 |
64 |
65 | {filteredProjects.map((project) => (
66 |
67 | ))}
68 |
69 |
70 | {filteredProjects.length === 0 && (
71 |
72 |
No projects found for this category.
73 |
74 | )}
75 |
76 | )
77 | }
78 |
79 |
--------------------------------------------------------------------------------
/packages/v5/src/routes/webstore.privacy.tsx:
--------------------------------------------------------------------------------
1 | import { createFileRoute } from '@tanstack/react-router'
2 | import html from '@/data/privacy/webstore.md?html'
3 | import { MarkdownView } from '@/components/MarkdownView'
4 |
5 | export const Route = createFileRoute('/webstore/privacy')({
6 | component: PrivacyPage,
7 | })
8 |
9 | function PrivacyPage() {
10 | return (
11 |
12 |
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/packages/v5/src/ssr.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createStartHandler,
3 | defaultStreamHandler,
4 | } from '@tanstack/react-start/server'
5 | import { getRouterManifest } from '@tanstack/react-start/router-manifest'
6 |
7 | import { createRouter } from './router'
8 |
9 | let streamHandler = defaultStreamHandler
10 |
11 | export default createStartHandler({
12 | createRouter,
13 | getRouterManifest,
14 | })(streamHandler)
15 |
--------------------------------------------------------------------------------
/packages/v5/src/styles/prism-custom.css:
--------------------------------------------------------------------------------
1 | .code-block {
2 | margin: 1.5rem 0;
3 | position: relative;
4 | }
5 |
6 | .code-block pre {
7 | margin: 0;
8 | padding: 1.5rem;
9 | overflow: auto;
10 | font-size: 0.9rem;
11 | line-height: 1.5;
12 | border-radius: 0.5rem;
13 | background-color: #1e1e1e;
14 | }
15 |
16 | /* Horizontal scrollbar styles */
17 | .code-block pre::-webkit-scrollbar {
18 | height: 6px;
19 | width: 6px;
20 | }
21 |
22 | .code-block pre::-webkit-scrollbar-thumb {
23 | background-color: rgba(255, 255, 255, 0.2);
24 | border-radius: 3px;
25 | }
26 |
27 | .code-block pre::-webkit-scrollbar-track {
28 | background-color: rgba(0, 0, 0, 0.1);
29 | }
--------------------------------------------------------------------------------
/packages/v5/src/types/blog.ts:
--------------------------------------------------------------------------------
1 | import type { TocItem } from '@/plugins/markdownToc'
2 |
3 | export type BlogPostStatus = 'published' | 'draft'
4 |
5 | export interface BlogPost {
6 | id: string
7 | title: string
8 | slug: string
9 | date: string // ISO format date string "2023-10-15"
10 | summary: string
11 | content?: string // Markdown content
12 | toc?: TocItem[]
13 | coverImage?: string // Optional cover image
14 | author?: {
15 | name: string
16 | avatar?: string
17 | }
18 | tags?: string[] // Blog tags for categorization
19 | status: BlogPostStatus // Publication status
20 | readingTime?: string // Estimated reading time, e.g. "5 min read"
21 | views?: number // Optional view count
22 | }
23 |
--------------------------------------------------------------------------------
/packages/v5/src/types/markdown.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.md' {
2 | import type { TocItem } from '../plugins/markdownToc'
3 |
4 | export const html: string
5 | export const data: Record
6 | export const toc: TocItem[]
7 | declare const component: React.ReactNode
8 | export default component
9 | }
10 |
11 | declare module '*.md?frontmatter' {
12 | const frontmatter: Record
13 | export default frontmatter
14 | }
15 |
16 | declare module '*.md?html' {
17 | const html: string
18 | export default html
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/packages/v5/src/types/project.ts:
--------------------------------------------------------------------------------
1 | export type ProjectType = 'Browser Extension' | 'Website' | 'App' | 'CLI Tool' | 'VSCode Extension'
2 |
3 | export interface ProjectLink {
4 | type: 'store' | 'social' | 'community' | 'other'
5 | name: string
6 | url: string
7 | icon: 'chrome' | 'firefox' | 'edge' | 'safari' | 'discord' | 'producthunt'
8 | }
9 |
10 | export interface ProjectMeta {
11 | id: string
12 | title: string
13 | description: string
14 | previewImage: string
15 | type: ProjectType
16 | tags?: string[] // 可选,用于进一步分类
17 | projectUrl?: string // 站外链接,可选
18 | sourceCodeUrl?: string // GitHub链接,可选
19 | links?: ProjectLink[]
20 | slug: string // 用于路由
21 | featured?: boolean // 是否在首页展示
22 | updated: string // 项目创建日期
23 | created?: string // 项目最后更新日期
24 | }
25 |
26 | export interface Project {
27 | meta: ProjectMeta
28 | html?: string
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/packages/v5/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "target": "ES2022",
5 | "jsx": "react-jsx",
6 | "module": "ESNext",
7 | "lib": ["ES2022", "DOM", "DOM.Iterable"],
8 | "types": ["vite/client"],
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "verbatimModuleSyntax": true,
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "skipLibCheck": true,
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "noUncheckedSideEffectImports": true,
23 | "baseUrl": ".",
24 | "paths": {
25 | "@/*": ["./src/*"],
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/v5'
3 |
--------------------------------------------------------------------------------