diff --git a/src/renderer/components/Header/Header.module.css b/src/renderer/components/Header/Header.module.css index c9379cec4..b0937fe59 100644 --- a/src/renderer/components/Header/Header.module.css +++ b/src/renderer/components/Header/Header.module.css @@ -12,7 +12,7 @@ } .header { - border-bottom: 1px solid var(--border-color); + border-bottom: solid 1px var(--border-color); background-color: var(--header-bg); color: var(--header-color); padding: 0 10px; @@ -57,3 +57,12 @@ min-width: 0; max-width: 600px; } + +.header__trackProgress { + -webkit-app-region: no-drag; + z-index: 1000; + position: absolute; + bottom: 0; + right: 0; + left: 0; +} diff --git a/src/renderer/components/PlayingBar/PlayingBar.module.css b/src/renderer/components/PlayingBar/PlayingBar.module.css index 2bbf77b45..bc21324cc 100644 --- a/src/renderer/components/PlayingBar/PlayingBar.module.css +++ b/src/renderer/components/PlayingBar/PlayingBar.module.css @@ -7,7 +7,7 @@ .playingBar__cover { flex-shrink: 0; - width: 49px; + width: 48px; overflow: hidden; } diff --git a/src/renderer/components/PlayingBarInfo/PlayingBarInfo.module.css b/src/renderer/components/PlayingBarInfo/PlayingBarInfo.module.css index 1a4679b0e..034f57a0a 100644 --- a/src/renderer/components/PlayingBarInfo/PlayingBarInfo.module.css +++ b/src/renderer/components/PlayingBarInfo/PlayingBarInfo.module.css @@ -4,7 +4,7 @@ display: flex; flex-direction: column; justify-content: flex-end; - padding: 0 6px; + padding: 0 4px; } .playingBar__info__metas { @@ -39,65 +39,3 @@ font-variant-numeric: tabular-nums; white-space: nowrap; } - -.playingBar__info__progress { - position: relative; -} - -.progressTooltip { - position: absolute; - background-color: var(--background); - border: 1px solid var(--border-color); - font-size: 10px; - padding: 2px 5px; - bottom: 10px; - z-index: 1; - transform: translateX(-11px); - - &::before, - &::after { - content: ''; - position: absolute; - width: 0; - height: 0; - border-style: solid; - border-color: transparent; - border-bottom: 0; - } - - /* Stroke */ - &::before { - top: 16px; - left: 5px; - border-top-color: var(--border-color); - border-width: 6px; - } - - /* Fill */ - &::after { - top: 16px; - left: 6px; - border-top-color: var(--background); - border-width: 5px; - } -} - -.progress { - -webkit-app-region: no-drag; - background-color: var(--progress-bg); - outline: none; - height: 5px; - margin-bottom: 0; - margin-top: 3px; - cursor: pointer; - - .progressBar { - background-color: var(--main-color); - pointer-events: none; - transition: none; - transform-origin: left; - will-change: transform; - width: 100%; - height: 100%; - } -} diff --git a/src/renderer/components/PlayingBarInfo/PlayingBarInfo.tsx b/src/renderer/components/PlayingBarInfo/PlayingBarInfo.tsx index 3d8a3bb3c..c8bbd3c25 100644 --- a/src/renderer/components/PlayingBarInfo/PlayingBarInfo.tsx +++ b/src/renderer/components/PlayingBarInfo/PlayingBarInfo.tsx @@ -1,9 +1,7 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; - import * as utils from '../../lib/utils'; import { TrackModel, Repeat } from '../../../shared/types/museeks'; -import { usePlayerAPI } from '../../stores/usePlayerStore'; -import player from '../../lib/player'; +import TrackProgress from '../TrackProgress/TrackProgress'; +import usePlayingTrackCurrentTime from '../../hooks/usePlayingTrackCurrentTime'; import styles from './PlayingBarInfo.module.css'; @@ -15,100 +13,7 @@ type Props = { export default function PlayingBarInfo(props: Props) { const { trackPlaying } = props; - - const playingBar = useRef(null); - - const [elapsed, setElapsed] = useState(0); - const [duration, setDuration] = useState(null); - const [x, setX] = useState(null); - const [dragging, setDragging] = useState(false); - - const playerAPI = usePlayerAPI(); - - const tick = useCallback(() => { - setElapsed(player.getCurrentTime()); - }, []); - - const jumpAudioTo = useCallback( - (e: React.MouseEvent) => { - setDragging(true); - - if (playingBar.current) { - const parent = playingBar.current.offsetParent as HTMLDivElement; - const percent = - ((e.pageX - (playingBar.current.offsetLeft + parent.offsetLeft)) / - playingBar.current.offsetWidth) * - 100; - - const to = (percent * trackPlaying.duration) / 100; - - playerAPI.jumpTo(to); - } - }, - [playingBar, trackPlaying, playerAPI], - ); - - const dragOver = useCallback( - (e: MouseEvent) => { - // Check if a currentTime update is needed - if (dragging) { - if (playingBar.current) { - const playingBarRect = playingBar.current.getBoundingClientRect(); - - const barWidth = playingBar.current.offsetWidth; - const offsetX = Math.min( - Math.max(0, e.pageX - playingBarRect.left), - barWidth, - ); - - const percent = (offsetX / barWidth) * 100; - - const to = (percent * trackPlaying.duration) / 100; - - playerAPI.jumpTo(to); - } - } - }, - [playingBar, dragging, trackPlaying, playerAPI], - ); - - const dragEnd = useCallback(() => { - setDragging(false); - }, []); - - const showTooltip = useCallback( - (e: React.MouseEvent) => { - const { offsetX } = e.nativeEvent; - const barWidth = e.currentTarget.offsetWidth; - - const percent = (offsetX / barWidth) * 100; - - const time = (percent * trackPlaying.duration) / 100; - - setDuration(time); - setX(percent); - }, - [trackPlaying], - ); - - const hideTooltip = useCallback(() => { - setDuration(null); - setX(null); - }, []); - - useEffect(() => { - player.getAudio().addEventListener('timeupdate', tick); - - window.addEventListener('mousemove', dragOver); - window.addEventListener('mouseup', dragEnd); - - return () => { - player.getAudio().removeEventListener('timeupdate', tick); - - window.removeEventListener('mousemove', dragOver); - window.removeEventListener('mouseup', dragEnd); - }; - }, [dragEnd, dragOver, tick]); + const elapsed = usePlayingTrackCurrentTime(); return (
@@ -129,36 +34,7 @@ export default function PlayingBarInfo(props: Props) { {utils.parseDuration(trackPlaying.duration)}
-
- -
-
-
-
+
); } diff --git a/src/renderer/components/TrackProgress/TrackProgress.module.css b/src/renderer/components/TrackProgress/TrackProgress.module.css new file mode 100644 index 000000000..fa63f8ca1 --- /dev/null +++ b/src/renderer/components/TrackProgress/TrackProgress.module.css @@ -0,0 +1,66 @@ +.trackRoot { + -webkit-app-region: no-drag; + position: relative; + display: flex; + align-items: center; + user-select: none; + touch-action: none; + height: 5px; + + /* the track progress is too close to the metadata, but using margin would + * push the whole section up */ + transform: translateY(3px); +} + +.trackProgress { + display: block; + width: 100%; + height: 100%; + background-color: var(--progress-bg); + box-shadow: inset 0 0 0 1px var(--border-color); +} + +.trackRange { + position: absolute; + height: 100%; + background-color: var(--main-color); + box-shadow: inset 0 0 0 1px rgba(0 0 0 / 0.2); +} + +.progressTooltip { + position: absolute; + background-color: var(--background); + border: 1px solid var(--border-color); + font-size: 10px; + padding: 2px 5px; + bottom: 10px; + z-index: 1; + transform: translateX(-11px); + + &::before, + &::after { + content: ''; + position: absolute; + width: 0; + height: 0; + border-style: solid; + border-color: transparent; + border-bottom: 0; + } + + /* Stroke */ + &::before { + top: 16px; + left: 5px; + border-top-color: var(--border-color); + border-width: 6px; + } + + /* Fill */ + &::after { + top: 15px; + left: 6px; + border-top-color: var(--background); + border-width: 5px; + } +} diff --git a/src/renderer/components/TrackProgress/TrackProgress.tsx b/src/renderer/components/TrackProgress/TrackProgress.tsx new file mode 100644 index 000000000..72f6354fa --- /dev/null +++ b/src/renderer/components/TrackProgress/TrackProgress.tsx @@ -0,0 +1,80 @@ +import { useCallback, useState } from 'react'; +import * as Slider from '@radix-ui/react-slider'; + +import { usePlayerAPI } from '../../stores/usePlayerStore'; +import { TrackModel } from '../../../shared/types/museeks'; +import usePlayingTrackCurrentTime from '../../hooks/usePlayingTrackCurrentTime'; +import { parseDuration } from '../../lib/utils'; + +import styles from './TrackProgress.module.css'; + +type Props = { + trackPlaying: TrackModel; +}; + +export default function TrackProgress(props: Props) { + const { trackPlaying } = props; + + const elapsed = usePlayingTrackCurrentTime(); + const playerAPI = usePlayerAPI(); + + const jumpAudioTo = useCallback( + (values: number[]) => { + const [to] = values; + + playerAPI.jumpTo(to); + }, + [playerAPI], + ); + + const [tooltipTargetTime, setTooltipTargetTime] = useState( + null, + ); + const [tooltipX, setTooltipX] = useState(null); + + const showTooltip = useCallback( + (e: React.MouseEvent) => { + const { offsetX } = e.nativeEvent; + const barWidth = e.currentTarget.offsetWidth; + + const percent = (offsetX / barWidth) * 100; + + const time = (percent * trackPlaying.duration) / 100; + + setTooltipTargetTime(time); + setTooltipX(percent); + }, + [trackPlaying], + ); + + const hideTooltip = useCallback(() => { + setTooltipTargetTime(null); + setTooltipX(null); + }, []); + + return ( + + + + {tooltipX !== null && ( +
+ {parseDuration(tooltipTargetTime)} +
+ )} +
+ +
+ ); +} diff --git a/src/renderer/components/VolumeControl/VolumeControl.module.css b/src/renderer/components/VolumeControl/VolumeControl.module.css index 83957c2fa..ef7e41098 100644 --- a/src/renderer/components/VolumeControl/VolumeControl.module.css +++ b/src/renderer/components/VolumeControl/VolumeControl.module.css @@ -3,7 +3,7 @@ } .volumeControl { - -webkit-app-region: nodrag; + -webkit-app-region: no-drag; background-color: var(--header-bg); position: absolute; z-index: 10; diff --git a/src/renderer/hooks/useTrackPlayingID.ts b/src/renderer/hooks/usePlayingTrack.ts similarity index 53% rename from src/renderer/hooks/useTrackPlayingID.ts rename to src/renderer/hooks/usePlayingTrack.ts index 6b55aa34d..6153b3c01 100644 --- a/src/renderer/hooks/useTrackPlayingID.ts +++ b/src/renderer/hooks/usePlayingTrack.ts @@ -1,9 +1,10 @@ +import { TrackModel } from '../../shared/types/museeks'; import usePlayerStore from '../stores/usePlayerStore'; -export default function useTrackPlayingID(): string | null { +export default function usePlayingTrack(): TrackModel | null { return usePlayerStore((state) => { if (state.queue.length > 0 && state.queueCursor !== null) { - return state.queue[state.queueCursor]._id; + return state.queue[state.queueCursor]; } return null; diff --git a/src/renderer/hooks/usePlayingTrackCurrentTime.ts b/src/renderer/hooks/usePlayingTrackCurrentTime.ts new file mode 100644 index 000000000..2fdd1ae3a --- /dev/null +++ b/src/renderer/hooks/usePlayingTrackCurrentTime.ts @@ -0,0 +1,24 @@ +import { useCallback, useEffect, useState } from 'react'; + +import player from '../lib/player'; + +/** + * Returns the current track elapsed time + */ +export default function usePlayingTrackCurrentTime(): number { + const [currentTime, setCurrentTime] = useState(player.getCurrentTime()); + + const tick = useCallback(() => { + setCurrentTime(player.getCurrentTime()); + }, [setCurrentTime]); + + useEffect(() => { + player.getAudio().addEventListener('timeupdate', tick); + + return () => { + player.getAudio().removeEventListener('timeupdate', tick); + }; + }, [tick]); + + return currentTime; +} diff --git a/src/renderer/hooks/usePlayingTrackID.ts b/src/renderer/hooks/usePlayingTrackID.ts new file mode 100644 index 000000000..dc7a925a5 --- /dev/null +++ b/src/renderer/hooks/usePlayingTrackID.ts @@ -0,0 +1,5 @@ +import usePlayingTrack from './usePlayingTrack'; + +export default function usePlayingTrackID(): string | null { + return usePlayingTrack()?._id ?? null; +} diff --git a/src/renderer/views/Library/Library.tsx b/src/renderer/views/Library/Library.tsx index 647e73336..6aac63e01 100644 --- a/src/renderer/views/Library/Library.tsx +++ b/src/renderer/views/Library/Library.tsx @@ -7,7 +7,7 @@ import appStyles from '../Root.module.css'; import { LoaderResponse } from '../router'; import useFilteredTracks from '../../hooks/useFilteredTracks'; import useLibraryStore from '../../stores/useLibraryStore'; -import useTrackPlayingID from '../../hooks/useTrackPlayingID'; +import usePlayingTrackID from '../../hooks/usePlayingTrackID'; import { PlaylistModel } from '../../../shared/types/museeks'; import { RootLoaderResponse } from '../Root'; @@ -16,7 +16,7 @@ import styles from './Library.module.css'; const { db } = window.MuseeksAPI; export default function Library() { - const trackPlayingId = useTrackPlayingID(); + const trackPlayingId = usePlayingTrackID(); const refreshing = useLibraryStore((state) => state.refreshing); const search = useLibraryStore((state) => state.search); diff --git a/src/renderer/views/Playlists/Playlist.tsx b/src/renderer/views/Playlists/Playlist.tsx index 28f71bfe0..4d9536193 100644 --- a/src/renderer/views/Playlists/Playlist.tsx +++ b/src/renderer/views/Playlists/Playlist.tsx @@ -12,7 +12,7 @@ import PlaylistsAPI from '../../stores/PlaylistsAPI'; import { filterTracks } from '../../lib/utils-library'; import { LoaderResponse } from '../router'; import useLibraryStore from '../../stores/useLibraryStore'; -import useTrackPlayingID from '../../hooks/useTrackPlayingID'; +import usePlayingTrackID from '../../hooks/usePlayingTrackID'; import { PlaylistModel, TrackModel } from '../../../shared/types/museeks'; const { db } = window.MuseeksAPI; @@ -21,7 +21,7 @@ export default function PlaylistView() { const { playlists, playlistTracks } = useLoaderData() as PlaylistLoaderResponse; const { playlistId } = useParams(); - const trackPlayingId = useTrackPlayingID(); + const trackPlayingId = usePlayingTrackID(); const search = useLibraryStore((state) => state.search); const filteredTracks = useMemo(