Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
dashboard
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Container Registry
Model registry
Operate
Environments
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
AURA
dashboard
Commits
2a68a7ac
Commit
2a68a7ac
authored
1 year ago
by
Konrad Mohrfeldt
Browse files
Options
Downloads
Patches
Plain Diff
feat: show intermissions and current time indicator in calendar day view
refs
#155
#156
parent
552edd3f
No related branches found
Branches containing commit
No related tags found
Tags containing commit
No related merge requests found
Pipeline
#3483
failed
1 year ago
Stage: test
Stage: release
Changes
3
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
src/components/CalendarDayView.vue
+330
-120
330 additions, 120 deletions
src/components/CalendarDayView.vue
src/i18n/de.js
+8
-4
8 additions, 4 deletions
src/i18n/de.js
src/i18n/en.js
+8
-4
8 additions, 4 deletions
src/i18n/en.js
with
346 additions
and
128 deletions
src/components/CalendarDayView.vue
+
330
−
120
View file @
2a68a7ac
<
template
>
<div
class=
"schedule-panel tw-w-full"
>
<div
class=
"tw-flex tw-items-center tw-justify-between tw-mb-4"
>
<h3>
{{
prettyDate
(
selectedDay
)
}}
</h3>
<div
class=
"fc fc-direction-ltr"
>
<div
class=
"fc-button-group"
>
<button
type=
"button"
class=
"fc-button fc-prev-button fc-button-primary"
@
click=
"emit('changeDay', -1)"
>
<span
class=
"fc-icon fc-icon-chevron-left"
></span>
</button>
<button
type=
"button"
class=
"fc-button fc-next-button fc-button-primary"
@
click=
"emit('changeDay', 1)"
>
<span
class=
"fc-icon fc-icon-chevron-right"
></span>
</button>
</div>
</div>
</div>
<div
v-for=
"
{
timeslot,
timeslotDurationInSeconds,
playlist,
playlistDurationInSeconds,
show,
} in slotData"
:key="timeslot.id"
class="timeslot tw-w-full tw-py-2 tw-px-3 tw-rounded hover:tw-bg-gray-200 hover:tw-text-gray-900 tw-cursor-pointer tw-border tw-border-solid tw-border-gray-200 tw-mb-2"
:class="{
'tw-bg-gray-900 tw-text-white':
now >= parseISO(timeslot.start)
&&
now
<
=
parseISO
(
timeslot.end
),
'
tw-bg-gray-200
tw-text-gray-600
tw-opacity-75
'
:
now
>
= parseISO(timeslot.end),
}"
@click="onTimeslotClick(timeslot)"
>
<div
class=
"tw-flex tw-justify-between tw-items-center"
>
<div>
<p
class=
"tw-mb-0 tw-leading-tight tw-font-bold"
>
<SafeHTML
v-if=
"show"
:html=
"show.name"
sanitize-preset=
"inline-noninteractive"
/>
</p>
<span
class=
"tw-text-sm"
>
{{
prettyTime
(
timeslot
.
start
)
}}
-
{{
prettyTime
(
timeslot
.
end
)
}}
<span
v-if=
"timeslot.repetitionOfId"
class=
"tw-text-gray-400"
>
{{
t
(
'
calendar.repetition
'
)
}}
</span>
</span>
</div>
<div>
<div
class=
"schedule-panel tw-w-full"
>
<div
class=
"tw-flex tw-items-center tw-justify-between tw-mb-4"
>
<h3>
{{
selectedDay
.
toLocaleString
(
locale
,
{
dateStyle
:
'
long
'
}
)
}}
<
/h3
>
<div
v-if=
"(timeslot.playlistId || show?.defaultPlaylistId) && playlist"
>
<p>
<strong>
{{
t
(
'
emissionTable.playlist
'
)
}}
:
</strong>
{{
playlist
.
description
}}
<span
v-if=
"!timeslot.playlistId && show?.defaultPlaylistId"
class=
"tw-text-xs tw-text-red-500"
<
div
class
=
"
fc fc-direction-ltr
"
>
<
div
class
=
"
fc-button-group
"
>
<
button
type
=
"
button
"
class
=
"
fc-button fc-prev-button fc-button-primary
"
@
click
=
"
emit('changeDay', -1)
"
>
{{
t
(
'
calendar.fallback
'
)
}}
</span>
</p>
<p
v-if=
"timeslot.playlistId"
class=
"tw-text-sm"
>
<strong>
{{
t
(
'
emissionTable.duration
'
)
}}
:
</strong>
{{
secondsToDurationString
(
playlistDurationInSeconds
??
timeslotDurationInSeconds
)
}}
<span
v-if=
"
playlistDurationInSeconds !== null &&
timeslotDurationInSeconds !== playlistDurationInSeconds
"
class=
"is-mismatched"
<
span
class
=
"
fc-icon fc-icon-chevron-left
"
><
/span
>
<
/button
>
<
button
type
=
"
button
"
class
=
"
fc-button fc-next-button fc-button-primary
"
@
click
=
"
emit('changeDay', 1)
"
>
{{
t
(
'
calendar.mismatchedLength
'
)
}}
</
spa
n>
</
p
>
<
span
class
=
"
fc-icon fc-icon-chevron-right
"
><
/span
>
<
/
butto
n
>
<
/
div
>
<
/div
>
<p
v-else
>
{{
t
(
'
calendar.empty
'
)
}}
</p>
<
/div
>
<
table
class
=
"
slot-table tw-block md:tw-table
"
role
=
"
table
"
>
<
colgroup
>
<
col
width
=
"
1%
"
/>
<
/colgroup
>
<
tbody
role
=
"
rowgroup
"
class
=
"
tw-block md:tw-table-row-group
"
>
<
template
v
-
for
=
"
(slot, index) in slots
"
:
key
=
"
index
"
>
<
tr
v
-
if
=
"
index !== 0
"
role
=
"
presentation
"
class
=
"
tw-block md:tw-table-row
"
>
<
td
colspan
=
"
2
"
class
=
"
tw-p-0 tw-block md:tw-table-cell tw-w-full
"
>
<
hr
class
=
"
tw-my-4
"
/>
<
/td
>
<
/tr
>
<
tr
class
=
"
slot tw-block md:tw-table-row tw-w-full tw-relative
"
role
=
"
row
"
:
class
=
"
{
'slot--today': nowDateString === selectedDayISODate,
'slot--past': now > slot.end,
'slot--current': now > slot.start && now < slot.end,
'slot--future': now < slot.start,
}
"
>
<
th
class
=
"
slot-time tw-block md:tw-table-cell tw-align-top tw-font-normal tw-text-gray-600
"
scope
=
"
row
"
role
=
"
rowheader
"
>
<
span
v
-
if
=
"
now > slot.start && now < slot.end
"
class
=
"
slot-time-indicator
"
>
<
span
:
style
=
"
{
top: `${mapToDomain(
now.getTime(),
[slot.start.getTime(), slot.end.getTime()],
[0, 100],
)
}
%`,
}
"
/>
<
/span
>
<
span
class
=
"
tw-whitespace-nowrap
"
>
{{
formatTime
(
slot
.
start
)
}}
-
{{
formatTime
(
slot
.
end
)
}}
<
/span
>
<
br
/>
<
span
class
=
"
tw-text-sm
"
>
{{
secondsToDurationString
(
slot
.
durationInSeconds
)
}}
<
/span
>
<
/th
>
<
td
role
=
"
cell
"
class
=
"
slot-data tw-block lg-tw-table-cell tw-w-full tw-align-top tw-rounded
"
:
class
=
"
{
'tw-bg-gray-50': slot.type === 'show',
'tw-bg-amber-50': slot.type === 'intermission',
}
"
>
<
template
v
-
if
=
"
slot.type === 'intermission'
"
>
{{
t
(
'
calendar.intermission
'
)
}}
<
/template
>
<
div
v
-
if
=
"
slot.type === 'show'
"
class
=
"
tw-flex tw-flex-col tw-gap-2
"
>
<
div
>
<
div
class
=
"
tw-float-right
"
>
<
button
v
-
if
=
"
slot.timeslot.showId === selectedShow.id
"
type
=
"
button
"
class
=
"
btn btn-sm btn-default
"
:
title
=
"
t('calendar.editTimeslot')
"
@
click
=
"
editTimeslot(slot.timeslot)
"
>
<
icon
-
system
-
uicons
-
pen
/>
<
/button
>
<
button
v
-
if
=
"
slot.timeslot.showId !== selectedShow.id
"
class
=
"
btn btn-sm btn-default
"
:
title
=
"
t('calendar.switchShow', { show: sanitizeHTML(slot.show?.name ?? '')
}
)
"
@
click
=
"
switchShow(slot.timeslot)
"
>
<
icon
-
system
-
uicons
-
reverse
/>
<
/button
>
<
/div
>
<
SafeHTML
:
html
=
"
slot.show?.name ?? ''
"
sanitize
-
preset
=
"
inline-noninteractive
"
as
=
"
p
"
class
=
"
tw-font-bold tw-m-0
"
/>
<
/div
>
<
div
class
=
"
tw-grid tw-gap-1 tw-text-sm empty:tw-hidden
"
style
=
"
grid-template-columns: max-content minmax(0, 1fr)
"
>
<
template
v
-
if
=
"
(slot.timeslot.playlistId || slot.show?.defaultPlaylistId) && slot.playlist
"
>
<
span
>
{{
t
(
'
emissionTable.playlist
'
)
}}
:
<
/span
>
<
span
>
{{
slot
.
playlist
.
description
}}
<
/span
>
<
/template
>
<
template
v
-
if
=
"
slot.playlistDurationInSeconds
"
>
<
span
>
{{
t
(
'
emissionTable.duration
'
)
}}
:
<
/span
>
<
span
>
{{
secondsToDurationString
(
slot
.
playlistDurationInSeconds
)
}}
<
/span
>
<
/template
>
<
/div
>
<
div
class
=
"
tw-flex tw-gap-2
"
>
<
span
v
-
if
=
"
now > slot.start && now < slot.end
"
:
class
=
"
pillClasses
"
class
=
"
tw-bg-teal-200
"
>
{{
t
(
'
calendar.playing
'
)
}}
<
/span
>
<
span
v
-
if
=
"
slot.timeslot.repetitionOfId
"
:
class
=
"
pillClasses
"
class
=
"
tw-bg-amber-200
"
>
{{
t
(
'
calendar.repetition
'
)
}}
<
/span
>
<
span
v
-
if
=
"
slot.playlistDurationInSeconds !== null &&
slot.timeslotDurationInSeconds !== slot.playlistDurationInSeconds
"
:
class
=
"
pillClasses
"
class
=
"
tw-bg-amber-200
"
>
{{
t
(
'
calendar.mismatchedLength
'
)
}}
<
/span
>
<
span
v
-
if
=
"
!slot.timeslot.playlistId && slot.show?.defaultPlaylistId
"
:
class
=
"
pillClasses
"
class
=
"
tw-bg-rose-200
"
>
{{
t
(
'
calendar.fallback
'
)
}}
<
/span
>
<
span
v
-
else
-
if
=
"
!slot.timeslot.playlistId && !slot.show?.defaultPlaylistId
"
:
class
=
"
pillClasses
"
class
=
"
tw-bg-rose-200
"
>
{{
t
(
'
calendar.empty
'
)
}}
<
/span
>
<
/div
>
<
/div
>
<
/td
>
<
/tr
>
<
/template
>
<
/tbody
>
<
/table
>
<
/div
>
<
/div
>
<
/template
>
<
script
lang
=
"
ts
"
setup
>
import
SafeHTML
from
'
@/components/generic/SafeHTML
'
import
{
usePrett
y
}
from
'
@/mixins/prettyDate
'
import
{
use
I18n
}
from
'
@
/i18n
'
import
{
endOfDay
,
parseISO
,
startOfDay
}
from
'
date-fns
'
import
{
sortB
y
}
from
'
lodash
'
import
{
use
Now
}
from
'
@
vueuse/core
'
import
{
computed
}
from
'
vue
'
import
{
getISODateString
,
useSelectedShow
}
from
'
@/utilities
'
import
{
useStore
}
from
'
vuex
'
import
SafeHTML
from
'
@/components/generic/SafeHTML
'
import
{
useI18n
}
from
'
@/i18n
'
import
{
Playlist
,
Show
,
TimeSlot
}
from
'
@/types
'
import
{
parseISO
}
from
'
date-fns
'
import
{
calculatePlaylistDurationInSeconds
,
usePlaylistStore
}
from
'
@/stores/playlists
'
import
{
calculateDurationSeconds
,
secondsToDurationString
}
from
'
@/util
'
import
{
useNow
}
from
'
@vueuse/core
'
import
{
getISODateString
}
from
'
@/utilities
'
import
{
calculateDurationSeconds
,
mapToDomain
,
sanitizeHTML
,
secondsToDurationString
,
useFormattedISODate
,
useSelectedShow
,
}
from
'
@/util
'
const
pillClasses
=
'
tw-py-1 tw-px-2 tw-text-xs tw-rounded-full
'
type
SlotData
=
{
type
BaseSlot
=
{
start
:
Date
end
:
Date
durationInSeconds
:
number
}
type
IntermissionSlot
=
BaseSlot
&
{
type
:
'
intermission
'
}
type
ShowSlot
=
BaseSlot
&
{
type
:
'
show
'
timeslot
:
TimeSlot
timeslotDurationInSeconds
:
number
show
:
Show
|
undefined
...
...
@@ -110,6 +226,8 @@ type SlotData = {
playlistDurationInSeconds
:
number
|
null
}
type
Slot
=
IntermissionSlot
|
ShowSlot
const
props
=
defineProps
<
{
selectedDay
:
Date
}
>
()
...
...
@@ -117,50 +235,142 @@ const emit = defineEmits<{
changeDay
:
[
offset
:
number
]
editTimeslot
:
[
timeslot
:
TimeSlot
]
}
>
()
const
{
t
}
=
useI18n
()
const
{
prettyDate
,
prettyTime
}
=
usePretty
()
const
now
=
useNow
({
interval
:
60
_000
})
const
{
t
,
locale
}
=
useI18n
()
const
store
=
useStore
()
const
selectedShow
=
useSelectedShow
()
const
{
itemMap
:
playlistMap
,
retrieve
:
retrievePlaylist
}
=
usePlaylistStore
()
const
shows
=
computed
<
Show
[]
>
(()
=>
store
.
state
.
shows
.
shows
)
const
timeslots
=
computed
<
TimeSlot
[]
>
(()
=>
store
.
state
.
shows
.
timeslots
)
const
{
itemMap
:
playlistMap
,
retrieve
:
retrievePlaylist
}
=
usePlaylistStore
()
const
now
=
useNow
({
interval
:
60
_000
}
)
const
nowDateString
=
useFormattedISODate
(
now
)
const
startOfSelectedDay
=
computed
(()
=>
startOfDay
(
props
.
selectedDay
))
const
endOfSelectedDay
=
computed
(()
=>
endOfDay
(
props
.
selectedDay
))
const
selectedDayISODate
=
computed
(()
=>
getISODateString
(
props
.
selectedDay
))
const
slotData
=
computed
<
SlotData
[]
>
(()
=>
{
const
result
:
SlotData
[]
=
[]
const
daySlots
=
timeslots
.
value
.
filter
(
(
timeslot
)
=>
timeslot
.
start
.
split
(
'
T
'
)[
0
]
===
selectedDayISODate
.
value
,
const
slots
=
computed
<
Slot
[]
>
(()
=>
{
const
result
:
Slot
[]
=
[]
let
start
=
new
Date
(
startOfSelectedDay
.
value
)
const
endOfToday
=
new
Date
(
endOfSelectedDay
.
value
)
const
daySlots
=
sortBy
(
timeslots
.
value
.
filter
((
timeslot
)
=>
// we want either the start date or the end date to be on the currently selected day
// so that timeslots that wrap around days are included
[
extractDateFromDateTimeString
(
timeslot
.
start
),
extractDateFromDateTimeString
(
timeslot
.
end
),
].
includes
(
selectedDayISODate
.
value
),
),
(
timeslot
)
=>
timeslot
.
start
,
)
for
(
const
timeslot
of
daySlots
)
{
const
show
=
shows
.
value
.
find
(({
id
})
=>
timeslot
.
showId
===
id
)
const
playlistId
=
timeslot
.
playlistId
??
show
?.
defaultPlaylistId
// enqueue a request for the playlist in case it’s not yet in the store
if
(
playlistId
)
retrievePlaylist
(
playlistId
,
undefined
,
{
useCached
:
true
})
const
playlist
=
playlistId
?
playlistMap
.
get
(
playlistId
)
:
undefined
const
timeslotDurationInSeconds
=
calculateDurationSeconds
(
parseISO
(
timeslot
.
start
),
parseISO
(
timeslot
.
end
),
)
const
playlistDurationInSeconds
=
playlist
?
calculatePlaylistDurationInSeconds
(
playlist
)
:
null
result
.
push
({
timeslot
,
timeslotDurationInSeconds
,
playlist
,
playlistDurationInSeconds
,
show
,
})
const
showSlot
=
createShowSlot
(
timeslot
)
if
(
showSlot
.
start
>
start
)
{
// this slot starts some time after the previous slot ended
// that means we got an intermission between the last timeslot and the current one
result
.
push
(
createIntermissionSlot
(
start
,
showSlot
.
start
))
}
start
=
showSlot
.
end
result
.
push
(
showSlot
)
}
if
(
start
<
endOfToday
)
{
// the last timeslot of this day ended before the end of the day
// that means we got an intermission between the end of the last show and the end of the selected day
result
.
push
(
createIntermissionSlot
(
start
,
endOfToday
))
}
return
result
}
)
function
onTimeslotClick
(
timeslot
:
TimeSlot
)
{
if
(
selectedShow
.
value
.
id
!==
timeslot
.
showId
)
{
selectedShow
.
value
=
{
id
:
timeslot
.
showId
}
as
Show
}
else
{
emit
(
'
editTimeslot
'
,
timeslot
)
function
extractDateFromDateTimeString
(
date
:
string
)
{
return
date
.
split
(
'
T
'
)[
0
]
}
function
createIntermissionSlot
(
start
:
Date
,
end
:
Date
):
IntermissionSlot
{
return
{
type
:
'
intermission
'
,
start
,
end
,
durationInSeconds
:
calculateDurationSeconds
(
start
,
end
),
}
}
function
createShowSlot
(
timeslot
:
TimeSlot
):
ShowSlot
{
const
show
=
shows
.
value
.
find
(({
id
}
)
=>
timeslot
.
showId
===
id
)
const
playlistId
=
timeslot
.
playlistId
??
show
?.
defaultPlaylistId
// enqueue a request for the playlist in case it’s not yet in the store
if
(
playlistId
)
retrievePlaylist
(
playlistId
,
undefined
,
{
useCached
:
true
}
)
const
playlist
=
playlistId
?
playlistMap
.
get
(
playlistId
)
:
undefined
const
start
=
parseISO
(
timeslot
.
start
)
const
end
=
parseISO
(
timeslot
.
end
)
const
timeslotDurationInSeconds
=
calculateDurationSeconds
(
start
,
end
)
const
playlistDurationInSeconds
=
playlist
?
calculatePlaylistDurationInSeconds
(
playlist
)
:
null
return
{
type
:
'
show
'
,
start
,
end
,
durationInSeconds
:
calculateDurationSeconds
(
start
,
end
),
timeslot
,
timeslotDurationInSeconds
,
playlist
,
playlistDurationInSeconds
,
show
,
}
}
function
formatTime
(
date
:
Date
)
{
if
(
date
<
startOfSelectedDay
.
value
||
date
>
endOfSelectedDay
.
value
)
{
return
date
.
toLocaleString
(
locale
.
value
,
{
dateStyle
:
'
short
'
,
timeStyle
:
'
short
'
,
}
)
}
return
date
.
toLocaleString
(
locale
.
value
,
{
timeStyle
:
'
short
'
,
}
)
}
function
switchShow
(
timeslot
:
TimeSlot
)
{
selectedShow
.
value
=
{
id
:
timeslot
.
showId
}
as
Show
}
function
editTimeslot
(
timeslot
:
TimeSlot
)
{
emit
(
'
editTimeslot
'
,
timeslot
)
}
<
/script
>
<
style
lang
=
"
postcss
"
scoped
>
.
slot
-
table
{
width
:
min
(
800
px
,
100
%
);
}
.
slot
-
table
:
is
(
th
,
td
)
{
@
apply
tw
-
p
-
2
;
}
.
slot
-
table
th
{
@
apply
tw
-
pr
-
4
;
}
.
slot
.
slot
--
today
.
slot
--
past
{
@
apply
tw
-
opacity
-
75
;
}
.
slot
-
time
-
indicator
{
position
:
absolute
;
top
:
0
;
bottom
:
0
;
right
:
100
%
;
width
:
0.35
rem
;
border
:
solid
theme
(
'
colors.gray.300
'
);
border
-
width
:
1
px
1
px
1
px
0
;
&
>
span
{
position
:
absolute
;
right
:
0
;
width
:
0.5
rem
;
height
:
1
px
;
background
-
color
:
var
(
--
fc
-
now
-
indicator
-
color
);
}
}
<
/style
>
This diff is collapsed.
Click to expand it.
src/i18n/de.js
+
8
−
4
View file @
2a68a7ac
...
...
@@ -468,14 +468,18 @@ export default {
calendar
:
{
today
:
'
Heute
'
,
empty
:
'
(
Keine Playlist
!)
'
,
fallback
:
'
Achtung!
Fallback-Playlist
!
'
,
repetition
:
'
(
Wiederholung
)
'
,
empty
:
'
Keine Playlist
'
,
fallback
:
'
Fallback-Playlist
'
,
repetition
:
'
Wiederholung
'
,
view
:
{
day
:
'
Tagesansicht
'
,
week
:
'
Wochenansicht
'
,
},
mismatchedLength
:
'
(Unpassende Länge)
'
,
switchShow
:
'
Zur „%{show}“ Sendereihe wechseln
'
,
editTimeslot
:
'
Sendung bearbeiten
'
,
intermission
:
'
Programmunterbrechung
'
,
mismatchedLength
:
'
Unpassende Länge
'
,
playing
:
'
Spielt gerade
'
,
},
// Etc
...
...
This diff is collapsed.
Click to expand it.
src/i18n/en.js
+
8
−
4
View file @
2a68a7ac
...
...
@@ -459,14 +459,18 @@ export default {
calendar
:
{
today
:
'
Today
'
,
empty
:
'
(
No playlist
! Station fallback will be used)
'
,
fallback
:
'
Warning!
Fallback playlist
!
'
,
repetition
:
'
(
Repetition
)
'
,
empty
:
'
No playlist
'
,
fallback
:
'
Fallback playlist
'
,
repetition
:
'
Repetition
'
,
view
:
{
day
:
'
Day view
'
,
week
:
'
Week view
'
,
},
mismatchedLength
:
'
(wrong duration)
'
,
switchShow
:
'
Switch to “%{show}” show
'
,
editTimeslot
:
'
Sendung bearbeiten
'
,
intermission
:
'
Program intermission
'
,
mismatchedLength
:
'
wrong duration
'
,
playing
:
'
Currently playing
'
,
},
// Etc
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment