improve player data display

This commit is contained in:
SrGooglo 2025-02-05 02:39:35 +00:00
parent ac93563a5e
commit 13ab074840
5 changed files with 240 additions and 168 deletions

View File

@ -30,7 +30,13 @@ const EventsHandlers = {
return app.cores.player.controls.mute("toggle") return app.cores.player.controls.mute("toggle")
}, },
"like": async (ctx) => { "like": async (ctx) => {
await app.cores.player.toggleCurrentTrackLike(!ctx.track_manifest?.liked) if (!ctx.track_manifest) {
return false
}
const track = app.cores.player.track()
return await track.manifest.serviceOperations.toggleItemFavourite("track", ctx.track_manifest._id)
}, },
} }
@ -84,12 +90,6 @@ const Controls = (props) => {
onClick={() => handleAction("next")} onClick={() => handleAction("next")}
disabled={playerState.control_locked} disabled={playerState.control_locked}
/> />
{
app.isMobile && <LikeButton
onClick={() => handleAction("like")}
liked={playerState.track_manifest?.liked}
/>
}
{ {
!app.isMobile && <antd.Popover !app.isMobile && <antd.Popover
content={React.createElement( content={React.createElement(
@ -113,6 +113,13 @@ const Controls = (props) => {
</button> </button>
</antd.Popover> </antd.Popover>
} }
{
app.isMobile && <LikeButton
liked={playerState.track_manifest?.serviceOperations.fetchLikeStatus}
onClick={() => handleAction("like")}
/>
}
</div> </div>
} }

View File

@ -6,12 +6,18 @@ import LikeButton from "@components/LikeButton"
import { usePlayerStateContext } from "@contexts/WithPlayerContext" import { usePlayerStateContext } from "@contexts/WithPlayerContext"
import MusicModel from "@models/music"
const ExtraActions = (props) => { const ExtraActions = (props) => {
const [playerState] = usePlayerStateContext() const [playerState] = usePlayerStateContext()
const handleClickLike = async () => { const handleClickLike = async () => {
await MusicModel.toggleItemFavourite("track", playerState.track_manifest._id) if (!playerState.track_manifest) {
console.error("Cannot like a track if nothing is playing")
return false
}
const track = app.cores.player.track()
await track.manifest.serviceOperations.toggleItemFavourite("track", playerState.track_manifest._id)
} }
return <div className="extra_actions"> return <div className="extra_actions">
@ -24,8 +30,8 @@ const ExtraActions = (props) => {
} }
{ {
!app.isMobile && <LikeButton !app.isMobile && playerState.track_manifest?._id && <LikeButton
liked={playerState.track_manifest?.fetchLikeStatus} liked={playerState.track_manifest?.serviceOperations.fetchLikeStatus}
onClick={handleClickLike} onClick={handleClickLike}
/> />
} }

View File

@ -118,6 +118,9 @@ export default class SeekBar extends React.Component {
this.calculateTime() this.calculateTime()
this.updateAll() this.updateAll()
}, },
"player.durationchange": () => {
this.calculateDuration()
},
} }
tick = () => { tick = () => {

View File

@ -25,6 +25,32 @@ function isOverflown(parent, element) {
return elementRect.width > parentRect.width return elementRect.width > parentRect.width
} }
const Indicators = (props) => {
const { track } = props
if (!track) {
return null
}
const indicators = []
if (track.metadata) {
if (track.metadata.lossless) {
indicators.push(<Icons.Lossless />)
}
}
if (indicators.length === 0) {
return null
}
return (
<div className="toolbar_player_indicators_wrapper">
<div className="toolbar_player_indicators">{indicators}</div>
</div>
)
}
const ServiceIndicator = (props) => { const ServiceIndicator = (props) => {
if (!props.service) { if (!props.service) {
return null return null
@ -32,9 +58,11 @@ const ServiceIndicator = (props) => {
switch (props.service) { switch (props.service) {
case "tidal": { case "tidal": {
return <div className="service_indicator"> return (
<div className="service_indicator">
<Icons.SiTidal /> <Icons.SiTidal />
</div> </div>
)
} }
default: { default: {
return null return null
@ -47,11 +75,10 @@ const Player = (props) => {
const contentRef = React.useRef() const contentRef = React.useRef()
const titleRef = React.useRef() const titleRef = React.useRef()
const subtitleRef = React.useRef()
const [topActionsVisible, setTopActionsVisible] = React.useState(false) const [topActionsVisible, setTopActionsVisible] = React.useState(false)
const [titleOverflown, setTitleOverflown] = React.useState(false) const [titleOverflown, setTitleOverflown] = React.useState(false)
const [subtitleOverflown, setSubtitleOverflown] = React.useState(false) const [coverAnalysis, setCoverAnalysis] = React.useState(null)
const handleOnMouseInteraction = (e) => { const handleOnMouseInteraction = (e) => {
if (e.type === "mouseenter") { if (e.type === "mouseenter") {
@ -61,65 +88,69 @@ const Player = (props) => {
} }
} }
const { const { title, artistStr, service, cover_analysis, cover } =
title, playerState.track_manifest ?? {}
album,
artistStr,
liked,
service,
lyrics_enabled,
cover_analysis,
cover,
} = playerState.track_manifest ?? {}
const playing = playerState.playback_status === "playing" const playing = playerState.playback_status === "playing"
const stopped = playerState.playback_status === "stopped" const stopped = playerState.playback_status === "stopped"
const titleText = (!playing && stopped) ? "Stopped" : (title ?? "Untitled") const titleText = !playing && stopped ? "Stopped" : (title ?? "Untitled")
const subtitleText = "" const subtitleText = ""
React.useEffect(() => { React.useEffect(() => {
const titleIsOverflown = isOverflown(contentRef.current, titleRef.current) const titleIsOverflown = isOverflown(
contentRef.current,
titleRef.current,
)
setTitleOverflown(titleIsOverflown) setTitleOverflown(titleIsOverflown)
}, [title]) }, [title])
return <div React.useEffect(() => {
className={classnames( const trackInstance = app.cores.player.track()
"toolbar_player_wrapper",
{ if (playerState.track_manifest && trackInstance) {
"hover": topActionsVisible, if (
"minimized": playerState.minimized, typeof trackInstance.manifest.analyzeCoverColor === "function"
"cover_light": cover_analysis?.isLight, ) {
trackInstance.manifest
.analyzeCoverColor()
.then((analysis) => {
setCoverAnalysis(analysis)
})
.catch((err) => {
console.error("Failed to get cover analysis", err)
})
} }
)} }
}, [playerState.track_manifest])
return (
<div
className={classnames("toolbar_player_wrapper", {
hover: topActionsVisible,
minimized: playerState.minimized,
cover_light: coverAnalysis?.isLight,
})}
style={{ style={{
"--cover_averageValues": RGBStringToValues(cover_analysis?.rgb), "--cover_averageValues": RGBStringToValues(
"--cover_isLight": cover_analysis?.isLight, coverAnalysis?.rgb ?? "0,0,0",
),
"--cover_isLight": coverAnalysis?.isLight ?? false,
}} }}
onMouseEnter={handleOnMouseInteraction} onMouseEnter={handleOnMouseInteraction}
onMouseLeave={handleOnMouseInteraction} onMouseLeave={handleOnMouseInteraction}
> >
<div <div className={classnames("toolbar_player_top_actions")}>
className={classnames( {!playerState.control_locked && (
"toolbar_player_top_actions", <antd.Button icon={<Icons.MdCast />} shape="circle" />
)} )}
>
{
!playerState.control_locked && <antd.Button
icon={<Icons.MdCast />}
shape="circle"
/> <antd.Button
} icon={<Icons.MdFullscreen />}
{
lyrics_enabled && <antd.Button
icon={<Icons.MdLyrics />}
shape="circle" shape="circle"
onClick={() => app.location.push("/lyrics")} onClick={() => app.location.push("/lyrics")}
/> />
}
{/* <antd.Button {/* <antd.Button
icon={<Icons.MdOfflineBolt />} icon={<Icons.MdOfflineBolt />}
@ -133,56 +164,42 @@ const Player = (props) => {
onClick={() => app.cores.player.close()} onClick={() => app.cores.player.close()}
/> />
</div> </div>
<div <div className={classnames("toolbar_player")}>
className={classnames(
"toolbar_player"
)}
>
<div <div
className="toolbar_cover_background" className="toolbar_cover_background"
style={{ style={{
backgroundImage: `url(${cover})` backgroundImage: `url(${cover})`,
}} }}
/> />
<div <div className="toolbar_player_content" ref={contentRef}>
className="toolbar_player_content"
ref={contentRef}
>
<div className="toolbar_player_info"> <div className="toolbar_player_info">
<h1 <h1
ref={titleRef} ref={titleRef}
className={classnames( className={classnames("toolbar_player_info_title", {
"toolbar_player_info_title", ["overflown"]: titleOverflown,
{ })}
["overflown"]: titleOverflown
}
)}
> >
<ServiceIndicator <ServiceIndicator service={service} />
service={service}
/>
{titleText} {titleText}
</h1> </h1>
{ {titleOverflown && (
titleOverflown && <Marquee <Marquee
gradientColor={RGBStringToValues(cover_analysis?.rgb)} gradientColor={RGBStringToValues(
coverAnalysis?.rgb ?? "0,0,0",
)}
gradientWidth={20} gradientWidth={20}
play={playerState.playback_status !== "stopped"} play={playerState.playback_status !== "stopped"}
> >
<h1 <h1 className="toolbar_player_info_title">
className="toolbar_player_info_title" <ServiceIndicator service={service} />
>
<ServiceIndicator
service={service}
/>
{titleText} {titleText}
</h1> </h1>
</Marquee> </Marquee>
} )}
<p className="toolbar_player_info_subtitle"> <p className="toolbar_player_info_subtitle">
{artistStr ?? ""} {artistStr ?? ""}
@ -201,9 +218,12 @@ const Player = (props) => {
<ExtraActions /> <ExtraActions />
</div> </div>
<Indicators track={playerState.track_manifest} />
</div> </div>
</div> </div>
</div> </div>
)
} }
export default Player export default Player

View File

@ -267,3 +267,39 @@
padding: 0; padding: 0;
} }
} }
.toolbar_player_indicators_wrapper {
position: absolute;
bottom: 0;
left: 0;
padding: 10px;
.toolbar_player_indicators {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: fit-content;
padding: 7px 10px;
border-radius: 12px;
background-color: rgba(var(--layoutBackgroundColor), 0.7);
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
font-size: 1rem;
svg {
margin: 0 !important;
color: white
}
}
}