summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/App.vue1
-rw-r--r--src/components/FeedItem.vue56
-rw-r--r--src/components/ItemSkeleton.vue28
-rw-r--r--src/components/Starred.vue20
-rw-r--r--src/components/VirtualScroll.vue135
-rw-r--r--src/store/item.ts20
-rw-r--r--src/types/FeedItem.ts4
7 files changed, 254 insertions, 10 deletions
diff --git a/src/App.vue b/src/App.vue
index 0f5c1a0dc..98c3e393c 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -24,6 +24,7 @@ export default Vue.extend({
async created() {
await this.$store.dispatch(ACTIONS.FETCH_FOLDERS)
await this.$store.dispatch(ACTIONS.FETCH_FEEDS)
+ await this.$store.dispatch(ACTIONS.FETCH_STARRED)
},
})
</script>
diff --git a/src/components/FeedItem.vue b/src/components/FeedItem.vue
new file mode 100644
index 000000000..880178e16
--- /dev/null
+++ b/src/components/FeedItem.vue
@@ -0,0 +1,56 @@
+<template>
+ <div style="padding: 5px 10px; display: flex;" @click="expand()">
+ <div style="padding: 0px 5px;">
+ <EarthIcon />
+ </div>
+ <div style="flex-grow: 1; overflow: hidden; text-overflow: ellipsis;">
+ <span style="text-overflow: ellipsis;" :style="{ 'white-space': !isExpanded ? 'nowrap' : 'normal' }">
+ {{ item.title }}
+ </span>
+ </div>
+ <div class="button-container" style="display: flex; flex-direction: row;">
+ <StarIcon />
+ <EyeOutline />
+ <ShareVariant />
+ </div>
+ </div>
+</template>
+
+<script>
+import EarthIcon from 'vue-material-design-icons/Earth.vue'
+import StarIcon from 'vue-material-design-icons/Star.vue'
+import EyeOutline from 'vue-material-design-icons/EyeOutline.vue'
+import ShareVariant from 'vue-material-design-icons/ShareVariant.vue'
+
+export default {
+ name: 'FeedItem',
+ components: {
+ EarthIcon,
+ StarIcon,
+ EyeOutline,
+ ShareVariant,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ },
+ data: () => {
+ return {
+ expanded: false,
+ }
+ },
+ computed: {
+ isExpanded() {
+ return this.expanded
+ },
+ },
+ methods: {
+ expand() {
+ this.expanded = !this.expanded
+ },
+ },
+}
+
+</script>
diff --git a/src/components/ItemSkeleton.vue b/src/components/ItemSkeleton.vue
new file mode 100644
index 000000000..3b3b3f50c
--- /dev/null
+++ b/src/components/ItemSkeleton.vue
@@ -0,0 +1,28 @@
+<!--
+ - Copyright (c) 2020. The Nextcloud Bookmarks contributors.
+ -
+ - This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
+ -->
+
+<template>
+ <div :class="{
+ item: true,
+ }"
+ :style="{ background: 'var(--color-placeholder-dark)', height: '45px'}">
+ <div class="item__labels" />
+ <div class="item__actions" />
+ </div>
+</template>
+<script>
+
+export default {
+ name: 'ItemSkeleton',
+ components: {
+ },
+ computed: {
+ viewMode() {
+ return this.$store.state.settings.viewMode
+ },
+ },
+}
+</script>
diff --git a/src/components/Starred.vue b/src/components/Starred.vue
index d0f51ab45..b35e472d1 100644
--- a/src/components/Starred.vue
+++ b/src/components/Starred.vue
@@ -1,18 +1,24 @@
<template>
- <div>
- Starred Items:
- <div v-for="item in items.starredItems" :key="item.id">
- {{ item.title }}
- </div>
+ <div style="margin-top: 40px;">
+ <VirtualScroll :reached-end="items.starredLoaded">
+ <template v-if="items.starredItems && items.starredItems.length > 0">
+ <template v-for="item in items.starredItems">
+ <FeedItem :key="item.id" :item="item" />
+ </template>
+ </template>
+ </VirtualScroll>
</div>
</template>
<script lang="ts">
-import { FEED_ITEM_ACTION_TYPES } from '../store/item'
import { mapState } from 'vuex'
+import FeedItem from './FeedItem.vue'
+import VirtualScroll from './VirtualScroll.vue'
export default {
components: {
+ VirtualScroll,
+ FeedItem,
},
props: {
@@ -24,7 +30,7 @@ export default {
// },
},
async created() {
- await this.$store.dispatch(FEED_ITEM_ACTION_TYPES.FETCH_STARRED)
+ // TODO: ?
},
methods: {
async fetchStarred() {
diff --git a/src/components/VirtualScroll.vue b/src/components/VirtualScroll.vue
new file mode 100644
index 000000000..cf5aeb24e
--- /dev/null
+++ b/src/components/VirtualScroll.vue
@@ -0,0 +1,135 @@
+<!--
+ - Copyright (c) 2022. The Nextcloud Bookmarks contributors.
+ -
+ - This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
+ -->
+<script>
+import ItemSkeleton from './ItemSkeleton.vue'
+
+const GRID_ITEM_HEIGHT = 200 + 10
+// const GRID_ITEM_WIDTH = 250 + 10
+const LIST_ITEM_HEIGHT = 45 + 1
+
+export default {
+ name: 'VirtualScroll',
+ props: {
+ reachedEnd: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ viewport: { width: 0, height: 0 },
+ scrollTop: 0,
+ scrollHeight: 500,
+ initialLoadingSkeleton: false,
+ initialLoadingTimeout: null,
+ }
+ },
+ computed: {
+ fetching() {
+ return this.$store.state.items.fetchingItems
+ },
+ },
+ watch: {
+ newBookmark() {
+ this.$el.scrollTop = 0
+ },
+ newFolder() {
+ this.$el.scrollTop = 0
+ },
+ },
+ mounted() {
+ this.onScroll()
+ window.addEventListener('resize', this.onScroll)
+ },
+ destroyed() {
+ window.removeEventListener('resize', this.onScroll)
+ },
+ methods: {
+ onScroll() {
+ this.scrollTop = this.$el.scrollTop
+ this.scrollHeight = this.$el.scrollHeight
+ },
+ },
+ render(h) {
+ let children = []
+ let renderedItems = 0
+ let upperPaddingItems = 0
+ let lowerPaddingItems = 0
+ let itemHeight = 1
+ const padding = GRID_ITEM_HEIGHT
+ if (this.$slots.default && this.$el && this.$el.getBoundingClientRect) {
+ const childComponents = this.$slots.default.filter(child => !!child.componentOptions)
+ const viewport = this.$el.getBoundingClientRect()
+ itemHeight = LIST_ITEM_HEIGHT
+ renderedItems = Math.floor((viewport.height + padding + padding) / itemHeight)
+ upperPaddingItems = Math.floor(Math.max(this.scrollTop - padding, 0) / itemHeight)
+ children = childComponents.slice(upperPaddingItems, upperPaddingItems + renderedItems)
+ renderedItems = children.length
+ lowerPaddingItems = Math.max(childComponents.length - upperPaddingItems - renderedItems, 0)
+ }
+
+ if (!this.reachedEnd && lowerPaddingItems === 0) {
+ if (!this.fetching) {
+ this.$emit('load-more')
+ }
+ if (upperPaddingItems + renderedItems + lowerPaddingItems === 0) {
+ if (!this.initialLoadingSkeleton) {
+ // The first 350ms don't display skeletons
+ this.initialLoadingTimeout = setTimeout(() => {
+ this.initialLoadingSkeleton = true
+ this.$forceUpdate()
+ }, 350)
+ return h('div', { class: 'virtual-scroll' })
+ }
+ }
+
+ children = [...children, ...Array(40).fill(0).map(() =>
+ h(ItemSkeleton),
+ )]
+ }
+
+ if (upperPaddingItems + renderedItems + lowerPaddingItems > 0) {
+ this.initialLoadingSkeleton = false
+ if (this.initialLoadingTimeout) {
+ clearTimeout(this.initialLoadingTimeout)
+ }
+ }
+
+ const scrollTop = this.scrollTop
+ this.$nextTick(() => {
+ this.$el.scrollTop = scrollTop
+ })
+
+ return h('div', {
+ class: 'virtual-scroll',
+ on: { scroll: () => this.onScroll() },
+ },
+ [
+ h('div', { class: 'upper-padding', style: { height: Math.max((upperPaddingItems) * itemHeight, 0) + 'px' } }),
+ h('div', { class: 'container-window', style: { height: Math.max((renderedItems) * itemHeight, 0) + 'px' } }, children),
+ h('div', { class: 'lower-padding', style: { height: Math.max((lowerPaddingItems) * itemHeight, 0) + 'px' } }),
+ ])
+ },
+}
+</script>
+
+<style scoped>
+.virtual-scroll {
+ height: calc(100vh - 50px - 50px - 10px);
+ position: relative;
+ overflow-y: scroll;
+}
+
+.bookmarkslist--gridview .container-window {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-content: start;
+ gap: 10px;
+ padding: 0 10px;
+ padding-top: 10px;
+}
+</style>
diff --git a/src/store/item.ts b/src/store/item.ts
index 09a6b6f96..d108e34a7 100644
--- a/src/store/item.ts
+++ b/src/store/item.ts
@@ -3,17 +3,24 @@ import axios from '@nextcloud/axios'
import { ActionParams } from '../store'
import { FEED_ITEM_MUTATION_TYPES } from '../types/MutationTypes'
import { API_ROUTES } from '../types/ApiRoutes'
+import { FeedItem } from '../types/FeedItem'
export const FEED_ITEM_ACTION_TYPES = {
FETCH_STARRED: 'FETCH_STARRED',
}
export type ItemState = {
- allItems: any[];
- starredItems: any[];
+ fetchingItems: boolean;
+ starredLoaded: boolean;
+
+ allItems: FeedItem[];
+ starredItems: FeedItem[];
}
const state: ItemState = {
+ fetchingItems: false,
+ starredLoaded: false,
+
allItems: [],
starredItems: [],
}
@@ -26,6 +33,7 @@ const getters = {
export const actions = {
async [FEED_ITEM_ACTION_TYPES.FETCH_STARRED]({ commit }: ActionParams) {
+ state.fetchingItems = true
const response = await axios.get(API_ROUTES.ITEMS, {
params: {
limit: 40,
@@ -33,15 +41,21 @@ export const actions = {
search: '',
showAll: false,
type: 2,
+ offset: 0,
},
})
commit(FEED_ITEM_MUTATION_TYPES.SET_STARRED, response.data.items)
+
+ if (response.data.items.length < 40) {
+ state.starredLoaded = true
+ }
+ state.fetchingItems = false
},
}
export const mutations = {
- [FEED_ITEM_MUTATION_TYPES.SET_STARRED](state: ItemState, items: any[]) {
+ [FEED_ITEM_MUTATION_TYPES.SET_STARRED](state: ItemState, items: FeedItem[]) {
items.forEach(it => {
state.starredItems.push(it)
})
diff --git a/src/types/FeedItem.ts b/src/types/FeedItem.ts
new file mode 100644
index 000000000..7ffe461c7
--- /dev/null
+++ b/src/types/FeedItem.ts
@@ -0,0 +1,4 @@
+export type FeedItem = {
+ id: string;
+ title: string;
+};