diff --git a/api/funkwhale_api/audio/serializers.py b/api/funkwhale_api/audio/serializers.py index cffa49b28e5370eedfdef800cbe516f0716a3eaf..6b9227dedc2d027248ca028e16af3233a9284609 100644 --- a/api/funkwhale_api/audio/serializers.py +++ b/api/funkwhale_api/audio/serializers.py @@ -384,12 +384,9 @@ def get_channel_from_rss_url(url, raise_exception=False): library=channel.library, delete_existing=True, ) - latest_upload_date = max([upload.creation_date for upload in uploads]) - if ( - not channel.artist.modification_date - or channel.artist.modification_date < latest_upload_date - ): - common_utils.update_modification_date(channel.artist) + if uploads: + latest_track_date = max([upload.track.creation_date for upload in uploads]) + common_utils.update_modification_date(channel.artist, date=latest_track_date) return channel, uploads diff --git a/api/funkwhale_api/common/utils.py b/api/funkwhale_api/common/utils.py index 98fd21355c33a62053926563c9563d105e756355..6c4238fc4ecc5d9165976a8af44c2c3e84c36c56 100644 --- a/api/funkwhale_api/common/utils.py +++ b/api/funkwhale_api/common/utils.py @@ -410,15 +410,15 @@ def get_audio_mimetype(mt): return aliases.get(mt, mt) -def update_modification_date(obj, field="modification_date"): +def update_modification_date(obj, field="modification_date", date=None): IGNORE_DELAY = 60 current_value = getattr(obj, field) - now = timezone.now() - ignore = current_value is not None and current_value < now - datetime.timedelta( + date = date or timezone.now() + ignore = current_value is not None and current_value < date - datetime.timedelta( seconds=IGNORE_DELAY ) if ignore: - setattr(obj, field, now) - obj.__class__.objects.filter(pk=obj.pk).update(**{field: now}) + setattr(obj, field, date) + obj.__class__.objects.filter(pk=obj.pk).update(**{field: date}) - return now + return date diff --git a/api/tests/audio/test_serializers.py b/api/tests/audio/test_serializers.py index f1dcd21c6a139e4bc0a649298e305ea5a0161b16..5ddef7c29bf8bc11ca634eddc4888d3b6413fca7 100644 --- a/api/tests/audio/test_serializers.py +++ b/api/tests/audio/test_serializers.py @@ -791,7 +791,7 @@ def test_get_channel_from_rss_url(db, r_mock, mocker): <itunes:subtitle>Subtitle</itunes:subtitle> <itunes:summary><![CDATA[<p>Html content</p>]]></itunes:summary> <guid isPermaLink="false"><![CDATA[16f66fff-41ae-4a1c-9101-2746218c4f32]]></guid> - <pubDate>Wed, 11 Mar 2020 16:00:00 GMT</pubDate> + <pubDate>Wed, 11 Mar 2020 18:00:00 GMT</pubDate> <itunes:duration>00:22:37</itunes:duration> <itunes:keywords>pop rock</itunes:keywords> <itunes:season>2</itunes:season> @@ -806,7 +806,7 @@ def test_get_channel_from_rss_url(db, r_mock, mocker): <itunes:subtitle>Subtitle</itunes:subtitle> <itunes:summary><![CDATA[<p>Html content</p>]]></itunes:summary> <guid isPermaLink="false"><![CDATA[16f66fff-41ae-4a1c-910e-2746218c4f32]]></guid> - <pubDate>Wed, 11 Mar 2020 16:00:00 GMT</pubDate> + <pubDate>Wed, 11 Mar 2020 17:00:00 GMT</pubDate> <itunes:duration>00:22:37</itunes:duration> <itunes:keywords>pop rock</itunes:keywords> <itunes:season>2</itunes:season> @@ -865,7 +865,9 @@ def test_get_channel_from_rss_url(db, r_mock, mocker): library=channel.library, delete_existing=True, ) - update_modification_date.assert_called_once_with(channel.artist) + update_modification_date.assert_called_once_with( + channel.artist, date=uploads[0].track.creation_date + ) def test_get_channel_from_rss_honor_mrf_inbox_before_http( diff --git a/front/src/components/RemoteSearchForm.vue b/front/src/components/RemoteSearchForm.vue new file mode 100644 index 0000000000000000000000000000000000000000..56167216f4a47888ed92021bfca1bf3626784e09 --- /dev/null +++ b/front/src/components/RemoteSearchForm.vue @@ -0,0 +1,188 @@ +<template> + <div> + <form id="remote-search" :class="['ui', {loading: isLoading}, 'form']" @submit.stop.prevent="submit"> + <div v-if="errors.length > 0" class="ui negative message"> + <div class="header"><translate translate-context="Content/*/Error message.Title">Error while fetching object</translate></div> + <ul class="list"> + <li v-for="error in errors">{{ error }}</li> + </ul> + </div> + <div class="ui required field"> + <label for="object-id"> + {{ labels.fieldLabel }} + </label> + <p v-if="type === 'rss'"> + <translate translate-context="Content/Fetch/Paragraph">Paste here the RSS url or the fediverse address to subscribe to its feed.</translate> + </p> + <p v-else> + <translate translate-context="Content/Fetch/Paragraph">Use this form to retrieve an object hosted somewhere else in the fediverse.</translate> + </p> + <input type="text" name="object-id" id="object-id" :placeholder="labels.fieldPlaceholder" v-model="id" required> + </div> + <button v-if="showSubmit" type="submit" :class="['ui', 'primary', {loading: isLoading}, 'button']" :disabled="isLoading || !id || id.length === 0"> + <translate translate-context="Content/Search/Input.Label/Noun">Search</translate> + </button> + </form> + <div v-if="!isLoading && fetch && fetch.status === 'finished' && !redirectRoute" class="ui warning message"> + <p><translate translate-context="Content/*/Error message.Title">This kind of object isn't supported yet</translate></p> + </div> + </div> +</template> +<script> +import axios from 'axios' + +export default { + props: { + initialId: { type: String, required: false}, + type: { type: String, required: false}, + redirect: { type: Boolean, default: true}, + showSubmit: { type: Boolean, default: true}, + standalone: { type: Boolean, default: true}, + }, + + data () { + return { + id: this.initialId, + fetch: null, + obj: null, + isLoading: false, + errors: [], + } + }, + created () { + if (this.id) { + if (this.type === 'rss') { + this.rssSubscribe() + + } else { + this.createFetch() + } + } + }, + computed: { + labels() { + let title = this.$pgettext('Head/Fetch/Title', "Search a remote object") + let fieldLabel = this.$pgettext('Head/Fetch/Field.Label', "URL or @username") + let fieldPlaceholder = "" + if (this.type === "rss") { + title = this.$pgettext('Head/Fetch/Title', "Subscribe to a podcast RSS feed") + fieldLabel = this.$pgettext('*/*/*', "Channel location") + fieldLabel = this.$pgettext('*/*/*', "Channel location") + fieldPlaceholder = this.$pgettext('Head/Fetch/Field.Placeholder', "@channel@pod.example or https://website.example/rss.xml") + } + return { + title, + fieldLabel, + fieldPlaceholder, + } + }, + objInfo () { + if (this.fetch && this.fetch.status === 'finished') { + return this.fetch.object + } + }, + redirectRoute () { + if (!this.objInfo) { + return + } + switch (this.objInfo.type) { + case 'account': + let [username, domain] = this.objInfo.full_username.split('@') + return {name: 'profile.full', params: {username, domain}} + case 'library': + return {name: 'library.detail', params: {id: this.objInfo.uuid}} + case 'artist': + return {name: 'library.artists.detail', params: {id: this.objInfo.id}} + case 'album': + return {name: 'library.albums.detail', params: {id: this.objInfo.id}} + case 'track': + return {name: 'library.tracks.detail', params: {id: this.objInfo.id}} + case 'upload': + return {name: 'library.uploads.detail', params: {id: this.objInfo.uuid}} + + default: + break; + } + } + }, + + methods: { + submit () { + if (this.type === 'rss') { + return this.rssSubscribe() + } else { + return this.createFetch() + } + }, + createFetch () { + if (!this.id) { + return + } + if (this.standalone) { + this.$router.replace({name: "search", query: {id: this.id}}) + } + this.fetch = null + let self = this + self.errors = [] + self.isLoading = true + let payload = { + object: this.id + } + + axios.post('federation/fetches/', payload).then((response) => { + self.isLoading = false + self.fetch = response.data + if (self.fetch.status === 'errored' || self.fetch.status === 'skipped') { + self.errors.push( + self.$pgettext("Content/*/Error message.Title", "This object cannot be retrieved") + ) + } + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + rssSubscribe () { + if (!this.id) { + return + } + if (this.standalone) { + console.log('HELLO') + this.$router.replace({name: "search", query: {id: this.id, type: 'rss'}}) + } + this.fetch = null + let self = this + self.errors = [] + self.isLoading = true + let payload = { + url: this.id + } + + axios.post('channels/rss-subscribe/', payload).then((response) => { + self.isLoading = false + self.$store.commit('channels/subscriptions', {uuid: response.data.channel.uuid, value: true}) + self.$emit('subscribed', response.data) + if (self.redirect) { + self.$router.push({name: 'channels.detail', params: {id: response.data.channel.uuid}}) + } + + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + }, + }, + + watch: { + initialId (v) { + this.id = v + this.createFetch() + }, + redirectRoute (v) { + if (v && this.redirect) { + this.$router.push(v) + } + } + } +} +</script> diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 732847ef56e15a47fdbe2ccc8a13d2cd45877d6e..a6e63f4d98c8150f996860512bd6570147eef15b 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -134,6 +134,9 @@ <router-link class="item" :to="{name: 'favorites'}"><i class="heart icon"></i><translate translate-context="Sidebar/Favorites/List item.Link/Noun">Favorites</translate></router-link> </div> </div> + <router-link class="header item" :to="{name: 'subscriptions'}" v-if="$store.state.auth.authenticated"> + <translate translate-context="*/*/*/Noun">Subscriptions</translate> + </router-link> <div class="item"> <header class="header"> <translate translate-context="Footer/About/List item.Link">More</translate> diff --git a/front/src/components/audio/ChannelCard.vue b/front/src/components/audio/ChannelCard.vue index 8adbb9d2ce43b55f72b6d10c10b227567b1bec85..37db4805e7e7d9fc28d19a5fce934239ba6f927d 100644 --- a/front/src/components/audio/ChannelCard.vue +++ b/front/src/components/audio/ChannelCard.vue @@ -12,16 +12,24 @@ </router-link> </strong> <div class="description"> + <translate class="meta ellipsis" translate-context="Content/Channel/Paragraph" + translate-plural="%{ count } episodes" + :translate-n="object.artist.tracks_count" + :translate-params="{count: object.artist.tracks_count}"> + %{ count } episode + </translate> <tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="object.artist.tags"></tags-list> </div> + </div> <div class="extra content"> - <translate translate-context="Content/Channel/Paragraph" - translate-plural="%{ count } episodes" - :translate-n="object.artist.tracks_count" - :translate-params="{count: object.artist.tracks_count}"> - %{ count } episode - </translate> + <time + v-translate + class="meta ellipsis" + :datetime="object.artist.modification_date" + :title="updatedTitle"> + {{ object.artist.modification_date | fromNow }} + </time> <play-button class="right floated basic icon" :dropdown-only="true" :is-playable="true" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :artist="object.artist"></play-button> </div> </div> @@ -31,6 +39,8 @@ import PlayButton from '@/components/audio/PlayButton' import TagsList from "@/components/tags/List" +import {momentFormat} from '@/filters' + export default { props: { object: {type: Object}, @@ -58,6 +68,11 @@ export default { } else { return this.object.uuid } + }, + updatedTitle () { + let d = momentFormat(this.object.artist.modification_date) + let message = this.$pgettext('*/*/*', 'Updated on %{ date }') + return this.$gettextInterpolate(message, {date: d}) } } } diff --git a/front/src/components/common/InlineSearchBar.vue b/front/src/components/common/InlineSearchBar.vue index 0ba6d85a9685d1743d8d3b00852afbf1c319c722..3a32b84c0fb736882895e4ddb45008edb1ccf584 100644 --- a/front/src/components/common/InlineSearchBar.vue +++ b/front/src/components/common/InlineSearchBar.vue @@ -4,8 +4,8 @@ <label for="search-query" class="hidden"> <translate translate-context="Content/Search/Input.Label/Noun">Search</translate> </label> - <input id="search-query" name="search-query" type="text" :placeholder="labels.searchPlaceholder" :value="value" @input="$emit('input', $event.target.value)"> - <i v-if="isClearable" class="x link icon" :title="labels.clear" @click="$emit('input', ''); $emit('search', value)"></i> + <input id="search-query" name="search-query" type="text" :placeholder="placeholder || labels.searchPlaceholder" :value="value" @input="$emit('input', $event.target.value)"> + <i v-if="isClearable" class="x link icon" :title="labels.clear" @click.stop.prevent="$emit('input', ''); $emit('search', value)"></i> <button type="submit" class="ui icon basic button"> <i class="search icon"></i> </button> @@ -15,7 +15,8 @@ <script> export default { props: { - value: {type: String, required: true} + value: {type: String, required: true}, + placeholder: {type: String, required: false}, }, computed: { labels () { diff --git a/front/src/components/semantic/Modal.vue b/front/src/components/semantic/Modal.vue index 599ef02229e61be5f35ec03d2832a0bbdc6c1cf2..e3334c685f3a58f772e049d4f5a1309602e0bbe9 100644 --- a/front/src/components/semantic/Modal.vue +++ b/front/src/components/semantic/Modal.vue @@ -1,5 +1,5 @@ <template> - <div :class="['ui', {'active': show}, {'overlay fullscreen': ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal']"> + <div :class="['ui', {'active': show}, {'overlay fullscreen': fullscreen && ['phone', 'tablet'].indexOf($store.getters['ui/windowSize']) > -1},'modal']"> <i class="close inside icon"></i> <slot v-if="show"> @@ -12,7 +12,8 @@ import $ from 'jquery' export default { props: { - show: {type: Boolean, required: true} + show: {type: Boolean, required: true}, + fullscreen: {type: Boolean, default: true}, }, data () { return { diff --git a/front/src/filters.js b/front/src/filters.js index 6465e973a8f5754853132dd6075504b87b7fe858..b88df2f0ea2b5f0ec84729eaf2f3b09a93a54537 100644 --- a/front/src/filters.js +++ b/front/src/filters.js @@ -38,12 +38,38 @@ export function ago (date, locale) { lastDay: 'L', lastWeek: 'L', sameElse: 'L' -}) - + }) } Vue.filter('ago', ago) +export function fromNow (date, locale) { + locale = 'en' + moment.locale('en', { + relativeTime: { + future: 'in %s', + past: '%s ago', + s: 'seconds', + ss: '%ss', + m: 'a minute', + mm: '%dm', + h: 'an hour', + hh: '%dh', + d: 'a day', + dd: '%dd', + M: 'a month', + MM: '%dM', + y: 'a year', + yy: '%dY' + } + }); + const m = moment(date) + m.locale(locale) + return m.fromNow(true) +} + +Vue.filter('fromNow', fromNow) + export function secondsToObject (seconds) { let m = moment.duration(seconds, 'seconds') return { diff --git a/front/src/router/index.js b/front/src/router/index.js index 71ef0c86cefc9a9233a1622163f7f84db8d93d82..cc5e166dbc39bb358789dc66b10bc8523a408cb2 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -906,6 +906,19 @@ export default new Router({ }, ] }, + { + path: "/subscriptions", + name: "subscriptions", + props: route => { + return { + defaultQuery: route.query.q + } + }, + component: () => + import( + /* webpackChunkName: "channels-auth" */ "@/views/channels/SubscriptionsList" + ), + }, { path: "*/index.html", redirect: "/" diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss index 76bbc35cb0272cdd3f53a42e07edbf866c5fd036..fbebc66ed4ce315701ab00cd64fab8f9550e40af 100644 --- a/front/src/style/_main.scss +++ b/front/src/style/_main.scss @@ -177,6 +177,9 @@ html { .ui.stripe.segment, #footer { padding: 1em; + &.ui.container { + margin: 0; + } @include media(">tablet") { padding: 2em; } @@ -372,9 +375,6 @@ input + .help { margin-top: 0.5em; } -.tag-list { - margin-top: 0.5em; -} .expandable { &:not(.expanded) { @@ -444,9 +444,9 @@ input + .help { } .ui.cards.app-cards { $card-width: 14em; - $card-height: 22em; + $card-height: 23em; $small-card-width: 11em; - $small-card-height: 19em; + $small-card-height: 20em; .app-card { display: flex; width: $small-card-width; @@ -619,9 +619,11 @@ input + .help { } } .header.with-actions { - display: flex; - justify-content: space-between; - align-items: center; + @include media(">tablet") { + display: flex; + justify-content: space-between; + align-items: center; + } .actions { font-weight: normal; font-size: 0.6em; @@ -662,5 +664,27 @@ input + .help { .ui.header .content { display: block; } +.with-image.item { + display: flex !important; + align-items: center; + height: 3em; + img.image { + width: 3em; + height: 3em; + margin-right: 1em; + } + .icon.image { + width: 3em; + margin-right: 1em; + display: block; + } + .content { + font-size: 1em; + } + .meta { + margin-top: 0.5em; + font-size: 0.8em; + } +} @import "./themes/_light.scss"; @import "./themes/_dark.scss"; diff --git a/front/src/views/Search.vue b/front/src/views/Search.vue index 06a355738d45d6283f8a6d7d986660448516ff41..f2c141bd524b421e7377492e2b9b24dba1d03d92 100644 --- a/front/src/views/Search.vue +++ b/front/src/views/Search.vue @@ -2,178 +2,34 @@ <main class="main pusher" v-title="labels.title"> <section class="ui vertical stripe segment"> <div class="ui small text container"> - <form :class="['ui', {loading: isLoading}, 'form']" @submit.stop.prevent="submit"> - <h2>{{ labels.title }}</h2> - <p v-if="type === 'rss'"> - <translate translate-context="Content/Fetch/Paragraph">Use this form to subscribe to a podcast using its RSS feed.</translate> - </p> - <p v-else> - <translate translate-context="Content/Fetch/Paragraph">Use this form to retrieve an object hosted somewhere else in the fediverse.</translate> - </p> - <div v-if="errors.length > 0" class="ui negative message"> - <div class="header"><translate translate-context="Content/*/Error message.Title">Error while fetching object</translate></div> - <ul class="list"> - <li v-for="error in errors">{{ error }}</li> - </ul> - </div> - <div class="ui required field"> - <label for="object-id"> - {{ labels.fieldLabel }} - </label> - <input type="text" name="object-id" id="object-id" v-model="id" required> - </div> - <button type="submit" :class="['ui', 'primary', {loading: isLoading}, 'button']" :disabled="isLoading || !id || id.length === 0"> - <translate translate-context="Content/Search/Input.Label/Noun">Search</translate> - </button> - </form> - <div v-if="!isLoading && fetch && fetch.status === 'finished' && !redirectRoute" class="ui warning message"> - <p><translate translate-context="Content/*/Error message.Title">This kind of object isn't supported yet</translate></p> - </div> - </div> + <h2>{{ labels.title }}</h2> + <remote-search-form :initial-id="initialId" :type="type"></remote-search-form> </div> </section> </main> </template> <script> -import axios from 'axios' - +import RemoteSearchForm from '@/components/RemoteSearchForm' export default { props: { initialId: { type: String, required: false}, type: { type: String, required: false}, }, - components: {}, - data () { - return { - id: this.initialId, - fetch: null, - obj: null, - isLoading: false, - errors: [], - } - }, - created () { - if (this.id) { - if (this.type === 'rss') { - this.rssSubscribe() - - } else { - this.createFetch() - } - } + components: { + RemoteSearchForm, }, computed: { labels() { let title = this.$pgettext('Head/Fetch/Title', "Search a remote object") - let fieldLabel = this.$pgettext('Head/Fetch/Field.Placeholder', "URL or @username") if (this.type === "rss") { title = this.$pgettext('Head/Fetch/Title', "Subscribe to a podcast RSS feed") - fieldLabel = this.$pgettext('*/*/*', "RSS Feed URL") } return { title, - fieldLabel - } - }, - objInfo () { - if (this.fetch && this.fetch.status === 'finished') { - return this.fetch.object } }, - redirectRoute () { - if (!this.objInfo) { - return - } - switch (this.objInfo.type) { - case 'account': - let [username, domain] = this.objInfo.full_username.split('@') - return {name: 'profile.full', params: {username, domain}} - case 'library': - return {name: 'library.detail', params: {id: this.objInfo.uuid}} - case 'artist': - return {name: 'library.artists.detail', params: {id: this.objInfo.id}} - case 'album': - return {name: 'library.albums.detail', params: {id: this.objInfo.id}} - case 'track': - return {name: 'library.tracks.detail', params: {id: this.objInfo.id}} - case 'upload': - return {name: 'library.uploads.detail', params: {id: this.objInfo.uuid}} - - default: - break; - } - } }, - methods: { - submit () { - if (this.type === 'rss') { - return this.rssSubscribe() - } else { - return this.createFetch() - } - }, - createFetch () { - if (!this.id) { - return - } - this.$router.replace({name: "search", query: {id: this.id}}) - this.fetch = null - let self = this - self.errors = [] - self.isLoading = true - let payload = { - object: this.id - } - - axios.post('federation/fetches/', payload).then((response) => { - self.isLoading = false - self.fetch = response.data - if (self.fetch.status === 'errored' || self.fetch.status === 'skipped') { - self.errors.push( - self.$pgettext("Content/*/Error message.Title", "This object cannot be retrieved") - ) - } - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - rssSubscribe () { - if (!this.id) { - return - } - this.$router.replace({name: "search", query: {id: this.id, type: 'rss'}}) - this.fetch = null - let self = this - self.errors = [] - self.isLoading = true - let payload = { - url: this.id - } - - axios.post('channels/rss-subscribe/', payload).then((response) => { - self.isLoading = false - self.$store.commit('channels/subscriptions', {uuid: response.data.channel.uuid, value: true}) - self.$router.push({name: 'channels.detail', params: {id: response.data.channel.uuid}}) - - }, error => { - self.isLoading = false - self.errors = error.backendErrors - }) - }, - }, - watch: { - initialId (v) { - this.id = v - this.createFetch() - }, - redirectRoute (v) { - if (v) { - this.$router.push(v) - } - } - } } </script> diff --git a/front/src/views/channels/SubscriptionsList.vue b/front/src/views/channels/SubscriptionsList.vue new file mode 100644 index 0000000000000000000000000000000000000000..f3b3ff796c69800fa56a43707c8f5632751a16f2 --- /dev/null +++ b/front/src/views/channels/SubscriptionsList.vue @@ -0,0 +1,103 @@ +<template> + <main class="main pusher" v-title="labels.title"> + <section class="ui head vertical stripe segment container"> + <h1 class="ui with-actions header"> + {{ labels.title }} + <div class="actions"> + <a @click.stop.prevent="showSubscribeModal = true"> + <i class="plus icon"></i> + <translate translate-context="Content/Profile/Button">Add new</translate> + </a> + </div> + </h1> + <modal class="tiny" :show.sync="showSubscribeModal" :fullscreen="false"> + <h2 class="header"> + <translate translate-context="*/*/*/Noun">Subscription</translate> + </h2> + <div class="scrolling content" ref="modalContent"> + <remote-search-form + type="rss" + :show-submit="false" + :standalone="false" + @subscribed="showSubscribeModal = false; reloadWidget()" + :redirect="false"></remote-search-form> + </div> + <div class="actions"> + <div class="ui basic deny button"> + <translate translate-context="*/*/Button.Label/Verb">Cancel</translate> + </div> + <button form="remote-search" type="submit" class="ui primary button"> + <i class="bookmark icon"></i> + <translate translate-context="*/*/*/Verb">Subscribe</translate> + </button> + </div> + </modal> + + + + <inline-search-bar v-model="query" @search="reloadWidget" :placeholder="labels.searchPlaceholder"></inline-search-bar> + <channels-widget + :key="widgetKey" + :limit="50" + :show-modification-date="true" + :filters="{q: query, subscribed: 'true', ordering: '-modification_date'}"></channels-widget> + </section> + </main> +</template> + +<script> +import axios from "axios" +import Modal from '@/components/semantic/Modal' + +import ChannelsWidget from "@/components/audio/ChannelsWidget" +import RemoteSearchForm from "@/components/RemoteSearchForm" + +export default { + props: ["defaultQuery"], + components: { + ChannelsWidget, + RemoteSearchForm, + Modal, + }, + data() { + return { + query: this.defaultQuery || '', + channels: [], + count: 0, + isLoading: false, + errors: null, + previousPage: null, + nextPage: null, + widgetKey: String(new Date()), + showSubscribeModal: false, + } + }, + created () { + this.fetchData() + }, + computed: { + labels () { + return { + title: this.$pgettext("Content/Subscriptions/Header", "Subscribed Channels"), + searchPlaceholder: this.$pgettext("Content/Subscriptions/Form.Placeholder", "Filter by name…"), + } + }, + }, + methods: { + fetchData() { + var self = this + this.isLoading = true + axios.get('channels/', {params: {subscribed: "true", q: this.query}}).then(response => { + self.previousPage = response.data.previous + self.nextPage = response.data.next + self.isLoading = false + self.channels = [...self.channels, ...response.data.results] + self.count = response.data.count + }) + }, + reloadWidget () { + this.widgetKey = String(new Date()) + } + }, +} +</script>