...
 
Commits (9)
......@@ -61,15 +61,17 @@
autoSize: Boolean,
contain: Boolean,
cover: Boolean,
coverScreen: Boolean,
calculateLuminosity: Boolean
},
computed: {
cssClasses () {
const result = []
if (this.contain) result.push('is-contained')
if (this.cover) result.push('is-covering')
if (this.size) result.push('is-fixed-size')
return result
return {
'is-contained': this.contain,
'is-covering': this.cover,
'is-covering-screen': this.coverScreen,
'is-fixed-size': this.size
}
},
style () {
const result = {}
......@@ -85,7 +87,7 @@
},
realSource () {
if (this.src) {
return this.autoSize ? this.createAutoSizeUrl() : this.src
return this.autoSize ? this.createAutoSizeUrl(this.size) : this.src
}
if (this.fallbackTransparent) {
......@@ -124,7 +126,9 @@
return `${this.src}?${createQueryString(params)}`
},
updateLuminosity (event) {
this.luminosity = calculateImageLuminosity(event.target)
if (this.calculateImageLuminosity) {
this.luminosity = calculateImageLuminosity(event.target)
}
},
updateWidth () {
if (this.$refs.container) {
......@@ -143,7 +147,9 @@
}
</script>
<style>
<style lang="scss">
@import '../../styles/variables';
.image {
overflow: hidden;
}
......@@ -166,4 +172,37 @@
.image.is-covering img {
object-fit: cover;
}
.image.is-covering-screen {
--luminosity: .5;
width: calc(100% + 2rem) !important;
height: 75vh !important;
overflow: hidden;
pointer-events: none;
filter: blur(10px);
opacity: .3;
position: fixed;
top: -1rem;
left: -1rem;
right: -1rem;
img {
max-height: none !important;
height: auto !important;
transform: translateY(-10%);
filter: brightness(calc(1 - var(--luminosity)));
}
&::after {
content: ' ';
display: block;
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 100%;
background-image: linear-gradient(fade_out($color-base, 1), $color-base);
z-index: 2;
}
}
</style>
<template>
<div class="paginator" :class="`is-${behavior}`" v-if="!isEmpty || !hideEmpty">
<slot name="header" :items="items" :count="count" :isLoading="isLoading"/>
<slot name="header" :items="items" :count="count" :isLoading="isLoading" :hasNext="hasNext" :hasPrev="hasPrev"/>
<div class="paginator-items" ref="items" v-bind="$attrs"
:style="isLoading && currentItemsHeight ? { height: `${currentItemsHeight}px` } : null">
<slot :items="items" :count="count" :isLoading="isLoading"></slot>
<slot :items="items" :count="count" :isLoading="isLoading" :hasNext="hasNext" :hasPrev="hasPrev"></slot>
<app-spinner-area v-if="!noLoader && isLoading"/>
<slot name="empty-items" v-if="emptyMessage">
<div class="paginator-not-found" v-if="isEmpty && !isLoading">
<div class="content" v-if="isEmpty && !isLoading">
<p>{{ emptyMessage }}</p>
</div>
</slot>
</div>
<slot name="footer" :items="items" :count="count" :isLoading="isLoading"/>
<slot name="footer" :items="items" :count="count" :isLoading="isLoading" :hasNext="hasNext" :hasPrev="hasPrev"/>
</div>
</template>
......
......@@ -27,6 +27,14 @@
default: null
},
invert: Boolean,
exactOptions: {
type: Object,
default: () => ({
dateStyle: 'long',
timeStyle: 'short'
})
},
useExact: Boolean,
useRelative: Boolean,
useDistance: Boolean,
useAutoDistance: Boolean
......@@ -45,6 +53,8 @@
return Math.abs(differenceInCalendarDays(date, reference)) > 7
? formatRelative(date, reference, opts)
: formatDistance(date, reference, opts)
} else if (this.useExact) {
return date.toLocaleDateString(this.$i18n.locale, this.exactOptions)
} else {
return format(date, opts)
}
......
<template>
<app-card :title="broadcast.subtitle || series.name" :description="description" :image="coverImage">
<template v-slot:footer>
<div class="f-1 opaque-7 mt-1">
<app-time :date="new Date(broadcast.start_time)" use-exact
:aria-label="$t('broadcast_preview.start_time')"/>
</div>
</template>
</app-card>
</template>
<script>
import { imageUrl } from './mixins'
export default {
mixins: [
imageUrl('series', 'cover_image', 'seriesCoverImage'),
imageUrl('broadcast', 'cover_image', 'broadcastCoverImage')
],
props: {
broadcast: Object,
series: Object
},
computed: {
coverImage () {
return {
src: this.broadcastCoverImage || this.seriesCoverImage,
size: 72,
autoSize: true,
aspectRatio: 1,
contain: true
}
},
description () {
return this.broadcast.description || this.$t('broadcast_preview.no_description')
}
},
inject: ['dataSources']
}
</script>
<i18n lang="yaml">
en:
broadcast_preview.no_description: No description available.
broadcast_preview.start_time: Start of program
de:
broadcast_preview.no_description: Keine Beschreibung verfügbar.
broadcast_preview.start_time: Sendungsbeginn
</i18n>
......@@ -16,21 +16,16 @@
<script>
import { secondsToDuration } from '../../util'
import { recordingTracks } from './mixins'
import { imageUrl, recordingTracks } from './mixins'
import Duration from '../generic/Duration'
export default {
components: { Duration },
mixins: [recordingTracks],
mixins: [recordingTracks, imageUrl('recording')],
props: {
recording: Object
},
computed: {
coverImage () {
return this.recording.cover_image
? this.dataSources.createImageUrl(this.recording.cover_image)
: null
},
productionDate () {
return new Date(this.recording.production_date)
},
......
......@@ -6,16 +6,16 @@
</template>
<script>
import { imageUrl } from './mixins'
export default {
mixins: [ imageUrl('series') ],
props: {
series: Object
},
computed: {
description () {
return this.series.teaser || this.series.description
},
coverImage () {
return this.dataSources.createImageUrl(this.series.cover_image)
}
},
inject: ['dataSources']
......
import { createHowlerTracks } from '../../util/audio'
const toCamelCase = s =>
s.replace(/([-_][a-z])/ig, t =>
t.toUpperCase()
.replace('-', '')
.replace('_', '')
)
export const recordingTracks = {
data () {
return {
......@@ -26,3 +33,16 @@ export const recordingTracks = {
}
}
}
export const imageUrl = (containerName, prop = 'cover_image', computed) => {
computed = computed || toCamelCase(prop)
return {
computed: {
[computed] () {
return this[containerName][prop]
? this.dataSources.createImageUrl(this[containerName][prop])
: null
}
}
}
}
<template>
<app-page :title="series.name">
<app-image :src="coverImage" :size="windowWidth / 4" auto-size :aspect-ratio="coverAspectRatio"
cover cover-screen calculate-luminosity role="presentation" v-if="coverImage"/>
<app-container class="my-5">
<app-header :title="series.name" class="mb-3"/>
<div class="mb-2">
<app-markdown v-if="series.description">{{ series.description }}</app-markdown>
<SuggestContent v-else missing-text-translation-key="series_page.missing_description"
:context="series"/>
</div>
<app-auto-grid>
<div>
<h2 class="t-4 mt-5">Kommende Sendungen</h2>
</div>
</app-auto-grid>
<app-section>
<app-auto-grid :sizes="{ md: [2, 1], lg: [2, 1], xl: [2, 1] }" :gap-modifier="2">
<div>
<app-markdown v-if="series.description">{{ series.description }}</app-markdown>
<SuggestContent v-else missing-text-translation-key="series_page.missing_description"
:context="{ title: series.name, ...series }" type="series"/>
</div>
</app-auto-grid>
</app-section>
<app-section>
<app-auto-grid :sizes="{ md: [2, 1], lg: [2, 1], xl: [2, 1] }" :gap-modifier="2">
<div>
<app-paginator :collection="dataSources.recordings" behavior="append"
no-loader :min-height="0" :page-size="recordingCollection.count"
:filters="recordingFilters" id="recent_recordings">
<template v-slot:header>
<app-header :title="$t('series_page.recent_posts')" :level="2" class="mb-3"/>
</template>
<template v-slot:default="{ items, isLoading }">
<app-auto-grid :default-size="recordingCollection.columns" v-if="items.length > 0">
<div v-for="recording in items" :key="recording.id">
<RecordingPreview :recording="recording" class="mb-3"/>
</div>
</app-auto-grid>
<div class="content" v-else-if="items.length === 0 && !isLoading">
<p>{{ $t('series_page.no_recordings') }}</p>
</div>
</template>
<template v-slot:footer="{ isLoading, hasNext }">
<div style="height: 100px; position: relative" v-if="hasNext">
<app-spinner-area v-if="isLoading"/>
<app-paginator-nav class="mt-3"/>
</div>
</template>
</app-paginator>
</div>
<div>
<app-paginator :collection="dataSources.broadcasts" :filters="broadcastFilters"
:page-size="3" :page-limit="1" :empty-message="$t('series_page.no_broadcasts')">
<template v-slot:header>
<app-header :title="$t('series_page.upcoming_broadcasts')" :level="2" class="mb-3"/>
</template>
<template v-slot:default="{ items }">
<app-auto-grid :default-size="1">
<div v-for="broadcast in items" :key="broadcast.id">
<BroadcastPreview :broadcast="broadcast" :series="series"/>
</div>
</app-auto-grid>
</template>
</app-paginator>
</div>
</app-auto-grid>
</app-section>
</app-container>
</app-page>
</template>
<script>
import SuggestContent from '../components/generic/SuggestContent'
import RecordingPreview from '../components/model/RecordingPreview'
import { imageUrl } from '../components/model/mixins'
import BroadcastPreview from '../components/model/BroadcastPreview'
export default {
name: 'SeriesPage',
components: { SuggestContent },
components: { BroadcastPreview, RecordingPreview, SuggestContent },
mixins: [imageUrl('series')],
props: {
series: Object
},
......@@ -28,6 +84,49 @@
return {
broadcasts: null
}
}
},
computed: {
recordingFilters () {
return {
is_available: 1,
series: this.series.id
}
},
broadcastFilters () {
return {
end_time__gt: new Date().toISOString(),
series: this.series.id
}
},
recordingCollection () {
return {
xs: { count: 5, columns: 1 },
md: { count: 15, columns: 3 }
}[this.$mq] || { count: 28, columns: 4 }
},
coverAspectRatio () {
return this.windowWidth / this.windowHeight
}
},
inject: ['dataSources']
}
</script>
<i18n lang="yaml">
en:
series_page.upcoming_broadcasts: Upcoming Broadcasts
series_page.recent_posts: Recent Posts
series_page.no_recordings: >
There haven’t been any posts for this program yet.
Please check back later.
series_page.no_broadcasts: >
No broadcasts are currently planned.
de:
series_page.upcoming_broadcasts: Kommende Sendungen
series_page.recent_posts: Letzte Beiträge
series_page.no_recordings: >
In dieser Sendereihe wurden bisher keine Beiträge veröffentlicht.
Probiere es doch später noch einmal.
series_page.no_broadcasts: >
Derzeit sind keine Sendungen geplant.
</i18n>