Skip to content

Commit c6635d1

Browse files
committed
Improve web highlight effect
1 parent a5dae89 commit c6635d1

File tree

4 files changed

+68
-31
lines changed

4 files changed

+68
-31
lines changed

app/javascript/components/Tracks.jsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useState, useEffect } from "react";
22
import { useOutletContext, Link } from "react-router-dom";
33
import { formatDurationTrack, formatDurationShow, formatDate, truncate } from "./helpers/utils";
44
import TagBadges from "./controls/TagBadges";
@@ -10,8 +10,32 @@ import CoverArt from "./CoverArt";
1010
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
1111
import { faScissors } from "@fortawesome/free-solid-svg-icons";
1212

13+
const SHIMMER_GRADIENT = 'linear-gradient(90deg, transparent 0%, transparent 20%, rgba(171, 217, 255, 0.45) 45%, rgba(171, 217, 255, 0.45) 55%, transparent 80%, transparent 100%)';
14+
const FOCUS_BOX_SHADOW = 'inset 0 -3px 0 #ABD9FF';
15+
1316
const Tracks = ({ tracks, viewStyle, numbering = false, omitSecondary = false, highlight, trackRefs, trackSlug }) => {
14-
const { playTrack, activeTrack, setCustomPlaylist } = useOutletContext();
17+
const { playTrack, activeTrack, setCustomPlaylist, isPlaying } = useOutletContext();
18+
const [isDarkMode, setIsDarkMode] = useState(
19+
typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
20+
);
21+
22+
useEffect(() => {
23+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
24+
const handleChange = (e) => setIsDarkMode(e.matches);
25+
mediaQuery.addEventListener('change', handleChange);
26+
return () => mediaQuery.removeEventListener('change', handleChange);
27+
}, []);
28+
29+
// Override DarkReader's color modifications, matching the play button's approach
30+
useEffect(() => {
31+
if (!isDarkMode) return;
32+
document.querySelectorAll('.track-shimmer').forEach(el => {
33+
el.style.setProperty('background', SHIMMER_GRADIENT, 'important');
34+
});
35+
document.querySelectorAll('.track-item.focus').forEach(el => {
36+
el.style.setProperty('box-shadow', FOCUS_BOX_SHADOW, 'important');
37+
});
38+
}, [isDarkMode, activeTrack?.id, trackSlug, isPlaying]);
1539

1640
const handleTrackClick = (track) => {
1741
if (track.audio_status === 'missing') return;
@@ -49,20 +73,22 @@ const Tracks = ({ tracks, viewStyle, numbering = false, omitSecondary = false, h
4973
const { actualDuration, isExcerpt } = calculateTrackDetails(track);
5074
const hasMissingAudio = track.audio_status === 'missing';
5175
const shouldFocus = viewStyle === "show" && track.slug === trackSlug;
76+
const isActive = track.id === activeTrack?.id;
5277

5378
return (
5479
<li
5580
key={track.id}
5681
className={[
5782
"list-item",
5883
viewStyle === "show" ? "track-item" : "",
59-
track.id === activeTrack?.id ? "active-item" : "",
84+
isActive ? "active-item" : "",
6085
shouldFocus ? "focus" : "",
6186
hasMissingAudio ? "no-audio" : ""
6287
].filter(Boolean).join(" ")}
6388
onClick={() => handleTrackClick(track)}
6489
ref={trackRefs ? (el) => (trackRefs.current[track.position - 1] = el) : null}
6590
>
91+
{isActive && isPlaying && <span className="track-shimmer" />}
6692
<div className="main-row">
6793
{numbering && <span className="leftside-numbering">#{index + 1}</span>}
6894
<span className="leftside-primary">

app/javascript/components/controls/Player.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import TrackInfo from "./TrackInfo";
1212
import ProgressBar from "./ProgressBar";
1313
import { useFeedback } from "../contexts/FeedbackContext";
1414

15-
const Player = ({ activePlaylist, activeTrack, setActiveTrack, customPlaylist, openAppModal, shouldAutoplay, setShouldAutoplay }) => {
15+
const Player = ({ activePlaylist, activeTrack, setActiveTrack, customPlaylist, openAppModal, shouldAutoplay, setShouldAutoplay, onPlayingChange }) => {
1616
const location = useLocation();
1717
const [isPlayerCollapsed, setIsPlayerCollapsed] = useState(false);
1818
const [hasPlayedInitially, setHasPlayedInitially] = useState(false);
@@ -49,6 +49,10 @@ const Player = ({ activePlaylist, activeTrack, setActiveTrack, customPlaylist, o
4949
handleScrubberClick,
5050
} = useGaplessPlayer(activePlaylist, activeTrack, setActiveTrack, setNotice, setAlert, hasPlayedInitially ? null : initialStartTime, shouldAutoplay, setShouldAutoplay);
5151

52+
useEffect(() => {
53+
if (onPlayingChange) onPlayingChange(isPlaying);
54+
}, [isPlaying, onPlayingChange]);
55+
5256
const togglePlayerPosition = () => {
5357
setIsPlayerCollapsed(!isPlayerCollapsed);
5458
};

app/javascript/components/layout/Layout.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const LayoutContent = ({ props, navigate }) => {
3131
const [viewMode, setViewMode] = useState("grid");
3232
const [sortOption, setSortOption] = useState("desc");
3333
const [shouldAutoplay, setShouldAutoplay] = useState(false);
34+
const [isPlaying, setIsPlaying] = useState(false);
3435
const { setNotice, setAlert } = useFeedback();
3536
const { isFilterLoading } = useAudioFilter();
3637

@@ -156,7 +157,8 @@ const LayoutContent = ({ props, navigate }) => {
156157
viewMode,
157158
setViewMode,
158159
sortOption,
159-
setSortOption
160+
setSortOption,
161+
isPlaying
160162
}}
161163
/>
162164
</main>
@@ -168,6 +170,7 @@ const LayoutContent = ({ props, navigate }) => {
168170
openAppModal={openAppModal}
169171
shouldAutoplay={shouldAutoplay}
170172
setShouldAutoplay={setShouldAutoplay}
173+
onPlayingChange={setIsPlaying}
171174
/>
172175
<AppModal
173176
isOpen={isAppModalOpen}

app/javascript/stylesheets/_content.scss

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,27 @@ main {
274274
background-repeat: no-repeat;
275275

276276
&.active-item {
277-
animation: pulse 3s infinite ease-in-out;
278-
background-image: none !important;
277+
overflow: hidden;
278+
}
279+
280+
.track-shimmer {
281+
position: absolute;
282+
top: 0;
283+
left: 0;
284+
width: 100%;
285+
height: 100%;
286+
background: linear-gradient(
287+
90deg,
288+
transparent 0%,
289+
transparent 20%,
290+
rgba(171, 217, 255, 0.45) 45%,
291+
rgba(171, 217, 255, 0.45) 55%,
292+
transparent 80%,
293+
transparent 100%
294+
);
295+
animation: track-shimmer 2.25s infinite linear;
296+
pointer-events: none;
297+
z-index: 1;
279298
}
280299

281300
.cover-art {
@@ -437,7 +456,7 @@ main {
437456
background-size: cover;
438457
background-repeat: no-repeat;
439458

440-
&:not(:last-child):not(.focus):not(.active-item)::after {
459+
&:not(:last-child):not(.focus)::after {
441460
content: "";
442461
display: block;
443462
position: absolute;
@@ -473,20 +492,12 @@ main {
473492
}
474493
}
475494

476-
&.focus {
477-
animation: pulse 3s infinite ease-in-out;
478-
background-image: none !important;
495+
&.active-item {
496+
overflow: hidden;
497+
}
479498

480-
&::after {
481-
content: "";
482-
display: block;
483-
position: absolute;
484-
bottom: 0;
485-
left: 1rem;
486-
right: 1rem;
487-
background-color: $highlight-blue;
488-
height: 2px;
489-
}
499+
&.focus {
500+
box-shadow: inset 0 -3px 0 #ABD9FF;
490501
}
491502
}
492503
}
@@ -632,16 +643,9 @@ main {
632643
}
633644
}
634645

635-
@keyframes pulse {
636-
0% {
637-
background-color: $highlight-blue;
638-
}
639-
50% {
640-
background-color: rgba(255, 255, 255, 0.8);
641-
}
642-
100% {
643-
background-color: $highlight-blue;
644-
}
646+
@keyframes track-shimmer {
647+
0% { transform: translateX(-100%); }
648+
100% { transform: translateX(100%); }
645649
}
646650

647651
@keyframes shimmer {

0 commit comments

Comments
 (0)