Skip to content

Commit cfd45a8

Browse files
committedFeb 28, 2025
feat(full-screen): add overview card with time display and controls
1 parent f9454d5 commit cfd45a8

File tree

10 files changed

+284
-12
lines changed

10 files changed

+284
-12
lines changed
 

‎Extensions/full-screen/.hintrc

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
"backdrop-filter"
1212
]
1313
}
14+
],
15+
"axe/name-role-value": [
16+
"default",
17+
{
18+
"button-name": "off"
19+
}
1420
]
1521
},
1622
"browserslist": [

‎Extensions/full-screen/package.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@
1515
"@types/react-dom": "18.3.1",
1616
"@typescript-eslint/eslint-plugin": "^8.10.0",
1717
"@typescript-eslint/parser": "^8.10.0",
18-
"eslint-plugin-react": "^7.37.1",
18+
"classnames": "^2.5.1",
1919
"eslint": "^9.13.0",
2020
"eslint-config-prettier": "^9.1.0",
21-
"typescript": "^5.6.3",
21+
"eslint-plugin-react": "^7.37.1",
2222
"lodash.debounce": "^4.0.8",
2323
"lodash.defaultsdeep": "^4.6.1",
2424
"marked": "^14.1.3",
2525
"semver": "^7.6.3",
2626
"spicetify-creator": "^1.0.17",
27-
"classnames": "^2.5.1"
27+
"typescript": "^5.6.3"
28+
},
29+
"dependencies": {
30+
"react-icons": "^5.5.0"
2831
}
2932
}

‎Extensions/full-screen/src/app.tsx

+11-9
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { initMoustrapRecord } from "./services/mousetrap-record";
3131

3232
import SeekableProgressBar from "./ui/components/ProgressBar/ProgressBar";
3333
import SeekableVolumeBar from "./ui/components/VolumeBar/VolumeBar";
34+
import OverviewCard from "./ui/components/OverviewPopup/OverviewCard";
3435

3536
import { settingsStyles } from "./styles/settings";
3637
import "./styles/base.scss";
@@ -298,15 +299,6 @@ async function main() {
298299

299300
function toggleLyrics() {
300301
container.classList.toggle("lyrics-hide-force");
301-
if (lyrics) {
302-
Utils.fadeAnimation(lyrics);
303-
lyrics.classList.toggle("button-active");
304-
lyrics.innerHTML =
305-
container.classList.contains("lyrics-unavailable") ||
306-
container.classList.contains("lyrics-hide-force")
307-
? ICONS.LYRICS_ACTIVE
308-
: ICONS.LYRICS_INACTIVE;
309-
}
310302
}
311303

312304
function toggleQueue() {
@@ -965,6 +957,15 @@ async function main() {
965957
container.querySelector("#fsd-progress-parent"),
966958
);
967959
}
960+
ReactDOM.render(
961+
<OverviewCard
962+
onExit={deactivate}
963+
onToggle={() => {
964+
CFM.getGlobal("tvMode") ? openwithDef() : openwithTV();
965+
}}
966+
/>,
967+
container.querySelector("#fsd-overview-card-parent"),
968+
);
968969
if (CFM.get("playerControls") !== "never") {
969970
updatePlayerControls({ data: { is_paused: !Spicetify.Player.isPlaying() } });
970971
Spicetify.Player.addEventListener("onplaypause", updatePlayerControls);
@@ -1017,6 +1018,7 @@ async function main() {
10171018
}
10181019
ReactDOM.unmountComponentAtNode(container.querySelector("#fsd-volume-parent")!);
10191020
ReactDOM.unmountComponentAtNode(container.querySelector("#fsd-progress-parent")!);
1021+
ReactDOM.unmountComponentAtNode(container.querySelector("#fsd-overview-card-parent")!);
10201022
if (CFM.get("icons")) {
10211023
Spicetify.Player.removeEventListener("onplaypause", updatePlayingIcon);
10221024
}

‎Extensions/full-screen/src/constants/defaults.ts

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const DEFAULTS: Config = {
3535
showRemainingTime: false,
3636
verticalMonitorSupport: false,
3737
sidebarQueue: true,
38+
overviewCardPinned: false,
3839
},
3940
def: {
4041
lyricsDisplay: true,
@@ -70,6 +71,7 @@ export const DEFAULTS: Config = {
7071
showRemainingTime: false,
7172
verticalMonitorSupport: true,
7273
sidebarQueue: true,
74+
overviewCardPinned: false,
7375
},
7476
tvMode: false,
7577
locale: "en-US",

‎Extensions/full-screen/src/services/html-creator.ts

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export const getHtmlContent = (areLyricsForceHidden: boolean) => {
3535
: ""
3636
}
3737
<div id="fsd-volume-parent"></div>
38+
<div id="fsd-overview-card-parent"></div>
39+
3840
${CFM.get("lyricsDisplay") ? `<div id="fad-lyrics-plus-container"></div>` : ""}
3941
<div id="fsd-foreground">
4042
<div id="fsd-art">

‎Extensions/full-screen/src/types/fullscreen.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export type Settings = {
4949
showRemainingTime: boolean;
5050
verticalMonitorSupport: boolean;
5151
sidebarQueue: boolean;
52+
overviewCardPinned: boolean;
5253
};
5354

5455
export type Cache = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import * as React from "react";
2+
import "./styles.scss";
3+
import Utils from "../../../utils/utils";
4+
import classNames from "classnames";
5+
import { BsPinAngle, BsPinAngleFill, BsUnlock, BsLockFill } from "react-icons/bs";
6+
import { MdAutorenew } from "react-icons/md";
7+
import CFM from "../../../utils/config";
8+
9+
interface OverviewCardProps {
10+
onExit: () => void;
11+
onToggle: () => void;
12+
}
13+
14+
const OverviewCard = ({ onExit, onToggle }: OverviewCardProps) => {
15+
const [visibility, setVisibility] = React.useState(true);
16+
const [pinned, setPinned] = React.useState(CFM.get("overviewCardPinned"));
17+
18+
const overviewCardTimer = React.useRef<NodeJS.Timeout | null>(null);
19+
20+
const hideCard = (timeout = 3000) => {
21+
if (overviewCardTimer.current) clearTimeout(overviewCardTimer.current);
22+
setVisibility(true);
23+
overviewCardTimer.current = setTimeout(() => {
24+
setVisibility(false);
25+
}, timeout);
26+
};
27+
28+
const handleMouseMove = (evt: MouseEvent) => {
29+
// Show card when mouse is on the top side of the screen and centered horizantally
30+
if (
31+
!pinned &&
32+
evt.clientY / window.innerHeight < 0.15 &&
33+
evt.clientX / window.innerWidth > 0.3 &&
34+
evt.clientX / window.innerWidth < 0.7
35+
) {
36+
hideCard();
37+
}
38+
};
39+
40+
React.useEffect(() => {
41+
if (overviewCardTimer.current) clearTimeout(overviewCardTimer.current);
42+
if (!pinned) {
43+
hideCard();
44+
}
45+
document.addEventListener("mousemove", handleMouseMove);
46+
47+
return () => {
48+
document.removeEventListener("mousemove", handleMouseMove);
49+
};
50+
}, [pinned]);
51+
52+
return (
53+
<div
54+
id="fsd-overview-card"
55+
className={classNames({
56+
"c-hidden": !visibility,
57+
"card-pinned": pinned,
58+
})}>
59+
<Spicetify.ReactComponent.TooltipWrapper
60+
label={pinned ? "Unpin Card" : "Pin Card"}
61+
placement="bottom">
62+
<button
63+
id="fsd-overview-pin-button"
64+
onClick={() => {
65+
if (!pinned) {
66+
if (overviewCardTimer.current) clearTimeout(overviewCardTimer.current);
67+
}
68+
setPinned(!pinned);
69+
CFM.set("overviewCardPinned", !pinned);
70+
}}>
71+
{pinned ? <BsPinAngleFill /> : <BsPinAngle />}
72+
</button>
73+
</Spicetify.ReactComponent.TooltipWrapper>
74+
<ClockSection />
75+
76+
<div id="fsd-overview-button-container">
77+
{/* <Spicetify.ReactComponent.TooltipWrapper label="Set Wake Lock" placement="bottom">
78+
<button
79+
id="overview-lock-button"
80+
className="fsd-overview-button"
81+
onClick={() => {}}>
82+
<BsUnlock />
83+
</button>
84+
</Spicetify.ReactComponent.TooltipWrapper> */}
85+
<Spicetify.ReactComponent.TooltipWrapper label="Toggle Mode" placement="bottom">
86+
<button
87+
id="overview-toggle-button"
88+
className="fsd-overview-button"
89+
onClick={onToggle}>
90+
<MdAutorenew />
91+
</button>
92+
</Spicetify.ReactComponent.TooltipWrapper>
93+
94+
<Spicetify.ReactComponent.TooltipWrapper label="Exit" placement="bottom">
95+
<button
96+
id="overview-exit-button"
97+
className="fsd-overview-button"
98+
dangerouslySetInnerHTML={{
99+
__html: `<svg width="1.5em" height="1.5em" viewBox="0 0 16 16" fill="currentColor">${
100+
Spicetify.SVGIcons.x
101+
}</svg>`,
102+
}}
103+
onClick={onExit}
104+
/>
105+
</Spicetify.ReactComponent.TooltipWrapper>
106+
</div>
107+
</div>
108+
);
109+
};
110+
111+
const ClockSection = () => {
112+
const [currentTime, setCurrentTime] = React.useState(Utils.getTimeFormatted());
113+
114+
function updateTimeDisplay() {
115+
setCurrentTime(Utils.getTimeFormatted());
116+
}
117+
118+
React.useEffect(() => {
119+
const updateInterval = setInterval(updateTimeDisplay, 1000);
120+
121+
return () => {
122+
clearInterval(updateInterval);
123+
};
124+
}, []);
125+
126+
return <div id="fsd-time-display">{currentTime}</div>;
127+
};
128+
export default OverviewCard;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#fsd-overview-card {
2+
display: flex;
3+
width: max-content;
4+
flex-direction: column;
5+
align-items: center;
6+
justify-content: center;
7+
background-color: rgba(0, 0, 0, 0.1);
8+
color: var(--secondary-color);
9+
position: fixed;
10+
top: 20px;
11+
left: 50%;
12+
text-align: center;
13+
z-index: 50;
14+
transition: all 0.8s ease-in-out;
15+
padding: 20px;
16+
border-radius: 20px;
17+
opacity: 1;
18+
transform: translateX(-50%) scale(1) translateY(0px);
19+
20+
#fsd-time-display {
21+
margin-bottom: 12px;
22+
font-size: 24px;
23+
font-weight: 700;
24+
line-height: initial;
25+
}
26+
27+
#fsd-overview-pin-button {
28+
position: absolute;
29+
top: -10px;
30+
right: -10px;
31+
border-radius: 50%;
32+
background: rgba(0, 0, 0, 0.1);
33+
border: 1px solid rgba(150, 150, 150, 0.1);
34+
width: 40px;
35+
height: 40px;
36+
padding: 4px;
37+
cursor: pointer;
38+
svg {
39+
width: 1.5em;
40+
height: 1.5em;
41+
}
42+
}
43+
44+
#fsd-overview-button-container {
45+
display: flex;
46+
flex-direction: row;
47+
column-gap: 12px;
48+
49+
.fsd-overview-button {
50+
border-radius: 50%;
51+
background: rgba(0, 0, 0, 0.1);
52+
border: 1px solid rgba(150, 150, 150, 0.1);
53+
width: 40px;
54+
height: 40px;
55+
padding: 8px;
56+
cursor: pointer;
57+
transition:
58+
all 0.3s var(--transition-function),
59+
transform 0.1s var(--transition-function);
60+
61+
&:hover {
62+
transform: scale(1.1);
63+
background: rgba(0, 0, 0, 0.5);
64+
}
65+
svg {
66+
width: 1.5em;
67+
height: 1.5em;
68+
}
69+
}
70+
}
71+
72+
&.c-hidden {
73+
transform: translateX(-50%) scale(0.5) translateY(-300px);
74+
}
75+
&:hover {
76+
transform: translateX(-50%) scale(1) translateY(0px);
77+
background-color: rgba(0, 0, 0, 0.2);
78+
79+
.fsd-overview-button {
80+
background: rgba(0, 0, 0, 0.3);
81+
}
82+
}
83+
&.card-pinned {
84+
background: transparent;
85+
#fsd-overview-button-container,
86+
#fsd-overview-pin-button {
87+
display: none;
88+
}
89+
90+
&:hover {
91+
background-color: rgba(0, 0, 0, 0.2);
92+
#fsd-overview-button-container {
93+
display: flex;
94+
}
95+
96+
#fsd-overview-pin-button {
97+
display: block;
98+
}
99+
}
100+
}
101+
}

‎Extensions/full-screen/src/utils/utils.ts

+5
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,11 @@ class Utils {
388388
document.body.classList.add("fsd-queue-panel-active");
389389
}
390390
}
391+
392+
static getTimeFormatted() {
393+
const now = new Date();
394+
return now.toLocaleString("en-US", { hour: "numeric", minute: "numeric", hour12: true });
395+
}
391396
}
392397

393398
export default Utils;

‎package-lock.json

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.