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

@ -15,195 +15,215 @@ import ExtraActions from "../ExtraActions"
import "./index.less" import "./index.less"
function isOverflown(parent, element) { function isOverflown(parent, element) {
if (!parent || !element) { if (!parent || !element) {
return false return false
} }
const parentRect = parent.getBoundingClientRect() const parentRect = parent.getBoundingClientRect()
const elementRect = element.getBoundingClientRect() const elementRect = element.getBoundingClientRect()
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
} }
switch (props.service) { switch (props.service) {
case "tidal": { case "tidal": {
return <div className="service_indicator"> return (
<Icons.SiTidal /> <div className="service_indicator">
</div> <Icons.SiTidal />
} </div>
default: { )
return null }
} default: {
} return null
}
}
} }
const Player = (props) => { const Player = (props) => {
const [playerState] = usePlayerStateContext() const [playerState] = usePlayerStateContext()
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") {
setTopActionsVisible(true) setTopActionsVisible(true)
} else { } else {
setTopActionsVisible(false) setTopActionsVisible(false)
} }
} }
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",
{
"hover": topActionsVisible,
"minimized": playerState.minimized,
"cover_light": cover_analysis?.isLight,
}
)}
style={{
"--cover_averageValues": RGBStringToValues(cover_analysis?.rgb),
"--cover_isLight": cover_analysis?.isLight,
}}
onMouseEnter={handleOnMouseInteraction}
onMouseLeave={handleOnMouseInteraction}
>
<div
className={classnames(
"toolbar_player_top_actions",
)}
>
{
!playerState.control_locked && <antd.Button
icon={<Icons.MdCast />}
shape="circle"
/> if (playerState.track_manifest && trackInstance) {
} if (
typeof trackInstance.manifest.analyzeCoverColor === "function"
) {
trackInstance.manifest
.analyzeCoverColor()
.then((analysis) => {
setCoverAnalysis(analysis)
})
.catch((err) => {
console.error("Failed to get cover analysis", err)
})
}
}
}, [playerState.track_manifest])
{ return (
lyrics_enabled && <antd.Button <div
icon={<Icons.MdLyrics />} className={classnames("toolbar_player_wrapper", {
shape="circle" hover: topActionsVisible,
onClick={() => app.location.push("/lyrics")} minimized: playerState.minimized,
/> cover_light: coverAnalysis?.isLight,
} })}
style={{
"--cover_averageValues": RGBStringToValues(
coverAnalysis?.rgb ?? "0,0,0",
),
"--cover_isLight": coverAnalysis?.isLight ?? false,
}}
onMouseEnter={handleOnMouseInteraction}
onMouseLeave={handleOnMouseInteraction}
>
<div className={classnames("toolbar_player_top_actions")}>
{!playerState.control_locked && (
<antd.Button icon={<Icons.MdCast />} shape="circle" />
)}
{/* <antd.Button <antd.Button
icon={<Icons.MdFullscreen />}
shape="circle"
onClick={() => app.location.push("/lyrics")}
/>
{/* <antd.Button
icon={<Icons.MdOfflineBolt />} icon={<Icons.MdOfflineBolt />}
> >
HyperDrive HyperDrive
</antd.Button> */} </antd.Button> */}
<antd.Button <antd.Button
icon={<Icons.FiX />} icon={<Icons.FiX />}
shape="circle" shape="circle"
onClick={() => app.cores.player.close()} onClick={() => app.cores.player.close()}
/> />
</div> </div>
<div <div className={classnames("toolbar_player")}>
className={classnames( <div
"toolbar_player" className="toolbar_cover_background"
)} style={{
> backgroundImage: `url(${cover})`,
<div }}
className="toolbar_cover_background" />
style={{
backgroundImage: `url(${cover})`
}}
/>
<div <div className="toolbar_player_content" ref={contentRef}>
className="toolbar_player_content" <div className="toolbar_player_info">
ref={contentRef} <h1
> ref={titleRef}
<div className="toolbar_player_info"> className={classnames("toolbar_player_info_title", {
<h1 ["overflown"]: titleOverflown,
ref={titleRef} })}
className={classnames( >
"toolbar_player_info_title", <ServiceIndicator service={service} />
{
["overflown"]: titleOverflown
}
)}
>
<ServiceIndicator
service={service}
/>
{titleText} {titleText}
</h1> </h1>
{ {titleOverflown && (
titleOverflown && <Marquee <Marquee
gradientColor={RGBStringToValues(cover_analysis?.rgb)} gradientColor={RGBStringToValues(
gradientWidth={20} coverAnalysis?.rgb ?? "0,0,0",
play={playerState.playback_status !== "stopped"} )}
> gradientWidth={20}
<h1 play={playerState.playback_status !== "stopped"}
className="toolbar_player_info_title" >
> <h1 className="toolbar_player_info_title">
<ServiceIndicator <ServiceIndicator service={service} />
service={service}
/>
{titleText} {titleText}
</h1> </h1>
</Marquee> </Marquee>
} )}
<p className="toolbar_player_info_subtitle"> <p className="toolbar_player_info_subtitle">
{artistStr ?? ""} {artistStr ?? ""}
</p> </p>
</div> </div>
<div className="toolbar_player_actions"> <div className="toolbar_player_actions">
<Controls /> <Controls />
<SeekBar <SeekBar
stopped={playerState.playback_status === "stopped"} stopped={playerState.playback_status === "stopped"}
playing={playerState.playback_status === "playing"} playing={playerState.playback_status === "playing"}
streamMode={playerState.livestream_mode} streamMode={playerState.livestream_mode}
disabled={playerState.control_locked} disabled={playerState.control_locked}
/> />
<ExtraActions /> <ExtraActions />
</div> </div>
</div>
</div> <Indicators track={playerState.track_manifest} />
</div> </div>
</div>
</div>
)
} }
export default Player export default Player

View File

@ -266,4 +266,40 @@
.ant-btn { .ant-btn {
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
}
}
} }