From f4f75dcb4f4c6f04d60f0f2c0c14cfe140ccf5e1 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Wed, 11 Apr 2018 20:40:47 +0200
Subject: [PATCH] Can now scan and follow library from front-end

---
 api/funkwhale_api/federation/serializers.py   |   1 +
 api/funkwhale_api/users/models.py             |   3 +
 front/src/components/Sidebar.vue              |   3 +
 .../src/components/federation/LibraryCard.vue |  82 +++++++++++++
 .../src/components/federation/LibraryForm.vue | 110 ++++++++++++++++++
 front/src/router/index.js                     |   5 +
 front/src/views/federation/Home.vue           |  40 +++++++
 7 files changed, 244 insertions(+)
 create mode 100644 front/src/components/federation/LibraryCard.vue
 create mode 100644 front/src/components/federation/LibraryForm.vue
 create mode 100644 front/src/views/federation/Home.vue

diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py
index cf59eaa6..bca35902 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 9516c108..572fa9dd 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 42a923b6..c04ebe5a 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 00000000..9676f2de
--- /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 00000000..5cf6dabb
--- /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>&nbsp;</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 d4176422..0981c37f 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 00000000..c9e3693d
--- /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>
-- 
GitLab