Skip to content

Commit fa79120

Browse files
Feat clickable playlist progress bar (FreeTubeApp#7782)
* Feat clickable playlist progress bar * * Make clicking on progress bar scroll the view instead of playing the video * * Update thumbnail size to be slightly bigger w/ aspect ratio 4/3 like small width one * * Use smaller iamges for preview * * Use template ref instead of querySelector * $ Refactor style & remove outdated comment * * Remove usage of template ref array as ordered array * ! Workaround preview covered by other elements when previewing earlier items * ! Fix preview out of screen * ! Fix preview out of screen 2 * ! Fix preview out of screen 3 * ! Fix preview out of screen 4 * * Update preview box box-shadow, remove arrow below & "ticks" * * Use fixed width preview box (diff value for smaller view port) * * Disable the preview for pointing device of limited accuracy * * Update preview style for smaller width viewport * $ Refactor style * ! Fix children elements handling when scrolling to current video --------- Co-authored-by: PikachuEXE <[email protected]>
1 parent 5e4a93b commit fa79120

File tree

3 files changed

+249
-26
lines changed

3 files changed

+249
-26
lines changed

src/renderer/components/watch-video-playlist/watch-video-playlist.css

Lines changed: 125 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,96 @@
2222
color: var(--tertiary-text-color);
2323
}
2424

25-
.playlistProgressBar {
25+
.playlistProgressBarContainer {
2626
margin-inline-start: 10px;
27+
position: relative;
28+
padding-block: 8px;
29+
}
2730

28-
/* > In order to let ::-webkit-progress-value take effect, appearance needs to be set to none on the <progress> element. */
31+
.playlistProgressBar {
32+
cursor: pointer;
33+
background-color: var(--secondary-text-color);
34+
block-size: 6px;
35+
border-radius: 3px;
36+
overflow: visible;
37+
position: relative;
38+
transition: block-size 0.2s ease, margin-block 0.2s ease;
39+
}
2940

30-
/* https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-progress-value */
31-
appearance: none;
41+
.playlistProgressBar.expanded {
42+
block-size: 12px;
43+
margin-block: -3px;
3244
}
3345

34-
.playlistProgressBar::-webkit-progress-value {
35-
/* Color for filled part */
46+
.playlistProgressBarFill {
47+
block-size: 100%;
48+
background-color: var(--accent-color);
49+
border-radius: inherit;
50+
transition: inline-size 0.2s ease;
51+
}
52+
53+
.progressBarPreview {
54+
position: absolute;
55+
inset-block-start: -65px;
56+
transform: translateX(-50%);
57+
z-index: 10;
58+
pointer-events: none;
59+
}
60+
61+
.previewTooltip {
62+
background-color: var(--card-bg-color);
63+
border: 1px solid var(--primary-border-color);
64+
border-radius: 8px;
65+
padding-block: 8px;
66+
padding-inline: 12px;
67+
font-size: 12px;
68+
color: var(--primary-text-color);
69+
white-space: nowrap;
70+
box-shadow: 0 4px 12px rgb(0 0 0 / 50%);
71+
margin-block-end: 4px;
72+
display: grid;
73+
grid-template:
74+
'thumb index title' auto /
75+
max-content max-content max-content;
76+
align-items: center;
77+
gap: 8px;
78+
}
3679

37-
/* https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-progress-value */
80+
.previewThumbnail {
81+
grid-area: thumb;
82+
block-size: 36px;
83+
inline-size: 48px;
84+
object-fit: cover;
85+
border-radius: 4px;
86+
}
87+
88+
.previewText {
89+
grid-area: index;
90+
}
91+
92+
.previewVideoTitle {
93+
grid-area: title;
94+
font-weight: 400;
95+
color: var(--secondary-text-color);
96+
inline-size: 240px;
97+
overflow: hidden;
98+
text-overflow: ellipsis;
99+
white-space: nowrap;
100+
font-size: 11px;
101+
}
38102

39-
/* background-color is required to be declared here to prevent color being green */
103+
.progressBarTick.current {
40104
background-color: var(--accent-color);
105+
opacity: 0.8;
106+
inline-size: 3px;
41107
}
42108

43-
.playlistProgressBar::-webkit-progress-bar {
44-
/* Color for unfilled part */
109+
.playlistProgressBar.expanded .progressBarTick {
110+
opacity: 0.5;
111+
}
45112

46-
/* https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-progress-bar */
47-
background-color: var(--secondary-text-color);
113+
.playlistProgressBar.expanded .progressBarTick.current {
114+
opacity: 1;
48115
}
49116

50117
.playlistIcon {
@@ -83,7 +150,7 @@
83150
transition: background 0.2s ease-in;
84151
}
85152

86-
.playlistItems {
153+
.playlistItemsWrapper {
87154
overflow-y: auto;
88155
block-size: 360px;
89156
}
@@ -110,3 +177,48 @@
110177
position: relative;
111178
inset-block-end: 7px;
112179
}
180+
181+
@media only screen and (width <= 768px) {
182+
.previewTooltip {
183+
inline-size: auto;
184+
padding: 6px;
185+
grid-template:
186+
'thumb index' auto
187+
'title title' auto /
188+
auto auto;
189+
column-gap: 0;
190+
}
191+
192+
.previewThumbnail {
193+
inline-size: 32px;
194+
block-size: 24px;
195+
}
196+
197+
.previewText {
198+
justify-self: start;
199+
}
200+
201+
.previewVideoTitle {
202+
inline-size: 200px;
203+
text-align: center;
204+
}
205+
}
206+
207+
@media only screen and (width <= 500px) {
208+
.previewVideoTitle {
209+
inline-size: 150px;
210+
}
211+
}
212+
213+
/* The primary input mechanism does not include a pointing device or includes a pointing device of limited accuracy, such as a finger on a touchscreen. */
214+
@media (pointer: none), (pointer: coarse) {
215+
.progressBarPreview {
216+
display: none;
217+
}
218+
219+
/* Show progress bar as always expanded */
220+
.playlistProgressBar {
221+
block-size: 12px;
222+
margin-block: -3px;
223+
}
224+
}

src/renderer/components/watch-video-playlist/watch-video-playlist.js

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export default defineComponent({
5757
randomizedPlaylistItems: [],
5858

5959
getPlaylistInfoRun: false,
60+
showProgressBarPreview: false,
61+
previewPosition: 0,
62+
previewVideoIndex: 1,
63+
windowWidth: window.innerWidth,
6064
}
6165
},
6266
computed: {
@@ -143,6 +147,42 @@ export default defineComponent({
143147
sortOrder: function () {
144148
return this.isUserPlaylist ? this.userPlaylistSortOrder : SORT_BY_VALUES.Custom
145149
},
150+
151+
previewTransformXPercentage() {
152+
// Breakpoint for single-column-template
153+
if (this.windowWidth > 1050) {
154+
// Align left when preview is on the right half to avoid going out of right side of the window
155+
return this.previewPosition <= 50 ? -50 : -100
156+
}
157+
158+
// Align left/right to avoid going out of either side of the window
159+
return this.previewPosition <= 50 ? 0 : -100
160+
},
161+
previewVideoTitle: function () {
162+
const index = this.previewVideoIndex - 1
163+
if (index >= 0 && index < this.playlistItems.length) {
164+
return this.playlistItems[index].title || 'Unknown Title'
165+
}
166+
return ''
167+
},
168+
previewVideoThumbnail: function () {
169+
const index = this.previewVideoIndex - 1
170+
if (index >= 0 && index < this.playlistItems.length) {
171+
const videoId = this.playlistItems[index].videoId
172+
if (videoId) {
173+
let baseUrl = 'https://i.ytimg.com'
174+
if (this.backendPreference === 'invidious') {
175+
baseUrl = this.currentInvidiousInstanceUrl
176+
}
177+
return `${baseUrl}/vi/${videoId}/default.jpg`
178+
}
179+
}
180+
return null
181+
},
182+
shouldShowTicks: function () {
183+
// Only show ticks if <= 50 videos in playlist to avoid clutter
184+
return this.playlistVideoCount <= 50
185+
}
146186
},
147187
watch: {
148188
userPlaylistsReady: function() {
@@ -217,12 +257,16 @@ export default defineComponent({
217257
navigator.mediaSession.setActionHandler('previoustrack', this.playPreviousVideo)
218258
navigator.mediaSession.setActionHandler('nexttrack', this.playNextVideo)
219259
}
260+
261+
window.addEventListener('resize', this.calculateWindowWidth)
220262
},
221263
beforeUnmount: function () {
222264
if ('mediaSession' in navigator) {
223265
navigator.mediaSession.setActionHandler('previoustrack', null)
224266
navigator.mediaSession.setActionHandler('nexttrack', null)
225267
}
268+
269+
window.removeEventListener('resize', this.calculateWindowWidth)
226270
},
227271
methods: {
228272
findIndexOfCurrentVideoInPlaylist: function (playlist) {
@@ -527,18 +571,53 @@ export default defineComponent({
527571
},
528572

529573
scrollToCurrentVideo: function () {
530-
const container = this.$refs.playlistItems
531-
const currentVideoItem = (this.$refs.currentVideoItem || [])[0]
532-
if (container != null && currentVideoItem != null) {
574+
const container = this.$refs.playlistItemsWrapper
575+
const currentVideoItemEl = container ? Array.from(container.children)[this.currentVideoIndexZeroBased] : null
576+
if (container != null && currentVideoItemEl != null) {
533577
// Watch view can be ready sooner than this component
534-
container.scrollTop = currentVideoItem.$el.offsetTop - container.offsetTop
578+
container.scrollTop = currentVideoItemEl.offsetTop - container.offsetTop
535579
}
536580
},
537581

538582
pausePlayer: function () {
539583
this.$emit('pause-player')
540584
},
541585

586+
updateProgressBarPreview: function (event) {
587+
if (!this.showProgressBarPreview) return
588+
589+
const rect = this.$refs.playlistProgressBar.getBoundingClientRect()
590+
const mouseX = event.clientX - rect.left
591+
const progressBarWidth = rect.width
592+
const percentage = Math.max(0, Math.min(100, (mouseX / progressBarWidth) * 100))
593+
594+
this.previewPosition = percentage
595+
this.previewVideoIndex = Math.max(1, Math.min(this.playlistVideoCount, Math.ceil((percentage / 100) * this.playlistVideoCount)))
596+
},
597+
598+
handleProgressBarClick: function (event) {
599+
const rect = event.currentTarget.getBoundingClientRect()
600+
const clickX = event.clientX - rect.left
601+
const progressBarWidth = rect.width
602+
const clickPercentage = clickX / progressBarWidth
603+
604+
const targetVideoIndex = Math.max(1, Math.min(this.playlistVideoCount, Math.ceil(clickPercentage * this.playlistVideoCount)))
605+
const targetArrayIndex = targetVideoIndex - 1
606+
607+
if (targetArrayIndex >= 0 && targetArrayIndex < this.playlistItems.length) {
608+
const container = this.$refs.playlistItemsWrapper
609+
const targetPlaylistItemEl = container ? Array.from(container.children)[targetArrayIndex] : null
610+
if (container != null && targetPlaylistItemEl != null) {
611+
// Watch view can be ready sooner than this component
612+
container.scrollTop = targetPlaylistItemEl.offsetTop - container.offsetTop
613+
}
614+
}
615+
},
616+
617+
calculateWindowWidth() {
618+
this.windowWidth = window.innerWidth
619+
},
620+
542621
...mapMutations([
543622
'setCachedPlaylist'
544623
])

src/renderer/components/watch-video-playlist/watch-video-playlist.vue

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,45 @@
4040
<label for="playlistProgressBar">
4141
{{ currentVideoIndexOneBased }} / {{ playlistVideoCount }}
4242
</label>
43-
<progress
43+
44+
<!-- eslint-disable vuejs-accessibility/mouse-events-have-key-events, vuejs-accessibility/click-events-have-key-events -->
45+
<div
4446
v-if="!shuffleEnabled && !reversePlaylist"
45-
id="playlistProgressBar"
46-
class="playlistProgressBar"
47-
:value="currentVideoIndexOneBased"
48-
:max="playlistVideoCount"
49-
/>
47+
class="playlistProgressBarContainer"
48+
@mouseenter="showProgressBarPreview = true"
49+
@mouseleave="showProgressBarPreview = false"
50+
@mousemove="updateProgressBarPreview"
51+
>
52+
<div
53+
ref="playlistProgressBar"
54+
class="playlistProgressBar"
55+
:class="{ expanded: showProgressBarPreview }"
56+
@click="handleProgressBarClick"
57+
>
58+
<div
59+
class="playlistProgressBarFill"
60+
:style="{ width: (currentVideoIndexOneBased / playlistVideoCount) * 100 + '%' }"
61+
/>
62+
<div
63+
v-if="showProgressBarPreview"
64+
class="progressBarPreview"
65+
:style="{ left: previewPosition + '%', transform: `translateX(${ previewTransformXPercentage }%)` }"
66+
>
67+
<div class="previewTooltip">
68+
<img
69+
v-if="previewVideoThumbnail"
70+
:src="previewVideoThumbnail"
71+
alt=""
72+
class="previewThumbnail"
73+
>
74+
<div class="previewText">
75+
{{ previewVideoIndex }} / {{ playlistVideoCount }}
76+
</div>
77+
<div class="previewVideoTitle">{{ previewVideoTitle }}</div>
78+
</div>
79+
</div>
80+
</div>
81+
</div>
5082
</span>
5183
<p>
5284
<font-awesome-icon
@@ -108,13 +140,13 @@
108140
</p>
109141
<div
110142
v-if="!isLoading"
111-
ref="playlistItems"
112-
class="playlistItems"
143+
ref="playlistItemsWrapper"
144+
class="playlistItemsWrapper"
113145
>
114146
<ft-list-video-numbered
115147
v-for="(item, index) in playlistItems"
116148
:key="item.playlistItemId || item.videoId"
117-
:ref="currentVideoIndexZeroBased === index ? 'currentVideoItem' : null"
149+
ref="playlistItem"
118150
class="playlistItem"
119151
:data="item"
120152
:playlist-id="playlistId"

0 commit comments

Comments
 (0)