Rewrite extensions components to setup syntax (#21)

* Rewrite extensions components to setup syntax.

* Format md.use.

* Import ElementPlus components directly for three-shaking.

* Fix SSR setting.

* Fix missing loading indicator in extensions page.
This commit is contained in:
Alessandro Jean 2023-09-08 02:42:53 -03:00 committed by GitHub
parent 359833502a
commit 1d1bf5fba9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 326 additions and 161 deletions

View File

@ -57,6 +57,7 @@
"stylelint": "^15.10.3", "stylelint": "^15.10.3",
"stylelint-stylus": "^0.18.0", "stylelint-stylus": "^0.18.0",
"stylus": "^0.60.0", "stylus": "^0.60.0",
"unplugin-element-plus": "^0.8.0",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vitepress": "1.0.0-rc.10", "vitepress": "1.0.0-rc.10",
"vitepress-plugin-tabs": "^0.3.0", "vitepress-plugin-tabs": "^0.3.0",

101
website/pnpm-lock.yaml generated
View File

@ -1,9 +1,5 @@
lockfileVersion: '6.0' lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies: dependencies:
'@iconify-prerendered/vue-mdi': '@iconify-prerendered/vue-mdi':
specifier: ^0.23.1689058119 specifier: ^0.23.1689058119
@ -109,6 +105,9 @@ devDependencies:
stylus: stylus:
specifier: ^0.60.0 specifier: ^0.60.0
version: 0.60.0 version: 0.60.0
unplugin-element-plus:
specifier: ^0.8.0
version: 0.8.0
vite-plugin-eslint: vite-plugin-eslint:
specifier: ^1.8.1 specifier: ^1.8.1
version: 1.8.1(eslint@8.48.0)(vite@4.4.9) version: 1.8.1(eslint@8.48.0)(vite@4.4.9)
@ -1013,6 +1012,20 @@ packages:
picomatch: 2.3.1 picomatch: 2.3.1
dev: true dev: true
/@rollup/pluginutils@5.0.4:
resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@types/estree': 1.0.1
estree-walker: 2.0.2
picomatch: 2.3.1
dev: true
/@shuding/opentype.js@1.4.0-beta.0: /@shuding/opentype.js@1.4.0-beta.0:
resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==}
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
@ -1540,6 +1553,14 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true dev: true
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
dev: true
/argparse@1.0.10: /argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
dependencies: dependencies:
@ -1673,6 +1694,11 @@ packages:
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
dev: false dev: false
/binary-extensions@2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
dev: true
/body-parser@1.20.1: /body-parser@1.20.1:
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@ -1781,6 +1807,21 @@ packages:
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
dev: true dev: true
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
dependencies:
anymatch: 3.1.3
braces: 3.0.2
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
dev: true
/cli-cursor@4.0.0: /cli-cursor@4.0.0:
resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -2189,6 +2230,10 @@ packages:
which-typed-array: 1.1.11 which-typed-array: 1.1.11
dev: true dev: true
/es-module-lexer@1.3.0:
resolution: {integrity: sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==}
dev: true
/es-set-tostringtag@2.0.1: /es-set-tostringtag@2.0.1:
resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3032,6 +3077,13 @@ packages:
has-bigints: 1.0.2 has-bigints: 1.0.2
dev: true dev: true
/is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
dependencies:
binary-extensions: 2.2.0
dev: true
/is-boolean-object@1.1.2: /is-boolean-object@1.1.2:
resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3986,6 +4038,13 @@ packages:
type-fest: 1.4.0 type-fest: 1.4.0
dev: true dev: true
/readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
dependencies:
picomatch: 2.3.1
dev: true
/redent@4.0.0: /redent@4.0.0:
resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -4711,6 +4770,27 @@ packages:
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dev: true dev: true
/unplugin-element-plus@0.8.0:
resolution: {integrity: sha512-jByUGY3FG2B8RJKFryqxx4eNtSTj+Hjlo8edcOdJymewndDQjThZ1pRUQHRjQsbKhTV2jEctJV7t7RJ405UL4g==}
engines: {node: '>=14.19.0'}
dependencies:
'@rollup/pluginutils': 5.0.4
es-module-lexer: 1.3.0
magic-string: 0.30.3
unplugin: 1.4.0
transitivePeerDependencies:
- rollup
dev: true
/unplugin@1.4.0:
resolution: {integrity: sha512-5x4eIEL6WgbzqGtF9UV8VEC/ehKptPXDS6L2b0mv4FRMkJxRtjaJfOWDd6a8+kYbqsjklix7yWP0N3SUepjXcg==}
dependencies:
acorn: 8.10.0
chokidar: 3.5.3
webpack-sources: 3.2.3
webpack-virtual-modules: 0.5.0
dev: true
/upath@2.0.1: /upath@2.0.1:
resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -4909,6 +4989,15 @@ packages:
'@vue/server-renderer': 3.3.4(vue@3.3.4) '@vue/server-renderer': 3.3.4(vue@3.3.4)
'@vue/shared': 3.3.4 '@vue/shared': 3.3.4
/webpack-sources@3.2.3:
resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
engines: {node: '>=10.13.0'}
dev: true
/webpack-virtual-modules@0.5.0:
resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==}
dev: true
/which-boxed-primitive@1.0.2: /which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
dependencies: dependencies:
@ -5022,3 +5111,7 @@ packages:
/yoga-wasm-web@0.3.3: /yoga-wasm-web@0.3.3:
resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==}
dev: true dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

View File

@ -1,4 +1,5 @@
import { defineConfig, loadEnv } from "vitepress"; import { defineConfig, loadEnv } from "vitepress";
import ElementPlus from "unplugin-element-plus/vite";
import markdownConfig from "./config/markdownConfig"; // For use with loading Markdown plugins import markdownConfig from "./config/markdownConfig"; // For use with loading Markdown plugins
import themeConfig from "./config/themeConfig"; // Theme related config import themeConfig from "./config/themeConfig"; // Theme related config
@ -30,4 +31,10 @@ export default defineConfig({
generateFeed(context, hostname); generateFeed(context, hostname);
generateOgImages(context); generateOgImages(context);
}, },
vite: {
plugins: [ElementPlus({})],
ssr: {
noExternal: ["element-plus"],
},
},
}); });

View File

@ -12,17 +12,17 @@ import shortcodes from "./shortcodes";
const markdownConfig: MarkdownOptions = { const markdownConfig: MarkdownOptions = {
config: (md) => { config: (md) => {
md.use(attrs), md
md.use(figure), .use(attrs)
md.use(imgLazyload), .use(figure)
md.use(imgMark), .use(imgLazyload)
md.use(imgSize), .use(imgMark)
md.use(include, { .use(imgSize)
.use(include, {
currentPath: (env) => env.filePath, currentPath: (env) => env.filePath,
}), })
md.use(tabsMarkdownPlugin); .use(tabsMarkdownPlugin)
.use(shortcode_plugin, shortcodes);
md.use(shortcode_plugin, shortcodes);
}, },
}; };

View File

@ -1,57 +1,80 @@
<script> <script setup lang="ts">
import { toRefs } from "vue";
import { langName, simpleLangName } from "../../../config/scripts/languages"; import { langName, simpleLangName } from "../../../config/scripts/languages";
import type { Extension } from "../../queries/useExtensionsRepositoryQuery";
export default { import {
props: ["extensions"], ElForm,
emits: ["filters"], ElFormItem,
data() { ElInput,
return { ElSelect,
filters: { ElOption,
search: "", ElRadioGroup,
lang: [], ElRadio,
nsfw: "Show all", } from "element-plus";
sort: "Ascending",
}, export type Nsfw = "Show all" | "NSFW" | "SFW";
}; export type Sort = "Ascending" | "Descending";
},
watch: { const props = defineProps<{
filters: { extensions: Extension[][];
handler(value) { search: string;
this.$emit("filters", this.filters); lang: string[];
}, nsfw: Nsfw;
deep: true, sort: Sort;
}, }>();
},
methods: { const { extensions } = toRefs(props);
simpleLangName,
langName, defineEmits<{
}, (e: 'update:search', search: string): void;
}; (e: 'update:lang', lang: string[]): void,
(e: 'update:nsfw', nsfw: Nsfw): void,
(e: 'update:sort', sort: Sort): void,
}>();
</script> </script>
<template> <template>
<div class="filters-list"> <div class="filters-list">
<el-form :model="filters" label-width="120px"> <el-form label-width="120px">
<el-form-item label="Search:"> <el-form-item label="Search:">
<el-input v-model="filters.search" placeholder="Search extensions by name or ID..." clearable /> <el-input
:model-value="search"
placeholder="Search extensions by name or ID..."
clearable
@update:model-value="$emit('update:search', $event)"
/>
</el-form-item> </el-form-item>
<el-form-item label="Languages:"> <el-form-item label="Languages:">
<el-select v-model="filters.lang" placeholder="Show specific languages..." multiple clearable> <el-select
<el-option :model-value="lang"
v-for="[group] in extensions" placeholder="Show specific languages..."
:key="group.lang" multiple
:label="group.lang === 'en' ? simpleLangName(group.lang) : langName(group.lang)" clearable
:value="group.lang" @update:model-value="$emit('update:lang', $event)"
/> >
</el-select> <el-option
v-for="[group] in extensions"
:key="group.lang"
:label="group.lang === 'en' ? simpleLangName(group.lang) : langName(group.lang)"
:value="group.lang"
/>
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="Sort by:"> <el-form-item label="Sort by:">
<el-radio-group v-model="filters.sort"> <el-radio-group
:model-value="sort"
@update:model-value="$emit('update:sort', $event)"
>
<el-radio label="Ascending"></el-radio> <el-radio label="Ascending"></el-radio>
<el-radio label="Descending"></el-radio> <el-radio label="Descending"></el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="Display mode:"> <el-form-item label="Display mode:">
<el-radio-group v-model="filters.nsfw"> <el-radio-group
:model-value="nsfw"
@update:model-value="$emit('update:nsfw', $event)"
>
<el-radio label="NSFW"></el-radio> <el-radio label="NSFW"></el-radio>
<el-radio label="SFW"></el-radio> <el-radio label="SFW"></el-radio>
<el-radio label="Show all"></el-radio> <el-radio label="Show all"></el-radio>
@ -67,4 +90,8 @@ export default {
flex-direction: column flex-direction: column
row-gap: 1rem row-gap: 1rem
} }
.el-select {
width: 100%
}
</style> </style>

View File

@ -1,27 +1,25 @@
<script> <script setup lang="ts">
import { computed, toRefs } from "vue";
import { langName, simpleLangName } from "../../../config/scripts/languages"; import { langName, simpleLangName } from "../../../config/scripts/languages";
import ExtensionItem from "./ExtensionItem.vue"; import ExtensionItem from "./ExtensionItem.vue";
import type { Extension } from "../../queries/useExtensionsRepositoryQuery";
export default { const props = defineProps<{ list: Extension[]; totalCount: number }>();
components: { ExtensionItem }, const { list } = toRefs(props);
props: ["list", "totalCount"],
computed: { const groupName = computed(() => {
groupName: function () { const firstItem = list.value[0];
const firstItem = this.list[0];
return firstItem.lang === "en" ? simpleLangName(firstItem.lang) : langName(firstItem.lang); return firstItem.lang === "en"
}, ? simpleLangName(firstItem.lang)
}, : langName(firstItem.lang);
methods: { });
simpleLangName,
langName,
},
};
</script> </script>
<template> <template>
<div class="extension-group"> <div class="extension-group">
<h2> <h2>
{{ groupName }} <span>{{ groupName }}</span>
<span class="extensions-total"> <span class="extensions-total">
Total: Total:
@ -30,6 +28,7 @@ export default {
</span> </span>
</span> </span>
</h2> </h2>
<ExtensionItem <ExtensionItem
v-for="extension in list" v-for="extension in list"
:id="extension.pkg.replace('eu.kanade.tachiyomi.extension.', '')" :id="extension.pkg.replace('eu.kanade.tachiyomi.extension.', '')"
@ -40,10 +39,12 @@ export default {
</template> </template>
<style lang="stylus"> <style lang="stylus">
.extensions-total { .extension-group h2 {
float: right display: flex
align-items: center
justify-content: space-between
&-sum { .extensions-total-sum {
color: var(--vp-c-brand) color: var(--vp-c-brand)
} }
} }

View File

@ -1,33 +1,24 @@
<script> <script setup lang="ts">
export default { import { computed, toRefs } from "vue";
props: ["item"], import type { Extension } from "../../queries/useExtensionsRepositoryQuery";
computed: {
pkgId: function () { const props = defineProps<{ item: Extension }>();
return this.item.pkg.replace("eu.kanade.tachiyomi.extension.", ""); const { item } = toRefs(props);
},
pkgName: function () { const pkgId = computed(() => {
return this.item.name.split(": ")[1]; return item.value.pkg.replace("eu.kanade.tachiyomi.extension.", "");
}, });
pkgVersion: function () {
return this.item.version; const pkgName = computed(() => item.value.name.split(": ")[1]);
}, const pkgIsNsfw = computed(() => item.value.nsfw === 1);
pkgIsNsfw: function () {
return !!parseInt(this.item.nsfw); const iconUrl = computed(() => {
}, return `https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/icon/${item.value.pkg}.png`;
pkgHasReadme: function () { });
return !!parseInt(this.item.hasReadme);
}, const apkUrl = computed(() => {
pkgHasChangelog: function () { return `https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/apk/${item.value.apk}`;
return !!parseInt(this.item.hasChangelog); });
},
iconUrl: function () {
return `https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/icon/${this.item.pkg}.png`;
},
apkUrl: function () {
return `https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/apk/${this.item.apk}`;
},
},
};
</script> </script>
<template> <template>
@ -42,8 +33,8 @@ export default {
{{ pkgId }} {{ pkgId }}
</div> </div>
</div> </div>
<Badge v-if="pkgIsNsfw" type="danger" :text="pkgVersion" title="This extension contains NSFW entries." /> <Badge v-if="pkgIsNsfw" type="danger" :text="item.version" title="This extension contains NSFW entries." />
<Badge v-else type="info" :text="pkgVersion" title="This extension is free from NSFW entries." /> <Badge v-else type="info" :text="item.version" title="This extension is free from NSFW entries." />
<a :href="apkUrl" class="extension-download" title="Download APK" download></a> <a :href="apkUrl" class="extension-download" title="Download APK" download></a>
</div> </div>
</template> </template>
@ -53,14 +44,20 @@ export default {
position: relative position: relative
align-items: center align-items: center
display: flex display: flex
width: 100% width: calc(100% + 1em)
padding: 0.5em 0 padding: 0.5em
margin: 0.8em 0 margin: 0.8em -0.5em
border-radius: 8px border-radius: 8px
gap: 0.675rem gap: 0.675rem
&:hover { &:hover {
background-color: var(--vp-c-bg-soft-mute) background-color: var(--vp-c-bg-soft)
}
&:target {
background-color: var(--vp-c-brand-soft)
border-radius: 8px
transition: 500ms background-color
} }
.anchor { .anchor {
@ -142,6 +139,8 @@ export default {
border: 1px solid var(--vp-c-divider) border: 1px solid var(--vp-c-divider)
border-radius: 8px border-radius: 8px
padding: 0.5em padding: 0.5em
margin: 0.8em 0
width: 100%
.extension-icon { .extension-icon {
margin-left: 0 margin-left: 0
@ -152,10 +151,4 @@ export default {
} }
} }
} }
&:target {
background-color: var(--vp-c-bg-soft-mute)
border-radius: 8px
transition: 500ms background-color
}
</style> </style>

View File

@ -1,21 +1,28 @@
<script> <script setup lang="ts">
import { computed, toRefs } from "vue";
import ExtensionGroup from "./ExtensionGroup.vue"; import ExtensionGroup from "./ExtensionGroup.vue";
import type { Extension } from "../../queries/useExtensionsRepositoryQuery";
export default { const props = defineProps<{ extensions: Extension[][] }>();
components: { ExtensionGroup }, const { extensions } = toRefs(props);
props: ["extensions"],
computed: { const totalCount = computed(() => {
totalCount() { return extensions.value.reduce((sum, item) => sum + item.length, 0);
return this.extensions.reduce((sum, item) => sum + item.length, 0); });
},
},
};
</script> </script>
<template> <template>
<div class="extension-list"> <div class="extension-list">
<ExtensionGroup v-for="group in extensions" :key="group[0].lang" :list="group" :class="group[0].lang" :total-count="totalCount" /> <ExtensionGroup
v-for="group in extensions"
:key="group[0].lang"
:list="group"
:class="group[0].lang"
:total-count="totalCount"
/>
</div> </div>
</template> </template>
<style lang="stylus"> <style lang="stylus">
.extension-list { .extension-list {
> div { > div {

View File

@ -1,19 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import groupBy from "lodash.groupby"; import groupBy from "lodash.groupby";
import { ElLoading } from "element-plus";
import { simpleLangName } from "../../../config/scripts/languages"; import { simpleLangName } from "../../../config/scripts/languages";
import ExtensionFilters from "./ExtensionFilters.vue"; import ExtensionFilters from "./ExtensionFilters.vue";
import ExtensionList from "./ExtensionList.vue"; import ExtensionList from "./ExtensionList.vue";
import useExtensionsRepositoryQuery from "../../queries/useExtensionsRepositoryQuery"; import useExtensionsRepositoryQuery from "../../queries/useExtensionsRepositoryQuery";
import { computed, onUpdated, ref } from 'vue'; import { computed, nextTick, onMounted, onUpdated, reactive, ref, watch } from 'vue';
import type { Extension } from "../../queries/useExtensionsRepositoryQuery"; import type { Extension } from "../../queries/useExtensionsRepositoryQuery";
import type { Nsfw, Sort } from "./ExtensionFilters.vue";
const { data: extensions, isLoading } = useExtensionsRepositoryQuery(); const { data: extensions, isLoading } = useExtensionsRepositoryQuery({
select: (response) => {
const values: Extension[][] = Object.values(groupBy(response, "lang"));
values.sort(languageComparator)
const filters = ref({ return values
},
});
const filters = reactive({
search: "", search: "",
lang: [] as string[], lang: [] as string[],
nsfw: "Show all", nsfw: "Show all" as Nsfw,
sort: "Ascending", sort: "Ascending" as Sort,
}); });
function languageComparator(a: Extension[], b: Extension[]) { function languageComparator(a: Extension[], b: Extension[]) {
@ -40,33 +50,26 @@ function languageComparator(a: Extension[], b: Extension[]) {
return 0; return 0;
} }
const groupedExtensions = computed(() => {
const values: Extension[][] = Object.values(groupBy(extensions.value, "lang"));
values.sort(languageComparator)
return values
})
const filteredExtensions = computed(() => { const filteredExtensions = computed(() => {
const filtered: Extension[][] = []; const filtered: Extension[][] = [];
for (const group of groupedExtensions.value) { for (const group of (extensions.value ?? [])) {
let filteredGroup = filters.value.lang.length let filteredGroup = filters.lang.length
? (filters.value.lang.includes(group[0].lang) ? group : []) ? (filters.lang.includes(group[0].lang) ? group : [])
: group; : group;
if (filters.value.search) { if (filters.search) {
filteredGroup = filteredGroup.filter( filteredGroup = filteredGroup.filter(
(ext) => (ext) =>
ext.name.toLowerCase().includes(filters.value.search.toLowerCase()) || ext.name.toLowerCase().includes(filters.search.toLowerCase()) ||
ext.sources.some((source) => source.id.includes(filters.value.search)) ext.sources.some((source) => source.id.includes(filters.search))
); );
} }
filteredGroup = filteredGroup.filter((ext) => filteredGroup = filteredGroup.filter((ext) =>
filters.value.nsfw === "Show all" ? true : ext.nsfw === (filters.value.nsfw === "NSFW" ? 1 : 0) filters.nsfw === "Show all" ? true : ext.nsfw === (filters.nsfw === "NSFW" ? 1 : 0)
); );
if (filters.value.sort && filters.value.sort === "Descending") { if (filters.sort && filters.sort === "Descending") {
filteredGroup = filteredGroup.reverse(); filteredGroup = filteredGroup.reverse();
} }
if (filteredGroup.length) { if (filteredGroup.length) {
@ -77,15 +80,46 @@ const filteredExtensions = computed(() => {
return filtered; return filtered;
}); });
onUpdated(() => { const loadingInstance = ref<ReturnType<typeof ElLoading['service']>>();
onMounted(() => {
loadingInstance.value = ElLoading.service({
target: ".extensions",
fullscreen: false,
background: "transparent",
});
})
watch(extensions, async () => {
if (window.location.hash) { if (window.location.hash) {
window.location.replace(window.location.hash); await nextTick()
document.getElementById(window.location.hash.substring(1))
?.scrollIntoView({ behavior: 'smooth' });
}
});
watch([isLoading, loadingInstance], async ([newIsLoading]) => {
if (!newIsLoading) {
loadingInstance.value?.close();
} }
}); });
</script> </script>
<template> <template>
<ExtensionFilters :extensions="groupedExtensions" @filters="filters = $event" /> <ExtensionFilters
<div class="loading" v-if="isLoading" v-loading.lock="isLoading" style="min-height: 200px"></div> :extensions="extensions ?? []"
<ExtensionList v-else :extensions="filteredExtensions" /> v-model:search="filters.search"
v-model:lang="filters.lang"
v-model:nsfw="filters.nsfw"
v-model:sort="filters.sort"
/>
<div class="extensions">
<ExtensionList v-if="!isLoading" :extensions="filteredExtensions" />
</div>
</template> </template>
<style lang="stylus" scoped>
.extensions {
min-height: 200px
}
</style>

View File

@ -5,8 +5,6 @@ import DefaultTheme from "vitepress/theme";
import "./styles/base.styl"; import "./styles/base.styl";
// Import Global plugins // Import Global plugins
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import "element-plus/theme-chalk/dark/css-vars.css"; import "element-plus/theme-chalk/dark/css-vars.css";
import { VueQueryPlugin } from "@tanstack/vue-query"; import { VueQueryPlugin } from "@tanstack/vue-query";
@ -19,7 +17,6 @@ import { IconDownload, IconNewspaperVariant, IconBugReport } from "@iconify-prer
export default { export default {
extends: DefaultTheme, extends: DefaultTheme,
enhanceApp({ app }) { enhanceApp({ app }) {
app.use(ElementPlus);
app.use(VueQueryPlugin); app.use(VueQueryPlugin);
enhanceAppWithTabs(app); enhanceAppWithTabs(app);
app.component("IconDownload", IconDownload); app.component("IconDownload", IconDownload);

View File

@ -1,4 +1,4 @@
import { useQuery } from "@tanstack/vue-query"; import { UseQueryOptions, useQuery } from "@tanstack/vue-query";
import axios from "axios"; import axios from "axios";
import { GITHUB_EXTENSION_JSON } from "../../config/constants"; import { GITHUB_EXTENSION_JSON } from "../../config/constants";
@ -26,14 +26,11 @@ export interface Source {
hasCloudflare: string; hasCloudflare: string;
} }
export interface GitHubAsset { type UseExtensionsRepositoryQueryOptions<S = Extension[]> =
name: string; UseQueryOptions<Extension[], Error, S>
content_type: string;
browser_download_url: string;
}
export default function useExtensionsRepositoryQuery() { export default function useExtensionsRepositoryQuery<S = Extension[]>(options: UseExtensionsRepositoryQueryOptions<S> = {}) {
return useQuery({ return useQuery<Extension[], Error, S>({
queryKey: ["extensions"], queryKey: ["extensions"],
queryFn: async () => { queryFn: async () => {
const { data } = await axios.get<Extension[]>(GITHUB_EXTENSION_JSON); const { data } = await axios.get<Extension[]>(GITHUB_EXTENSION_JSON);
@ -42,5 +39,6 @@ export default function useExtensionsRepositoryQuery() {
}, },
initialData: () => [], initialData: () => [],
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
...options,
}); });
} }

View File

@ -217,3 +217,10 @@ main :where(h1, h2, h3, h4, h5, h6) + figure {
margin-right: 4px margin-right: 4px
} }
} }
/**
* Component: Element Plus
*/
body {
--el-color-primary: var(--vp-c-brand-1)
}