Use browser level lazy loading in Extension List (#689)

* Use Intersection Observer in Extension List

- Adds lazy loading to images

* Load Polyfill library when IntersectionObserver doesn't exist

* Replace Intersection Observer with browser level lazy loading

Remove old ExtensionList
Lint

* Clean up
This commit is contained in:
Andreas 2021-06-14 04:51:00 +02:00 committed by GitHub
parent a03f834988
commit 690111d366
8 changed files with 348 additions and 252 deletions

View File

@ -0,0 +1,55 @@
<template>
<div class="extension-group">
<h3>
{{ groupName }}
<span class="extensions-total">
Total:
<span class="extensions-total-sum">
{{ totalCount }}
</span>
</span>
</h3>
<div
v-for="extension in list"
:id="extension.pkg.replace('eu.kanade.tachiyomi.extension.', '')"
:key="extension.apk"
class="anchor"
>
<ExtensionItem :item="extension" />
</div>
</div>
</template>
<script>
import { simpleLangName, langName } from "../scripts/languages";
import ExtensionItem from "./ExtensionItem.vue";
export default {
components: { ExtensionItem },
props: ["list", "totalCount"],
computed: {
groupName: function() {
const firstItem = this.list[0]
return firstItem.lang === "en" ? simpleLangName(firstItem.lang) : langName(firstItem.lang)
}
},
methods: {
simpleLangName,
langName,
},
};
</script>
<style lang="stylus">
.extensions-total
float right
&-sum
color $accentColor
.anchor
margin-top -3.9em
padding-bottom 0.2em
padding-top 4.5em
&:first-child
border-top 1px solid $borderColor
</style>

View File

@ -0,0 +1,105 @@
<template>
<div v-if="item" class="extension">
<a :href="`#${pkgId}`" class="header-anchor" aria-hidden="true" @click.stop>#</a>
<img class="extension-icon" :src="iconUrl" loading="lazy" width="42" height="42" />
<div class="extension-text">
<div class="upper">
{{ pkgName }}
<Badge :text="pkgVersion" />
</div>
<div class="lower">
{{ pkgId }}
</div>
</div>
<a :href="apkUrl" class="extension-download" title="Download APK" download>
<MaterialIcon icon="cloud_download" />
<span>Download</span>
</a>
</div>
</template>
<script>
export default {
props: ["item"],
computed: {
pkgId: function() {
return this.item.pkg.replace("eu.kanade.tachiyomi.extension.", "");
},
pkgName: function() {
return this.item.name.split(": ")[1]
},
pkgVersion: function() {
return 'v' + this.item.version;
},
iconUrl: function() {
const pkgName = this.item.apk.substring(0, this.item.apk.lastIndexOf("."));
return `https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/icon/${pkgName}.png`;
},
apkUrl: function() {
return `https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/apk/${this.item.apk}`
},
},
};
</script>
<style lang="stylus">
.extension
align-items center
display flex
padding 0.4em 1.5em
.header-anchor
padding-left 0.2em
padding-right 0.2em
font-size 1.4em
opacity 0
&:hover .header-anchor
opacity 1
.extension-icon
margin-right 0.5em
.extension-text
flex 1
.upper
font-weight: 600
.badge
font-weight: 400
margin-left 8px
.lower
color #6c757d
font-family monospace
font-size 0.9rem
.extension-download
margin-right 0.5em
padding-left 1rem
padding-right 1rem
padding-top .5rem
padding-bottom .5rem
font-weight 700
border-radius 4px
color white
background-color $accentColor
border 1px solid $accentColor
.material-icons
color white
max-width 18px
&:hover
background-color white
color $accentColor
text-decoration none
.material-icons
color $accentColor
@media (max-width 767px)
padding 0.4em 0em
.extension-text .lower,
.extension-download span
display none
@media (max-width 767px)
.extension
border 1px solid $borderColor
border-radius 8px
.extension-download
background-color $accentColor
&:target
.extension
background-color $containerBackground
border-radius 8px
transition 500ms background-color
</style>

View File

@ -1,191 +1,23 @@
<template> <template>
<div class="extension-list"> <div class="extension-list">
<span class="filters-list"> <div v-for="group in extensions" :key="group[0].lang">
<ElInput v-model="filters.search" placeholder="Search extensions by name..." clearable /> <ExtensionGroup :list="group" :total-count="totalCount" />
<ElSelect v-model="filters.lang" placeholder="Show specific languages..." multiple clearable>
<ElOption
v-for="[group] in extensions"
:key="group.lang"
:label="group.lang === 'en' ? simpleLangName(group.lang) : langName(group.lang)"
:value="group.lang"
/>
</ElSelect>
<div>
Sort by
<ElRadioGroup v-model="filters.sort">
<ElRadioButton label="Ascending"></ElRadioButton>
<ElRadioButton label="Descending"></ElRadioButton>
</ElRadioGroup>
</div>
<div>
Display extensions with NSFW content?
<ElRadioGroup v-model="filters.nsfw">
<ElRadioButton label="Yes"></ElRadioButton>
<ElRadioButton label="No"></ElRadioButton>
<ElRadioButton label="Don't care"></ElRadioButton>
</ElRadioGroup>
</div>
</span>
<div v-if="loading" v-loading.lock="loading" style="min-height: 200px"></div>
<div v-for="extensionGroup in filteredExtensions" :key="extensionGroup[0].lang">
<h3>
<span>
{{
extensionGroup[0].lang === "en"
? simpleLangName(extensionGroup[0].lang)
: langName(extensionGroup[0].lang)
}}
</span>
<span class="extensions-total">
Total:
<span class="extensions-total-sum">
{{ filteredExtensions.reduce((sum, item) => sum + item.length, 0) }}
</span>
</span>
</h3>
<div
v-for="extension in extensionGroup"
:id="extension.pkg.replace('eu.kanade.tachiyomi.extension.', '')"
:key="extension.apk"
class="anchor"
>
<div class="extension">
<a
:href="`#${extension.pkg.replace('eu.kanade.tachiyomi.extension.', '')}`"
class="header-anchor"
aria-hidden="true"
@click.stop
>
#
</a>
<img class="extension-icon" :src="iconUrl(extension.apk)" width="42" height="42" />
<div class="extension-text">
<div class="upper">
<span class="font-semibold">{{ extension.name.split(": ")[1] }}</span>
<Badge :text="'v' + extension.version" />
</div>
<div class="down">
{{ extension.pkg.replace("eu.kanade.tachiyomi.extension.", "") }}
</div>
</div>
<a :href="apkUrl(extension.apk)" class="extension-download" title="Download APK" download>
<MaterialIcon icon="cloud_download" />
<span>Download</span>
</a>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import axios from "axios"; import ExtensionGroup from "./ExtensionGroup.vue";
import groupBy from "lodash.groupby";
import ISO6391 from "iso-639-1";
import { GITHUB_EXTENSION_JSON } from "../constants";
export default { export default {
data() { components: { ExtensionGroup },
return { props: ["extensions"],
extensions: [],
filters: {
search: "",
lang: [],
nsfw: "Don't care",
sort: "Ascending",
},
loading: true,
};
},
computed: { computed: {
filteredExtensions() { totalCount() {
const { extensions, filters } = this; return this.extensions.reduce((sum, item) => sum + item.length, 0);
const filtered = [];
for (const group of extensions) {
let filteredGroup = filters.lang.length ? (filters.lang.includes(group[0].lang) ? group : []) : group;
if (filters.search) {
filteredGroup = filteredGroup.filter((ext) =>
ext.name.toLowerCase().includes(filters.search.toLowerCase())
);
}
filteredGroup = filteredGroup.filter((ext) =>
filters.nsfw === "Don't care" ? true : ext.nsfw === (filters.nsfw === "Yes" ? 1 : 0)
);
if (filters.sort && filters.sort === "Descending") {
filteredGroup = filteredGroup.reverse();
}
if (filteredGroup.length) {
filtered.push(filteredGroup);
}
}
return filtered;
}, },
}, },
async beforeMount() {
const { data } = await axios.get(GITHUB_EXTENSION_JSON);
const values = Object.values(groupBy(data, "lang"));
values.sort((a, b) => {
const langA = this.simpleLangName(a[0].lang);
const langB = this.simpleLangName(b[0].lang);
if (langA === "All" && langB === "English") {
return -1;
}
if (langA === "English" && langB === "All") {
return 1;
}
if (langA === "English") {
return -1;
}
if (langB === "English") {
return 1;
}
if (langA < langB) {
return -1;
}
if (langA > langB) {
return 1;
}
return 0;
});
this.$data.extensions = values;
this.$nextTick(() => {
this.loading = false;
});
},
updated() {
if (window.location.hash) {
window.location.replace(window.location.hash);
}
},
methods: {
simpleLangName: (code) => (code === "all" ? "All" : ISO6391.getName(code)),
langName: (code) => (code === "all" ? "All" : `${ISO6391.getName(code)} (${ISO6391.getNativeName(code)})`),
iconUrl(pkg) {
const pkgName = pkg.substring(0, pkg.lastIndexOf("."));
return `https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/icon/${pkgName}.png`;
},
apkUrl: (apk) => `https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/apk/${apk}`,
},
}; };
</script> </script>
<style lang="stylus"> <style lang="stylus">
.extension-list .extension-list
h3 h3
@ -195,79 +27,4 @@ export default {
&:not(:first-of-type) &:not(:first-of-type)
.extensions-total .extensions-total
display none display none
.filters-list
display flex
flex-direction column
row-gap 1rem
.extensions-total
float right
&-sum
color $accentColor
.anchor
margin-top -3.9em
padding-bottom 0.2em
padding-top 4.5em
.extension
align-items center
display flex
padding 0.4em 1.5em
.header-anchor
padding-left 0.2em
padding-right 0.2em
font-size 1.4em
opacity 0
&:hover .header-anchor
opacity 1
.extension-icon
margin-right 0.5em
.extension-text
flex 1
.upper
.badge
margin-left 8px
.down
color #6c757d
font-family monospace
font-size 0.9rem
.extension-download
margin-right 0.5em
padding-left 1rem
padding-right 1rem
padding-top .5rem
padding-bottom .5rem
font-weight 700
border-radius 4px
color white
background-color $accentColor
border 1px solid $accentColor
.material-icons
color white
max-width 18px
&:hover
background-color white
color $accentColor
text-decoration none
.material-icons
color $accentColor
@media (max-width 767px)
padding 0.4em 0em
.extension-text .down,
.extension-download span
display none
@media (max-width 767px)
.extension
border 1px solid $borderColor
border-radius 8px
.extension-download
background-color $accentColor
&:target
.extension
background-color $containerBackground
border-radius 8px
transition 500ms background-color
&:first-child
border-top 1px solid $borderColor
</style> </style>

View File

@ -0,0 +1,103 @@
<template>
<div v-if="loading" v-loading.lock="loading" style="min-height: 200px"></div>
<div v-else>
<Filters :extensions="extensions" @filters="filters = $event" />
<ExtensionList :extensions="filteredExtensions" />
</div>
</template>
<script>
import axios from "axios";
import groupBy from "lodash.groupby";
import { simpleLangName } from "../scripts/languages";
import { GITHUB_EXTENSION_JSON } from "../constants";
import ExtensionList from "./ExtensionList.vue";
import Filters from "./Filters.vue";
export default {
components: { ExtensionList, Filters },
data() {
return {
extensions: [],
filters: {
search: "",
lang: [],
nsfw: "Don't care",
sort: "Ascending",
},
loading: true,
observer: null,
};
},
computed: {
filteredExtensions() {
const { extensions, filters } = this;
const filtered = [];
for (const group of extensions) {
let filteredGroup = filters.lang.length ? (filters.lang.includes(group[0].lang) ? group : []) : group;
if (filters.search) {
filteredGroup = filteredGroup.filter((ext) =>
ext.name.toLowerCase().includes(filters.search.toLowerCase())
);
}
filteredGroup = filteredGroup.filter((ext) =>
filters.nsfw === "Don't care" ? true : ext.nsfw === (filters.nsfw === "Yes" ? 1 : 0)
);
if (filters.sort && filters.sort === "Descending") {
filteredGroup = filteredGroup.reverse();
}
if (filteredGroup.length) {
filtered.push(filteredGroup);
}
}
return filtered;
},
},
async beforeMount() {
const { data } = await axios.get(GITHUB_EXTENSION_JSON);
const values = Object.values(groupBy(data, "lang"));
values.sort(this.sortLanguages);
this.$data.extensions = values;
this.$nextTick(() => {
this.loading = false;
});
},
updated() {
if (window.location.hash) {
window.location.replace(window.location.hash);
}
},
methods: {
sortLanguages(a, b) {
const langA = simpleLangName(a[0].lang);
const langB = simpleLangName(b[0].lang);
if (langA === "All" && langB === "English") {
return -1;
}
if (langA === "English" && langB === "All") {
return 1;
}
if (langA === "English") {
return -1;
}
if (langB === "English") {
return 1;
}
if (langA < langB) {
return -1;
}
if (langA > langB) {
return 1;
}
return 0;
},
},
};
</script>

View File

@ -0,0 +1,67 @@
<template>
<span class="filters-list">
<ElInput v-model="filters.search" placeholder="Search extensions by name..." clearable />
<ElSelect v-model="filters.lang" placeholder="Show specific languages..." multiple clearable>
<ElOption
v-for="[group] in extensions"
:key="group.lang"
:label="group.lang === 'en' ? simpleLangName(group.lang) : langName(group.lang)"
:value="group.lang"
/>
</ElSelect>
<div>
Sort by
<ElRadioGroup v-model="filters.sort">
<ElRadioButton label="Ascending"></ElRadioButton>
<ElRadioButton label="Descending"></ElRadioButton>
</ElRadioGroup>
</div>
<div>
Display extensions with NSFW content?
<ElRadioGroup v-model="filters.nsfw">
<ElRadioButton label="Yes"></ElRadioButton>
<ElRadioButton label="No"></ElRadioButton>
<ElRadioButton label="Don't care"></ElRadioButton>
</ElRadioGroup>
</div>
</span>
</template>
<script>
import { simpleLangName, langName } from "../scripts/languages";
export default {
props: ["extensions"],
emits: ["filters"],
data() {
return {
filters: {
search: "",
lang: [],
nsfw: "Don't care",
sort: "Ascending",
},
};
},
watch: {
filters: {
handler(value) {
this.$emit("filters", this.filters);
},
deep: true,
},
},
methods: {
simpleLangName,
langName
},
};
</script>
<style lang="stylus">
.filters-list
display flex
flex-direction column
row-gap 1rem
</style>

View File

@ -19,7 +19,7 @@ module.exports = {
// Custom headers // Custom headers
["link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin:""}], ["link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin:""}],
["link", { rel: "stylesheet", href: "https://fonts.googleapis.com/css?family=Open+Sans"}], ["link", { rel: "stylesheet", href: "https://fonts.googleapis.com/css?family=Open+Sans"}],
["script", {src: "/scripts/remove_service_worker.js"}] ["script", {src: "/scripts/remove_service_worker.js"}],
], ],
themeConfig: { themeConfig: {

View File

@ -0,0 +1,9 @@
import ISO6391 from "iso-639-1";
export function simpleLangName(code) {
return code === "all" ? "All" : ISO6391.getName(code);
}
export function langName(code) {
return code === "all" ? "All" : `${ISO6391.getName(code)} (${ISO6391.getNativeName(code)})`;
}

View File

@ -8,4 +8,4 @@ lang: en-US
List of available extensions to use with Tachiyomi, you can download them from here or from the app. List of available extensions to use with Tachiyomi, you can download them from here or from the app.
<ExtensionList/> <Extensions/>