diff --git a/api/funkwhale_api/federation/filters.py b/api/funkwhale_api/federation/filters.py index 12cab7f892a555248a369635f452f84411755331..2803186baf036b6418d806a493adcb142af49d95 100644 --- a/api/funkwhale_api/federation/filters.py +++ b/api/funkwhale_api/federation/filters.py @@ -7,6 +7,9 @@ from . import models class LibraryFilter(django_filters.FilterSet): approved = django_filters.BooleanFilter('following__approved') + q = fields.SearchFilter(search_fields=[ + 'actor__domain', + ]) class Meta: model = models.Library diff --git a/front/src/components/Home.vue b/front/src/components/Home.vue index 0e24dcd59008c52fe198e6a1778d9d1e0c00545c..ce1307ff0c8a84b934f027c7a27509644edc32a5 100644 --- a/front/src/components/Home.vue +++ b/front/src/components/Home.vue @@ -3,7 +3,7 @@ <div class="ui vertical center aligned stripe segment"> <div class="ui text container"> <h1 class="ui huge header"> - Welcome on Funkwhale + Welcome on Funkwhale </h1> <p>We think listening music should be simple.</p> <router-link class="ui icon button" to="/about"> diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index c04ebe5a866959c44dd74a8797d68f7b4f119e75..96047ab9848621c55fe5b75971bfca47e6c9ba62 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -47,7 +47,7 @@ class="item" :to="{path: '/activity'}"><i class="bell icon"></i> Activity</router-link> <router-link class="item" v-if="$store.state.auth.availablePermissions['federation.manage']" - :to="{path: '/manage/federation'}"><i class="sitemap icon"></i> Federation</router-link> + :to="{path: '/manage/federation/libraries'}"><i class="sitemap icon"></i> Federation</router-link> </div> <player></player> diff --git a/front/src/components/federation/LibraryCard.vue b/front/src/components/federation/LibraryCard.vue index 9676f2de5ff03000e8e4490ca607f842953158b1..267d41bd0796bbe5e20c1f0d2bcc74ff745aa244 100644 --- a/front/src/components/federation/LibraryCard.vue +++ b/front/src/components/federation/LibraryCard.vue @@ -2,33 +2,39 @@ <div class="ui card"> <div class="content"> <div class="header"> - {{ libraryData.display_name }} + {{ displayName }} </div> </div> <div class="content"> - <span class="right floated" v-if="libraryData.actor.manuallyApprovesFollowers"> + <span class="right floated" v-if="following"> + <i class="check icon"></i> Following + </span> + <span class="right floated" v-else-if="manuallyApprovesFollowers"> <i class="lock icon"></i> Followers only </span> + <span class="right floated" v-else> + <i class="open lock icon"></i> Open + </span> <span> <i class="music icon"></i> - {{ libraryData.library.totalItems }} tracks + {{ totalItems }} tracks </span> </div> <div class="extra content"> - <template v-if="libraryData.local.awaiting_approval"> + <template v-if="awaitingApproval"> <i class="clock icon"></i> Follow request pending approval </template> - <template v-else-if="libraryData.local.following">Pending follow request + <template v-else-if="following"> <i class="check icon"></i> Already following this library </template> <div - v-else-if="!library" + v-if="!library" @click="follow" :disabled="isLoading" :class="['ui', 'basic', {loading: isLoading}, 'green', 'button']"> - <template v-if="libraryData.actor.manuallyApprovesFollowers"> + <template v-if="manuallyApprovesFollowers"> Send a follow request </template> <template v-else> @@ -49,13 +55,13 @@ import axios from 'axios' export default { - props: ['libraryData'], + props: ['libraryData', 'libraryInstance'], data () { return { + library: this.libraryInstance, isLoading: false, data: null, - errors: [], - library: null + errors: [] } }, methods: { @@ -77,6 +83,43 @@ export default { self.errors = error.backendErrors }) } + }, + computed: { + displayName () { + if (this.libraryData) { + return this.libraryData.display_name + } else { + return `${this.library.actor.preferred_username}@${this.library.actor.domain}` + } + }, + manuallyApprovesFollowers () { + if (this.libraryData) { + return this.libraryData.actor.manuallyApprovesFollowers + } else { + return this.library.actor.manually_approves_followers + } + }, + totalItems () { + if (this.libraryData) { + return this.libraryData.library.totalItems + } else { + return this.library.tracks_count + } + }, + awaitingApproval () { + if (this.libraryData) { + return this.libraryData.local.awaiting_approval + } else { + return this.library.follow.approved === null + } + }, + following () { + if (this.libraryData) { + return this.libraryData.local.following + } else { + return this.library.follow.approved + } + } } } </script> diff --git a/front/src/router/index.js b/front/src/router/index.js index 394a12849d6104c95e2427b9c31e05bc98992267..2b78375c1068233d801f2ac77b4da121f3f3fedc 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -26,8 +26,9 @@ import PlaylistDetail from '@/views/playlists/Detail' import PlaylistList from '@/views/playlists/List' import Favorites from '@/components/favorites/List' import FederationBase from '@/views/federation/Base' -import FederationHome from '@/views/federation/Home' +import FederationScan from '@/views/federation/Scan' import FederationLibraryDetail from '@/views/federation/LibraryDetail' +import FederationLibraryList from '@/views/federation/LibraryList' Vue.use(Router) @@ -90,15 +91,29 @@ export default new Router({ path: '/manage/federation', component: FederationBase, children: [ - { path: '', component: FederationHome }, - { path: 'library/:id', name: 'federation.libraries.detail', component: FederationLibraryDetail, props: true } + { + path: 'scan', + name: 'federation.libraries.scan', + component: FederationScan }, + { + path: 'libraries', + name: 'federation.libraries.list', + component: FederationLibraryList, + props: (route) => ({ + defaultOrdering: route.query.ordering, + defaultQuery: route.query.query, + defaultPaginateBy: route.query.paginateBy, + defaultPage: route.query.page + }) + }, + { path: 'libraries/:id', name: 'federation.libraries.detail', component: FederationLibraryDetail, props: true } ] }, { path: '/library', component: Library, children: [ - { path: '', component: LibraryHome }, + { path: 'scan', component: LibraryHome }, { path: 'artists/', name: 'library.artists.browse', diff --git a/front/src/views/federation/Base.vue b/front/src/views/federation/Base.vue index 6add8e5e4c1eed00615bb4cd965c738d98e67d21..1919fd8556c61237d8c6301eb141740e951857ba 100644 --- a/front/src/views/federation/Base.vue +++ b/front/src/views/federation/Base.vue @@ -1,7 +1,23 @@ <template> <div class="main pusher" v-title="'Federation'"> <div class="ui secondary pointing menu"> + <router-link + class="ui item" + :to="{name: 'federation.libraries.list'}">Libraries</router-link> </div> <router-view :key="$route.fullPath"></router-view> </div> </template> +<style lang="scss"> +@import '../../style/vendor/media'; + +.main.pusher > .ui.secondary.menu { + @include media(">tablet") { + margin: 0 2.5rem; + } + .item { + padding-top: 1.5em; + padding-bottom: 1.5em; + } +} +</style> diff --git a/front/src/views/federation/LibraryList.vue b/front/src/views/federation/LibraryList.vue new file mode 100644 index 0000000000000000000000000000000000000000..5c9d413a9cf66fb828900cf4c201ad0945b2c9c1 --- /dev/null +++ b/front/src/views/federation/LibraryList.vue @@ -0,0 +1,172 @@ +<template> + <div v-title="'Artists'"> + <div class="ui vertical stripe segment"> + <h2 class="ui header">Browsing libraries</h2> + <router-link + class="ui basic green button" + :to="{name: 'federation.libraries.scan'}"> + <i class="plus icon"></i> + Add a new library + </router-link> + <div class="ui hidden divider"></div> + <div :class="['ui', {'loading': isLoading}, 'form']"> + <div class="fields"> + <div class="field"> + <label>Search</label> + <input type="text" v-model="query" placeholder="Enter an library domain name..."/> + </div> + <div class="field"> + <label>Ordering</label> + <select class="ui dropdown" v-model="ordering"> + <option v-for="option in orderingOptions" :value="option[0]"> + {{ option[1] }} + </option> + </select> + </div> + <div class="field"> + <label>Ordering direction</label> + <select class="ui dropdown" v-model="orderingDirection"> + <option value="">Ascending</option> + <option value="-">Descending</option> + </select> + </div> + <div class="field"> + <label>Results per page</label> + <select class="ui dropdown" v-model="paginateBy"> + <option :value="parseInt(12)">12</option> + <option :value="parseInt(25)">25</option> + <option :value="parseInt(50)">50</option> + </select> + </div> + </div> + </div> + <div class="ui hidden divider"></div> + <div + v-if="result" + v-masonry + transition-duration="0" + item-selector=".column" + percent-position="true" + stagger="0" + class="ui stackable three column doubling grid"> + <div + v-masonry-tile + v-if="result.results.length > 0" + v-for="library in result.results" + :key="library.id" + class="column"> + <library-card class="fluid" :library-instance="library"></library-card> + </div> + </div> + <div class="ui center aligned basic segment"> + <pagination + v-if="result && result.results.length > 0" + @page-changed="selectPage" + :current="page" + :paginate-by="paginateBy" + :total="result.count" + ></pagination> + </div> + </div> + </div> +</template> + +<script> +import axios from 'axios' +import _ from 'lodash' +import $ from 'jquery' + +import logger from '@/logging' + +import OrderingMixin from '@/components/mixins/Ordering' +import PaginationMixin from '@/components/mixins/Pagination' +import LibraryCard from '@/components/federation/LibraryCard' +import Pagination from '@/components/Pagination' + +const FETCH_URL = 'federation/libraries/' + +export default { + mixins: [OrderingMixin, PaginationMixin], + props: { + defaultQuery: {type: String, required: false, default: ''} + }, + components: { + LibraryCard, + Pagination + }, + data () { + let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') + return { + isLoading: true, + result: null, + page: parseInt(this.defaultPage), + query: this.defaultQuery, + paginateBy: parseInt(this.defaultPaginateBy || 50), + orderingDirection: defaultOrdering.direction, + ordering: defaultOrdering.field, + orderingOptions: [ + ['creation_date', 'Creation date'], + ['tracks_count', 'Available tracks'] + ] + } + }, + created () { + this.fetchData() + }, + mounted () { + $('.ui.dropdown').dropdown() + }, + methods: { + updateQueryString: _.debounce(function () { + this.$router.replace({ + query: { + query: this.query, + page: this.page, + paginateBy: this.paginateBy, + ordering: this.getOrderingAsString() + } + }) + }, 500), + fetchData: _.debounce(function () { + var self = this + this.isLoading = true + let url = FETCH_URL + let params = { + page: this.page, + q: this.query, + ordering: this.getOrderingAsString() + } + logger.default.debug('Fetching libraries') + axios.get(url, {params: params}).then((response) => { + self.result = response.data + self.isLoading = false + }) + }, 500), + selectPage: function (page) { + this.page = page + } + }, + watch: { + page () { + this.updateQueryString() + this.fetchData() + }, + ordering () { + this.updateQueryString() + this.fetchData() + }, + orderingDirection () { + this.updateQueryString() + this.fetchData() + }, + query () { + this.updateQueryString() + this.fetchData() + } + } +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +</style> diff --git a/front/src/views/federation/Home.vue b/front/src/views/federation/Scan.vue similarity index 94% rename from front/src/views/federation/Home.vue rename to front/src/views/federation/Scan.vue index 89048aac58299e140f88c87807e19e9f9b1733ba..5caa2f5405b43a4194e9571edd22c2277a9adc36 100644 --- a/front/src/views/federation/Home.vue +++ b/front/src/views/federation/Scan.vue @@ -1,7 +1,6 @@ <template> <div> <div class="ui vertical stripe segment"> - <h1 class="ui header">Manage federation</h1> <library-form @scanned="updateLibraryData"></library-form> <library-card v-if="libraryData" :library-data="libraryData"></library-card> </div>