forked from platypush/platypush
[WIP] [#414] [UI] Initial implementation of the Jellyfin UI.
This commit is contained in:
parent
841516d9de
commit
bf82ad9bf0
11 changed files with 577 additions and 4 deletions
|
@ -175,7 +175,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
toggle(event) {
|
toggle(event) {
|
||||||
event.stopPropagation()
|
event?.stopPropagation()
|
||||||
this.$emit('click', event)
|
this.$emit('click', event)
|
||||||
this.visible ? this.close() : this.open()
|
this.visible ? this.close() : this.open()
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,13 +3,16 @@
|
||||||
<div class="media-browser">
|
<div class="media-browser">
|
||||||
<div class="media-index grid" v-if="!mediaProvider">
|
<div class="media-index grid" v-if="!mediaProvider">
|
||||||
<div class="item"
|
<div class="item"
|
||||||
v-for="(provider, name) in mediaProviders"
|
v-for="(provider, name) in visibleMediaProviders"
|
||||||
:key="name"
|
:key="name"
|
||||||
@click="mediaProvider = provider">
|
@click="mediaProvider = provider">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<i v-bind="providersMetadata[name].icon"
|
<i v-bind="providersMetadata[name].icon"
|
||||||
:style="{ color: providersMetadata[name].icon?.color || 'inherit' }"
|
:style="{ color: providersMetadata[name].icon?.color || 'inherit' }"
|
||||||
v-if="providersMetadata[name].icon" />
|
v-if="providersMetadata[name].icon?.class" />
|
||||||
|
|
||||||
|
<img :src="providersMetadata[name].icon.url"
|
||||||
|
v-else-if="providersMetadata[name].icon?.url" />
|
||||||
</div>
|
</div>
|
||||||
<div class="name">
|
<div class="name">
|
||||||
{{ providersMetadata[name].name }}
|
{{ providersMetadata[name].name }}
|
||||||
|
@ -104,6 +107,15 @@ export default {
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
visibleMediaProviders() {
|
||||||
|
return Object.entries(this.mediaProviders)
|
||||||
|
.filter(([provider, component]) => component && (!this.filter || provider.toLowerCase().includes(this.filter.toLowerCase())))
|
||||||
|
.reduce((acc, [provider, component]) => {
|
||||||
|
acc[provider] = component
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -124,13 +136,16 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
async refreshMediaProviders() {
|
async refreshMediaProviders() {
|
||||||
const config = await this.request('config.get')
|
const config = this.$root.config
|
||||||
this.mediaProviders = {}
|
this.mediaProviders = {}
|
||||||
// The local File provider is always enabled
|
// The local File provider is always enabled
|
||||||
this.registerMediaProvider('File')
|
this.registerMediaProvider('File')
|
||||||
|
|
||||||
if (config.youtube)
|
if (config.youtube)
|
||||||
this.registerMediaProvider('YouTube')
|
this.registerMediaProvider('YouTube')
|
||||||
|
|
||||||
|
if (config['media.jellyfin'])
|
||||||
|
this.registerMediaProvider('Jellyfin')
|
||||||
},
|
},
|
||||||
|
|
||||||
onPlaylistChange() {
|
onPlaylistChange() {
|
||||||
|
|
|
@ -43,6 +43,20 @@
|
||||||
<div class="row creation-date" v-if="item.created_at">
|
<div class="row creation-date" v-if="item.created_at">
|
||||||
{{ formatDateTime(item.created_at, true) }}
|
{{ formatDateTime(item.created_at, true) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row creation-date" v-text="item.year" v-else-if="item.year" />
|
||||||
|
|
||||||
|
<div class="row ratings" v-if="item.critic_rating != null || item.community_rating != null">
|
||||||
|
<span class="rating" title="Critic rating" v-if="item.critic_rating != null">
|
||||||
|
<i class="fa fa-star" />
|
||||||
|
<span v-text="Math.round(item.critic_rating)" />%
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="rating" title="Community rating" v-if="item.community_rating != null">
|
||||||
|
<i class="fa fa-users" />
|
||||||
|
<span v-text="Math.round(item.community_rating)" />%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -229,5 +243,23 @@ export default {
|
||||||
color: $default-fg-2;
|
color: $default-fg-2;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ratings {
|
||||||
|
width: 100%;
|
||||||
|
font-size: .75em;
|
||||||
|
opacity: .75;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.rating {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 1em;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: .25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
<i :class="overlayIconClass" />
|
<i :class="overlayIconClass" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="backdrop" v-if="item?.image"
|
||||||
|
:style="{ backgroundImage: `url(${item.image})` }" />
|
||||||
|
|
||||||
<span class="icon type-icon" v-if="typeIcons[item?.type]">
|
<span class="icon type-icon" v-if="typeIcons[item?.type]">
|
||||||
<a :href="item.url" target="_blank" v-if="item.url">
|
<a :href="item.url" target="_blank" v-if="item.url">
|
||||||
<i :class="typeIcons[item.type]" :title="item.type">
|
<i :class="typeIcons[item.type]" :title="item.type">
|
||||||
|
@ -119,6 +122,7 @@ export default {
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
color: $default-media-img-fg;
|
color: $default-media-img-fg;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -159,6 +163,7 @@ export default {
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.25em 0.5em;
|
padding: 0.25em 0.5em;
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.type-icon {
|
.type-icon {
|
||||||
|
@ -194,6 +199,7 @@ export default {
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.image {
|
div.image {
|
||||||
|
@ -229,6 +235,7 @@ div.image {
|
||||||
border-radius: 2em;
|
border-radius: 2em;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -239,4 +246,14 @@ div.image {
|
||||||
color: $default-media-img-fg;
|
color: $default-media-img-fg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-size: cover;
|
||||||
|
filter: blur(5px) brightness(0.5);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
<template>
|
||||||
|
<div class="media-jellyfin-container browser">
|
||||||
|
<MediaNav :path="path" @back="$emit('back')" />
|
||||||
|
|
||||||
|
<div class="media-jellyfin-browser">
|
||||||
|
<Loading v-if="isLoading" />
|
||||||
|
|
||||||
|
<Index v-bind="componentData.props"
|
||||||
|
v-on="componentData.on"
|
||||||
|
@select="selectCollection"
|
||||||
|
v-else-if="currentView === 'index'" />
|
||||||
|
|
||||||
|
<Movies v-bind="componentData.props"
|
||||||
|
v-on="componentData.on"
|
||||||
|
:collection="collection"
|
||||||
|
@select="select"
|
||||||
|
v-else-if="currentView === 'movies'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Index from "./Jellyfin/Index";
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import MediaNav from "./Nav";
|
||||||
|
import MediaProvider from "./Mixin";
|
||||||
|
import Movies from "./Jellyfin/Collections/Movies/Index";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [MediaProvider],
|
||||||
|
components: {
|
||||||
|
Index,
|
||||||
|
Loading,
|
||||||
|
MediaNav,
|
||||||
|
Movies,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: [
|
||||||
|
'add-to-playlist',
|
||||||
|
'back',
|
||||||
|
'download',
|
||||||
|
'download-audio',
|
||||||
|
'play',
|
||||||
|
'play-with-opts',
|
||||||
|
],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
collection: null,
|
||||||
|
loading_: false,
|
||||||
|
path: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
componentData() {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
collection: this.collection,
|
||||||
|
filter: this.filter,
|
||||||
|
loading: this.isLoading,
|
||||||
|
},
|
||||||
|
|
||||||
|
on: {
|
||||||
|
'add-to-playlist': (item) => this.$emit('add-to-playlist', item),
|
||||||
|
'download': (item) => this.$emit('download', item),
|
||||||
|
'download-audio': (item) => this.$emit('download-audio', item),
|
||||||
|
'play': (item) => this.$emit('play', item),
|
||||||
|
'play-with-opts': (item) => this.$emit('play-with-opts', item),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
currentView() {
|
||||||
|
if (!this.collection) {
|
||||||
|
return 'index'
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.collection.type) {
|
||||||
|
case 'movies':
|
||||||
|
return 'movies'
|
||||||
|
default:
|
||||||
|
return 'index'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isLoading() {
|
||||||
|
return this.loading_ || this.loading
|
||||||
|
},
|
||||||
|
|
||||||
|
rootItem() {
|
||||||
|
const item = {
|
||||||
|
id: '',
|
||||||
|
title: 'Jellyfin',
|
||||||
|
type: 'index',
|
||||||
|
icon: {
|
||||||
|
class: 'fas fa-server',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
item.click = () => {
|
||||||
|
this.collection = null
|
||||||
|
this.select(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
select(item) {
|
||||||
|
if (item) {
|
||||||
|
if (this.path.length > 0 && this.path[this.path.length - 1].id === item.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === 'index') {
|
||||||
|
this.path = [this.rootItem]
|
||||||
|
} else {
|
||||||
|
this.path.push({
|
||||||
|
title: item.name,
|
||||||
|
...item,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.path = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectCollection(collection) {
|
||||||
|
this.collection = collection
|
||||||
|
this.select(collection)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
collection() {
|
||||||
|
this.setUrlArgs({ collection: this.collection?.id })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.path = [this.rootItem]
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted() {
|
||||||
|
this.setUrlArgs({ collection: null })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../style.scss";
|
||||||
|
|
||||||
|
.media-jellyfin-container {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.media-jellyfin-browser {
|
||||||
|
height: calc(100% - $media-nav-height - 2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,116 @@
|
||||||
|
<template>
|
||||||
|
<div class="movies index">
|
||||||
|
<Loading v-if="isLoading" />
|
||||||
|
|
||||||
|
<NoItems :with-shadow="false"
|
||||||
|
v-else-if="sortedMovies.length === 0">
|
||||||
|
No movies found.
|
||||||
|
</NoItems>
|
||||||
|
|
||||||
|
<Results :results="sortedMovies"
|
||||||
|
:sources="{'jellyfin': true}"
|
||||||
|
:filter="filter"
|
||||||
|
:selected-result="selectedResult"
|
||||||
|
@add-to-playlist="$emit('add-to-playlist', $event)"
|
||||||
|
@download="$emit('download', $event)"
|
||||||
|
@play="$emit('play', $event)"
|
||||||
|
@play-with-opts="$emit('play-with-opts', $event)"
|
||||||
|
@remove-from-playlist="$emit('remove-from-playlist', $event)"
|
||||||
|
@select="selectedResult = $event"
|
||||||
|
v-else />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import MediaProvider from "@/components/panels/Media/Providers/Mixin";
|
||||||
|
import NoItems from "@/components/elements/NoItems";
|
||||||
|
import Results from "@/components/panels/Media/Results";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [MediaProvider],
|
||||||
|
components: {
|
||||||
|
Loading,
|
||||||
|
NoItems,
|
||||||
|
Results,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: [
|
||||||
|
'add-to-playlist',
|
||||||
|
'back',
|
||||||
|
'download',
|
||||||
|
'play',
|
||||||
|
'play-with-opts',
|
||||||
|
],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
collection: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
movies: {},
|
||||||
|
loading_: false,
|
||||||
|
selectedResult: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isLoading() {
|
||||||
|
return this.loading_ || this.loading
|
||||||
|
},
|
||||||
|
|
||||||
|
sortedMovies() {
|
||||||
|
return [...this.movies].sort((a, b) => {
|
||||||
|
return a.title.localeCompare(b.title)
|
||||||
|
}).map((movie) => {
|
||||||
|
return {
|
||||||
|
item_type: movie.type,
|
||||||
|
...movie,
|
||||||
|
type: 'jellyfin',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async refresh() {
|
||||||
|
const collection = this.collection?.name
|
||||||
|
if (!collection?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading_ = true
|
||||||
|
try {
|
||||||
|
this.movies = await this.request(
|
||||||
|
'media.jellyfin.search',
|
||||||
|
{ collection, limit: 1000 },
|
||||||
|
)
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
this.loading_ = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
collection: {
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.refresh()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.refresh()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@/components/panels/Media/Providers/Jellyfin/common.scss";
|
||||||
|
</style>
|
|
@ -0,0 +1,131 @@
|
||||||
|
<template>
|
||||||
|
<div class="collections index">
|
||||||
|
<Loading v-if="isLoading" />
|
||||||
|
|
||||||
|
<div class="collections items" v-else>
|
||||||
|
<div class="collection item"
|
||||||
|
v-for="collection in filteredCollections"
|
||||||
|
:key="collection.id"
|
||||||
|
@click="$emit('select', collection)">
|
||||||
|
<div class="image">
|
||||||
|
<img :src="collection.image"
|
||||||
|
:alt="collection.name"
|
||||||
|
@error="onImageError(collection)"
|
||||||
|
v-if="!fallbackImageCollections[collection.id]">
|
||||||
|
<i :class="collectionsIcons[collection.type] ?? 'fas fa-folder'" v-else />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="name" v-if="fallbackImageCollections[collection.id]">
|
||||||
|
<h2>{{ collection.name }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
import MediaProvider from "../Mixin";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [MediaProvider],
|
||||||
|
components: {
|
||||||
|
Loading,
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: [
|
||||||
|
'add-to-playlist',
|
||||||
|
'back',
|
||||||
|
'download',
|
||||||
|
'download-audio',
|
||||||
|
'play',
|
||||||
|
'play-with-opts',
|
||||||
|
'select',
|
||||||
|
],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
collections: {},
|
||||||
|
fallbackImageCollections: {},
|
||||||
|
loading_: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
collectionsIcons() {
|
||||||
|
return {
|
||||||
|
books: "fas fa-book",
|
||||||
|
homevideos: "fas fa-video",
|
||||||
|
movies: "fas fa-film",
|
||||||
|
music: "fas fa-music",
|
||||||
|
playlists: "fas fa-list",
|
||||||
|
photos: "fas fa-image",
|
||||||
|
series: "fas fa-tv",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
filteredCollections() {
|
||||||
|
return Object.values(this.collections).filter(
|
||||||
|
(collection) => !this.filter || collection.name.toLowerCase().includes(this.filter.toLowerCase())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
isLoading() {
|
||||||
|
return this.loading_ || this.loading
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onImageError(collection) {
|
||||||
|
this.fallbackImageCollections[collection.id] = true
|
||||||
|
},
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
this.loading_ = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.collections = (
|
||||||
|
await this.request('media.jellyfin.get_collections')
|
||||||
|
).reduce((acc, collection) => {
|
||||||
|
acc[collection.id] = collection
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
} finally {
|
||||||
|
this.loading_ = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initCollection() {
|
||||||
|
const collectionId = this.getUrlArgs().collection
|
||||||
|
if (collectionId == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const collection = this.collections[collectionId]
|
||||||
|
if (!collection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('select', collection)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.refresh()
|
||||||
|
this.initCollection()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "./common.scss";
|
||||||
|
|
||||||
|
.index {
|
||||||
|
.item {
|
||||||
|
h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,62 @@
|
||||||
|
@import "@/components/panels/Media/style.scss";
|
||||||
|
|
||||||
|
.index {
|
||||||
|
height: 100%;
|
||||||
|
margin: 2px 0 -2px 0;
|
||||||
|
|
||||||
|
.items {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
gap: 1em;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(20em, 1fr));
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
width: 100%;
|
||||||
|
height: 15em;
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@extend .dim;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.75);
|
||||||
|
color: white;
|
||||||
|
font-size: 1.75em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"Jellyfin": {
|
||||||
|
"name": "Jellyfin",
|
||||||
|
"icon": {
|
||||||
|
"url": "https://static.platypush.tech/icons/media.jellyfin-64.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"YouTube": {
|
"YouTube": {
|
||||||
"name": "YouTube",
|
"name": "YouTube",
|
||||||
"icon": {
|
"icon": {
|
||||||
|
|
|
@ -20,6 +20,11 @@
|
||||||
i {
|
i {
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,22 @@
|
||||||
-webkit-animation-name: unfold;
|
-webkit-animation-name: unfold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dim {
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
-webkit-animation-duration: 0.5s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
animation-name: dim;
|
||||||
|
-webkit-animation-name: dim;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brighten {
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
-webkit-animation-duration: 0.5s;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
animation-name: brighten;
|
||||||
|
-webkit-animation-name: brighten;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
0% {opacity: 0;}
|
0% {opacity: 0;}
|
||||||
100% {opacity: 1;}
|
100% {opacity: 1;}
|
||||||
|
@ -79,6 +95,16 @@
|
||||||
100% {transform: scale(1, 1);}
|
100% {transform: scale(1, 1);}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes dim {
|
||||||
|
0% {filter: brightness(1);}
|
||||||
|
100% {filter: brightness(0.5);}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes brighten {
|
||||||
|
0% {filter: brightness(0.5);}
|
||||||
|
100% {filter: brightness(1);}
|
||||||
|
}
|
||||||
|
|
||||||
.glow {
|
.glow {
|
||||||
animation-duration: 2s;
|
animation-duration: 2s;
|
||||||
-webkit-animation-duration: 2s;
|
-webkit-animation-duration: 2s;
|
||||||
|
|
Loading…
Reference in a new issue