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>
<div class="extension-list">
<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>
<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 v-for="group in extensions" :key="group[0].lang">
<ExtensionGroup :list="group" :total-count="totalCount" />
</div>
</div>
</template>
<script>
import axios from "axios";
import groupBy from "lodash.groupby";
import ISO6391 from "iso-639-1";
import { GITHUB_EXTENSION_JSON } from "../constants";
import ExtensionGroup from "./ExtensionGroup.vue";
export default {
data() {
return {
extensions: [],
filters: {
search: "",
lang: [],
nsfw: "Don't care",
sort: "Ascending",
},
loading: true,
};
},
components: { ExtensionGroup },
props: ["extensions"],
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;
totalCount() {
return this.extensions.reduce((sum, item) => sum + item.length, 0);
},
},
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>
<style lang="stylus">
.extension-list
h3
@ -195,79 +27,4 @@ export default {
&:not(:first-of-type)
.extensions-total
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>

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
["link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin:""}],
["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: {

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.
<ExtensionList/>
<Extensions/>