Merge branch 'proxy-attachments' into 'develop'
Fixed broken attachment fetching with long filename See merge request funkwhale/funkwhale!982
This commit is contained in:
commit
82692f1170
|
@ -80,9 +80,10 @@ def fetch_remote_attachment(attachment, filename=None, save=True):
|
|||
for chunk in r.iter_content():
|
||||
tf.write(chunk)
|
||||
tf.seek(0)
|
||||
attachment.file.save(
|
||||
filename or attachment.url.split("/")[-1], File(tf), save=save
|
||||
)
|
||||
if not filename:
|
||||
filename = attachment.url.split("/")[-1]
|
||||
filename = filename[-50:]
|
||||
attachment.file.save(filename, File(tf), save=save)
|
||||
|
||||
|
||||
@celery.app.task(name="common.prune_unattached_attachments")
|
||||
|
|
|
@ -216,6 +216,27 @@ def test_attachment_proxy_redirects_original(
|
|||
assert response["Location"] == urls[expected]
|
||||
|
||||
|
||||
def test_attachment_proxy_dont_crash_on_long_filename(
|
||||
factories, logged_in_api_client, avatar, r_mock, now
|
||||
):
|
||||
long_filename = "a" * 400
|
||||
attachment = factories["common.Attachment"](
|
||||
file=None, url="https://domain/{}.jpg".format(long_filename)
|
||||
)
|
||||
|
||||
avatar_content = avatar.read()
|
||||
r_mock.get(attachment.url, body=io.BytesIO(avatar_content))
|
||||
proxy_url = reverse("api:v1:attachments-proxy", kwargs={"uuid": attachment.uuid})
|
||||
|
||||
response = logged_in_api_client.get(proxy_url, {"next": next})
|
||||
attachment.refresh_from_db()
|
||||
|
||||
assert response.status_code == 302
|
||||
assert attachment.file.read() == avatar_content
|
||||
assert attachment.file.name.endswith("/{}.jpg".format("a" * 46))
|
||||
assert attachment.last_fetch_date == now
|
||||
|
||||
|
||||
def test_attachment_create(logged_in_api_client, avatar):
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
url = reverse("api:v1:attachments-list")
|
||||
|
|
|
@ -1,109 +1,51 @@
|
|||
<template>
|
||||
<div :class="['ui', 'card', mode]">
|
||||
<div class="content">
|
||||
<div class="right floated tiny ui image">
|
||||
<img v-if="album.cover.original" v-lazy="$store.getters['instance/absoluteUrl'](album.cover.square_crop)">
|
||||
<img v-else src="../../../assets/audio/default-cover.png">
|
||||
</div>
|
||||
<div class="header">
|
||||
<router-link class="discrete link" :to="{name: 'library.albums.detail', params: {id: album.id }}">{{ album.title }} </router-link>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span>
|
||||
<router-link :title="album.artist.name" tag="span" :to="{name: 'library.artists.detail', params: {id: album.artist.id }}">
|
||||
<span v-translate="{artist: album.artist.name}" translate-context="Content/Album/Card" :translate-params="{artist: album.artist.name}">By %{ artist }</span>
|
||||
</router-link>
|
||||
</span><span class="time" v-if="album.release_date">– {{ album.release_date | year }}</span>
|
||||
</div>
|
||||
<div class="description" v-if="mode === 'rich'">
|
||||
<table class="ui very basic fixed single line compact unstackable table">
|
||||
<tbody>
|
||||
<tr v-for="track in tracks">
|
||||
<td class="play-cell">
|
||||
<play-button :class="['basic', {orange: currentTrack && isPlaying && track.id === currentTrack.id}, 'icon']" :discrete="true" :track="track"></play-button>
|
||||
</td>
|
||||
<td class="content-cell" colspan="5">
|
||||
<track-favorite-icon :track="track"></track-favorite-icon>
|
||||
<router-link :title="track.title" class="track discrete link" :to="{name: 'library.tracks.detail', params: {id: track.id }}">
|
||||
<template v-if="track.position">
|
||||
{{ track.position }}.
|
||||
</template>
|
||||
{{ track.title }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="center aligned segment" v-if="album.tracks.length > initialTracks">
|
||||
<em v-if="!showAllTracks" @click="showAllTracks = true" class="expand">
|
||||
<translate translate-context="Content/Album/Card.Link/Verb" :translate-params="{count: album.tracks.length - initialTracks}" :translate-n="album.tracks.length - initialTracks" translate-plural="Show %{ count } more tracks">Show %{ count } more track</translate>
|
||||
</em>
|
||||
<em v-else @click="showAllTracks = false" class="expand">
|
||||
<translate translate-context="*/*/Button,Label">Collapse</translate>
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<play-button class="mini basic orange right floated" :tracks="tracksWithAlbum" :album="album">
|
||||
<translate translate-context="Content/Queue/Button.Label/Short, Verb">Play all</translate>
|
||||
</play-button>
|
||||
<div class="card app-card">
|
||||
<div
|
||||
@click="$router.push({name: 'library.albums.detail', params: {id: album.id}})"
|
||||
:class="['ui', 'head-image', 'image', {'default-cover': !album.cover.original}]" v-lazy:background-image="imageUrl">
|
||||
<play-button :icon-only="true" :is-playable="album.is_playable" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :album="album"></play-button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<strong>
|
||||
<router-link class="discrete link" :title="album.title" :to="{name: 'library.albums.detail', params: {id: album.id}}">
|
||||
{{ album.title }}
|
||||
</router-link>
|
||||
</strong>
|
||||
<div class="description">
|
||||
<span>
|
||||
<i class="music icon"></i>
|
||||
<translate translate-context="*/*/*" :translate-params="{count: album.tracks.length}" :translate-n="album.tracks.length" translate-plural="%{ count } tracks">%{ count } track</translate>
|
||||
<router-link :title="album.artist.name" class="discrete link" :to="{name: 'library.artists.detail', params: {id: album.artist.id}}">
|
||||
{{ album.artist.name }}
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<translate translate-context="*/*/*" :translate-params="{count: album.tracks.length}" :translate-n="album.tracks.length" translate-plural="%{ count } tracks">%{ count } track</translate>
|
||||
<play-button class="right floated basic icon" :dropdown-only="true" :is-playable="album.is_playable" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :album="album"></play-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex"
|
||||
import backend from '@/audio/backend'
|
||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
album: {type: Object},
|
||||
mode: {type: String, default: 'rich'},
|
||||
},
|
||||
components: {
|
||||
TrackFavoriteIcon,
|
||||
PlayButton
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
backend: backend,
|
||||
initialTracks: 5,
|
||||
showAllTracks: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tracks () {
|
||||
if (this.showAllTracks) {
|
||||
return this.album.tracks
|
||||
imageUrl () {
|
||||
let url = '../../../assets/audio/default-cover.png'
|
||||
|
||||
if (this.album.cover.original) {
|
||||
url = this.$store.getters['instance/absoluteUrl'](this.album.cover.medium_square_crop)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return this.album.tracks.slice(0, this.initialTracks)
|
||||
},
|
||||
...mapGetters({
|
||||
currentTrack: "queue/currentTrack",
|
||||
}),
|
||||
isPlaying () {
|
||||
return this.$store.state.player.playing
|
||||
},
|
||||
tracksWithAlbum () {
|
||||
// needed to include album data (especially cover)
|
||||
// with tracks appended in queue (#795)
|
||||
let self = this
|
||||
return this.album.tracks.map(t => {
|
||||
return {
|
||||
...t,
|
||||
album: {
|
||||
...self.album,
|
||||
tracks: []
|
||||
}
|
||||
}
|
||||
})
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -111,35 +53,13 @@ export default {
|
|||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
.content-cell {
|
||||
.link,
|
||||
.button {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
.link {
|
||||
margin-left: 0.5em;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
tr {
|
||||
.favorite-icon:not(.favorited) {
|
||||
visibility: hidden;
|
||||
}
|
||||
&:hover .favorite-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
.favorite-icon {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
.expand {
|
||||
cursor: pointer;
|
||||
|
||||
.default-cover {
|
||||
background-image: url("../../../assets/audio/default-cover.png") !important;
|
||||
}
|
||||
|
||||
.ui .card.rich {
|
||||
align-self: flex-start;
|
||||
.card.app-card > .head-image > .icon {
|
||||
margin: 0.5em;
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,31 +9,11 @@
|
|||
<button v-if="controls" :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle right', 'icon']"></i></button>
|
||||
<button v-if="controls" @click="fetchData('albums/')" :class="['ui', 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'refresh', 'icon']"></i></button>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui five cards">
|
||||
<div class="ui app-cards cards">
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
</div>
|
||||
<div class="card" v-for="album in albums" :key="album.id">
|
||||
<div :class="['ui', 'image', 'with-overlay', {'default-cover': !album.cover.original}]" v-lazy:background-image="getImageUrl(album)">
|
||||
<play-button class="play-overlay" :icon-only="true" :is-playable="album.is_playable" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :album="album"></play-button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<router-link :title="album.title" :to="{name: 'library.albums.detail', params: {id: album.id}}">
|
||||
{{ album.title|truncate(25) }}
|
||||
</router-link>
|
||||
<div class="description">
|
||||
<span>
|
||||
<router-link :title="album.artist.name" class="discrete link" :to="{name: 'library.artists.detail', params: {id: album.artist.id}}">
|
||||
{{ album.artist.name|truncate(23) }}
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<human-date class="left floated" :date="album.creation_date"></human-date>
|
||||
<play-button class="right floated basic icon" :dropdown-only="true" :is-playable="album.is_playable" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :album="album"></play-button>
|
||||
</div>
|
||||
</div>
|
||||
<album-card v-for="album in albums" :album="album" :key="album.id" />
|
||||
</div>
|
||||
<template v-if="!isLoading && albums.length === 0">
|
||||
<div class="ui placeholder segment">
|
||||
|
@ -49,7 +29,7 @@
|
|||
<script>
|
||||
import _ from '@/lodash'
|
||||
import axios from 'axios'
|
||||
import PlayButton from '@/components/audio/PlayButton'
|
||||
import AlbumCard from '@/components/audio/album/Card'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -59,7 +39,7 @@ export default {
|
|||
limit: {type: Number, default: 12},
|
||||
},
|
||||
components: {
|
||||
PlayButton
|
||||
AlbumCard
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -102,16 +82,6 @@ export default {
|
|||
this.offset = Math.max(this.offset - this.limit, 0)
|
||||
}
|
||||
},
|
||||
getImageUrl (album) {
|
||||
let url = '../../../assets/audio/default-cover.png'
|
||||
|
||||
if (album.cover.original) {
|
||||
url = this.$store.getters['instance/absoluteUrl'](album.cover.medium_square_crop)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return url
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
offset () {
|
||||
|
@ -124,11 +94,7 @@ export default {
|
|||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "../../../style/vendor/media";
|
||||
|
||||
.default-cover {
|
||||
background-image: url("../../../assets/audio/default-cover.png") !important;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
|
@ -136,18 +102,6 @@ export default {
|
|||
.ui.cards {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.ui.five.cards > .card {
|
||||
width: 15em;
|
||||
}
|
||||
.with-overlay {
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
height: 15em;
|
||||
width: 15em;
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.ui.cards .ui.button {
|
||||
|
|
|
@ -1,28 +1,22 @@
|
|||
<template>
|
||||
<div class="flat inline card">
|
||||
<div :class="['ui', 'image', 'with-overlay', {'default-cover': !cover.original}]" v-lazy:background-image="imageUrl">
|
||||
<play-button class="play-overlay" :icon-only="true" :is-playable="artist.is_playable" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :artist="artist"></play-button>
|
||||
<div class="app-card card">
|
||||
<div
|
||||
@click="$router.push({name: 'library.artists.detail', params: {id: artist.id}})"
|
||||
:class="['ui', 'head-image', 'circular', 'image', {'default-cover': !cover.original}]" v-lazy:background-image="imageUrl">
|
||||
<play-button :icon-only="true" :is-playable="artist.is_playable" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :artist="artist"></play-button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<router-link :title="artist.name" :to="{name: 'library.artists.detail', params: {id: artist.id}}">
|
||||
{{ artist.name|truncate(30) }}
|
||||
</router-link>
|
||||
<div v-if="artist.albums.length > 0">
|
||||
<i class="small sound icon"></i>
|
||||
<translate translate-context="Content/Artist/Card" :translate-params="{count: artist.albums.length}" :translate-n="artist.albums.length" translate-plural="%{ count } albums">1 album</translate>
|
||||
</div>
|
||||
<div v-else-if="artist.tracks_count">
|
||||
<i class="small sound icon"></i>
|
||||
<translate translate-context="Content/Artist/Card" :translate-params="{count: artist.tracks_count}" :translate-n="artist.tracks_count" translate-plural="%{ count } tracks">1 track</translate>
|
||||
</div>
|
||||
<tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="artist.tags"></tags-list>
|
||||
<strong>
|
||||
<router-link class="discrete link" :title="artist.name" :to="{name: 'library.artists.detail', params: {id: artist.id}}">
|
||||
{{ artist.name|truncate(30) }}
|
||||
</router-link>
|
||||
</strong>
|
||||
|
||||
<play-button
|
||||
class="play-button basic icon"
|
||||
:dropdown-only="true"
|
||||
:is-playable="artist.is_playable"
|
||||
:dropdown-icon-classes="['ellipsis', 'vertical', 'large', 'grey']"
|
||||
:artist="artist"></play-button>
|
||||
<tags-list label-classes="tiny" :truncate-size="20" :limit="2" :show-more="false" :tags="artist.tags"></tags-list>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<translate translate-context="*/*/*" :translate-params="{count: artist.tracks_count}" :translate-n="artist.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate>
|
||||
<play-button class="right floated basic icon" :dropdown-only="true" :is-playable="artist.is_playable" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :artist="artist"></play-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -72,24 +66,4 @@ export default {
|
|||
.default-cover {
|
||||
background-image: url("../../../assets/audio/default-cover.png") !important;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 40%;
|
||||
}
|
||||
|
||||
.with-overlay {
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
height: 8em;
|
||||
width: 8em;
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
.flat.card .with-overlay.image {
|
||||
border-radius: 50% !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -51,10 +51,8 @@
|
|||
class="ui stackable three column doubling grid">
|
||||
<div
|
||||
v-if="result.results.length > 0"
|
||||
class="ui cards">
|
||||
class="ui app-cards cards">
|
||||
<album-card
|
||||
:mode="'simple'"
|
||||
v-masonry-tile
|
||||
v-for="album in result.results"
|
||||
:key="album.id"
|
||||
:album="album"></album-card>
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
<h2>
|
||||
<translate translate-context="Content/Artist/Title">Albums by this artist</translate>
|
||||
</h2>
|
||||
<div class="ui cards">
|
||||
<album-card :mode="'rich'" :album="album" :key="album.id" v-for="album in allAlbums"></album-card>
|
||||
<div class="ui cards app-cards">
|
||||
<album-card :album="album" :key="album.id" v-for="album in allAlbums"></album-card>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<button :class="['ui', {loading: isLoadingMoreAlbums}, 'button']" v-if="nextAlbumsUrl && loadMoreAlbumsUrl" @click="loadMoreAlbums(loadMoreAlbumsUrl)">
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div v-if="result && result.results.length > 0" class="ui three cards">
|
||||
<div v-if="result && result.results.length > 0" class="ui five app-cards cards">
|
||||
<div v-if="isLoading" class="ui inverted active dimmer">
|
||||
<div class="ui loader"></div>
|
||||
</div>
|
||||
|
|
|
@ -1,45 +1,24 @@
|
|||
<template>
|
||||
<div class="ui playlist card">
|
||||
<div class="ui top attached icon button" :style="coversStyle">
|
||||
<div class="ui app-card card">
|
||||
<div
|
||||
@click="$router.push({name: 'library.playlists.detail', params: {id: playlist.id }})"
|
||||
:class="['ui', 'head-image', 'squares']">
|
||||
<img v-lazy="url" v-for="(url, idx) in images" :key="idx" />
|
||||
<play-button :icon-only="true" :is-playable="playlist.is_playable" :button-classes="['ui', 'circular', 'large', 'orange', 'icon', 'button']" :playlist="playlist"></play-button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
<div class="right floated">
|
||||
<play-button
|
||||
:is-playable="playlist.is_playable"
|
||||
:icon-only="true" class="ui inline"
|
||||
:button-classes="['ui', 'circular', 'large', {orange: playlist.tracks_count > 0}, 'icon', 'button', {disabled: playlist.tracks_count === 0}]"
|
||||
:playlist="playlist"></play-button>
|
||||
<play-button
|
||||
:is-playable="playlist.is_playable"
|
||||
class="basic inline icon"
|
||||
:dropdown-only="true"
|
||||
:dropdown-icon-classes="['ellipsis', 'vertical', 'large', {disabled: playlist.tracks_count === 0}, 'grey']"
|
||||
:account="playlist.actor"
|
||||
:playlist="playlist"></play-button>
|
||||
</div>
|
||||
<router-link :title="playlist.name" class="discrete link" :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">
|
||||
{{ playlist.name | truncate(30) }}
|
||||
<strong>
|
||||
<router-link class="discrete link" :title="playlist.name" :to="{name: 'library.playlists.detail', params: {id: playlist.id }}">
|
||||
{{ playlist.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<duration :seconds="playlist.duration" />
|
||||
|
|
||||
<i class="sound icon"></i>
|
||||
<translate translate-context="Content/*/Card/List item"
|
||||
translate-plural="%{ count } tracks"
|
||||
:translate-n="playlist.tracks_count"
|
||||
:translate-params="{count: playlist.tracks_count}">
|
||||
%{ count} track
|
||||
</translate>
|
||||
</strong>
|
||||
<div class="description">
|
||||
<user-link :user="playlist.user" class="left floated" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<user-link :user="playlist.user" class="left floated" />
|
||||
<span class="right floated">
|
||||
<i class="clock outline icon" />
|
||||
<human-date :date="playlist.creation_date" />
|
||||
</span>
|
||||
<translate translate-context="*/*/*" :translate-params="{count: playlist.tracks_count}" :translate-n="playlist.tracks_count" translate-plural="%{ count } tracks">%{ count } track</translate>
|
||||
<play-button class="right floated basic icon" :dropdown-only="true" :is-playable="playlist.is_playable" :dropdown-icon-classes="['ellipsis', 'horizontal', 'large', 'grey']" :playlist="playlist"></play-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -53,41 +32,18 @@ export default {
|
|||
PlayButton
|
||||
},
|
||||
computed: {
|
||||
coversStyle () {
|
||||
images () {
|
||||
let self = this
|
||||
let urls = this.playlist.album_covers.map((url) => {
|
||||
url = self.$store.getters['instance/absoluteUrl'](url)
|
||||
return `url("${url}")`
|
||||
return self.$store.getters['instance/absoluteUrl'](url)
|
||||
}).slice(0, 4)
|
||||
return {
|
||||
'background-image': urls.join(', ')
|
||||
while (urls.length < 4) {
|
||||
urls.push(
|
||||
'../../../assets/audio/default-cover.png'
|
||||
)
|
||||
}
|
||||
return urls
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style>
|
||||
.playlist.card .header .ellipsis.vertical.large.grey {
|
||||
font-size: 1.2em;
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.card .header {
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.attached.button {
|
||||
background-size: 25%;
|
||||
background-repeat: no-repeat;
|
||||
background-origin: border-box;
|
||||
background-position: 0 0, 33.33% 0, 66.67% 0, 100% 0;
|
||||
/* background-position: 0 0, 50% 0, 100% 0; */
|
||||
/* background-position: 0 0, 25% 0, 50% 0, 75% 0, 100% 0; */
|
||||
font-size: 4em;
|
||||
box-shadow: 0px 0px 0px 1px rgba(34, 36, 38, 0.15) inset !important;
|
||||
padding: unset;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="playlists.length > 0"
|
||||
v-masonry
|
||||
transition-duration="0"
|
||||
item-selector=".card"
|
||||
percent-position="true"
|
||||
stagger="0">
|
||||
<div class="ui cards">
|
||||
<div v-if="playlists.length > 0">
|
||||
<div class="ui app-cards cards">
|
||||
<playlist-card
|
||||
:playlist="playlist"
|
||||
v-masonry-tile
|
||||
v-for="playlist in playlists"
|
||||
:key="playlist.id"
|
||||
></playlist-card>
|
||||
|
|
|
@ -354,20 +354,6 @@ td.align.right {
|
|||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.ui.cards > .flat.card, .flat.card {
|
||||
box-shadow: none;
|
||||
.content {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui.cards > .inline.card {
|
||||
flex-direction: row;
|
||||
.content {
|
||||
padding: 0.5em 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
.ui.checkbox label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -438,5 +424,61 @@ input + .help {
|
|||
}
|
||||
}
|
||||
}
|
||||
.ui.cards.app-cards {
|
||||
$card-width: 14em;
|
||||
$card-hight: 22em;
|
||||
.app-card {
|
||||
display: flex;
|
||||
width: $card-width;
|
||||
height: $card-hight;
|
||||
.head-image {
|
||||
height: $card-width;
|
||||
background-size: cover !important;
|
||||
background-position: center !important;
|
||||
display: flex !important;
|
||||
justify-content: flex-end !important;
|
||||
align-items: flex-end !important;
|
||||
.button {
|
||||
margin: 0;
|
||||
}
|
||||
&.circular {
|
||||
overflow: visible;
|
||||
border-radius: 50% !important;
|
||||
height: $card-width - 1em;
|
||||
width: $card-width - 1em;
|
||||
margin: 0.5em;
|
||||
|
||||
}
|
||||
&.squares {
|
||||
display: block !important;
|
||||
position: relative;
|
||||
.button {
|
||||
position: absolute;
|
||||
bottom: 0.5em;
|
||||
right: 0.5em;
|
||||
}
|
||||
img {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.extra {
|
||||
border-top: 0 !important;
|
||||
}
|
||||
.content:not(.extra) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.floating.dropdown > .icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@import "./themes/_light.scss";
|
||||
@import "./themes/_dark.scss";
|
||||
|
|
Loading…
Reference in New Issue