Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
funkwhale
funkwhale
Commits
82692f11
Commit
82692f11
authored
Jan 02, 2020
by
Eliot Berriot
Browse files
Merge branch 'proxy-attachments' into 'develop'
Fixed broken attachment fetching with long filename See merge request
!982
parents
9b23fb70
7d528ba2
Pipeline
#9082
passed with stages
in 7 minutes and 10 seconds
Changes
11
Pipelines
7
Hide whitespace changes
Inline
Side-by-side
api/funkwhale_api/common/tasks.py
View file @
82692f11
...
...
@@ -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"
)
...
...
api/tests/common/test_views.py
View file @
82692f11
...
...
@@ -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"
)
...
...
front/src/components/audio/album/Card.vue
View file @
82692f11
<
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
>
front/src/components/audio/album/Widget.vue
View file @
82692f11
...
...
@@ -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
{
...
...
front/src/components/audio/artist/Card.vue
View file @
82692f11
<
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
>
front/src/components/library/Albums.vue
View file @
82692f11
...
...
@@ -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>
...
...
front/src/components/library/ArtistDetail.vue
View file @
82692f11
...
...
@@ -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)">
...
...
front/src/components/library/Artists.vue
View file @
82692f11
...
...
@@ -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>
...
...
front/src/components/playlists/Card.vue
View file @
82692f11
<
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
>
front/src/components/playlists/CardList.vue
View file @
82692f11
<
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>
...
...
front/src/style/_main.scss
View file @
82692f11
...
...
@@ -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
;