From 7df9112d551a77776a281755a73abc1c1f44fbc4 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Tue, 29 May 2018 00:07:38 +0200
Subject: [PATCH] See #223: front-end to browse/manage library files

---
 front/src/components/Sidebar.vue              |   6 +
 .../components/manage/library/FilesTable.vue  | 205 ++++++++++++++++++
 front/src/router/index.js                     |  13 ++
 front/src/views/admin/library/Base.vue        |  28 +++
 front/src/views/admin/library/FilesList.vue   |  23 ++
 5 files changed, 275 insertions(+)
 create mode 100644 front/src/components/manage/library/FilesTable.vue
 create mode 100644 front/src/views/admin/library/Base.vue
 create mode 100644 front/src/views/admin/library/FilesList.vue

diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index e8f330c3..72c55847 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -68,6 +68,12 @@
                 :title="$t('Pending import requests')">
                 {{ notifications.importRequests }}</div>
             </router-link>
+            <router-link
+              class="item"
+              v-if="$store.state.auth.availablePermissions['library']"
+              :to="{name: 'manage.library.files'}">
+              <i class="book icon"></i>{{ $t('Library') }}
+            </router-link>
             <router-link
               class="item"
               v-else-if="$store.state.auth.availablePermissions['upload']"
diff --git a/front/src/components/manage/library/FilesTable.vue b/front/src/components/manage/library/FilesTable.vue
new file mode 100644
index 00000000..7d230cc8
--- /dev/null
+++ b/front/src/components/manage/library/FilesTable.vue
@@ -0,0 +1,205 @@
+<template>
+  <div>
+    <div class="ui inline form">
+      <div class="fields">
+        <div class="ui field">
+          <label>{{ $t('Search') }}</label>
+          <input type="text" v-model="search" placeholder="Search by title, artist, domain..." />
+        </div>
+        <div class="field">
+          <i18next tag="label" path="Ordering"/>
+          <select class="ui dropdown" v-model="ordering">
+            <option v-for="option in orderingOptions" :value="option[0]">
+              {{ option[1] }}
+            </option>
+          </select>
+        </div>
+        <div class="field">
+          <i18next tag="label" path="Ordering direction"/>
+          <select class="ui dropdown" v-model="orderingDirection">
+            <option value="+">Ascending</option>
+            <option value="-">Descending</option>
+          </select>
+        </div>
+      </div>
+      </div>
+    <div class="dimmable">
+      <div v-if="isLoading" class="ui active inverted dimmer">
+          <div class="ui loader"></div>
+      </div>
+      <action-table
+        v-if="result"
+        @action-launched="fetchData"
+        :objects-data="result"
+        :actions="actions"
+        :action-url="'manage/library/track-files/action/'"
+        :filters="actionFilters">
+        <template slot="header-cells">
+          <th>{{ $t('Title') }}</th>
+          <th>{{ $t('Artist') }}</th>
+          <th>{{ $t('Album') }}</th>
+          <th>{{ $t('Import date') }}</th>
+          <th>{{ $t('Type') }}</th>
+          <th>{{ $t('Bitrate') }}</th>
+          <th>{{ $t('Duration') }}</th>
+          <th>{{ $t('Size') }}</th>
+        </template>
+        <template slot="row-cells" slot-scope="scope">
+          <td>
+            <span :title="scope.obj.track.title">{{ scope.obj.track.title|truncate(30) }}</span>
+          </td>
+          <td>
+            <span :title="scope.obj.track.artist.name">{{ scope.obj.track.artist.name|truncate(30) }}</span>
+          </td>
+          <td>
+            <span :title="scope.obj.track.album.title">{{ scope.obj.track.album.title|truncate(20) }}</span>
+          </td>
+          <td>
+            <human-date :date="scope.obj.creation_date"></human-date>
+          </td>
+          <td v-if="scope.obj.audio_mimetype">
+            {{ scope.obj.audio_mimetype }}
+          </td>
+          <td v-else>
+            {{ $t('N/A') }}
+          </td>
+          <td v-if="scope.obj.bitrate">
+            {{ scope.obj.bitrate | humanSize }}/s
+          </td>
+          <td v-else>
+            {{ $t('N/A') }}
+          </td>
+          <td v-if="scope.obj.duration">
+            {{ time.parse(scope.obj.duration) }}
+          </td>
+          <td v-else>
+            {{ $t('N/A') }}
+          </td>
+          <td v-if="scope.obj.size">
+            {{ scope.obj.size | humanSize }}
+          </td>
+          <td v-else>
+            {{ $t('N/A') }}
+          </td>
+        </template>
+      </action-table>
+    </div>
+    <div>
+      <pagination
+        v-if="result && result.results.length > 0"
+        @page-changed="selectPage"
+        :compact="true"
+        :current="page"
+        :paginate-by="paginateBy"
+        :total="result.count"
+        ></pagination>
+
+      <span v-if="result && result.results.length > 0">
+        {{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}}
+      </span>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from 'lodash'
+import time from '@/utils/time'
+import Pagination from '@/components/Pagination'
+import ActionTable from '@/components/common/ActionTable'
+import OrderingMixin from '@/components/mixins/Ordering'
+
+export default {
+  mixins: [OrderingMixin],
+  props: {
+    filters: {type: Object, required: false}
+  },
+  components: {
+    Pagination,
+    ActionTable
+  },
+  data () {
+    let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
+    return {
+      time,
+      isLoading: false,
+      result: null,
+      page: 1,
+      paginateBy: 25,
+      search: '',
+      orderingDirection: defaultOrdering.direction || '+',
+      ordering: defaultOrdering.field,
+      orderingOptions: [
+        ['creation_date', 'Creation date'],
+        ['accessed_date', 'Accessed date'],
+        ['modification_date', 'Modification date'],
+        ['size', 'Size'],
+        ['bitrate', 'Bitrate'],
+        ['duration', 'Duration']
+      ]
+
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      let params = _.merge({
+        'page': this.page,
+        'page_size': this.paginateBy,
+        'q': this.search,
+        'ordering': this.getOrderingAsString()
+      }, this.filters)
+      let self = this
+      self.isLoading = true
+      self.checked = []
+      axios.get('/manage/library/track-files/', {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    selectPage: function (page) {
+      this.page = page
+    }
+  },
+  computed: {
+    actionFilters () {
+      var currentFilters = {
+        q: this.search
+      }
+      if (this.filters) {
+        return _.merge(currentFilters, this.filters)
+      } else {
+        return currentFilters
+      }
+    },
+    actions () {
+      return [
+        {
+          name: 'delete',
+          label: this.$t('Delete')
+        }
+      ]
+    }
+  },
+  watch: {
+    search (newValue) {
+      this.page = 1
+      this.fetchData()
+    },
+    page () {
+      this.fetchData()
+    },
+    ordering () {
+      this.fetchData()
+    },
+    orderingDirection () {
+      this.fetchData()
+    }
+  }
+}
+</script>
diff --git a/front/src/router/index.js b/front/src/router/index.js
index f71dab7f..a52070e3 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -29,6 +29,8 @@ import PlaylistDetail from '@/views/playlists/Detail'
 import PlaylistList from '@/views/playlists/List'
 import Favorites from '@/components/favorites/List'
 import AdminSettings from '@/views/admin/Settings'
+import AdminLibraryBase from '@/views/admin/library/Base'
+import AdminLibraryFilesList from '@/views/admin/library/FilesList'
 import FederationBase from '@/views/federation/Base'
 import FederationScan from '@/views/federation/Scan'
 import FederationLibraryDetail from '@/views/federation/LibraryDetail'
@@ -167,6 +169,17 @@ export default new Router({
         { path: 'libraries/:id', name: 'federation.libraries.detail', component: FederationLibraryDetail, props: true }
       ]
     },
+    {
+      path: '/manage/library',
+      component: AdminLibraryBase,
+      children: [
+        {
+          path: 'files',
+          name: 'manage.library.files',
+          component: AdminLibraryFilesList
+        }
+      ]
+    },
     {
       path: '/library',
       component: Library,
diff --git a/front/src/views/admin/library/Base.vue b/front/src/views/admin/library/Base.vue
new file mode 100644
index 00000000..834fca92
--- /dev/null
+++ b/front/src/views/admin/library/Base.vue
@@ -0,0 +1,28 @@
+<template>
+  <div class="main pusher"  v-title="'Manage library'">
+    <div class="ui secondary pointing menu">
+      <router-link
+        class="ui item"
+        :to="{name: 'manage.library.files'}">{{ $t('Files') }}</router-link>
+    </div>
+    <router-view :key="$route.fullPath"></router-view>
+  </div>
+</template>
+
+<script>
+export default {}
+</script>
+
+<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/admin/library/FilesList.vue b/front/src/views/admin/library/FilesList.vue
new file mode 100644
index 00000000..9c52de57
--- /dev/null
+++ b/front/src/views/admin/library/FilesList.vue
@@ -0,0 +1,23 @@
+<template>
+  <div v-title="'Files'">
+    <div class="ui vertical stripe segment">
+      <h2 class="ui header">{{ $t('Library files') }}</h2>
+      <div class="ui hidden divider"></div>
+      <library-files-table :show-library="true"></library-files-table>
+    </div>
+  </div>
+</template>
+
+<script>
+import LibraryFilesTable from '@/components/manage/library/FilesTable'
+
+export default {
+  components: {
+    LibraryFilesTable
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
-- 
GitLab