From c85808386a6bc7afb45b3e2c08654b0268c6c6ca Mon Sep 17 00:00:00 2001
From: UK <41271523+NeloBlivion@users.noreply.github.com>
Date: Tue, 3 Jan 2023 20:16:56 +0000
Subject: [PATCH 01/34] Add search button to channel pages
---
src/pages/Channel.tsx | 50 ++++++++++++++++++++++++++++++-------------
1 file changed, 35 insertions(+), 15 deletions(-)
diff --git a/src/pages/Channel.tsx b/src/pages/Channel.tsx
index 6db5cdc3..5928acb4 100644
--- a/src/pages/Channel.tsx
+++ b/src/pages/Channel.tsx
@@ -10,7 +10,7 @@ import {
useColorModeValue,
} from "@chakra-ui/react";
import axios from "axios";
-import { FiList, FiShare2, FiTwitter, FiYoutube } from "react-icons/fi";
+import { FiList, FiShare2, FiTwitter, FiYoutube, FiSearch } from "react-icons/fi";
import { useQuery } from "react-query";
import { Link, Route, Routes, useParams } from "react-router-dom";
import { ChannelCard } from "../components/channel/ChannelCard";
@@ -204,6 +204,35 @@ function ChannelSocialButtons({
);
}
+interface PlaylistButton {
+ label: string,
+ link: string,
+ icon: any,
+}
+
+function getPlaylistButtons({
+ channel,
+ buttons,
+}: {
+ channel: any,
+ buttons: PlaylistButton[],
+}) {
+ return (buttons.map( button =>
+ }
+ as={Link}
+ to={button.link}
+ float="right"
+ textTransform="uppercase"
+ >
+ {button.label}
+
+ ));
+}
+
function ChannelContent({
discovery,
trending,
@@ -218,6 +247,10 @@ function ChannelContent({
channel: any;
}) {
const { t } = useTranslation();
+ const ButtonData = [
+ {label: "See All Songs", link: "/channel/" + channel.id + "/songs", icon: FiList},
+ {label: "Search Songs", link: `/searchV2?mode=fuzzy&ch=["${channel.name}"]`, icon: FiSearch}
+ ];
return (
<>
{trending && (
@@ -246,20 +279,7 @@ function ChannelContent({
showArtwork: true,
}}
limit={5}
- appendRight={
- }
- as={Link}
- to={"/channel/" + channel.id + "/songs"}
- float="right"
- textTransform="uppercase"
- >
- {t("See All Songs")}
-
- }
+ appendRight={getPlaylistButtons({ channel: channel, buttons: ButtonData})}
/>
>
From 69c04aa76b9c63ab0be547d1d62040ddcae2dfd4 Mon Sep 17 00:00:00 2001
From: David Chen <3145205+RiceCakess@users.noreply.github.com>
Date: Wed, 4 Jan 2023 23:02:04 -0800
Subject: [PATCH 02/34] Fix queue menu not showing
---
src/components/data/SongTable/UpcomingSongList.tsx | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/components/data/SongTable/UpcomingSongList.tsx b/src/components/data/SongTable/UpcomingSongList.tsx
index 5632a0e6..70ed24ae 100644
--- a/src/components/data/SongTable/UpcomingSongList.tsx
+++ b/src/components/data/SongTable/UpcomingSongList.tsx
@@ -9,7 +9,11 @@ import {
} from "@chakra-ui/react";
import React, { useCallback, useContext, useEffect, useMemo } from "react";
import { VariableSizeList } from "react-window";
-import { DEFAULT_MENU_ID, QUEUE_MENU_ID } from "../../song/SongContextMenu";
+import {
+ DEFAULT_MENU_ID,
+ QUEUE_MENU_ID,
+ SongContextMenu,
+} from "../../song/SongContextMenu";
import { RowProps, SongRow } from "./SongRow";
import { useTranslation } from "react-i18next";
import { SongTableCol, SongTableProps } from ".";
@@ -244,6 +248,7 @@ export const UpcomingSongList = ({
{renderList}
)}
+
);
};
From 2c5512f220f84b95109f10c330f96181b5e2a250 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Wed, 11 Jan 2023 11:28:31 -0800
Subject: [PATCH 03/34] New translations translation.json (Turkish)
---
public/locales/tr-TR/translation.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/public/locales/tr-TR/translation.json b/public/locales/tr-TR/translation.json
index c071aabf..a276a1bc 100644
--- a/public/locales/tr-TR/translation.json
+++ b/public/locales/tr-TR/translation.json
@@ -104,7 +104,7 @@
"Organization Ordering": "Organizasyon Sıralaması",
"Drag and Drop to reorder list of orgs in the org dropdown": "Org menüsündeki orgların sırasını değiştirmek için Sürükleyip Bırakın",
"Interface Language": "Arayüz Dili",
- "Channel Name": "Kanal Adı",
+ "Channel Name": "Kanal İsimleri",
"ErrorPart2LetUsKnow": "Yukarıdaki adımlar probleminizi çözmüyorsa lütfen Discord veya Twitter üzerinden bize bir ekran görüntüsü gönderip hata ile nasıl karşılaştığınızı anlatın.",
"Please log in to use this feature.": "Bu özelliği kullanabilmek için lütfen oturum açın.",
"Sign in to Musicdex": "Musicdex'e giriş yap",
From a24ffc2d16a0c1a9f1cda5adc2386f1b66e5b0da Mon Sep 17 00:00:00 2001
From: David Chen <3145205+RiceCakess@users.noreply.github.com>
Date: Wed, 11 Jan 2023 19:07:15 -0800
Subject: [PATCH 04/34] Update service worker
---
src/service-worker.ts | 23 ++++++++++++-----------
1 file changed, 12 insertions(+), 11 deletions(-)
diff --git a/src/service-worker.ts b/src/service-worker.ts
index 98760f2a..9716fdfe 100644
--- a/src/service-worker.ts
+++ b/src/service-worker.ts
@@ -64,15 +64,17 @@ registerRoute(
// Return true to signal that we want to use the handler.
return true;
},
- createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html")
+ createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html"),
);
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
+// Caches music.holodex.net images
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) =>
- url.origin === self.location.origin &&
+ (url.origin === self.location.origin ||
+ url.origin === self.location.origin.replace(/^[^.]+\./g, "")) &&
(url.pathname.endsWith(".png") ||
url.pathname.endsWith(".jpg") ||
url.pathname.endsWith(".jpeg")),
@@ -83,31 +85,30 @@ registerRoute(
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({
- maxEntries: 400,
+ maxEntries: 100,
maxAgeSeconds: 12 * 60 * 60, // 12 hours of cache?
}),
],
- })
+ }),
);
+// Cache holodex.net images
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) =>
- (url.host === "i.ytimg.com" || url.host.endsWith("mzstatic.com")) &&
- (url.pathname.endsWith(".png") ||
- url.pathname.endsWith(".jpg") ||
- url.pathname.endsWith(".jpeg")),
+ url.pathname.match(new RegExp(`${self.location.origin}/statics/.*.json$`)),
// Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
- cacheName: "ytimgs",
+ cacheName: "holodex static json",
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({
- maxEntries: 160,
+ maxEntries: 50,
+ maxAgeSeconds: 24 * 60 * 60, // 12 hours of cache?
}),
],
- })
+ }),
);
// This allows the web app to trigger skipWaiting via
From 30ca63e49630ac753dd4c02b918afdd39e11f912 Mon Sep 17 00:00:00 2001
From: P-man2976
Date: Tue, 7 Feb 2023 22:15:47 +0900
Subject: [PATCH 05/34] Global styles move into chakra theme settings
---
public/index.html | 16 ----------------
src/components/layout/Frame.tsx | 6 ++----
src/theme.ts | 21 ++++++++++++++++++---
3 files changed, 20 insertions(+), 23 deletions(-)
diff --git a/public/index.html b/public/index.html
index d7a90111..a1759026 100644
--- a/public/index.html
+++ b/public/index.html
@@ -68,23 +68,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
-
-
diff --git a/src/components/layout/Frame.tsx b/src/components/layout/Frame.tsx
index 6cb5eb03..d7df376f 100644
--- a/src/components/layout/Frame.tsx
+++ b/src/components/layout/Frame.tsx
@@ -166,10 +166,8 @@ export default function Frame({ children }: { children?: ReactNode }) {
return (
Date: Mon, 13 Feb 2023 01:26:01 +0900
Subject: [PATCH 06/34] Refactor player
---
src/components/layout/Frame.tsx | 228 +++++----
src/components/player/FullPlayer.tsx | 194 ++++++++
src/components/player/Player.tsx | 343 ++------------
src/components/player/PlayerBar.tsx | 433 ++++--------------
.../player/PlayerKeyboardControls.tsx | 145 +++++-
src/components/player/YoutubePlayer.tsx | 45 +-
.../player/controls/PlaybackControl.tsx | 32 +-
.../player/controls/PlayerOption.tsx | 10 +-
.../player/controls/PlayerSongInfo.tsx | 8 +-
src/components/player/controls/TimeInfo.tsx | 17 +
src/components/player/controls/TimeSlider.tsx | 53 ++-
.../player/controls/VolumeSlider.tsx | 81 ++--
src/theme.ts | 6 +-
13 files changed, 749 insertions(+), 846 deletions(-)
create mode 100644 src/components/player/FullPlayer.tsx
create mode 100644 src/components/player/controls/TimeInfo.tsx
diff --git a/src/components/layout/Frame.tsx b/src/components/layout/Frame.tsx
index d7df376f..8f43aae9 100644
--- a/src/components/layout/Frame.tsx
+++ b/src/components/layout/Frame.tsx
@@ -4,7 +4,7 @@ import {
Drawer,
DrawerContent,
Flex,
- useBreakpointValue,
+ Show,
useColorModeValue,
useDisclosure,
} from "@chakra-ui/react";
@@ -15,9 +15,12 @@ import {
useMemo,
useRef,
useState,
+ Dispatch,
+ SetStateAction,
} from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useLocation, useNavigate } from "react-router-dom";
+import { useMotionValue, useTransform } from "framer-motion";
import { YouTubePlayer } from "youtube-player/dist/types";
import { useStoreState, useStoreActions } from "../../store";
import { SongContextMenu } from "../song/SongContextMenu";
@@ -32,6 +35,7 @@ import { YoutubePlayer } from "../player/YoutubePlayer";
import { AddToPlaylistModal } from "../playlist/AddToPlaylistModal";
import { useClient } from "../../modules/client";
import Footer from "./Footer";
+import FullPlayer from "../player/FullPlayer";
const POSITIONS: { [key: string]: ChakraProps } = {
background: {
@@ -43,37 +47,49 @@ const POSITIONS: { [key: string]: ChakraProps } = {
sidebar: {
position: "absolute",
zIndex: 1,
- bottom: "0px",
+ bottom: 0,
ml: -60,
w: 60,
+ sx: {
+ aspectRatio: "16 / 9",
+ },
},
"hover-top": {
width: "400px",
maxWidth: "100%",
- height: "225px",
position: "absolute",
top: "20px",
right: "20px",
paddingLeft: "30px",
zIndex: 10,
+ sx: {
+ aspectRatio: "16 / 9",
+ },
},
"hover-bottom": {
width: "400px",
maxWidth: "100%",
- height: "225px",
position: "absolute",
- bottom: "20px",
+ bottom: { base: "4.5rem", md: "20px" },
right: "20px",
paddingLeft: "30px",
zIndex: 10,
+ sx: {
+ aspectRatio: "16 / 9",
+ },
},
"full-player": {
position: "fixed",
- top: "calc(40vh/2 - min(40vh, calc(100vw * .5625))/2 + 56px + env(safe-area-inset-top))",
- left: { base: "0", lg: "6" },
- width: { base: "100vw", lg: "calc(50vw - 2.75rem)" },
- h: "min(40vh, calc(100vw * .5625))",
+ top: { base: "10vh", md: 12 },
+ right: { base: undefined, md: "calc(50vw + 1rem)" },
+ w: {
+ base: "100vw",
+ md: "min(calc(50vw - 2.5rem), calc(var(--chakra-sizes-container-xl) / 2 - 2.5rem))",
+ },
zIndex: 15,
+ sx: {
+ aspectRatio: "16 / 9",
+ },
},
hidden: {
display: "none",
@@ -84,16 +100,21 @@ const POSITIONS: { [key: string]: ChakraProps } = {
export const FrameRef = createContext(null);
+// YouTubePlayer context
+export const PlayerContext = createContext<
+ [YouTubePlayer | null, Dispatch>]
+>([null, () => {}]);
+
export default function Frame({ children }: { children?: ReactNode }) {
const { isOpen, onOpen, onClose } = useDisclosure();
const client = useClient();
+ // Full player drag value
+ const y = useMotionValue(0);
+ const playerY = useTransform(y, [-500, 0, 500], [-500, 0, 500]);
+ const opacity = useTransform(y, [0, 500], [1, 0.5]);
+
const colorMode = useColorModeValue("applight", "appdark");
- const showBottomNav = useBreakpointValue({
- base: true,
- md: false,
- default: false,
- });
const pos = useStoreState((state) => state.player.position);
const props = useMemo(() => POSITIONS[pos], [pos]);
@@ -109,8 +130,6 @@ export default function Frame({ children }: { children?: ReactNode }) {
}, [pathname]);
// END navigation scrolling behavior.
- const [player, setPlayer] = useState(null);
-
const currentlyPlaying = useStoreState(
(state) => state.playback.currentlyPlaying,
);
@@ -125,10 +144,6 @@ export default function Frame({ children }: { children?: ReactNode }) {
if (!loadPlayer && currentlyPlaying.song) setLoadPlayer(true);
}, [currentlyPlaying.song, currentlyPlaying.repeat, loadPlayer]);
- function onReady(event: { target: YouTubePlayer }) {
- setPlayer(event.target);
- }
-
const location = useLocation();
useEffect(() => {
if (isOpen) {
@@ -172,92 +187,105 @@ export default function Frame({ children }: { children?: ReactNode }) {
bg={useColorModeValue("bg.100", "bg.900")}
overflow="hidden"
>
-
- {/* Generic Display: always present */}
-
-
- {/* Mobile Display: (provide close method) */}
-
-
-
-
-
-
- (null)}>
+
+ {/* Generic Display: always present */}
+
-
+
+ {/* Mobile Display: (provide close method) */}
+
+
+
+
+
-
-
- {children}
-
- {/* */}
-
-
- {loadPlayer && (
-
-
-
-
- )}
+
+
+
+
+
+ {children}
+
+ {/* */}
+
+
+ {loadPlayer && (
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
-
- {showBottomNav && }
-
-
+
diff --git a/src/components/player/FullPlayer.tsx b/src/components/player/FullPlayer.tsx
new file mode 100644
index 00000000..1d06f9f4
--- /dev/null
+++ b/src/components/player/FullPlayer.tsx
@@ -0,0 +1,194 @@
+import {
+ AspectRatio,
+ Container,
+ VStack,
+ Heading,
+ Box,
+ Flex,
+ Button,
+ StackProps,
+ Slide,
+ useBreakpointValue,
+ IconButton,
+ Icon,
+} from "@chakra-ui/react";
+import { MotionValue, PanInfo } from "framer-motion";
+import React, { Suspense, useCallback } from "react";
+import { useTranslation } from "react-i18next";
+import { FiMinimize2, FiMinus } from "react-icons/fi";
+import { Link as NavLink } from "react-router-dom";
+import { useStoreActions, useStoreState } from "../../store";
+import { MotionBox } from "../common/MotionBox";
+import { UpcomingSongList } from "../data/SongTable/UpcomingSongList";
+import { PlaybackControl } from "./controls/PlaybackControl";
+import { PlayerOption } from "./controls/PlayerOption";
+import { SongInfo } from "./controls/PlayerSongInfo";
+import { TimeSlider } from "./controls/TimeSlider";
+
+interface FullPlayerProps {
+ y: MotionValue;
+ opacity: MotionValue;
+}
+
+const FullPlayer = React.memo(({ y, opacity }: FullPlayerProps) => {
+ const isMobile = useBreakpointValue({ base: true, md: false });
+
+ const currentSong = useStoreState(
+ (state) => state.playback.currentlyPlaying.song,
+ );
+
+ const fullPlayer = useStoreState((state) => state.player.fullPlayer);
+ const setFullPlayer = useStoreActions((store) => store.player.setFullPlayer);
+ const setPos = useStoreActions((store) => store.player.setOverridePosition);
+
+ const onClose = useCallback(() => {
+ setPos(undefined);
+ setFullPlayer(false);
+ }, [setFullPlayer, setPos]);
+
+ return (
+
+ {
+ if (info.offset.y > 180) {
+ onClose();
+ }
+ }}
+ style={{ y }}
+ >
+
+
+ {isMobile && (
+ }
+ size="sm"
+ w="100%"
+ variant="ghost"
+ onClick={() => {
+ setFullPlayer(false);
+ setPos(undefined);
+ }}
+ />
+ )}
+ {/* YouTube frame space */}
+
+
+
+
+
+ {currentSong && }
+
+
+
+
+
+ {isMobile && (
+
+
+
+ )}
+
+
+ {!isMobile && }
+
+
+
+ );
+});
+
+const PlayerBarExpandedRightSide = React.memo(({ ...rest }: StackProps) => {
+ const { t } = useTranslation();
+
+ const queue = useStoreState((state) => state.playback.queue);
+ const playlistQueue = useStoreState((state) => state.playback.playlistQueue);
+ const playlist = useStoreState((state) => state.playback.currentPlaylist);
+ const next = useStoreActions((actions) => actions.playback.next);
+ const setFullPlayer = useStoreActions((store) => store.player.setFullPlayer);
+ const setOverridePos = useStoreActions(
+ (store) => store.player.setOverridePosition,
+ );
+
+ return (
+
+ {t("Upcoming")}
+ {t("Loading...")}}>
+
+ {queue.length === 0 && playlistQueue.length === 0 ? (
+
+
+ {t("No Songs")}
+
+
+
+ ) : (
+
+ next({ count: idx + 1, userSkipped: true }),
+ }}
+ />
+ )}
+
+
+ }
+ onClick={() => {
+ setFullPlayer(false);
+ setOverridePos(undefined);
+ }}
+ size="lg"
+ variant="ghost"
+ alignSelf="end"
+ />
+
+ );
+});
+
+export default FullPlayer;
diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx
index 216e6e1e..6d073618 100644
--- a/src/components/player/Player.tsx
+++ b/src/components/player/Player.tsx
@@ -1,189 +1,93 @@
import { useToast } from "@chakra-ui/react";
-import { useHotkeys } from "react-hotkeys-hook";
-import { YouTubePlayer } from "youtube-player/dist/types";
-import { useStoreState, useStoreActions, store } from "../../store";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useStoreState, useStoreActions } from "../../store";
+import { useCallback, useEffect, useContext } from "react";
import PlayerStates from "youtube-player/dist/constants/PlayerStates";
-import { formatSeconds } from "../../utils/SongHelper";
import { PlayerBar } from "./PlayerBar";
-import { usePlayer, getID } from "./YoutubePlayer";
+import { usePlayer, getID, usePlayerStats } from "./YoutubePlayer";
import { useTrackSong } from "../../modules/services/songs.service";
+import { PlayerContext } from "../layout/Frame";
+import { useKeyboardEvents } from "./PlayerKeyboardControls";
const retryCounts: Record = {};
const trackedSongs: Set = new Set();
-export function Player({ player }: { player: YouTubePlayer | null }) {
+export function Player() {
+ const [player] = useContext(PlayerContext);
const toast = useToast();
- const position = useStoreState((store) => store.player.position);
- const setOverridePos = useStoreActions(
- (store) => store.player.setOverridePosition,
- );
- // Current song
+
const currentSong = useStoreState(
(state) => state.playback.currentlyPlaying.song,
);
const repeat = useStoreState(
(state) => state.playback.currentlyPlaying.repeat,
);
- const isPlaying = useStoreState((actions) => actions.playback.isPlaying);
const setIsPlaying = useStoreActions(
(actions) => actions.playback.setIsPlaying,
);
- const previous = useStoreActions((actions) => actions.playback.previous);
const next = useStoreActions((actions) => actions.playback.next);
- const toggleShuffleMode = useStoreActions(
- (actions) => actions.playback.toggleShuffle,
- );
- const toggleRepeatMode = useStoreActions(
- (actions) => actions.playback.toggleRepeat,
- );
-
- const setFullPlayer = useStoreActions(
- (actions) => actions.player.setFullPlayer,
- );
- const totalDuration = useMemo(
- () => (currentSong ? currentSong.end - currentSong.start : 0),
- [currentSong],
- );
+ const { progress } = usePlayerStats();
const { mutate: trackSong } = useTrackSong();
- const {
- currentVideo,
- state,
- currentTime,
- setError,
- hasError,
- volume,
- muted,
- } = usePlayer(player);
- const [progress, setProgress] = useState(0);
- const [volumeSlider, setVolumeSlider] = useState(0);
- // Stop song from playing on initial page load
- const [firstLoadPauseId, setFirstLoadPauseId] = useState("");
+ const { state, setError, hasError } = usePlayer();
- // Player volume change event
- useEffect(() => {
- setVolumeSlider(volume ?? 0);
- }, [volume]);
+ // Keyboard controls
+ useKeyboardEvents();
const loadVideoAtTime = useCallback(
async (video_id: string, time: number) => {
- if (!player) return;
+ if (!player || !currentSong) return;
if (getID(await player.getVideoUrl()) !== video_id) {
await player.loadVideoById({
videoId: video_id,
startSeconds: time,
});
+ } else {
+ await player.seekTo(currentSong.start, true);
}
+ // Comment the line below cuz `seekTo` resets progress
// NOTE: Bad YouTube cookies let the player ignores startSeconds so here is explicit `seekTo`
- player.seekTo(time, true);
+ // player.seekTo(time, true);
},
- [player],
+ [player, currentSong],
);
- // Jot down the song that the page loaded on, and keep this paused
+ // keep the song that the page loaded on paused
useEffect(() => {
- if (currentSong?.id) {
- setFirstLoadPauseId(`${currentSong?.id || ""}${repeat || ""}`);
- } else {
- setOverridePos("hidden");
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- // Player State Event
- useEffect(() => {
- // A seek to caused player to unpause, pause on loaded
- if (firstLoadPauseId) {
+ if (currentSong) {
setIsPlaying(false);
- player?.pauseVideo();
- return;
- }
- setIsPlaying(
- state === PlayerStates.BUFFERING || state === PlayerStates.PLAYING,
- );
- }, [firstLoadPauseId, player, setIsPlaying, state]);
-
- // Sanity video id check event
- useEffect(() => {
- if (
- player &&
- currentSong?.video_id &&
- currentVideo !== currentSong?.video_id
- ) {
- loadVideoAtTime(currentSong.video_id, currentSong.start).then(() => {
- setError(false);
- });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- player,
- currentVideo,
- currentSong?.video_id,
- currentSong?.start,
- setError,
- ]);
+ }, []);
// CurrentSong/repeat update event
useEffect(() => {
- if (!player) return;
-
- // Song changed, and is no longer the pause locked song, allow autoplay
- if (
- firstLoadPauseId &&
- firstLoadPauseId !== `${currentSong?.id || ""}${repeat || ""}`
- ) {
- setFirstLoadPauseId("");
- }
-
if (currentSong) {
- console.log("[Player] Playing Song:", currentSong.name);
- loadVideoAtTime(currentSong.video_id, currentSong.start).then(() => {
- player.playVideo();
- setProgress(0);
- setError(false);
- if (position === "hidden") setOverridePos(undefined);
- });
+ loadVideoAtTime(currentSong?.video_id, currentSong?.start);
+ state === PlayerStates.PLAYING && setIsPlaying(true);
} else {
- player?.pauseVideo();
- setProgress(0);
- setOverridePos("hidden");
+ player?.stopVideo();
+ setIsPlaying(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [player, currentSong, repeat, setError]);
+ }, [player, currentSong, repeat]);
- // CurrentTime Event
+ // Progress Event
useEffect(() => {
- if (
- currentTime === undefined ||
- currentSong === undefined ||
- currentSong.video_id !== currentVideo
- ) {
- return setProgress(0);
- }
- const newProgress =
- ((currentTime - currentSong.start) * 100) /
- (currentSong.end - currentSong.start);
-
- // Something caused it to skip far ahead (e.g. user scrubbed, song time changed on the same video)
- if (newProgress > 105) {
- loadVideoAtTime(currentSong.video_id, currentSong.start);
- return;
- }
-
// Prevent time from playing before start time
- if (newProgress < 0) {
+ if (currentSong && progress <= 0) {
player?.seekTo(currentSong.start, true);
- setProgress(0);
+ player?.playVideo();
return;
}
+ // Track song to history if the song has listened > 80%
if (
- newProgress > 80 &&
- newProgress < 105 &&
+ currentSong &&
+ progress > 80 &&
+ progress < 105 &&
!trackedSongs.has(currentSong.id)
) {
console.log("[Player] Track song play: ", currentSong.name);
@@ -191,37 +95,24 @@ export function Player({ player }: { player: YouTubePlayer | null }) {
trackSong({ song_id: currentSong.id });
}
- setProgress(newProgress);
- }, [
- currentSong,
- currentTime,
- currentVideo,
- loadVideoAtTime,
- player,
- trackSong,
- ]);
-
- // End Progress Event
- useEffect(() => {
- if (!player || !currentSong || currentTime === undefined) return;
- if (currentSong.video_id !== currentVideo) return;
+ // Something caused it to skip far ahead (e.g. user scrubbed, song time changed on the same video)
+ if (currentSong && progress > 105) {
+ loadVideoAtTime(currentSong.video_id, currentSong.start);
+ player?.playVideo();
+ return;
+ }
- // Progress will never reach 100 because player ended. Video length != song start/end
- // Example id: KiUvL-rp1zg
const earlyEnd = state === PlayerStates.ENDED && progress < 100;
- // Proceed to next song
if ((progress >= 100 && state === PlayerStates.PLAYING) || earlyEnd) {
console.log(
- `Auto advancing due to: ${
- progress >= 100 ? "prog>100" : "playerStatus=Ended"
+ `[Player] Auto advancing due to ${
+ progress >= 100 ? "Song progress >= 100" : "Player status === ENDED"
}`,
);
- setProgress(0);
next({ count: 1, userSkipped: false });
- return;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currentSong, player, progress, state]);
+ }, [progress, state]);
// Error handling: try to reload
useEffect(() => {
@@ -259,153 +150,5 @@ export function Player({ player }: { player: YouTubePlayer | null }) {
setError(false);
}, [hasError, currentSong, toast, next, setError, player]);
- const onProgressChange = useCallback(
- (e: number) => {
- if (!currentSong) return;
- setProgress(e);
- player?.seekTo(currentSong.start + (e / 100) * totalDuration, true);
- },
- [currentSong, player, totalDuration],
- );
-
- const onVolumeChange = useCallback(
- (e: number) => {
- player?.unMute();
- player?.setVolume(e);
- setVolumeSlider(e);
- },
- [player, setVolumeSlider],
- );
-
- const seconds = useMemo(() => {
- return formatSeconds((progress / 100) * totalDuration);
- }, [progress, totalDuration]);
-
- const togglePlay = useCallback(() => {
- if (!currentSong) return;
- // User action, unlock the first load pause
- if (firstLoadPauseId) setFirstLoadPauseId("");
- if (player) {
- isPlaying ? player.pauseVideo() : player.playVideo();
- setIsPlaying(!isPlaying);
- }
- }, [currentSong, firstLoadPauseId, isPlaying, player, setIsPlaying]);
-
- // Keyboard shortcuts
- // Follows Spotify keyboard shortcuts
-
- // Toggle play/pause
- useHotkeys(
- "space",
- (e) => {
- e.preventDefault();
- togglePlay();
- },
- [togglePlay, currentSong, firstLoadPauseId, isPlaying, player],
- );
-
- // Toggle repeat / shuffle mode
- useHotkeys("ctrl+r, cmd+r, ctrl+s, cmd+s", (e, handler) => {
- e.preventDefault();
-
- switch (handler.key) {
- case "ctrl+r":
- case "cmd+r":
- toggleRepeatMode();
- break;
-
- case "ctrl+s":
- case "cmd+s":
- toggleShuffleMode();
- break;
- }
- });
-
- // Volume control
- useHotkeys(
- "ctrl+up, cmd+up, ctrl+down, cmd+down, ctrl+shift+up, cmd+shift+up, ctrl+shift+down, cmd+shift+down",
- (e, handler) => {
- e.preventDefault();
- player?.getVolume().then((currentVol = 100) => {
- switch (handler.key) {
- case "ctrl+up":
- case "cmd+up":
- player?.unMute();
- player?.setVolume(currentVol !== 100 ? currentVol + 5 : 100);
- break;
- case "ctrl+down":
- case "cmd+down":
- player?.unMute();
- player?.setVolume(currentVol !== 0 ? currentVol - 5 : 0);
- break;
- case "ctrl+shift+up":
- case "cmd+shift+up":
- // Unmute / Max volume (when unmuted)
- muted ? player?.unMute() : player?.setVolume(100);
- break;
- case "ctrl+shift+down":
- case "cmd+shift+down":
- player?.mute();
- break;
- }
- });
- },
- [volumeSlider, muted],
- );
-
- // Forward / Backtrack tracks
- useHotkeys(
- "ctrl+left, cmd+left, ctrl+right, cmd+right",
- (e, handler) => {
- e.preventDefault();
-
- switch (handler.key) {
- case "ctrl+left":
- case "cmd+left":
- if (player && currentSong) {
- if (progress <= 2) {
- previous();
- } else {
- player.seekTo(currentSong.start, true);
- setProgress(0);
- }
- }
- break;
- case "ctrl+right":
- case "cmd+right":
- next({ count: 1, userSkipped: true });
- break;
- }
- },
- [player, currentSong, progress],
- );
-
- // Open / Close full-screen player
- useHotkeys("f, esc", (e, handler) => {
- e.preventDefault();
- switch (handler.key) {
- case "f":
- setFullPlayer(true);
- setOverridePos("full-player");
- break;
- case "esc":
- setFullPlayer(false);
- setOverridePos(undefined);
- break;
- }
- });
-
- return (
-
- );
+ return ;
}
diff --git a/src/components/player/PlayerBar.tsx b/src/components/player/PlayerBar.tsx
index 2d727759..ef01aff9 100644
--- a/src/components/player/PlayerBar.tsx
+++ b/src/components/player/PlayerBar.tsx
@@ -1,370 +1,127 @@
import {
Box,
Flex,
- Button,
- IconButton,
- Heading,
- Text,
- useBreakpoint,
useBreakpointValue,
VStack,
+ HStack,
} from "@chakra-ui/react";
-import styled from "@emotion/styled";
-import { AnimatePresence, LayoutGroup } from "framer-motion";
-import React, { Suspense, useEffect, useState } from "react";
-import { FaChevronDown } from "react-icons/fa";
-import { Link as navLink, useLocation } from "react-router-dom";
-import { useTranslation } from "react-i18next";
+import { LayoutGroup } from "framer-motion";
+import React, { useEffect, useCallback } from "react";
+import { useLocation } from "react-router-dom";
import { useStoreActions, useStoreState } from "../../store";
-import { formatSeconds } from "../../utils/SongHelper";
import { MotionBox } from "../common/MotionBox";
import { PlaybackControl } from "./controls/PlaybackControl";
import { PlayerOption } from "./controls/PlayerOption";
import { SongInfo } from "./controls/PlayerSongInfo";
import { TimeSlider } from "./controls/TimeSlider";
import { VolumeSlider } from "./controls/VolumeSlider";
-import { UpcomingSongList } from "../data/SongTable/UpcomingSongList";
+import FullPlayer from "./FullPlayer";
+import TimeInfo from "./controls/TimeInfo";
-interface PlayerBarProps {
- progress: number;
- onProgressChange: (e: number) => void;
- currentSong: Song | undefined;
- togglePlay: () => void;
- seconds: string;
- volume: number;
- muted: boolean;
- onVolumeChange: (e: number) => void;
- totalDuration: number;
-}
+export const PlayerBar = React.memo(() => {
+ const isMobile = useBreakpointValue({ base: true, md: false });
-const springTransition = {
- type: "spring",
- stiffness: 350,
- damping: 23,
-};
-
-export const PlayerBar = React.memo(
- ({
- progress,
- onProgressChange,
- currentSong,
- togglePlay,
- seconds,
- volume,
- muted,
- onVolumeChange,
- totalDuration,
- }: PlayerBarProps) => {
- const fullPlayer = useStoreState((state) => state.player.fullPlayer);
- const setFullPlayer = useStoreActions(
- (store) => store.player.setFullPlayer,
- );
- const setPos = useStoreActions((store) => store.player.setOverridePosition);
- const location = useLocation();
- const breakpoint = useBreakpoint();
+ const currentSong = useStoreState(
+ (state) => state.playback.currentlyPlaying.song,
+ );
- const [dragStartY, setDragStartY] = useState(0);
- const isMobile = useBreakpointValue({ base: true, lg: false });
- useEffect(() => {
- if (fullPlayer) {
- toggleFullPlayer();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [location]);
+ const fullPlayer = useStoreState((state) => state.player.fullPlayer);
+ const setFullPlayer = useStoreActions((store) => store.player.setFullPlayer);
+ const setPos = useStoreActions((store) => store.player.setOverridePosition);
+ const location = useLocation();
- function toggleFullPlayer() {
- setFullPlayer(!fullPlayer);
- if (fullPlayer) {
- setPos(undefined);
- } else {
- setPos("full-player");
- }
+ useEffect(() => {
+ if (fullPlayer) {
+ toggleFullPlayer();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [location]);
+
+ const toggleFullPlayer = useCallback(() => {
+ setFullPlayer(!fullPlayer);
+ if (fullPlayer) {
+ setPos(undefined);
+ } else {
+ setPos("full-player");
}
+ }, [fullPlayer, setFullPlayer, setPos]);
- function handlePlayerbarClick(e: any) {
+ const handlePlayerbarClick = useCallback(
+ (e: any) => {
if (
+ currentSong &&
+ isMobile &&
e?.target?.className &&
typeof e.target.className === "string" &&
- e?.target?.className.split(" ").length === 1 &&
- isMobile &&
- currentSong
- ) {
+ e?.target?.className.split(" ").length === 1
+ )
toggleFullPlayer();
- }
- }
+ },
+ [currentSong, isMobile, toggleFullPlayer],
+ );
- function handleSongInfoClick(e: any) {
- if (breakpoint === "base") {
+ const handleSongInfoClick = useCallback(
+ (e: any) => {
+ if (isMobile) {
e.preventDefault();
e.stopPropagation();
toggleFullPlayer();
}
- }
-
- return (
-
- {!fullPlayer && (
- <>
-
-
-
-
- {currentSong && (
-
- )}
-
-
-
-
-
-
-
-
-
- {seconds} /{" "}
- {formatSeconds(totalDuration)}
-
-
-
-
-
-
-
- >
- )}
-
- {fullPlayer && (
-
- {
- setDragStartY(info.point.y);
- }}
- onDragEnd={(event: any, info: any) => {
- if (info.point.y - dragStartY > 180) {
- toggleFullPlayer();
- }
- }}
- >
-
- toggleFullPlayer()}
- icon={}
- variant="ghost"
- size="lg"
- margin={2}
- position="absolute"
- left={0}
- top="env(safe-area-inset-top)"
- >
-
- {currentSong && (
-
- )}
-
-
-
-
-
-
-
-
- {isMobile && (
-
-
-
- )}
-
-
- {!isMobile && }
-
- )}
-
-
- );
- },
-);
-// const MemoizedPlayerBarLower = React.memo(PlayerBar);
-
-const PlayerContainer = styled.div<{
- expanded: boolean;
- backgroundUrl?: string;
- dense?: boolean;
-}>`
- height: ${({ expanded, dense }) =>
- expanded ? "100vh" : dense ? "64px" : "80px"};
- position: ${({ expanded }) => (expanded ? "absolute" : "relative")};
- /* padding-top: ${({ expanded }) =>
- expanded ? "calc(env(safe-area-inset-top))" : "0"};
- padding-bottom: env(safe-area-inset-top); */
- background: var(--chakra-colors-bg-800);
-
- width: 100%;
- flex-shrink: 0;
- bottom: 0;
- transition: all 0.3s ease-out;
- flex-direction: column;
- display: flex;
- z-index: 10;
-
- .main {
- display: flex;
- width: 100%;
- align-items: center;
- /* Need static margin, cuz Framer motion is miscalculating layout */
- margin-top: ${({ dense }) => (dense ? "3.5px" : "11.5px")};
- }
-`;
-
-const PlayerBarExpandedRightSide = React.memo(() => {
- const { t } = useTranslation();
-
- const queue = useStoreState((state) => state.playback.queue);
- const playlistQueue = useStoreState((state) => state.playback.playlistQueue);
- const playlist = useStoreState((state) => state.playback.currentPlaylist);
- const next = useStoreActions((actions) => actions.playback.next);
+ },
+ [isMobile, toggleFullPlayer],
+ );
return (
-
- {t("Upcoming")}
- {t("Loading...")}}>
-
- {queue.length === 0 && playlistQueue.length === 0 ? (
-
-
- {t("No Songs")}
-
-
-
- ) : (
-
- next({ count: idx + 1, userSkipped: true }),
- }}
- />
- )}
-
-
-
+
+
+
+
+
+ {currentSong && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
});
diff --git a/src/components/player/PlayerKeyboardControls.tsx b/src/components/player/PlayerKeyboardControls.tsx
index 6704755c..4a1975a7 100644
--- a/src/components/player/PlayerKeyboardControls.tsx
+++ b/src/components/player/PlayerKeyboardControls.tsx
@@ -1,18 +1,135 @@
-import { useCallback, useEffect } from "react";
-
-export function useKeyboardEvents(
- callback: (event: KeyboardEvent) => void,
- deps: React.DependencyList = []
-) {
- const onKeyPressed = useCallback(
- (event: KeyboardEvent) => {
- callback(event);
+import { useContext } from "react";
+import { useHotkeys } from "react-hotkeys-hook";
+import { useStoreActions, useStoreState } from "../../store";
+import { PlayerContext } from "../layout/Frame";
+import { usePlayer } from "./YoutubePlayer";
+
+// Keyboard shortcuts
+// Follows Spotify keyboard shortcuts
+export function useKeyboardEvents() {
+ const [player] = useContext(PlayerContext);
+ const { muted } = usePlayer();
+ const setFullPlayer = useStoreActions(
+ (action) => action.player.setFullPlayer,
+ );
+ const setOverridePos = useStoreActions(
+ (action) => action.player.setOverridePosition,
+ );
+ const isPlaying = useStoreState((state) => state.playback.isPlaying);
+ const setIsPlaying = useStoreActions(
+ (action) => action.playback.setIsPlaying,
+ );
+
+ const previous = useStoreActions((actions) => actions.playback.previous);
+ const next = useStoreActions((actions) => actions.playback.next);
+
+ const toggleShuffleMode = useStoreActions(
+ (actions) => actions.playback.toggleShuffle,
+ );
+ const toggleRepeatMode = useStoreActions(
+ (actions) => actions.playback.toggleRepeat,
+ );
+
+ // Open / Close full-screen player
+ useHotkeys(
+ "f, esc",
+ (e, handler) => {
+ e.preventDefault();
+ switch (handler.key) {
+ case "f":
+ setFullPlayer(true);
+ setOverridePos("full-player");
+ break;
+ case "esc":
+ setFullPlayer(false);
+ setOverridePos(undefined);
+ break;
+ }
+ },
+ [setFullPlayer, setOverridePos],
+ );
+
+ // Toggle play/pause
+ useHotkeys(
+ "space",
+ (e) => {
+ e.preventDefault();
+ setIsPlaying(!isPlaying);
},
- [callback]
+ [setIsPlaying, isPlaying],
);
- useEffect(() => {
- document.addEventListener("keydown", onKeyPressed);
- return () => document.removeEventListener("keydown", onKeyPressed);
- }, [deps, onKeyPressed]);
+ // Toggle repeat / shuffle mode
+ useHotkeys(
+ "ctrl+r, cmd+r, ctrl+s, cmd+s",
+ (e, handler) => {
+ e.preventDefault();
+
+ switch (handler.key) {
+ case "ctrl+r":
+ case "cmd+r":
+ toggleRepeatMode();
+ break;
+
+ case "ctrl+s":
+ case "cmd+s":
+ toggleShuffleMode();
+ break;
+ }
+ },
+ [toggleRepeatMode, toggleShuffleMode],
+ );
+
+ // Volume control
+ useHotkeys(
+ "ctrl+up, cmd+up, ctrl+down, cmd+down, ctrl+shift+up, cmd+shift+up, ctrl+shift+down, cmd+shift+down",
+ (e, handler) => {
+ e.preventDefault();
+ const currentVol = player?.getVolume() as unknown as number;
+ switch (handler.key) {
+ case "ctrl+up":
+ case "cmd+up":
+ player?.unMute();
+ player?.setVolume(currentVol !== 100 ? currentVol + 5 : 100);
+ break;
+ case "ctrl+down":
+ case "cmd+down":
+ player?.unMute();
+ player?.setVolume(currentVol !== 0 ? currentVol - 5 : 0);
+ break;
+ case "ctrl+shift+up":
+ case "cmd+shift+up":
+ // Unmute / Max volume (when unmuted)
+ muted ? player?.unMute() : player?.setVolume(100);
+ break;
+ case "ctrl+shift+down":
+ case "cmd+shift+down":
+ player?.mute();
+ break;
+ }
+ },
+ [player, muted],
+ );
+
+ // Forward / Backtrack tracks
+ useHotkeys(
+ "ctrl+left, cmd+left, ctrl+right, cmd+right",
+ (e, handler) => {
+ e.preventDefault();
+
+ switch (handler.key) {
+ case "ctrl+left":
+ case "cmd+left":
+ if (player) {
+ previous();
+ }
+ break;
+ case "ctrl+right":
+ case "cmd+right":
+ next({ count: 1, userSkipped: true });
+ break;
+ }
+ },
+ [player],
+ );
}
diff --git a/src/components/player/YoutubePlayer.tsx b/src/components/player/YoutubePlayer.tsx
index 4f8075d3..c400b508 100644
--- a/src/components/player/YoutubePlayer.tsx
+++ b/src/components/player/YoutubePlayer.tsx
@@ -1,12 +1,11 @@
-import { useCallback, useEffect, useState } from "react";
-import YouTube, { YouTubeProps } from "react-youtube";
-import { YouTubePlayer } from "youtube-player/dist/types";
+import { useCallback, useEffect, useState, useContext, useMemo } from "react";
+import YouTube from "react-youtube";
+import { useStoreState } from "../../store";
+import { PlayerContext } from "../layout/Frame";
+
+export function YoutubePlayer() {
+ const [_, setPlayer] = useContext(PlayerContext);
-export function YoutubePlayer({
- onReady,
-}: {
- onReady: YouTubeProps["onReady"];
-}) {
return (
setPlayer(e.target)}
loading="lazy"
/>
);
@@ -33,7 +32,8 @@ export function getID(url: string | undefined) {
return url?.match(VideoIDRegex)?.[2] || "";
}
-export function usePlayer(player: YouTubePlayer | null) {
+export function usePlayer() {
+ const [player] = useContext(PlayerContext);
// const [status, setStatus] =
// useState>(INITIALSTATE);
const [currentTime, setCurrentTime] = useState(0);
@@ -41,7 +41,7 @@ export function usePlayer(player: YouTubePlayer | null) {
const [currentVideo, setCurrentVideo] = useState("");
const [volume, setVolume] = useState(0);
const [muted, setMuted] = useState(false);
- const [state, setState] = useState(0);
+ const [state, setState] = useState(-1);
const [hasError, setError] = useState(false);
const errorHandler = useCallback((e: any) => {
console.warn("PLAYER ERROR OCCURRED", e);
@@ -87,3 +87,26 @@ export function usePlayer(player: YouTubePlayer | null) {
hasError,
};
}
+
+export const usePlayerStats = () => {
+ const { currentTime } = usePlayer();
+ const currentSong = useStoreState(
+ (state) => state.playback.currentlyPlaying.song,
+ );
+
+ const totalDuration = useMemo(
+ () => (currentSong ? currentSong.end - currentSong.start : 0),
+ [currentSong],
+ );
+
+ const progress = Math.max(
+ (((currentTime || 0) - (currentSong?.start ?? 0)) * 100) /
+ (totalDuration || 1),
+ 0,
+ );
+
+ return {
+ totalDuration,
+ progress,
+ };
+};
diff --git a/src/components/player/controls/PlaybackControl.tsx b/src/components/player/controls/PlaybackControl.tsx
index 485e3eb1..a71c8463 100644
--- a/src/components/player/controls/PlaybackControl.tsx
+++ b/src/components/player/controls/PlaybackControl.tsx
@@ -1,22 +1,20 @@
import {
BoxProps,
Flex,
- IconButton,
FlexProps,
useBreakpointValue,
} from "@chakra-ui/react";
-import React, { useMemo } from "react";
+import React, { useMemo, useContext } from "react";
import { ReactElement } from "react";
import { useStoreActions, useStoreState } from "../../../store";
import { FaStepBackward, FaPause, FaPlay, FaStepForward } from "react-icons/fa";
import { MotionBox } from "../../common/MotionBox";
import { ChangePlayerLocationButton } from "../ChangePlayerLocationButton";
import { ShuffleIcon, RepeatIcon } from "./PlayerOption";
+import { PlayerContext } from "../../layout/Frame";
interface PlaybackControlProps extends FlexProps {
- togglePlay: () => void;
fullPlayer?: boolean;
- mobilePlayer?: boolean;
}
interface PlaybackButtonProps extends BoxProps {
@@ -44,13 +42,12 @@ const PlaybackButton = ({ icon, ...rest }: PlaybackButtonProps) => {
};
export const PlaybackControl = React.memo(
- ({
- togglePlay,
- fullPlayer = false,
- mobilePlayer = false,
- ...rest
- }: PlaybackControlProps) => {
+ ({ fullPlayer, ...rest }: PlaybackControlProps) => {
+ const [player] = useContext(PlayerContext);
const isPlaying = useStoreState((state) => state.playback.isPlaying);
+ const setIsPlaying = useStoreActions(
+ (actions) => actions.playback.setIsPlaying,
+ );
const previous = useStoreActions((actions) => actions.playback.previous);
const next = useStoreActions((actions) => actions.playback.next);
@@ -71,10 +68,10 @@ export const PlaybackControl = React.memo(
- {mobilePlayer && }
+ {!fullPlayer && isMobile && }
{fullPlayer && !isMobile && (
toggleShuffleMode()}
/>
)}
- {!mobilePlayer && (
+ {(fullPlayer || !isMobile) && (
}
onClick={() => previous()}
- marginX={3}
+ mx={3}
/>
)}
)
}
- onClick={togglePlay}
+ onClick={() => {
+ setIsPlaying(!isPlaying);
+ isPlaying ? player?.pauseVideo() : player?.playVideo();
+ }}
padding={sizeMultiplier * 3}
/>
}
onClick={() => next({ count: 1, userSkipped: true })}
- marginX={3}
+ mx={3}
/>
{fullPlayer && !isMobile && (
{
+ ({ fullPlayer, ...rest }: PlayerOptionProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
@@ -54,16 +54,16 @@ export const PlayerOption = React.memo(
const shuffleMode = useStoreState((state) => state.playback.shuffleMode);
const toggleShuffleMode = useStoreActions(
- (actions) => actions.playback.toggleShuffle
+ (actions) => actions.playback.toggleShuffle,
);
const repeatMode = useStoreState((state) => state.playback.repeatMode);
const toggleRepeatMode = useStoreActions(
- (actions) => actions.playback.toggleRepeat
+ (actions) => actions.playback.toggleRepeat,
);
const setFullPlayer = useStoreActions(
- (store) => store.player.setFullPlayer
+ (store) => store.player.setFullPlayer,
);
const setPos = useStoreActions((store) => store.player.setOverridePosition);
const displayFullscreenButton = useBreakpointValue({
@@ -120,5 +120,5 @@ export const PlayerOption = React.memo(
)}
);
- }
+ },
);
diff --git a/src/components/player/controls/PlayerSongInfo.tsx b/src/components/player/controls/PlayerSongInfo.tsx
index 19d7e88a..734337f8 100644
--- a/src/components/player/controls/PlayerSongInfo.tsx
+++ b/src/components/player/controls/PlayerSongInfo.tsx
@@ -13,13 +13,15 @@ import useNamePicker from "../../../modules/common/useNamePicker";
import { DEFAULT_MENU_ID } from "../../song/SongContextMenu";
import { SongArtwork } from "../../song/SongArtwork";
import { SongLikeButton } from "../../song/SongLikeButton";
+import { useStoreState } from "../../../store";
interface SongInfoProps extends StackProps {
song: Song;
fullPlayer?: boolean;
}
+
export const SongInfo = React.memo(
- ({ song, fullPlayer = false, ...rest }: SongInfoProps) => {
+ ({ song, fullPlayer, ...rest }: SongInfoProps) => {
const { show } = useContextMenu({ id: DEFAULT_MENU_ID });
const tn = useNamePicker();
const isMobile = useBreakpointValue({ base: true, md: false });
@@ -28,7 +30,7 @@ export const SongInfo = React.memo(
if (isMobile) return;
else show(e, { props: song });
},
- [isMobile, show, song]
+ [isMobile, show, song],
);
return (
@@ -74,5 +76,5 @@ export const SongInfo = React.memo(
/>
);
- }
+ },
);
diff --git a/src/components/player/controls/TimeInfo.tsx b/src/components/player/controls/TimeInfo.tsx
new file mode 100644
index 00000000..fba5c27e
--- /dev/null
+++ b/src/components/player/controls/TimeInfo.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { Text } from "@chakra-ui/react";
+import { formatSeconds } from "../../../utils/SongHelper";
+import { usePlayerStats } from "../YoutubePlayer";
+
+const TimeInfo = React.memo(() => {
+ const { totalDuration, progress } = usePlayerStats();
+
+ return (
+
+ {formatSeconds((progress / 100) * totalDuration)} /{" "}
+ {formatSeconds(totalDuration)}
+
+ );
+});
+
+export default TimeInfo;
diff --git a/src/components/player/controls/TimeSlider.tsx b/src/components/player/controls/TimeSlider.tsx
index bb6330bc..bc6d88b5 100644
--- a/src/components/player/controls/TimeSlider.tsx
+++ b/src/components/player/controls/TimeSlider.tsx
@@ -7,45 +7,58 @@ import {
SliderProps,
SliderMark,
} from "@chakra-ui/react";
-import React from "react";
-import { useState } from "react";
+import React, { useEffect } from "react";
+import { useState, useContext } from "react";
+import { useStoreState } from "../../../store";
import { formatSeconds } from "../../../utils/SongHelper";
+import { PlayerContext } from "../../layout/Frame";
+import { usePlayerStats } from "../YoutubePlayer";
interface TimeSliderProps extends SliderProps {
- progress: number;
- onChange: (e: number) => void;
- totalDuration: number;
fullPlayer?: boolean;
}
export const TimeSlider = React.memo(
- ({
- progress,
- onChange,
- totalDuration,
- fullPlayer = false,
- ...rest
- }: TimeSliderProps) => {
+ ({ fullPlayer = false, ...rest }: TimeSliderProps) => {
+ const [player] = useContext(PlayerContext);
+ const { totalDuration, progress } = usePlayerStats();
+ const currentSong = useStoreState(
+ (state) => state.playback.currentlyPlaying.song,
+ );
+
+ const [progressSlider, setProgressSlider] = useState(0);
const [hovering, setHovering] = useState(false);
+
+ useEffect(() => {
+ setProgressSlider(progress);
+ }, [progress]);
+
return (
setHovering(true)}
onMouseLeave={() => setHovering(false)}
- onChange={onChange}
+ onChange={(value) => {
+ if (!currentSong) return;
+ setProgressSlider(value);
+ player?.seekTo(
+ currentSong.start + (value / 100) * totalDuration,
+ true,
+ );
+ }}
focusThumbOnChange={false}
{...rest}
>
{fullPlayer && (
<>
- {formatSeconds((progress / 100) * totalDuration)}
+ {formatSeconds((progressSlider / 100) * totalDuration)}
{formatSeconds(totalDuration)}
@@ -57,9 +70,7 @@ export const TimeSlider = React.memo(
height={hovering ? "10px" : "6px"}
transition="height var(--chakra-transition-duration-fast) ease-in"
>
-
+
{formatSeconds((progress / 100) * totalDuration)}}
+ label={
+ {formatSeconds((progressSlider / 100) * totalDuration)}
+ }
>
);
- }
+ },
);
diff --git a/src/components/player/controls/VolumeSlider.tsx b/src/components/player/controls/VolumeSlider.tsx
index 1b39544f..ddc433ec 100644
--- a/src/components/player/controls/VolumeSlider.tsx
+++ b/src/components/player/controls/VolumeSlider.tsx
@@ -5,42 +5,51 @@ import {
SliderThumb,
Box,
} from "@chakra-ui/react";
-import React from "react";
+import React, { useContext, useState, useEffect, useCallback } from "react";
import { FiVolumeX, FiVolume1, FiVolume2 } from "react-icons/fi";
+import { PlayerContext } from "../../layout/Frame";
+import { usePlayer } from "../YoutubePlayer";
-interface VolumeSliderProps {
- volume: number;
- muted: boolean;
- onChange: (e: number) => void;
-}
+export const VolumeSlider = React.memo(() => {
+ const [player] = useContext(PlayerContext);
+ const { volume, muted } = usePlayer();
+ const [volumeSlider, setVolumeSlider] = useState(volume);
-export const VolumeSlider = React.memo(
- ({ volume, muted, onChange }: VolumeSliderProps) => {
- return (
-
-
-
-
-
- 50
- ? FiVolume2
- : FiVolume1
- }
- />
-
-
- );
- }
-);
+ useEffect(() => {
+ setVolumeSlider(volume);
+ }, [volume]);
+
+ const onChange = useCallback(
+ (value: number) => {
+ setVolumeSlider(value);
+ player?.unMute();
+ player?.setVolume(value);
+ },
+ [player],
+ );
+
+ return (
+
+
+
+
+
+ 50
+ ? FiVolume2
+ : FiVolume1
+ }
+ />
+
+
+ );
+});
diff --git a/src/theme.ts b/src/theme.ts
index 5674aeee..df085be9 100644
--- a/src/theme.ts
+++ b/src/theme.ts
@@ -66,11 +66,11 @@ export const theme = extendTheme(
body: {
bg: "bg.900",
overflow: "hidden",
- "-webkit-font-smoothing": "antialiased",
- "-moz-osx-font-smoothing": "grayscale",
+ WebkitFontSmoothing: "antialiased",
+ MozOsxFontSmoothing: "grayscale",
},
"#root": {
- h: ["100vh", "100dvh"],
+ h: "100vh",
overflow: "hidden",
},
},
From 19457a6b47780d6f8d9c574bc6282205776fed2e Mon Sep 17 00:00:00 2001
From: P-man2976
Date: Mon, 13 Feb 2023 02:19:14 +0900
Subject: [PATCH 07/34] Pause video on load
---
src/components/player/Player.tsx | 12 +++++++++++-
src/components/player/YoutubePlayer.tsx | 6 ++----
src/components/player/controls/TimeInfo.tsx | 6 ++++--
src/components/player/controls/TimeSlider.tsx | 2 +-
4 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx
index 6d073618..af9741f0 100644
--- a/src/components/player/Player.tsx
+++ b/src/components/player/Player.tsx
@@ -74,10 +74,18 @@ export function Player() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [player, currentSong, repeat]);
+ // Player State Event
+ useEffect(() => {
+ setIsPlaying(
+ state === PlayerStates.BUFFERING || state === PlayerStates.PLAYING,
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [state]);
+
// Progress Event
useEffect(() => {
// Prevent time from playing before start time
- if (currentSong && progress <= 0) {
+ if (currentSong && progress < 0) {
player?.seekTo(currentSong.start, true);
player?.playVideo();
return;
@@ -102,6 +110,8 @@ export function Player() {
return;
}
+ // Progress will never reach 100 because player ended. Video length != song start/end
+ // Example id: KiUvL-rp1zg
const earlyEnd = state === PlayerStates.ENDED && progress < 100;
if ((progress >= 100 && state === PlayerStates.PLAYING) || earlyEnd) {
console.log(
diff --git a/src/components/player/YoutubePlayer.tsx b/src/components/player/YoutubePlayer.tsx
index c400b508..a311e015 100644
--- a/src/components/player/YoutubePlayer.tsx
+++ b/src/components/player/YoutubePlayer.tsx
@@ -99,11 +99,9 @@ export const usePlayerStats = () => {
[currentSong],
);
- const progress = Math.max(
+ const progress =
(((currentTime || 0) - (currentSong?.start ?? 0)) * 100) /
- (totalDuration || 1),
- 0,
- );
+ (totalDuration || 1);
return {
totalDuration,
diff --git a/src/components/player/controls/TimeInfo.tsx b/src/components/player/controls/TimeInfo.tsx
index fba5c27e..d50a8e4b 100644
--- a/src/components/player/controls/TimeInfo.tsx
+++ b/src/components/player/controls/TimeInfo.tsx
@@ -8,8 +8,10 @@ const TimeInfo = React.memo(() => {
return (
- {formatSeconds((progress / 100) * totalDuration)} /{" "}
- {formatSeconds(totalDuration)}
+
+ {formatSeconds((Math.max(progress, 0) / 100) * totalDuration)}
+ {" "}
+ / {formatSeconds(totalDuration)}
);
});
diff --git a/src/components/player/controls/TimeSlider.tsx b/src/components/player/controls/TimeSlider.tsx
index bc6d88b5..b118bf53 100644
--- a/src/components/player/controls/TimeSlider.tsx
+++ b/src/components/player/controls/TimeSlider.tsx
@@ -30,7 +30,7 @@ export const TimeSlider = React.memo(
const [hovering, setHovering] = useState(false);
useEffect(() => {
- setProgressSlider(progress);
+ setProgressSlider(Math.max(progress, 0));
}, [progress]);
return (
From 0b06bb2ffebf91930a7cfcf8bf7935931eb81bd2 Mon Sep 17 00:00:00 2001
From: P-man2976
Date: Mon, 13 Feb 2023 08:14:31 +0900
Subject: [PATCH 08/34] FullPlayer animation more smoothly
---
src/components/player/FullPlayer.tsx | 2 ++
src/components/player/PlayerKeyboardControls.tsx | 3 ++-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/components/player/FullPlayer.tsx b/src/components/player/FullPlayer.tsx
index 1d06f9f4..48fe9940 100644
--- a/src/components/player/FullPlayer.tsx
+++ b/src/components/player/FullPlayer.tsx
@@ -65,6 +65,8 @@ const FullPlayer = React.memo(({ y, opacity }: FullPlayerProps) => {
h="full"
drag="y"
dragConstraints={{ top: 0, bottom: 0 }}
+ dragElastic={{ top: 0, bottom: 1 }}
+ dragMomentum={false}
onDragEnd={(event: any, info: PanInfo) => {
if (info.offset.y > 180) {
onClose();
diff --git a/src/components/player/PlayerKeyboardControls.tsx b/src/components/player/PlayerKeyboardControls.tsx
index 4a1975a7..3c00e5ea 100644
--- a/src/components/player/PlayerKeyboardControls.tsx
+++ b/src/components/player/PlayerKeyboardControls.tsx
@@ -55,8 +55,9 @@ export function useKeyboardEvents() {
(e) => {
e.preventDefault();
setIsPlaying(!isPlaying);
+ isPlaying ? player?.pauseVideo() : player?.playVideo();
},
- [setIsPlaying, isPlaying],
+ [setIsPlaying, isPlaying, player],
);
// Toggle repeat / shuffle mode
From 8ff058dd2615807a658aa3fada5c64b3e7f4229b Mon Sep 17 00:00:00 2001
From: P-man2976
Date: Thu, 23 Feb 2023 00:11:27 +0900
Subject: [PATCH 09/34] Change layout, fix overflow issue on mobile
---
src/components/settings/LanguageSettings.tsx | 73 +++++++++++---------
src/components/settings/OrgManagement.tsx | 24 +++----
src/components/settings/SettingsSection.tsx | 19 +++--
src/components/settings/UserSettings.tsx | 4 +-
src/pages/Settings.tsx | 43 +++++++++---
5 files changed, 98 insertions(+), 65 deletions(-)
diff --git a/src/components/settings/LanguageSettings.tsx b/src/components/settings/LanguageSettings.tsx
index c6d694e9..78fc9cc1 100644
--- a/src/components/settings/LanguageSettings.tsx
+++ b/src/components/settings/LanguageSettings.tsx
@@ -1,3 +1,4 @@
+import { useMemo } from "react";
import { Stack, Select, RadioGroup, Radio, Box, Icon } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { FaHeart } from "react-icons/fa";
@@ -6,40 +7,46 @@ import { SettingsSection } from "./SettingsSection";
export function LanguageSettings() {
const { t, i18n } = useTranslation();
- const displayLangPrefs = [
- { value: "en", display: "English", credit: "@Holodex" },
- { value: "en-GB", display: "English (British)", credit: "@Holodex" },
- { value: "lol-PEKO", display: "English (PEKO)", credit: "@Holodex" },
- {
- value: "ja-JP",
- display: "日本語",
- credit: "Saginomiya#2353, ぴーまん#2976",
- },
- { value: "zh-TW", display: "繁體中文", credit: "angel84326#7887" },
- { value: "zh-CN", display: "简体中文", credit: "ttg#6038" },
- { value: "ko-KR", display: "한국어", credit: "AlexKoala#0253" },
- { value: "es-MX", display: "Español Latino", credit: "Aldo#3682" },
- { value: "ms-MY", display: "Bahasa Melayu", credit: "Admiy#8261" },
- { value: "id-ID", display: "Bahasa Indonesia", credit: "alcyneous#2803" },
- // { value: "ru", display: "Русский язык", credit: "kirillbarnaul#8499" },
- // { value: "pt", display: "Português Brasileiro", credit: "Ash Niartis#5090" },
- {
- value: "de-DE",
- display: "Deutsch",
- credit: "Doubleturtle#3660",
- },
- // { value: "it", display: "Italiano", credit: "テオさん#0139" },
- { value: "tr-TR", display: "Türkçe", credit: "creeperkafasipw#1861" },
- { value: "vi-VN", display: "Tiếng Việt", credit: "Frincess" },
- { value: "hu-HU", display: "Magyar", credit: "kuroihikikomori#7216" },
- // { value: "th", display: "ไทย", credit: "SnowNeko#0282" },
- // { value: "cimode", display: "Internal Translation Use" },
- ];
+ const displayLangPrefs = useMemo(
+ () => [
+ { value: "en", display: "English", credit: "@Holodex" },
+ { value: "en-GB", display: "English (British)", credit: "@Holodex" },
+ { value: "lol-PEKO", display: "English (PEKO)", credit: "@Holodex" },
+ {
+ value: "ja-JP",
+ display: "日本語",
+ credit: "Saginomiya#2353, ぴーまん#2976",
+ },
+ { value: "zh-TW", display: "繁體中文", credit: "angel84326#7887" },
+ { value: "zh-CN", display: "简体中文", credit: "ttg#6038" },
+ { value: "ko-KR", display: "한국어", credit: "AlexKoala#0253" },
+ { value: "es-MX", display: "Español Latino", credit: "Aldo#3682" },
+ { value: "ms-MY", display: "Bahasa Melayu", credit: "Admiy#8261" },
+ { value: "id-ID", display: "Bahasa Indonesia", credit: "alcyneous#2803" },
+ // { value: "ru", display: "Русский язык", credit: "kirillbarnaul#8499" },
+ // { value: "pt", display: "Português Brasileiro", credit: "Ash Niartis#5090" },
+ {
+ value: "de-DE",
+ display: "Deutsch",
+ credit: "Doubleturtle#3660",
+ },
+ // { value: "it", display: "Italiano", credit: "テオさん#0139" },
+ { value: "tr-TR", display: "Türkçe", credit: "creeperkafasipw#1861" },
+ { value: "vi-VN", display: "Tiếng Việt", credit: "Frincess" },
+ { value: "hu-HU", display: "Magyar", credit: "kuroihikikomori#7216" },
+ // { value: "th", display: "ไทย", credit: "SnowNeko#0282" },
+ // { value: "cimode", display: "Internal Translation Use" },
+ ],
+ [],
+ );
- const channelNamePrefs = [
- { value: "english_name", display: t("English") },
- { value: "name", display: t("Original Name on YouTube (Japanese, etc)") },
- ];
+ const channelNamePrefs = useMemo(
+ () => [
+ { value: "english_name", display: t("English") },
+ { value: "name", display: t("Original Name on YouTube (Japanese, etc)") },
+ ],
+ [t],
+ );
const useEN = useStoreState((s) => s.settings.useEN);
const changeUseEN = useStoreActions((s) => s.settings.setUseEN);
diff --git a/src/components/settings/OrgManagement.tsx b/src/components/settings/OrgManagement.tsx
index 147cab47..aee3fa15 100644
--- a/src/components/settings/OrgManagement.tsx
+++ b/src/components/settings/OrgManagement.tsx
@@ -8,6 +8,7 @@ import {
CloseButton,
Divider,
Flex,
+ Icon,
IconButton,
Input,
InputGroup,
@@ -25,14 +26,6 @@ import {
import { MdDragHandle } from "react-icons/md";
import { Org } from "../../store/org";
-export default function OrgManager() {
- return (
-
-
-
- );
-}
-
export function OrgPickerPanel({
pickOrg,
...rest
@@ -92,7 +85,7 @@ export function OrgPickerPanel({
>
{orglist
.filter((x) =>
- search ? x.toLowerCase().includes(search.toLowerCase()) : true
+ search ? x.toLowerCase().includes(search.toLowerCase()) : true,
)
.map((org) => (
{nonfavs
?.filter((x) =>
- search ? x.name.toLowerCase().includes(search.toLowerCase()) : true
+ search ? x.name.toLowerCase().includes(search.toLowerCase()) : true,
)
.map((org) => {
return (
@@ -193,13 +186,14 @@ function ReorderableOrgItem({
>
{org}
- controls.start(e)}
cursor="move"
- >
+ />
);
}
diff --git a/src/components/settings/SettingsSection.tsx b/src/components/settings/SettingsSection.tsx
index 49687312..94728601 100644
--- a/src/components/settings/SettingsSection.tsx
+++ b/src/components/settings/SettingsSection.tsx
@@ -1,4 +1,4 @@
-import { Flex, Heading, Divider } from "@chakra-ui/react";
+import { Wrap, Flex, Heading, Divider } from "@chakra-ui/react";
export function SettingsSection({
title,
@@ -9,10 +9,17 @@ export function SettingsSection({
}) {
return (
<>
-
-
- {title}
-
+
+
+ {title}
+
{children}
-
+
>
);
diff --git a/src/components/settings/UserSettings.tsx b/src/components/settings/UserSettings.tsx
index a2b68ea1..3cef278d 100644
--- a/src/components/settings/UserSettings.tsx
+++ b/src/components/settings/UserSettings.tsx
@@ -32,7 +32,7 @@ export function UserSettings() {
onSuccess: (data, payload, ...rest) => {
refreshUser();
},
- }
+ },
);
if (!isLoggedIn) {
return (
@@ -50,7 +50,7 @@ export function UserSettings() {
type="text"
ref={ref}
/>
-
+
+
+ )}
}
@@ -175,7 +222,7 @@ function ChannelSocialButtons({
aria-label="Holodex"
title={t("Open in Holodex")}
as="a"
- href={"https://holodex.net/channel/" + channel.id}
+ href={"https://holodex.net/channel/" + channel?.id}
target="_blank"
/>
- {channel.twitter && (
+ {channel?.twitter && (
)}
-
+
);
}
interface PlaylistButton {
- label: string,
- link: string,
- icon: any,
+ label: string;
+ link: string;
+ icon: any;
}
-function getPlaylistButtons({
- channel,
- buttons,
-}: {
- channel: any,
- buttons: PlaylistButton[],
-}) {
- return (buttons.map( button =>
- }
- as={Link}
- to={button.link}
- float="right"
- textTransform="uppercase"
- >
- {button.label}
-
- ));
+function PlaylistButtons({ buttons }: { buttons: PlaylistButton[] }) {
+ const { t } = useTranslation();
+ return (
+ <>
+ {buttons.map((button) => (
+ }
+ as={Link}
+ to={button.link}
+ float="right"
+ textTransform="uppercase"
+ >
+ {t(button.label)}
+
+ ))}
+ >
+ );
}
function ChannelContent({
@@ -244,13 +290,26 @@ function ChannelContent({
trending: Song[] | undefined;
queueSongs: (_: { songs: Song[]; immediatelyPlay: boolean }) => void;
name: any;
- channel: any;
+ channel?: Channel;
}) {
const { t } = useTranslation();
- const ButtonData = [
- {label: "See All Songs", link: "/channel/" + channel.id + "/songs", icon: FiList},
- {label: "Search Songs", link: `/searchV2?mode=fuzzy&ch=["${channel.name}"]`, icon: FiSearch}
- ];
+
+ const buttonData = useMemo(
+ () => [
+ {
+ label: "See All Songs",
+ link: "/channel/" + channel?.id + "/songs",
+ icon: FiList,
+ },
+ {
+ label: "Search Songs",
+ link: `/searchV2?mode=fuzzy&ch=["${channel?.name}"]`,
+ icon: FiSearch,
+ },
+ ],
+ [channel],
+ );
+
return (
<>
{trending && (
@@ -279,7 +338,7 @@ function ChannelContent({
showArtwork: true,
}}
limit={5}
- appendRight={getPlaylistButtons({ channel: channel, buttons: ButtonData})}
+ appendRight={}
/>
>
@@ -321,7 +380,7 @@ function ChannelContent({
>
)}
- {t("Discover more from {{org}}", { org: channel.org })}
+ {t("Discover more from {{org}}", { org: channel?.org })}
{discovery && (
diff --git a/src/pages/Channels.tsx b/src/pages/Channels.tsx
index c10d1c74..d6118274 100644
--- a/src/pages/Channels.tsx
+++ b/src/pages/Channels.tsx
@@ -19,9 +19,9 @@ import { useChannelListForOrg } from "../modules/services/channels.service";
import { useStoreState } from "../store";
import { useNavigate, useParams } from "react-router-dom";
import { Helmet } from "react-helmet-async";
+import { useFavorites } from "../modules/services/favorite.service";
-export default function Channels() {
- // let params = useParams();
+export default function Channels({ type }: { type: "org" | "favorites" }) {
const storeOrg = useStoreState((store) => store.org.currentOrg);
const { org: paramOrg } = useParams();
const org = paramOrg || storeOrg.name;
@@ -32,7 +32,11 @@ export default function Channels() {
hasNextPage,
status,
...rest
- } = useChannelListForOrg(org);
+ } = useChannelListForOrg(org, { enabled: type === "org" });
+
+ const { data: favoriteChannels, isLoading } = useFavorites({
+ enabled: type === "favorites",
+ });
useEffect(() => {
if (hasNextPage && !isFetchingNextPage) fetchNextPage();
@@ -43,9 +47,18 @@ export default function Channels() {
// const { data: song, ...rest } = useSong(songId);
+ const pageTitle = useMemo(
+ () =>
+ type === "org" ? t("{{org}} Channels", { org }) : t("Favorite Channels"),
+ [type, org, t],
+ );
+
const channels = useMemo(() => {
- return new Array().concat(...(channelPages?.pages || [[]]));
- }, [channelPages]);
+ // return new Array().concat(...(channelPages?.pages || [[]]));
+ return type === "org"
+ ? [...(channelPages?.pages.flat() || [])]
+ : [...(favoriteChannels ?? [])];
+ }, [type, channelPages, favoriteChannels]);
const channelsGrouped = useMemo(() => {
return groupBy(channels, (c) => (c as any).group || "");
@@ -62,7 +75,7 @@ export default function Channels() {
return (
- {t("{{org}} Channels", { org })} - Musicdex
+ {pageTitle} - Musicdex
@@ -72,8 +85,9 @@ export default function Channels() {
variant="ghost"
size="sm"
onClick={() => navigate(-1)}
+ _hover={{ cursor: "pointer" }}
/>
- {t("{{org}} Channels", { org })}
+ {pageTitle}
diff --git a/src/pages/Favorites.tsx b/src/pages/Favorites.tsx
new file mode 100644
index 00000000..a46dc620
--- /dev/null
+++ b/src/pages/Favorites.tsx
@@ -0,0 +1,144 @@
+import { useMemo } from "react";
+import { useBreakpointValue, Box, BoxProps } from "@chakra-ui/react";
+import { Helmet } from "react-helmet-async";
+import { useTranslation } from "react-i18next";
+import { CardCarousel } from "../components/common/CardCarousel";
+import { ContainerInlay } from "../components/layout/ContainerInlay";
+import { PageContainer } from "../components/layout/PageContainer";
+import { PlaylistCard } from "../components/playlist/PlaylistCard";
+import { VideoPlaylistCarousel } from "../components/playlist/VideoPlaylistCarousel";
+import { useDiscoveryFavorites } from "../modules/services/discovery.service";
+import { useLikedSongs } from "../modules/services/like.service";
+import { SongCard } from "../components/song/SongCard";
+import { HomeHeading } from "./Home";
+import { ChannelCard } from "../components/channel/ChannelCard";
+
+export default function Favorites() {
+ const { t } = useTranslation();
+ const isMobile = useBreakpointValue({ base: true, md: false });
+
+ const {
+ data: paginatedSongs,
+ isPreviousData,
+ ...status
+ } = useLikedSongs(1, { keepPreviousData: true });
+
+ const { data: discovery } = useDiscoveryFavorites();
+
+ const {
+ sgp: recPlaylists,
+ radios: recRadios,
+ ugp: communityPlaylists,
+ } = useMemo(() => {
+ const sgp: PlaylistStub[] = [];
+ const radios: PlaylistStub[] = [];
+ const ugp: PlaylistStub[] = [];
+ discovery?.recommended?.playlists.forEach((p: PlaylistStub) => {
+ if (p.type === "ugp") ugp.push(p);
+ else if (p.type.startsWith("radio")) radios.push(p);
+ else sgp.push(p);
+ });
+ return { sgp, radios, ugp };
+ }, [discovery]);
+
+ return (
+
+
+ {t("Favorites")}
+
+
+
+ {t("Recent Singing Streams")}
+
+ {isMobile ? (
+
+ {discovery?.recentSingingStreams
+ .filter((stream: any) => stream.playlist?.content?.length)
+ .map((stream: any) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+
+ {recRadios?.length ? (
+
+
+ {t("Favorites Radios")}
+
+
+ {recRadios?.map((p: Partial) => (
+
+ ))}
+
+
+ ) : null}
+
+ {paginatedSongs?.content.length && (
+
+ {t("Liked Songs")}
+
+ {[...paginatedSongs.content]
+ .sort(() => Math.random() - 0.5)
+ .map((song) => (
+
+ ))}
+
+
+ )}
+
+
+
+ {t("Discover Favorites")}
+
+
+ {discovery?.channels?.slice(0, 10).map((c: Channel) => (
+
+ ))}
+
+
+
+
+ );
+}
+
+function HomeSection({ children }: BoxProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index bca40f52..f3300ecd 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -25,9 +25,8 @@ import { Link, useParams } from "react-router-dom";
import { Helmet } from "react-helmet-async";
import { useEffect, useMemo } from "react";
import { useServerOrgList } from "../modules/services/statics.service";
-import { useQueryState } from "react-router-use-location-state";
-const HomeHeading = function ({
+export function HomeHeading({
children,
seeMoreTo,
seeMoreText,
@@ -63,7 +62,7 @@ const HomeHeading = function ({
)}
);
-};
+}
export default function Home() {
const { t } = useTranslation();
@@ -117,7 +116,7 @@ export default function Home() {
{isMobile ? (
{discovery?.recentSingingStreams
- .filter((stream: any) => stream.playlist?.content?.length)
+ ?.filter((stream: any) => stream.playlist?.content?.length)
.map((stream: any) => (
store.org.currentOrg);
const { org: paramOrg } = useParams();
@@ -32,26 +41,47 @@ export default function Channels({ type }: { type: "ugp" | "sgp" | "radio" }) {
hasNextPage,
status,
...rest
- } = useAllPlaylistDiscovery(org, type);
+ } = useAllPlaylistDiscoveryForOrg(org, playlistType, {
+ enabled: type === "org",
+ });
+
+ const {
+ data: favPlaylistPages,
+ fetchNextPage: fetchFavNextPage,
+ isFetchingNextPage: isFavFetchingNextPage,
+ hasNextPage: hasFavNextPage,
+ } = useAllPlaylistDiscoveryForFavorites(playlistType, {
+ enabled: type === "favorites",
+ });
const pageTitle = useMemo(() => {
- switch (type) {
- case "ugp":
- return t("{{org}} Community Playlists", { org });
- case "radio":
- return t("{{org}} Radios", { org });
- default:
- return t("{{org}} Playlists", { org });
+ if (type === "org") {
+ switch (playlistType) {
+ case "ugp":
+ return t("{{org}} Community Playlists", { org });
+ case "radio":
+ return t("{{org}} Radios", { org });
+ default:
+ return t("{{org}} Playlists", { org });
+ }
+ } else {
+ switch (playlistType) {
+ case "ugp":
+ return t("Favorites Community Playlists");
+ case "radio":
+ return t("Favorites Radios");
+ default:
+ return t("Favorites Playlists");
+ }
}
- }, [org, t, type]);
+ }, [t, org, type, playlistType]);
const playlists = useMemo(
() =>
- playlistPages?.pages.reduce(
- (prev, curr) => prev.concat(curr || []),
- []
- ),
- [playlistPages]
+ type === "org"
+ ? playlistPages?.pages.flat() ?? []
+ : favPlaylistPages?.pages.flat() ?? [],
+ [type, playlistPages, favPlaylistPages],
);
const navigate = useNavigate();
return (
@@ -89,9 +119,16 @@ export default function Channels({ type }: { type: "ugp" | "sgp" | "radio" }) {
))}
- {hasNextPage && !isFetchingNextPage && (
+ {((hasNextPage && !isFetchingNextPage) ||
+ (hasFavNextPage && !isFavFetchingNextPage)) && (
- fetchNextPage()}>
+ {
+ fetchNextPage();
+ fetchFavNextPage();
+ }}
+ >
Load More
diff --git a/src/routes.tsx b/src/routes.tsx
index f2803408..4b000e17 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -12,6 +12,7 @@ import { RequireLogin } from "./components/login/RequireLogin";
import Channels from "./pages/Channels";
import { useStoreState } from "./store";
import SeeMoreCardGrid from "./pages/SeeMoreCardGrid";
+import Favorites from "./pages/Favorites";
const Radio = React.lazy(() => import("./pages/Radio"));
const Channel = React.lazy(() => import("./pages/Channel"));
@@ -41,19 +42,19 @@ const routes: RouteObject[] = [
},
{
path: "/org/:org/channels",
- element: ,
+ element: ,
},
{
path: "/org/:org/playlists",
- element: ,
+ element: ,
},
{
path: "/org/:org/radios",
- element: ,
+ element: ,
},
{
path: "/org/:org/community",
- element: ,
+ element: ,
},
{
path: "/playlists/:playlistId",
@@ -83,6 +84,26 @@ const routes: RouteObject[] = [
),
},
+ {
+ path: "/favorites",
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: "/favorites/channels",
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: "/favorites/radios",
+ element: ,
+ },
{
path: "/search",
element: ,
diff --git a/src/types/discovery.d.ts b/src/types/discovery.d.ts
new file mode 100644
index 00000000..6420b79a
--- /dev/null
+++ b/src/types/discovery.d.ts
@@ -0,0 +1,35 @@
+interface OrgDiscovery extends Recommendation {
+ version: 1;
+ recentSingingStreams?: [
+ {
+ video: Video; // for generating any information around the video.
+ playlist?: PlaylistFull; // for generating the playlist. (playlist may not be available)
+ },
+ ];
+ channels: ChannelStub[];
+}
+
+interface ChannelDiscovery extends Recommendation {
+ version: 1;
+ recentSingingStreams?: [
+ {
+ video: Video; // for generating any information around the video.
+ playlist?: PlaylistFull; // for generating the playlist. (playlist may not be available)
+ },
+ ];
+ channels?: ChannelStub[]; //recommended channels?
+ // backgroundVideo?: string;
+}
+
+interface VideoDiscovery extends Recommendation {}
+
+interface Recommendation {
+ recommended: {
+ playlists: PlaylistFull[];
+ };
+}
+
+interface PlaylistList {
+ items: PlaylistStub[];
+ total: number;
+}
From c5107e67dec016c93a598541eec4e5c1d7e45252 Mon Sep 17 00:00:00 2001
From: P-man2976
Date: Sun, 19 Mar 2023 04:30:43 +0900
Subject: [PATCH 14/34] Update translation keys
---
public/locales/en/translation.json | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index f8f63968..d4b9f999 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -176,5 +176,11 @@
"Find similar": "Find similar",
"You are not logged in": "You're not logged in!",
"My Weekly Mix": "My Weekly Mix",
- "Crafted for you based on your listening habits": "Crafted for you based on your listening habits"
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From 676681a8fa0f09a7a8a3faf9492a0c7f7deda85c Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:03 -0700
Subject: [PATCH 15/34] New translations translation.json (French)
---
public/locales/fr-FR/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/fr-FR/translation.json b/public/locales/fr-FR/translation.json
index d714fe3d..f1887256 100644
--- a/public/locales/fr-FR/translation.json
+++ b/public/locales/fr-FR/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "Copied to clipboard",
"Popular": "Popular",
"Queue ({{amount}})": "Queue ({{amount}})",
- "See All Songs": "See All Songs",
"Latest Streams": "Latest Streams",
"Featuring {{name}}": "Featuring {{name}}",
"Discover more from {{org}}": "Discover more from {{org}}",
@@ -175,5 +174,13 @@
"Oldest": "Oldest",
"Open on YouTube": "Open on YouTube",
"Find similar": "Find similar",
- "You are not logged in": "You're not logged in!"
+ "You are not logged in": "You're not logged in!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From 249814c3df62f6efba20eb3344a23c455a658fd8 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:04 -0700
Subject: [PATCH 16/34] New translations translation.json (German)
---
public/locales/de-DE/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/de-DE/translation.json b/public/locales/de-DE/translation.json
index ed210240..c68aedf4 100644
--- a/public/locales/de-DE/translation.json
+++ b/public/locales/de-DE/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "In die Zwischenablage kopiert",
"Popular": "Beliebt",
"Queue ({{amount}})": "Warteschlange ({{amount}})",
- "See All Songs": "Alle Lieder anzeigen",
"Latest Streams": "Neueste Streams",
"Featuring {{name}}": "Featuring {{name}}",
"Discover more from {{org}}": "Entdecke mehr von {{org}}",
@@ -175,5 +174,13 @@
"Oldest": "Alter (steigend)",
"Open on YouTube": "Auf YouTube öffnen",
"Find similar": "Ähnliches finden",
- "You are not logged in": "Du bist nicht angemeldet!"
+ "You are not logged in": "Du bist nicht angemeldet!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From 8a32f3502b6696eb95038c4885f04b6a98948e0f Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:05 -0700
Subject: [PATCH 17/34] New translations translation.json (Hungarian)
---
public/locales/hu-HU/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/hu-HU/translation.json b/public/locales/hu-HU/translation.json
index e5e7938e..d106b833 100644
--- a/public/locales/hu-HU/translation.json
+++ b/public/locales/hu-HU/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "Vágólapra másolva",
"Popular": "Népszerű",
"Queue ({{amount}})": "Sorban ({{amount}})",
- "See All Songs": "Minden dal megtekintése",
"Latest Streams": "Legútóbbi streamek",
"Featuring {{name}}": "Tartalmazza {{name}}-t",
"Discover more from {{org}}": "Ismerj meg többet: {{org}}",
@@ -175,5 +174,13 @@
"Oldest": "Legrégibb",
"Open on YouTube": "Megnyitás YouTube-on",
"Find similar": "Hasonlóak",
- "You are not logged in": "Nem vagy bejelentkezve!"
+ "You are not logged in": "Nem vagy bejelentkezve!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From 8c22988443de750edf04b2204407481cb9cfbbab Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:06 -0700
Subject: [PATCH 18/34] New translations translation.json (Japanese)
---
public/locales/ja-JP/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/ja-JP/translation.json b/public/locales/ja-JP/translation.json
index 41e609be..8d3668de 100644
--- a/public/locales/ja-JP/translation.json
+++ b/public/locales/ja-JP/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "クリップボードにコピー",
"Popular": "人気の楽曲",
"Queue ({{amount}})": "キューに追加 ({{amount}})",
- "See All Songs": "すべての楽曲を見る",
"Latest Streams": "最新の配信",
"Featuring {{name}}": "ここでも聴ける {{name}}",
"Discover more from {{org}}": "{{org}} の楽曲をもっと見る",
@@ -175,5 +174,13 @@
"Oldest": "古い順",
"Open on YouTube": "YouTubeで開く",
"Find similar": "関連するものを検索",
- "You are not logged in": "ログインしていません"
+ "You are not logged in": "ログインしていません",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From d5c850b25c8000c8eafcda2c73f5b4781302e58c Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:07 -0700
Subject: [PATCH 19/34] New translations translation.json (Korean)
---
public/locales/ko-KR/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/ko-KR/translation.json b/public/locales/ko-KR/translation.json
index 7dc04ef5..eecbb10f 100644
--- a/public/locales/ko-KR/translation.json
+++ b/public/locales/ko-KR/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "클립보드에 복사됨",
"Popular": "인기",
"Queue ({{amount}})": "대기열 ({{amount}})",
- "See All Songs": "모든 노래 보기",
"Latest Streams": "마지막 실시간 방송",
"Featuring {{name}}": "{{name}} 팬이 좋아할 만한 다른 아티스트",
"Discover more from {{org}}": "{{org}}와 관련이 있는 아티스트",
@@ -175,5 +174,13 @@
"Oldest": "오래된순",
"Open on YouTube": "YouTube에서 열기",
"Find similar": "유사한 노래",
- "You are not logged in": "로그인되어 있지 않습니다!"
+ "You are not logged in": "로그인되어 있지 않습니다!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From c8b3208027bde3e03857bbd48e10f1c7d8e0ac69 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:08 -0700
Subject: [PATCH 20/34] New translations translation.json (Russian)
---
public/locales/ru-RU/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/ru-RU/translation.json b/public/locales/ru-RU/translation.json
index f7f19009..98829363 100644
--- a/public/locales/ru-RU/translation.json
+++ b/public/locales/ru-RU/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "Copied to clipboard",
"Popular": "Popular",
"Queue ({{amount}})": "Queue ({{amount}})",
- "See All Songs": "See All Songs",
"Latest Streams": "Latest Streams",
"Featuring {{name}}": "Featuring {{name}}",
"Discover more from {{org}}": "Discover more from {{org}}",
@@ -175,5 +174,13 @@
"Oldest": "Oldest",
"Open on YouTube": "Open on YouTube",
"Find similar": "Find similar",
- "You are not logged in": "You're not logged in!"
+ "You are not logged in": "You're not logged in!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From 3da836e6385baa71120f5595c9f500f03dbb578d Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:09 -0700
Subject: [PATCH 21/34] New translations translation.json (Turkish)
---
public/locales/tr-TR/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/tr-TR/translation.json b/public/locales/tr-TR/translation.json
index a276a1bc..1f410e49 100644
--- a/public/locales/tr-TR/translation.json
+++ b/public/locales/tr-TR/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "Panoya kopyalandı",
"Popular": "Popüler",
"Queue ({{amount}})": "Sıra ({{amount}})",
- "See All Songs": "Tüm Şarkıları Gör",
"Latest Streams": "En Son Yayınlar",
"Featuring {{name}}": "Featuring {{name}}",
"Discover more from {{org}}": "{{org}}'dan daha fazla Keşfet",
@@ -175,5 +174,13 @@
"Oldest": "En eski",
"Open on YouTube": "Youtube'da Aç",
"Find similar": "Benzerlerini bul",
- "You are not logged in": "Giriş yapmadınız!"
+ "You are not logged in": "Giriş yapmadınız!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From d25dd36a4fa07bf07767ebe27f6b1d81f9f46ed9 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:09 -0700
Subject: [PATCH 22/34] New translations translation.json (Chinese Simplified)
---
public/locales/zh-CN/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/zh-CN/translation.json b/public/locales/zh-CN/translation.json
index d35bc871..abf0b4e1 100644
--- a/public/locales/zh-CN/translation.json
+++ b/public/locales/zh-CN/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "已复制到剪贴板",
"Popular": "热门",
"Queue ({{amount}})": "加入队列 ({{amount}})",
- "See All Songs": "查看全部歌曲",
"Latest Streams": "最新的直播",
"Featuring {{name}}": "精选{{name}}",
"Discover more from {{org}}": "探索更多来自{{org}}的歌曲",
@@ -175,5 +174,13 @@
"Oldest": "最早",
"Open on YouTube": "在Youtube上打开",
"Find similar": "查找相似...",
- "You are not logged in": "你尚未登录!"
+ "You are not logged in": "你尚未登录!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From 4021e08adbbe281c369f9c412867abeed277a41c Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:10 -0700
Subject: [PATCH 23/34] New translations translation.json (Chinese Traditional)
---
public/locales/zh-TW/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/zh-TW/translation.json b/public/locales/zh-TW/translation.json
index 368c83c5..ebc62554 100644
--- a/public/locales/zh-TW/translation.json
+++ b/public/locales/zh-TW/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "已複製至剪貼簿",
"Popular": "熱門歌曲",
"Queue ({{amount}})": "加入待播清單 ({{amount}})",
- "See All Songs": "顯示所有歌曲",
"Latest Streams": "最新實況影片",
"Featuring {{name}}": "內含 {{name}} 的播放清單",
"Discover more from {{org}}": "探索更多 {{org}} 的相關歌曲",
@@ -175,5 +174,13 @@
"Oldest": "新增日期(最舊)",
"Open on YouTube": "於 YouTube 開啟",
"Find similar": "搜尋相關歌曲",
- "You are not logged in": "您尚未登入"
+ "You are not logged in": "您尚未登入",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From c56ba0000b867c69ace561b5e1b39ff77560a857 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:11 -0700
Subject: [PATCH 24/34] New translations translation.json (Vietnamese)
---
public/locales/vi-VN/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/vi-VN/translation.json b/public/locales/vi-VN/translation.json
index eb79c211..1b831975 100644
--- a/public/locales/vi-VN/translation.json
+++ b/public/locales/vi-VN/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "Đã sao chép vào bộ nhớ tạm",
"Popular": "Phổ biến",
"Queue ({{amount}})": "Hàng chờ ({{amount}})",
- "See All Songs": "Xem mọi bài hát",
"Latest Streams": "Các stream gần đây nhất",
"Featuring {{name}}": "Góp mặt bởi {{name}}",
"Discover more from {{org}}": "Khám phá thêm từ {{org}}",
@@ -175,5 +174,13 @@
"Oldest": "Oldest",
"Open on YouTube": "Open on YouTube",
"Find similar": "Find similar",
- "You are not logged in": "You're not logged in!"
+ "You are not logged in": "You're not logged in!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From 2e37160d6b31e94c71dee215115a53a0d4cddbe0 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:12 -0700
Subject: [PATCH 25/34] New translations translation.json (Indonesian)
---
public/locales/id-ID/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/id-ID/translation.json b/public/locales/id-ID/translation.json
index 4dd8bf0f..eecf0a8e 100644
--- a/public/locales/id-ID/translation.json
+++ b/public/locales/id-ID/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "Disalin ke papanklip",
"Popular": "Populer",
"Queue ({{amount}})": "Antrean ({{amount}})",
- "See All Songs": "Lihat Semua Lagu",
"Latest Streams": "Stream Terbaru",
"Featuring {{name}}": "Menampilkan {{name}}",
"Discover more from {{org}}": "Temukan lebih banyak dari {{org}}",
@@ -175,5 +174,13 @@
"Oldest": "Terlama",
"Open on YouTube": "Buka di YouTube",
"Find similar": "Cari yang serupa",
- "You are not logged in": "Kamu belum masuk!"
+ "You are not logged in": "Kamu belum masuk!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From d699bd3a5d6e01d973a149389e81d25a78dd1f4e Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:13 -0700
Subject: [PATCH 26/34] New translations translation.json (Spanish, Mexico)
---
public/locales/es-MX/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/es-MX/translation.json b/public/locales/es-MX/translation.json
index de1fc91f..9c535237 100644
--- a/public/locales/es-MX/translation.json
+++ b/public/locales/es-MX/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "Copied to clipboard",
"Popular": "Popular",
"Queue ({{amount}})": "Queue ({{amount}})",
- "See All Songs": "See All Songs",
"Latest Streams": "Latest Streams",
"Featuring {{name}}": "Featuring {{name}}",
"Discover more from {{org}}": "Discover more from {{org}}",
@@ -175,5 +174,13 @@
"Oldest": "Oldest",
"Open on YouTube": "Open on YouTube",
"Find similar": "Find similar",
- "You are not logged in": "You're not logged in!"
+ "You are not logged in": "You're not logged in!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From a846d539a3b8e6ca1574e83dc120dd9eb17609e0 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:14 -0700
Subject: [PATCH 27/34] New translations translation.json (Malay)
---
public/locales/ms-MY/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/ms-MY/translation.json b/public/locales/ms-MY/translation.json
index 1d763c32..766a33e4 100644
--- a/public/locales/ms-MY/translation.json
+++ b/public/locales/ms-MY/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "Disalin ke papan klip",
"Popular": "Lagu Terkenal",
"Queue ({{amount}})": "Baris gilir ({{amount}})",
- "See All Songs": "Lihat Semua Lagu",
"Latest Streams": "Siaran Terbaru",
"Featuring {{name}}": "Menampilkan {{name}}",
"Discover more from {{org}}": "Lihat lebih banyak lagu daripada {{org}}",
@@ -175,5 +174,13 @@
"Oldest": "Terlama",
"Open on YouTube": "Buka di YouTube",
"Find similar": "Gelintar yang serupa",
- "You are not logged in": "Belum log masuk lagi!"
+ "You are not logged in": "Belum log masuk lagi!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From fa0268e67e0ade99ac7f42428dd682df24012a41 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:15 -0700
Subject: [PATCH 28/34] New translations translation.json (Uwu)
---
public/locales/lol-UWU/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/lol-UWU/translation.json b/public/locales/lol-UWU/translation.json
index 727e499a..d4b9f999 100644
--- a/public/locales/lol-UWU/translation.json
+++ b/public/locales/lol-UWU/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "Copied to clipboard",
"Popular": "Popular",
"Queue ({{amount}})": "Queue ({{amount}})",
- "See All Songs": "See All Songs",
"Latest Streams": "Latest Streams",
"Featuring {{name}}": "Featuring {{name}}",
"Discover more from {{org}}": "Discover more from {{org}}",
@@ -175,5 +174,13 @@
"Oldest": "Oldest",
"Open on YouTube": "Open on YouTube",
"Find similar": "Find similar",
- "You are not logged in": "You're not logged in!"
+ "You are not logged in": "You're not logged in!",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From 06f5e1aa7077307e2380a5e40bf92cec33a70470 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 14:54:15 -0700
Subject: [PATCH 29/34] New translations translation.json (PEKOEnglish)
---
public/locales/lol-PEKO/translation.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/locales/lol-PEKO/translation.json b/public/locales/lol-PEKO/translation.json
index bd2f3e66..696b7ef5 100644
--- a/public/locales/lol-PEKO/translation.json
+++ b/public/locales/lol-PEKO/translation.json
@@ -74,7 +74,6 @@
"Copied to clipboard": "Copied to clipboard",
"Popular": "Popular",
"Queue ({{amount}})": "Queue ({{amount}})",
- "See All Songs": "See All Songs",
"Latest Streams": "Latest Streams",
"Featuring {{name}}": "With {{name}} peko!",
"Discover more from {{org}}": "Motto from {{org}}?",
@@ -175,5 +174,13 @@
"Oldest": "Oldest",
"Open on YouTube": "Open on YouTube",
"Find similar": "Find similar",
- "You are not logged in": "Anata wa not login peko."
+ "You are not logged in": "Anata wa not login peko.",
+ "My Weekly Mix": "My Weekly Mix",
+ "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
+ "Favorites": "Favorites",
+ "Favorite Channels": "Favorite Channels",
+ "Favorites Radios": "Favorites Radios",
+ "Discover Favorites": "Discover Favorites",
+ "Favorites Community Playlists": "Favorites Community Playlists",
+ "Favorites Playlists": "Favorites Playlists"
}
From 86ba3eae69e5bbe3b5f88dcb2822ddea054b4929 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 16:09:14 -0700
Subject: [PATCH 30/34] New translations translation.json (Japanese)
---
public/locales/ja-JP/translation.json | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/public/locales/ja-JP/translation.json b/public/locales/ja-JP/translation.json
index 8d3668de..094fe226 100644
--- a/public/locales/ja-JP/translation.json
+++ b/public/locales/ja-JP/translation.json
@@ -176,11 +176,11 @@
"Find similar": "関連するものを検索",
"You are not logged in": "ログインしていません",
"My Weekly Mix": "My Weekly Mix",
- "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
- "Favorites": "Favorites",
- "Favorite Channels": "Favorite Channels",
- "Favorites Radios": "Favorites Radios",
- "Discover Favorites": "Discover Favorites",
- "Favorites Community Playlists": "Favorites Community Playlists",
- "Favorites Playlists": "Favorites Playlists"
+ "Crafted for you based on your listening habits": "あなたが聞く曲の傾向を基に作成されました",
+ "Favorites": "お気に入り",
+ "Favorite Channels": "お気に入りチャンネル",
+ "Favorites Radios": "お気に入りのラジオ",
+ "Discover Favorites": "お気に入りを開拓する",
+ "Favorites Community Playlists": "お気に入りのコミュニティプレイリスト",
+ "Favorites Playlists": "お気に入りのプレイリスト"
}
From 5291909a4821da04fee0144097bca43da9841f65 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 16:09:15 -0700
Subject: [PATCH 31/34] New translations translation.json (Chinese Simplified)
---
public/locales/zh-CN/translation.json | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/public/locales/zh-CN/translation.json b/public/locales/zh-CN/translation.json
index abf0b4e1..caee5812 100644
--- a/public/locales/zh-CN/translation.json
+++ b/public/locales/zh-CN/translation.json
@@ -175,12 +175,12 @@
"Open on YouTube": "在Youtube上打开",
"Find similar": "查找相似...",
"You are not logged in": "你尚未登录!",
- "My Weekly Mix": "My Weekly Mix",
- "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
- "Favorites": "Favorites",
- "Favorite Channels": "Favorite Channels",
- "Favorites Radios": "Favorites Radios",
- "Discover Favorites": "Discover Favorites",
- "Favorites Community Playlists": "Favorites Community Playlists",
- "Favorites Playlists": "Favorites Playlists"
+ "My Weekly Mix": "我的每周组合",
+ "Crafted for you based on your listening habits": "根据你的收听习惯为你量身定做",
+ "Favorites": "收藏夹",
+ "Favorite Channels": "收藏的频道",
+ "Favorites Radios": "收藏的广播",
+ "Discover Favorites": "探索收藏",
+ "Favorites Community Playlists": "收藏社区播放列表",
+ "Favorites Playlists": "收藏播放列表"
}
From e3df5a65aaa80e037fc52538052e42b1f7461242 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Sat, 18 Mar 2023 20:20:51 -0700
Subject: [PATCH 32/34] New translations translation.json (Chinese Traditional)
---
public/locales/zh-TW/translation.json | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/public/locales/zh-TW/translation.json b/public/locales/zh-TW/translation.json
index ebc62554..7d91e8d4 100644
--- a/public/locales/zh-TW/translation.json
+++ b/public/locales/zh-TW/translation.json
@@ -175,12 +175,12 @@
"Open on YouTube": "於 YouTube 開啟",
"Find similar": "搜尋相關歌曲",
"You are not logged in": "您尚未登入",
- "My Weekly Mix": "My Weekly Mix",
- "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
- "Favorites": "Favorites",
- "Favorite Channels": "Favorite Channels",
- "Favorites Radios": "Favorites Radios",
- "Discover Favorites": "Discover Favorites",
- "Favorites Community Playlists": "Favorites Community Playlists",
- "Favorites Playlists": "Favorites Playlists"
+ "My Weekly Mix": "每週專屬推薦",
+ "Crafted for you based on your listening habits": "根據聆聽習慣為您量身打造的播放清單",
+ "Favorites": "我的收藏",
+ "Favorite Channels": "收藏頻道",
+ "Favorites Radios": "收藏音樂電台",
+ "Discover Favorites": "探索我的收藏",
+ "Favorites Community Playlists": "收藏社群播放清單",
+ "Favorites Playlists": "收藏播放清單"
}
From e13cab775e292ebd9d44bd3137181bc427561c80 Mon Sep 17 00:00:00 2001
From: P-man2976
Date: Mon, 20 Mar 2023 23:55:05 +0900
Subject: [PATCH 33/34] Fix a bug that multiple songs are skipped
---
src/components/player/Player.tsx | 32 ++++++++++---------------
src/components/player/YoutubePlayer.tsx | 2 +-
2 files changed, 14 insertions(+), 20 deletions(-)
diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx
index 19ca6aa5..02c7aa7e 100644
--- a/src/components/player/Player.tsx
+++ b/src/components/player/Player.tsx
@@ -30,7 +30,7 @@ export function Player() {
const { mutate: trackSong } = useTrackSong();
- const { state, setError, hasError } = usePlayer();
+ const { currentVideo, currentTime, state, setError, hasError } = usePlayer();
// Keyboard controls
useKeyboardEvents();
@@ -54,14 +54,6 @@ export function Player() {
[player, currentSong],
);
- // keep the song that the page loaded on paused
- useEffect(() => {
- if (currentSong) {
- setIsPlaying(false);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
// CurrentSong/repeat update event
useEffect(() => {
if (currentSong) {
@@ -84,27 +76,30 @@ export function Player() {
// Progress Event
useEffect(() => {
+ if (
+ !player ||
+ !currentSong ||
+ currentTime === undefined ||
+ currentSong.video_id !== currentVideo
+ )
+ return;
+
// Prevent time from playing before start time
- if (currentSong && progress < 0) {
+ if (progress < 0) {
player?.seekTo(currentSong.start, true);
player?.playVideo();
return;
}
// Track song to history if the song has listened > 80%
- if (
- currentSong &&
- progress > 80 &&
- progress < 105 &&
- !trackedSongs.has(currentSong.id)
- ) {
+ if (progress > 80 && progress < 105 && !trackedSongs.has(currentSong.id)) {
console.log("[Player] Track song play: ", currentSong.name);
trackedSongs.add(currentSong.id);
trackSong({ song_id: currentSong.id });
}
// Something caused it to skip far ahead (e.g. user scrubbed, song time changed on the same video)
- if (currentSong && progress > 105) {
+ if (progress > 105) {
loadVideoAtTime(currentSong.video_id, currentSong.start);
player?.playVideo();
return;
@@ -112,8 +107,7 @@ export function Player() {
// Progress will never reach 100 because player ended. Video length != song start/end
// Example id: KiUvL-rp1zg
- const earlyEnd =
- currentSong && state === PlayerStates.ENDED && progress < 100;
+ const earlyEnd = state === PlayerStates.ENDED && progress < 100;
if ((progress >= 100 && state === PlayerStates.PLAYING) || earlyEnd) {
console.log(
`[Player] Auto advancing due to ${
diff --git a/src/components/player/YoutubePlayer.tsx b/src/components/player/YoutubePlayer.tsx
index a311e015..d998e341 100644
--- a/src/components/player/YoutubePlayer.tsx
+++ b/src/components/player/YoutubePlayer.tsx
@@ -13,7 +13,7 @@ export function YoutubePlayer() {
opts={{
playerVars: {
// https://developers.google.com/youtube/player_parameters
- // autoplay: 0,
+ autoplay: 0,
rel: 0,
modestbranding: 1,
origin: window.location.origin,
From 5761c97f145e2630594c12aade2273a076cdeb09 Mon Sep 17 00:00:00 2001
From: sphinxrave <62570796+sphinxrave@users.noreply.github.com>
Date: Tue, 21 Mar 2023 15:44:06 -0700
Subject: [PATCH 34/34] New translations translation.json (German)
---
public/locales/de-DE/translation.json | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/public/locales/de-DE/translation.json b/public/locales/de-DE/translation.json
index c68aedf4..731e9c2f 100644
--- a/public/locales/de-DE/translation.json
+++ b/public/locales/de-DE/translation.json
@@ -175,12 +175,12 @@
"Open on YouTube": "Auf YouTube öffnen",
"Find similar": "Ähnliches finden",
"You are not logged in": "Du bist nicht angemeldet!",
- "My Weekly Mix": "My Weekly Mix",
- "Crafted for you based on your listening habits": "Crafted for you based on your listening habits",
- "Favorites": "Favorites",
- "Favorite Channels": "Favorite Channels",
- "Favorites Radios": "Favorites Radios",
- "Discover Favorites": "Discover Favorites",
- "Favorites Community Playlists": "Favorites Community Playlists",
- "Favorites Playlists": "Favorites Playlists"
+ "My Weekly Mix": "Mein Wochenmix",
+ "Crafted for you based on your listening habits": "Basierend auf deinen Hörgewohnheiten",
+ "Favorites": "Favoriten",
+ "Favorite Channels": "Lieblingskanäle",
+ "Favorites Radios": "Lieblingsradios",
+ "Discover Favorites": "Favoriten entdecken",
+ "Favorites Community Playlists": "Favoriten-Community-Playlisten",
+ "Favorites Playlists": "Favoriten-Playlisten"
}