Merge pull request #265 from JsSucks/content-browser

Merge for now
This commit is contained in:
Alexei Stukov 2018-12-02 05:06:02 +02:00 committed by GitHub
commit ead51ae676
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 326 additions and 120 deletions

View File

@ -19,6 +19,54 @@ const ENDPOINTS = {
'statistics': `${APIBASE}/statistics`
};
const dummyTags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'];
const dummyRepo = {
name: 'ExampleRepository',
baseUri: 'https://github.com/Jiiks/ExampleRepository',
rawUri: 'https://github.com/Jiiks/ExampleRepository/raw/master'
};
const dummyVersion = () => `${Math.round(Math.random() * 3)}.${Math.round(Math.random() * 10)}.${Math.round(Math.random() * 10)}`;
const dummyFiles = {
readme: 'Example/readme.md',
previews: [{
large: 'Example/preview1-big.png',
thumb: 'Example/preview1-small.png'
}]
};
const dummyAuthor = 'DummyAuthor';
const dummyTimestamp = () => `2018-${Math.floor((Math.random() * 12) + 1).toString().padStart(2, '0')}-${Math.floor((Math.random() * 30) + 1).toString().padStart(2, '0')}T14:51:32.057Z`;
async function dummyThemes() {
// Simulate get
await new Promise(r => setTimeout(r, Math.random() * 3000));
const dummies = [];
for (let i = 0; i < 10; i++) {
dummies.push({
id: `theme${i}${btoa(Math.random()).substring(3, 9)}`,
name: `Dummy ${i}`,
tags: dummyTags,
installs: Math.floor(Math.random() * 10000),
updated: dummyTimestamp(),
rating: Math.floor(Math.random() * 1000),
activeUsers: Math.floor(Math.random() * 1000),
rated: Math.random() > .5,
version: dummyVersion(),
repository: dummyRepo,
files: dummyFiles,
author: dummyAuthor
});
}
return {
docs: dummies,
pagination: {
total: 25,
pages: 3,
limit: 9,
page: 1
}
};
}
export default class BdWebApi {
static get themes() {
@ -41,11 +89,14 @@ export default class BdWebApi {
}
static getThemes(args) {
return dummyThemes();
/*
if (!args) return request.get(ENDPOINTS.themes);
const { id } = args;
if (id) return request.get(ENDPOINTS.theme(id));
return request.get(ENDPOINTS.themes);
*/
}
static getUsers(args) {

View File

@ -12,6 +12,7 @@ import asar from 'asar';
import path, { dirname } from 'path';
import rimraf from 'rimraf';
import { remote } from 'electron';
import Content from './content';
import Globals from './globals';
import Database from './database';
@ -71,6 +72,28 @@ export default class {
return Globals.getPath(this.pathId);
}
static async packContent(path, contentPath) {
return new Promise((resolve, reject) => {
remote.dialog.showSaveDialog({
title: 'Save Package',
defaultPath: path,
filters: [
{
name: 'BetterDiscord Package',
extensions: ['bd']
}
]
}, filepath => {
if (!filepath) return;
asar.uncache(filepath);
asar.createPackage(contentPath, filepath, () => {
resolve(filepath);
});
});
});
}
/**
* Load all locally stored content.
* @param {bool} suppressErrors Suppress any errors that occur during loading of content

View File

@ -13,6 +13,7 @@ import Security from './security';
import { ReactComponents } from './reactcomponents';
import Reflection from './reflection';
import DiscordApi from './discordapi';
import ThemeManager from './thememanager';
export default class PackageInstaller {
@ -64,47 +65,39 @@ export default class PackageInstaller {
/**
* Installs or updates defined package
* @param {Byte[]|String} bytesOrPath byte array of binary or path to local file
* @param {String} name Package name
* @param {String} nameOrId Package name
* @param {Boolean} update Does an older version already exist
*/
static async installPackage(bytesOrPath, id, update = false) {
static async installPackage(bytesOrPath, nameOrId, contentType, update = false) {
let outputPath = null;
try {
const bytes = typeof bytesOrPath === 'string' ? fs.readFileSync(bytesOrPath) : bytesOrPath;
const outputName = `${id}.bd`;
outputPath = path.join(Globals.getPath('plugins'), outputName);
const bytes = typeof bytesOrPath === 'string' ? fs.readFileSync(bytesOrPath) : bytesOrPath;
const outputName = `${nameOrId}.bd`;
outputPath = path.join(Globals.getPath(`${contentType}s`), outputName);
fs.writeFileSync(outputPath, bytes);
if (!update) return PluginManager.preloadPackedContent(outputName);
const manager = contentType === 'plugin' ? PluginManager : ThemeManager;
const oldContent = PluginManager.getPluginById(id);
if (!update) return manager.preloadPackedContent(outputName);
if (update && oldContent.packed && oldContent.packed.packageName !== id) {
await oldContent.unload(true);
const oldContent = manager.findContent(nameOrId);
await oldContent.unload(true);
if (oldContent.packed && oldContent.packed.packageName !== nameOrId) {
rimraf(oldContent.packed.packagePath, err => {
if(err) console.log(err);
if (err) throw err;
});
return PluginManager.preloadPackedContent(outputName);
}
if (update && !oldContent.packed) {
await oldContent.unload(true);
} else {
rimraf(oldContent.contentPath, err => {
if (err) console.log(err);
if (err) throw err;
});
return PluginManager.preloadPackedContent(outputName);
}
return PluginManager.reloadContent(oldContent);
return manager.preloadPackedContent(outputName);
} catch (err) {
if (outputPath) {
rimraf(outputPath, err => {
if (err) console.log(err);
});
}
throw err;
}
}

View File

@ -1,9 +1,56 @@
.bd-pluginsview,
.bd-themesview {
.bd-onlinePh {
.bd-localPh {
.bd-scroller {
padding: 0 20px 0 0;
}
}
.bd-onlinePh,
.bd-localPh {
display: flex;
flex-direction: column;
margin: 10% 0;
margin: 10px 0;
.bd-spinnerContainer {
display: flex;
justify-content: center;
}
.bd-onlinePhHeader {
display: flex;
padding: 0 20px 0 10px;
min-height: 80px;
.bd-flexRow {
min-height: 40px;
}
.bd-searchHint {
flex-grow: 1;
line-height: 40px;
color: #fff;
}
.bd-searchSort {
span {
color: #fff;
line-height: 40px;
}
}
}
.bd-onlinePhBody {
margin-top: 10px;
.bd-spinnerContainer {
padding: 0;
}
.bd-scroller {
padding: 0 20px 0 0;
}
}
h3 {
color: #fff;

View File

@ -1,8 +1,7 @@
.bd-remoteCard {
display: flex;
flex-direction: column;
margin-top: 10px;
padding: 10px 0;
padding: 10px;
border-radius: 0;
border-bottom: 1px solid rgba(114, 118, 126, .3);
@ -52,17 +51,19 @@
.bd-remoteCardTags {
color: #828a97;
font-size: 10px;
line-height: 20px;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.bd-buttonGroup {
align-self: flex-end;
justify-content: flex-end;
flex-grow: 1;
max-height: 20px;
max-height: 30px;
.bd-button {
font-size: 12px;
font-size: 16px;
padding: 5px 10px;
}
}

View File

@ -164,11 +164,11 @@
.bd-fancySearch {
display: flex;
justify-content: flex-end;
transform: translateY(80px) translateX(-140px);
transition: all .5s ease-in-out;
// transform: translateY(80px) translateX(-140px);
// transition: all .5s ease-in-out;
&::before {
content: 'Search by name, description or tag...';
// content: 'Search by name, description or tag...';
color: #f6f6f7;
position: relative;
top: -20px;
@ -184,6 +184,12 @@
}
}
&.bd-disabled {
.bd-textInput {
opacity: .8;
}
}
.bd-textInput {
padding: 10px;
display: flex;

View File

@ -120,8 +120,14 @@
}
.bd-settingswrapContents {
padding: 0 20px;
margin-bottom: 84px;
padding: 0 0 0 20px;
}
.bd-scroller {
.bd-settingswrapContents {
margin-bottom: 84px;
padding: 0 20px;
}
}
}
}

View File

@ -23,11 +23,8 @@
<script>
// Imports
import asar from 'asar';
import electron from 'electron';
import fs from 'fs';
import { Toasts } from 'ui';
import { Settings } from 'modules';
import { Settings, PluginManager } from 'modules';
import { ClientLogger as Logger } from 'common';
import { shell } from 'electron';
import Card from './Card.vue';
@ -45,24 +42,13 @@
MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension, MiBoxDownload
},
methods: {
package() {
electron.remote.dialog.showSaveDialog({
title: 'Save Plugin Package',
defaultPath: this.plugin.name,
filters: [
{
name: 'BetterDiscord Package',
extensions: ['bd']
}
]
}, filepath => {
if (!filepath) return;
asar.uncache(filepath);
asar.createPackage(this.plugin.contentPath, filepath, () => {
Toasts.success('Plugin Packaged!');
});
});
async package() {
try {
const packagePath = await PluginManager.packContent(this.plugin.name, this.plugin.contentPath);
Toasts.success(`Plugin Packaged: ${packagePath}`);
} catch (err) {
Logger.log('PluginCard', err);
}
},
editPlugin() {
try {

View File

@ -9,7 +9,7 @@
*/
<template>
<SettingsWrapper headertext="Plugins">
<SettingsWrapper headertext="Plugins" :noscroller="true">
<div class="bd-tabbar" slot="header">
<div class="bd-button" :class="{'bd-active': local}" @click="showLocal">
<h3>Installed</h3>
@ -22,12 +22,26 @@
</div>
<div class="bd-flex bd-flexCol bd-pluginsview">
<div v-if="local" class="bd-flex bd-flexGrow bd-flexCol bd-pluginsContainer bd-localPlugins">
<PluginCard v-for="plugin in localPlugins" :plugin="plugin" :key="plugin.id" :data-plugin-id="plugin.id" @toggle-plugin="togglePlugin(plugin)" @reload-plugin="reloadPlugin(plugin)" @delete-plugin="unload => deletePlugin(plugin, unload)" @show-settings="dont_clone => showSettings(plugin, dont_clone)" />
<div v-if="local" class="bd-flex bd-flexGrow bd-flexCol bd-pluginsContainer bd-localPlugins bd-localPh">
<ScrollerWrap>
<PluginCard v-for="plugin in localPlugins" :plugin="plugin" :key="plugin.id" :data-plugin-id="plugin.id" @toggle-plugin="togglePlugin(plugin)" @reload-plugin="reloadPlugin(plugin)" @delete-plugin="unload => deletePlugin(plugin, unload)" @show-settings="dont_clone => showSettings(plugin, dont_clone)" />
</ScrollerWrap>
</div>
<div v-if="!local" class="bd-onlinePh">
<h3>Coming Soon</h3>
<a href="https://v2.betterdiscord.net/plugins" target="_new">Website Browser</a>
<div v-else class="bd-onlinePh">
<div class="bd-onlinePhHeader">
<div class="bd-fancySearch" :class="{'bd-disabled': loadingOnline, 'bd-active': loadingOnline || (onlinePlugins && onlinePlugins.docs)}">
<input type="text" class="bd-textInput" placeholder="Search" @keydown.enter="searchInput" @keyup.stop />
</div>
<div v-if="loadingOnline" class="bd-spinnerContainer">
<div class="bd-spinner7" />
</div>
</div>
<ScrollerWrap class="bd-onlinePhBody" v-if="!loadingOnline && onlinePlugins" :scrollend="scrollend">
<RemoteCard v-if="onlinePlugins && onlinePlugins.docs" v-for="plugin in onlinePlugins.docs" :key="plugin.id" :item="plugin" />
<div v-if="loadingMore" class="bd-spinnerContainer">
<div class="bd-spinner7" />
</div>
</ScrollerWrap>
</div>
</div>
</SettingsWrapper>
@ -38,7 +52,7 @@
import { PluginManager } from 'modules';
import { Modals } from 'ui';
import { ClientLogger as Logger } from 'common';
import { MiRefresh } from '../common';
import { MiRefresh, ScrollerWrap } from '../common';
import SettingsWrapper from './SettingsWrapper.vue';
import PluginCard from './PluginCard.vue';
import RefreshBtn from '../common/RefreshBtn.vue';
@ -48,12 +62,15 @@
return {
PluginManager,
local: true,
localPlugins: PluginManager.localPlugins
localPlugins: PluginManager.localPlugins,
onlinePlugins: null,
loadingOnline: false,
loadingMore: false
};
},
components: {
SettingsWrapper, PluginCard,
MiRefresh,
MiRefresh, ScrollerWrap,
RefreshBtn
},
methods: {
@ -96,6 +113,24 @@
return Modals.contentSettings(plugin, null, {
dont_clone
});
},
searchInput(e) {
if (this.loadingOnline || this.loadingMore) return;
this.refreshOnline();
},
async scrollend(e) {
// TODO
return;
if (this.loadingOnline || this.loadingMore) return;
this.loadingMore = true;
try {
const getPlugins = await BdWebApi.plugins.get();
this.onlinePlugins.docs = [...this.onlinePlugins.docs, ...getPlugins.docs];
} catch (err) {
Logger.err('PluginsView', err);
} finally {
this.loadingMore = false;
}
}
}
}

View File

@ -20,7 +20,7 @@
<div class="bd-remoteCardInfoBox bd-flex bd-flexGrow bd-flexCol">
<div class="bd-remoteCardInfo">{{item.installs}} Installs</div>
<div class="bd-remoteCardInfo">{{item.activeUsers}} Active Users</div>
<div class="bd-remoteCardInfo">Updated: Some time ago</div>
<div class="bd-remoteCardInfo">Updated {{fromNow()}}</div>
</div>
</div>
</div>
@ -29,13 +29,16 @@
<div class="bd-buttonGroup">
<div class="bd-button">Install</div>
<div class="bd-button">Preview</div>
<div class="bd-button">Source</div>
<div class="bd-button" @click="openSourceUrl">Source</div>
</div>
</div>
</div>
</template>
<script>
import { Reflection } from 'modules';
import { shell } from 'electron';
export default {
props: ['item'],
data() {
@ -44,6 +47,15 @@
methods: {
resolveThumb() {
return `${this.item.repository.rawUri}/${this.item.files.previews[0].thumb}`;
},
fromNow() {
const { Moment } = Reflection.modules;
return Moment(this.item.updated).fromNow();
},
openSourceUrl() {
if (!this.item.repository || !this.item.repository.baseUri) return;
if (Object.assign(document.createElement('a'), { href: this.item.repository.baseUri }).hostname !== 'github.com') return;
shell.openExternal(this.item.repository.baseUri);
}
}
}

View File

@ -10,7 +10,16 @@
<template>
<div class="bd-settingswrap">
<ScrollerWrap>
<div v-if="noscroller" class="bd-flex bd-flexCol">
<div class="bd-settingswrapHeader">
<span class="bd-settingswrapHeaderText">{{ headertext }}</span>
<slot name="header" />
</div>
<div class="bd-settingswrapContents bd-flex bd-flexGrow bd-flexCol">
<slot />
</div>
</div>
<ScrollerWrap v-else :scrollend="scrollend">
<div class="bd-settingswrapHeader">
<span class="bd-settingswrapHeaderText">{{ headertext }}</span>
<slot name="header" />
@ -27,7 +36,7 @@
import { ScrollerWrap } from '../common';
export default {
props: ['headertext'],
props: ['headertext', 'scrollend', 'noscroller'],
components: {
ScrollerWrap
}

View File

@ -12,6 +12,7 @@
<Card :item="theme">
<SettingSwitch slot="toggle" :value="theme.enabled" @input="$emit('toggle-theme')" />
<ButtonGroup slot="controls" v-if="!online">
<Button v-if="devmode && !theme.packed" v-tooltip="'Package Theme'" @click="package"><MiBoxDownload size="18" /></Button>
<Button v-tooltip="'Settings (shift + click to open settings without cloning the set)'" v-if="theme.hasSettings" @click="$emit('show-settings', $event.shiftKey)"><MiSettings size="18" /></Button>
<Button v-tooltip="'Recompile (shift + click to reload)'" @click="$emit('reload-theme', $event.shiftKey)"><MiRefresh size="18" /></Button>
<Button v-tooltip="'Edit'" @click="editTheme"><MiPencil size="18" /></Button>
@ -22,17 +23,33 @@
<script>
// Imports
import { Toasts } from 'ui';
import { Settings, ThemeManager } from 'modules';
import { ClientLogger as Logger } from 'common';
import { shell } from 'electron';
import Card from './Card.vue';
import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension } from '../common';
import { Button, ButtonGroup, SettingSwitch, MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension, MiBoxDownload } from '../common';
export default {
data() {
return {
devmode: Settings.getSetting('core', 'advanced', 'developer-mode').value
}
},
props: ['theme', 'online'],
components: {
Card, Button, ButtonGroup, SettingSwitch,
MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension
MiSettings, MiRefresh, MiPencil, MiDelete, MiExtension, MiBoxDownload
},
methods: {
async package() {
try {
const packagePath = await ThemeManager.packContent(this.theme.name, this.theme.contentPath);
Toasts.success(`Theme Packaged: ${packagePath}`);
} catch (err) {
Logger.log('ThemeCard', err);
}
},
editTheme() {
try {
shell.openItem(this.theme.contentPath);

View File

@ -9,7 +9,7 @@
*/
<template>
<SettingsWrapper headertext="Themes">
<SettingsWrapper headertext="Themes" :noscroller="true">
<div class="bd-tabbar" slot="header">
<div class="bd-button" :class="{'bd-active': local}" @click="showLocal">
<h3>Installed</h3>
@ -22,15 +22,38 @@
</div>
<div class="bd-flex bd-flexCol bd-themesview">
<div v-if="local" class="bd-flex bd-flexGrow bd-flexCol bd-themesContainer bd-localThemes">
<ThemeCard v-for="theme in localThemes" :theme="theme" :key="theme.id" :data-theme-id="theme.id" @toggle-theme="toggleTheme(theme)" @reload-theme="reload => reloadTheme(theme, reload)" @show-settings="dont_clone => showSettings(theme, dont_clone)" @delete-theme="unload => deleteTheme(theme, unload)" />
<div v-if="local" class="bd-flex bd-flexGrow bd-flexCol bd-themesContainer bd-localPh">
<ScrollerWrap>
<ThemeCard v-for="theme in localThemes" :theme="theme" :key="theme.id" :data-theme-id="theme.id" @toggle-theme="toggleTheme(theme)" @reload-theme="reload => reloadTheme(theme, reload)" @show-settings="dont_clone => showSettings(theme, dont_clone)" @delete-theme="unload => deleteTheme(theme, unload)" />
</ScrollerWrap>
</div>
<div v-if="!local" class="bd-onlinePh">
<div class="bd-fancySearch" :class="{'bd-active': loadingOnline || (onlineThemes && onlineThemes.docs)}">
<input type="text" class="bd-textInput" @keydown.enter="searchInput" @keyup.stop/>
<div v-else class="bd-onlinePh">
<div class="bd-onlinePhHeader bd-flexCol">
<div class="bd-flex bd-flexRow">
<div v-if="loadingOnline" class="bd-spinnerContainer">
<div class="bd-spinner7" />
</div>
<div class="bd-searchHint">{{searchHint}}</div>
<div class="bd-fancySearch" :class="{'bd-disabled': loadingOnline, 'bd-active': loadingOnline || (onlineThemes && onlineThemes.docs)}">
<input type="text" class="bd-textInput" placeholder="Search" @keydown.enter="searchInput" @keyup.stop />
</div>
</div>
<div class="bd-flex bd-flexRow" v-if="onlineThemes && onlineThemes.docs && onlineThemes.docs.length">
<div class="bd-searchSort bd-flex bd-flexGrow">
<span class="bd-flexGrow">Sort by:</span>
<div class="bd-sort">Name</div>
<div class="bd-sort">Updated</div>
<div class="bd-sort">Installs</div>
<div class="bd-sort">Users</div>
</div>
</div>
</div>
<h2 v-if="loadingOnline">Loading</h2>
<RemoteCard v-else-if="onlineThemes && onlineThemes.docs" v-for="theme in onlineThemes.docs" :key="theme.id" :item="theme"/>
<ScrollerWrap class="bd-onlinePhBody" v-if="!loadingOnline && onlineThemes" :scrollend="scrollend">
<RemoteCard v-if="onlineThemes && onlineThemes.docs" v-for="theme in onlineThemes.docs" :key="theme.id" :item="theme" />
<div v-if="loadingMore" class="bd-spinnerContainer">
<div class="bd-spinner7"/>
</div>
</ScrollerWrap>
</div>
</div>
</SettingsWrapper>
@ -41,7 +64,7 @@
import { ThemeManager, BdWebApi } from 'modules';
import { Modals } from 'ui';
import { ClientLogger as Logger } from 'common';
import { MiRefresh } from '../common';
import { MiRefresh, ScrollerWrap } from '../common';
import SettingsWrapper from './SettingsWrapper.vue';
import ThemeCard from './ThemeCard.vue';
import RemoteCard from './RemoteCard.vue';
@ -54,12 +77,14 @@
local: true,
localThemes: ThemeManager.localThemes,
onlineThemes: null,
loadingOnline: false
loadingOnline: false,
loadingMore: false,
searchHint: ''
};
},
components: {
SettingsWrapper, ThemeCard, RemoteCard,
MiRefresh,
MiRefresh, ScrollerWrap,
RefreshBtn
},
methods: {
@ -74,38 +99,14 @@
await this.ThemeManager.refreshThemes();
},
async refreshOnline() {
this.searchHint = '';
if (this.loadingOnline || this.loadingMore) return;
this.loadingOnline = true;
try {
// const getThemes = await BdWebApi.themes.get();
// this.onlineThemes = JSON.parse(getThemes);
const dummies = [];
for (let i = 0; i < 10; i++) {
dummies.push({
id: `theme${i}`,
name: `Dummy ${i}`,
tags: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'],
installs: Math.floor(Math.random() * 10000),
updated: '2018-07-21T14:51:32.057Z',
rating: Math.floor(Math.random() * 1000),
activeUsers: Math.floor(Math.random() * 1000),
rated: Math.random() > .5,
version: '1.0.0',
repository: {
name: 'ExampleRepository',
baseUri: 'https://github.com/Jiiks/ExampleRepository',
rawUri: 'https://github.com/Jiiks/ExampleRepository/raw/master'
},
files: {
readme: 'Example/readme.md',
previews: [{
large: 'Example/preview1-big.png',
thumb: 'Example/preview1-small.png'
}]
},
author: 'Jiiks'
});
}
this.onlineThemes = { docs: dummies };
const getThemes = await BdWebApi.themes.get();
this.onlineThemes = getThemes;
if (!this.onlineThemes.docs) return;
this.searchHint = `${this.onlineThemes.pagination.total} Results`;
} catch (err) {
Logger.err('ThemesView', err);
} finally {
@ -140,8 +141,20 @@
});
},
searchInput(e) {
this.loadingOnline = true;
setTimeout(this.refreshOnline, 1000);
if (this.loadingOnline || this.loadingMore) return;
this.refreshOnline();
},
async scrollend(e) {
if (this.loadingOnline || this.loadingMore) return;
this.loadingMore = true;
try {
const getThemes = await BdWebApi.themes.get();
this.onlineThemes.docs = [...this.onlineThemes.docs, ...getThemes.docs];
} catch (err) {
Logger.err('ThemesView', err);
} finally {
this.loadingMore = false;
}
}
}
}

View File

@ -109,7 +109,7 @@
},
async install() {
try {
const installed = await PackageInstaller.installPackage(this.modal.filePath, this.modal.config.info.id, this.alreadyInstalled);
const installed = await PackageInstaller.installPackage(this.modal.filePath, this.modal.config.info.id || this.modal.config.info.name, this.modal.contentType, this.alreadyInstalled);
this.installed = installed;
} catch (err) {
console.log(err);

View File

@ -10,7 +10,7 @@
<template>
<div class="bd-scrollerWrap" :class="{'bd-dark': dark}">
<div class="bd-scroller">
<div class="bd-scroller" @scroll="onscroll">
<slot/>
</div>
</div>
@ -18,6 +18,13 @@
<script>
export default {
props: ['dark']
props: ['dark', 'scrollend'],
methods: {
onscroll(e) {
if (!this.scrollend) return;
const { offsetHeight, scrollTop, scrollHeight } = e.target;
if (offsetHeight + scrollTop >= scrollHeight) this.scrollend(e);
}
}
}
</script>