diff --git a/CHANGELOG.md b/CHANGELOG.md index 626471219..c919fac70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ You can also check [on GitHub](https://github.com/nextcloud/news/releases), the # Unreleased ## [25.x.x] ### Changed +- add an information list for feeds that displays data such as last or next update dates ### Fixed - OPML import use text field for title if title field is missing (#3016) diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue index 411077a58..d8621004f 100644 --- a/src/components/Sidebar.vue +++ b/src/components/Sidebar.vue @@ -158,6 +158,10 @@ {{ t('news', 'Keyboard shortcuts') }} </NcButton> <HelpModal v-if="showHelp" @close="showHelp=false" /> + <NcButton @click="showFeedInfoTable = true"> + {{ t('news', 'Article feed information') }} + </NcButton> + <FeedInfoTable v-if="showFeedInfoTable" @close="showFeedInfoTable = false" /> <div> <div class="select-container"> <label> @@ -289,6 +293,7 @@ import MoveFeed from './MoveFeed.vue' import SidebarFeedLinkActions from './SidebarFeedLinkActions.vue' import HelpModal from './modals/HelpModal.vue' +import FeedInfoTable from './modals/FeedInfoTable.vue' import { Folder } from '../types/Folder' import { Feed } from '../types/Feed' @@ -315,6 +320,7 @@ export default Vue.extend({ DownloadIcon, SidebarFeedLinkActions, HelpModal, + FeedInfoTable, }, data: () => { return { @@ -323,6 +329,7 @@ export default Vue.extend({ feedToMove: undefined, ROUTES, showHelp: false, + showFeedInfoTable: false, polling: null, uploadStatus: null, selectedFile: null, @@ -475,6 +482,9 @@ export default Vue.extend({ subscribe('news:global:toggle-help-dialog', () => { this.showHelp = !this.showHelp }) + subscribe('news:global:toggle-feed-info', () => { + this.showFeedInfoTable = !this.showFeedInfoTable + }) }, beforeDestroy() { clearInterval(this.polling) diff --git a/src/components/modals/FeedInfoTable.vue b/src/components/modals/FeedInfoTable.vue new file mode 100644 index 000000000..9f7d3d7bc --- /dev/null +++ b/src/components/modals/FeedInfoTable.vue @@ -0,0 +1,202 @@ +<!-- + - SPDX-FileCopyrightText: 2025 Nextcloud News + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <NcModal size="large" + @close="$emit('close')"> + <div class="table-modal"> + <h2>{{ t('news', 'Article feed information') }}</h2> + <table> + <thead> + <tr> + <th @click="sortBy('id')"> + <span class="column-title"> + ID + <SortAscIcon v-if="sortKey === 'id' && sortOrder === 1" /> + <SortDescIcon v-if="sortKey === 'id' && sortOrder !== 1" /> + </span> + </th> + <th @click="sortBy('title')"> + <span class="column-title"> + {{ t('news', 'Title') }} + <SortAscIcon v-if="sortKey === 'title' && sortOrder === 1" /> + <SortDescIcon v-if="sortKey === 'title' && sortOrder !== 1" /> + </span> + </th> + <th @click="sortBy('lastModified')"> + <span class="column-title"> + {{ t('news', 'Last update') }} + <SortAscIcon v-if="sortKey === 'lastModified' && sortOrder === 1" /> + <SortDescIcon v-if="sortKey === 'lastModified' && sortOrder !== 1" /> + </span> + </th> + <th @click="sortBy('nextUpdateTime')"> + <span class="column-title"> + {{ t('news', 'Next update') }} + <SortAscIcon v-if="sortKey === 'nextUpdateTime' && sortOrder === 1" /> + <SortDescIcon v-if="sortKey === 'nextUpdateTime' && sortOrder !== 1" /> + </span> + </th> + <th :title="t('news', 'Articles per update')" + @click="sortBy('articlesPerUpdate')"> + <span class="column-title"> + APU + <SortAscIcon v-if="sortKey === 'articlesPerUpdate' && sortOrder === 1" /> + <SortDescIcon v-if="sortKey === 'articlesPerUpdate' && sortOrder !== 1" /> + </span> + </th> + <th :title="t('news', 'Error Count') " + @click="sortBy('updateErrorCount')"> + <span class="column-title"> + EC + <SortAscIcon v-if="sortKey === 'updateErrorCount' && sortOrder === 1" /> + <SortDescIcon v-if="sortKey === 'updateErrorCount' && sortOrder !== 1" /> + </span> + </th> + </tr> + </thead> + <tbody> + <tr v-for="feed in sortedFeeds" :key="feed.id"> + <td>{{ feed.id }}</td> + <td>{{ feed.title }}</td> + <td>{{ formatDate(feed.lastModified/1000) }}</td> + <td>{{ formatDate(feed.nextUpdateTime*1000) }}</td> + <td>{{ feed.articlesPerUpdate }}</td> + <td :title="feed.lastUpdateError"> + {{ feed.updateErrorCount }} + </td> + </tr> + </tbody> + </table> + </div> + </NcModal> +</template> + +<script> +import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' +import SortAscIcon from 'vue-material-design-icons/SortAscending.vue' +import SortDescIcon from 'vue-material-design-icons/SortDescending.vue' +import { mapState } from 'vuex' + +export default { + name: 'FeedInfoTable', + components: { + NcModal, + SortAscIcon, + SortDescIcon, + }, + data() { + return { + sortKey: 'title', + sortOrder: 1, + } + }, + computed: { + ...mapState({ + feeds: state => state.feeds.feeds, + }), + sortedFeeds() { + const sorted = this.feeds + if (this.sortKey) { + sorted.sort((a, b) => { + const valueA = a[this.sortKey] + const valueB = b[this.sortKey] + if (typeof valueA === 'string') { + return valueA.localeCompare(valueB) * this.sortOrder + } else { + return (valueA - valueB) * this.sortOrder + } + }) + } + return sorted + }, + }, + methods: { + formatDate(timestamp) { + return new Date(timestamp).toLocaleDateString(undefined, { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) + }, + sortBy(key) { + if (this.sortKey === key) { + this.sortOrder *= -1 + } else { + this.sortKey = key + this.sortOrder = 1 + } + }, + }, +} +</script> + +<style lang="scss" scoped> + .table-modal { + width: max-content; + padding: 30px 40px 20px; + + h2 { + font-weight: bold; + } + } + + table { + margin-top: 24px; + border-collapse: collapse; + + tbody tr { + &:hover, &:focus, &:active { + background-color: transparent !important; + } + } + + thead tr { + border: none; + } + + th { + cursor: pointer; + font-weight: bold; + padding: .75rem 1rem .75rem 0; + border-bottom: 2px solid var(--color-background-darker); + &:hover { + background-color: var(--color-background-hover); + } + } + + td { + padding: .75rem 1rem .75rem 0; + border-top: 1px solid var(--color-background-dark); + border-bottom: unset; + + &.noborder { + border-top: unset; + } + + &.ellipsis_top { + padding-bottom: 0; + } + + &.ellipsis { + padding-top: 0; + padding-bottom: 0; + } + + &.ellipsis_bottom { + padding-top: 0; + } + } + + .column-title { + display: flex; + align-items: center; + gap: 4px; + } + + } +</style>