diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index cf59eaa63ac0ad0cce1246b7637fffdc6ea3fbd6..bca35902d7e3dfaee8cf03f879c022c4fed3da30 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -159,6 +159,7 @@ class APILibrarySerializer(serializers.ModelSerializer): class APILibraryCreateSerializer(serializers.ModelSerializer): actor = serializers.URLField() + federation_enabled = serializers.BooleanField() class Meta: model = models.Library diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index 9516c108f896275837be3161fe80e07dda6273e7..572fa9ddca7c1457db1e6a00956c105874e19c62 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -31,6 +31,9 @@ class User(AbstractUser): 'dynamic_preferences.change_globalpreferencemodel': { 'external_codename': 'settings.change', }, + 'federation.change_library': { + 'external_codename': 'federation.manage', + }, } privacy_level = fields.get_privacy_field() diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 42a923b6b3faaf5586a35613277bbcdd7ea8c27d..c04ebe5a866959c44dd74a8797d68f7b4f119e75 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -45,6 +45,9 @@ <router-link v-if="$store.state.auth.authenticated" 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> </div> <player></player> diff --git a/front/src/components/federation/LibraryCard.vue b/front/src/components/federation/LibraryCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..9676f2de5ff03000e8e4490ca607f842953158b1 --- /dev/null +++ b/front/src/components/federation/LibraryCard.vue @@ -0,0 +1,82 @@ +<template> + <div class="ui card"> + <div class="content"> + <div class="header"> + {{ libraryData.display_name }} + </div> + </div> + <div class="content"> + <span class="right floated" v-if="libraryData.actor.manuallyApprovesFollowers"> + <i class="lock icon"></i> Followers only + </span> + <span> + <i class="music icon"></i> + {{ libraryData.library.totalItems }} tracks + </span> + </div> + <div class="extra content"> + <template v-if="libraryData.local.awaiting_approval"> + <i class="clock icon"></i> + Follow request pending approval + </template> + <template v-else-if="libraryData.local.following">Pending follow request + <i class="check icon"></i> + Already following this library + </template> + <div + v-else-if="!library" + @click="follow" + :disabled="isLoading" + :class="['ui', 'basic', {loading: isLoading}, 'green', 'button']"> + <template v-if="libraryData.actor.manuallyApprovesFollowers"> + Send a follow request + </template> + <template v-else> + Follow + </template> + </div> + <router-link + v-else + class="ui basic button" + :to="{name: 'federation.libraries.detail', params: {id: library.uuid }}"> + Detail + </router-link> + </div> + </div> +</template> + +<script> +import axios from 'axios' + +export default { + props: ['libraryData'], + data () { + return { + isLoading: false, + data: null, + errors: [], + library: null + } + }, + methods: { + follow () { + let params = { + 'actor': this.libraryData['actor']['id'], + 'autoimport': false, + 'download_files': false, + 'federation_enabled': true + } + let self = this + self.isLoading = true + axios.post('/federation/libraries/', params).then((response) => { + self.$emit('follow', {data: self.libraryData, library: response.data}) + self.library = response.data + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + } + } +} +</script> diff --git a/front/src/components/federation/LibraryForm.vue b/front/src/components/federation/LibraryForm.vue new file mode 100644 index 0000000000000000000000000000000000000000..5cf6dabb2f4dd8323af9972cfd21bdeebdac0175 --- /dev/null +++ b/front/src/components/federation/LibraryForm.vue @@ -0,0 +1,110 @@ +<template> + <form class="ui form" @submit.prevent="fetchInstanceInfo"> + <h3 class="ui header">Federate with a new instance</h3> + <p>Use this form to scan an instance and setup federation.</p> + <div v-if="errors.length > 0 || scanErrors.length > 0" class="ui negative message"> + <div class="header">Error while scanning library</div> + <ul class="list"> + <li v-for="error in errors">{{ error }}</li> + <li v-for="error in scanErrors">{{ error }}</li> + </ul> + </div> + <div class="ui two fields"> + <div class="ui field"> + <label>Library name</label> + <input v-model="libraryUsername" type="text" placeholder="library@demo.funkwhale.audio" /> + </div> + <div class="ui field"> + <label> </label> + <button + type="submit" + :disabled="isLoading" + :class="['ui', 'icon', {loading: isLoading}, 'button']"> + <i class="search icon"></i> + Launch scan + </button> + </div> + </div> + </form> +</template> + +<script> +import axios from 'axios' +import TrackTable from '@/components/audio/track/Table' +import RadioButton from '@/components/radios/Button' +import Pagination from '@/components/Pagination' + +export default { + components: { + TrackTable, + RadioButton, + Pagination + }, + data () { + return { + isLoading: false, + libraryUsername: 'library@node2.funkwhale.test', + result: null, + errors: [] + } + }, + methods: { + follow () { + let params = { + 'actor': this.result['actor']['id'], + 'autoimport': false, + 'download_files': false, + 'federation_enabled': true + } + let self = this + self.isFollowing = false + axios.post('/federation/libraries/', params).then((response) => { + self.$emit('follow', {data: self.result, library: response.data}) + self.result = response.data + self.isFollowing = false + }, error => { + self.isFollowing = false + self.errors = error.backendErrors + }) + }, + fetchInstanceInfo () { + let self = this + this.isLoading = true + self.errors = [] + self.result = null + axios.get('/federation/libraries/scan/', {params: {account: this.libraryUsername}}).then((response) => { + self.result = response.data + self.result.display_name = self.libraryUsername + self.isLoading = false + }, error => { + self.isLoading = false + self.errors = error.backendErrors + }) + } + }, + computed: { + scanErrors () { + let errors = [] + if (!this.result) { + return errors + } + let keys = ['webfinger', 'actor', 'library'] + keys.forEach(k => { + if (this.result[k]) { + if (this.result[k].errors) { + this.result[k].errors.forEach(e => { + errors.push(e) + }) + } + } + }) + return errors + } + }, + watch: { + result (newValue, oldValue) { + this.$emit('scanned', newValue) + } + } +} +</script> diff --git a/front/src/router/index.js b/front/src/router/index.js index d41764227bb1a1cbd3536791ae24275b7228d249..0981c37f9a3b07dead17c087ed001fbb06884318 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -25,6 +25,7 @@ import RequestsList from '@/components/requests/RequestsList' import PlaylistDetail from '@/views/playlists/Detail' import PlaylistList from '@/views/playlists/List' import Favorites from '@/components/favorites/List' +import Federation from '@/views/federation/Home' Vue.use(Router) @@ -83,6 +84,10 @@ export default new Router({ defaultPaginateBy: route.query.paginateBy }) }, + { + path: '/manage/federation', + component: Federation + }, { path: '/library', component: Library, diff --git a/front/src/views/federation/Home.vue b/front/src/views/federation/Home.vue new file mode 100644 index 0000000000000000000000000000000000000000..c9e3693d6d7e20905145183f18015981f5799318 --- /dev/null +++ b/front/src/views/federation/Home.vue @@ -0,0 +1,40 @@ +<template> + <div class="main pusher" v-title="'Federation'"> + <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> + <div class="ui vertical stripe segment"> + </div> + </div> +</template> + +<script> +// import axios from 'axios' +import TrackTable from '@/components/audio/track/Table' +import RadioButton from '@/components/radios/Button' +import Pagination from '@/components/Pagination' +import LibraryForm from '@/components/federation/LibraryForm' +import LibraryCard from '@/components/federation/LibraryCard' + +export default { + components: { + TrackTable, + RadioButton, + Pagination, + LibraryForm, + LibraryCard + }, + data () { + return { + libraryData: null + } + }, + methods: { + updateLibraryData (data) { + this.libraryData = data + } + } +} +</script>