diff --git a/package-lock.json b/package-lock.json index 6aa4a668de5fe10d69877598481f004bd6e601d4..f6bc7dff255fbaaa4c57f7256814d58707850ca0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3259,6 +3259,11 @@ "resolve": "^1.12.0" } }, + "babel-helper-vue-jsx-merge-props": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz", + "integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==" + }, "babel-loader": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.1.tgz", @@ -11316,6 +11321,14 @@ "tinycolor2": "^1.1.2" } }, + "vue-content-loader": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vue-content-loader/-/vue-content-loader-0.2.3.tgz", + "integrity": "sha512-gJlNEdXkuHGvgnyY0lBMsrSsOMk+TTog5uNAil5MSLv08f/mE7Apag0VavpxJ6ieb4P5J1iVKEIhHI41HQNq9Q==", + "requires": { + "babel-helper-vue-jsx-merge-props": "^2.0.3" + } + }, "vue-eslint-parser": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.1.1.tgz", diff --git a/package.json b/package.json index d173c44a685e3bc8c9c21ba780f8ba9d8699c78b..6507d10792a26694e79b9ed94833fe166795ea24 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "vue": "^2.6.12", "vue-blurhash": "^0.1.2", "vue-clipboard2": "^0.3.1", + "vue-content-loader": "^0.2.3", "vue-resize-observer": "^1.0.32", "vue-router": "^3.4.9", "vue-show-more-text": "^2.0.2", diff --git a/src/router.js b/src/router.js index 5eaaa6907040e1add837634e12311ba07198f677..bc7e9c6045e9618ca698752fc607d6df57715125 100644 --- a/src/router.js +++ b/src/router.js @@ -27,6 +27,7 @@ import axios from '@nextcloud/axios' import Main from './components/Main' import Show from './views/Show' +import ShowEmpty from './views/ShowEmpty' import Episode from './views/Episode' import store from './store.js' @@ -39,7 +40,7 @@ const router = new Router({ routes: [ { path: '/listening', - component: Main, + component: ShowEmpty, name: 'LISTENING', }, { diff --git a/src/views/Show.vue b/src/views/Show.vue index 0847e7775ddb8ef699a1b113730c2ac565bd58c7..76e8bc90de903948b982cc1e69c2a02ad63d883e 100644 --- a/src/views/Show.vue +++ b/src/views/Show.vue @@ -19,52 +19,51 @@ - along with this program. If not, see <http://www.gnu.org/licenses/>. - --> - <template> <div> - <div - class="podcastHeaderBg" - :style="{ backgroundImage: `url(${podcast.smallImageURL})` }"> - <div class="podcastHeader"> + <ShowEmpty v-show="pageLoading" /> + <transition name="fade"> + <div v-show="!pageLoading"> <div - class="podcastImage" - :style="{ backgroundImage: `url(${podcast.smallImageURL})` }" /> - <div class="podcastDescription"> - <h1>{{ podcast.title }}</h1> - <div class="podcastAuthor"> - by <a :href="podcast.htmlURL" target="_blank">{{ podcast.author }}</a> - </div> - <div class="podcastControls"> - <a href="#" class="button">Subscribe</a> - <ul class="podcastCategory"> - <a v-for="(category, idx) in podcast.categories" - :key="idx" - :href="`#/browse/category/${podcast.categories[idx]}`"> - <li> - {{ getCategoryName(podcast.categories[idx]) }} - </li> - </a> - </ul> + class="podcastHeaderBg" + :style="{ backgroundImage: `url(${podcast.smallImageURL})` }"> + <div class="podcastHeader"> + <div + class="podcastImage" + :style="{ backgroundImage: `url(${podcast.smallImageURL})` }" /> + <div class="podcastDescription"> + <h1>{{ podcast.title }}</h1> + <div class="podcastAuthor"> + by <a :href="podcast.htmlURL" target="_blank">{{ podcast.author }}</a> + </div> + <div class="podcastControls"> + <a href="#" class="button">Subscribe</a> + <ul class="podcastCategory"> + <a v-for="(category, idx) in podcast.categories" + :key="idx" + :href="`#/browse/category/${podcast.categories[idx]}`"> + <li> + {{ getCategoryName(podcast.categories[idx]) }} + </li> + </a> + </ul> + </div> + <vue-show-more-text + :text="podcast.description" + additional-container-css="padding: 0px;" /> + </div> </div> - <vue-show-more-text - :text="podcast.description" - additional-container-css="padding: 0px;" /> </div> + <Table + v-resize="onResize" + :episodes="podcast.episodes" + @doPlay="doPlay" /> </div> - </div> - <Table - v-show="!pageLoading && podcast.episodes.length > 0" - v-resize="onResize" - :episodes="podcast.episodes" - @doPlay="doPlay" /> - <EmptyContent - v-if="pageLoading" - icon="icon-loading" /> + </transition> </div> </template> <script> -import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' import Table from '../components/Table' import { showError } from '@nextcloud/dialogs' @@ -75,12 +74,14 @@ import vueShowMoreText from 'vue-show-more-text' import { audioPlayer, doPlay } from '../services/player' import { getCategoryName } from '../services/podcastApi' +import ShowEmpty from './ShowEmpty' + export default { name: 'Show', components: { Table, - EmptyContent, vueShowMoreText, + ShowEmpty, }, data: () => ({ podcast: { diff --git a/src/views/ShowEmpty.vue b/src/views/ShowEmpty.vue new file mode 100644 index 0000000000000000000000000000000000000000..969ff8efdebc3ec68c9d9b1b0d9c9400df42352e --- /dev/null +++ b/src/views/ShowEmpty.vue @@ -0,0 +1,180 @@ +<!-- + - @copyright Copyright (c) 2020 Jonas Heinrich + - + - @author Jonas Heinrich <onny@project-insanity.org> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div> + <div class="podcastHeaderEmpty"> + <div class="podcastImageEmpty" /> + <div class="podcastDescriptionEmpty"> + <div class="podcastDescriptionTitle" /> + <div + class="podcastDescriptionSubTitle" + style="width: 300px;" /> + <div + class="podcastDescriptionSubTitle" + style="width: 400px;" /> + <div + class="podcastDescriptionSubTitle" + style="width: 200px;" /> + </div> + </div> + <table> + <thead> + <tr> + <th class="iconColumn" /> + <th class="nameColumn"> + {{ t('podcast', 'Name') }} + </th> + <th class="actionColumn" /> + <th class="durationColumn"> + {{ t('podcast', 'Duration') }} + </th> + <th class="dateColumn"> + {{ t('podcast', 'Date') }} + </th> + </tr> + </thead> + <tbody> + <tr v-for="index in 3" :key="index"> + <td class="iconColumn"> + <div class="episodeImageEmpty" /> + </td> + <td class="nameColumn"> + <div + class="episodeDescription" + style="width: 500px;" /> + <div + class="episodeDescription" + style="width: 400px;" /> + </td> + <td class="actionColumn" /> + <td class="durationColumn" /> + <td class="dateColumn" /> + </tr> + </tbody> + </table> + </div> +</template> + +<script> + +export default { + name: 'ShowEmpty', +} +</script> + +<style lang="scss"> + +$base-color: #ddd; +$shine-color: #e8e8e8; +$animation-duration: 1.6s; +$avatar-offset: 52 + 16; + +@mixin background-gradient { + background-image: linear-gradient(90deg, $base-color 0px, $shine-color 40px, $base-color 80px); + background-size: 600px; +} + +.podcastHeaderEmpty { + width: 100%; + min-height: 300px; + display: flex; + justify-content: center; + padding: 40px 20px; + background-color: #f2f2f2; +} + +.podcastImageEmpty { + width: 200px; + height: 200px; + background-color: #ccc; + margin-right: 25px; + border-radius: 5px; + + @include background-gradient; + animation: shine-avatar $animation-duration infinite linear; +} + +.podcastDescriptionEmpty { + max-width: 500px; +} + +.podcastDescriptionTitle { + width: 500px; + height: 35px; + border-radius: 5px; + margin-bottom: 30px; + background-color: #ddd; + + @include background-gradient; + animation: shine-lines $animation-duration infinite linear; +} + +.podcastDescriptionSubTitle { + height: 20px; + border-radius: 5px; + margin-bottom: 10px; + background-color: #ddd; + + @include background-gradient; + animation: shine-lines $animation-duration infinite linear; +} + +.episodeImageEmpty { + width: 74px; + height: 74px; + background-color: #ccc; + border-radius: 5px; + + @include background-gradient; + animation: shine-avatar $animation-duration infinite linear; +} + +.episodeDescription { + height: 20px; + border-radius: 5px; + margin-bottom: 10px; + background-color: #ddd; + + @include background-gradient; + animation: shine-lines $animation-duration infinite linear; +} + +@keyframes shine-lines { + 0% { + background-position: -100px; + } + 40%, 100% { + background-position: 140px; + } +} + +@keyframes shine-avatar { + 0% { + background-position: -100px + $avatar-offset; + } + 40%, 100% { + background-position: 140px + $avatar-offset; + } +} + +</style>