Skip to content

Commit

Permalink
feat(/playing): Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
busybox11 committed Nov 26, 2024
1 parent 4d51726 commit 6c4ac89
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 6 deletions.
9 changes: 9 additions & 0 deletions app/hooks/usePlayer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { usePlayerProviders } from "@/components/contexts/PlayerProviders";
import { activePlayerAtom, playerStateAtom } from "@/state/player";
import { useAtomValue } from "jotai";
import { usePrevious } from "node_modules/@tanstack/react-router/dist/esm/utils";

export default function usePlayer() {
const { providers } = usePlayerProviders();

const activePlayer = useAtomValue(activePlayerAtom);
const activeProvider = activePlayer ? providers[activePlayer] : null;

const playerState = useAtomValue(playerStateAtom);
const previousPlayerState = usePrevious(playerState);

return {
activePlayer,
activeProvider,
playerState,
previousPlayerState,
};
}
2 changes: 2 additions & 0 deletions app/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
import { Body, Head, Html, Meta, Scripts } from "@tanstack/start";
import type { ReactNode } from "react";

import "@fontsource-variable/outfit";

import appCss from "../styles/app.css?url";
import { PlayerProvidersProvider } from "@/components/contexts/PlayerProviders";

Expand Down
196 changes: 191 additions & 5 deletions app/routes/playing.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,210 @@
import * as React from "react";
import "@/styles/playing.css";

import { createFileRoute, useNavigate } from "@tanstack/react-router";
import usePlayer from "@/hooks/usePlayer";

import { twMerge } from "tailwind-merge";

export const Route = createFileRoute("/playing")({
component: PlayingRouteComponent,
});

const msToTime = (ms: number) => {
const minutes = Math.floor(ms / 60000);
const seconds = parseInt(((ms % 60000) / 1000).toFixed(0));
return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
};

function PlayingRouteComponent() {
const navigate = useNavigate();
const { activePlayer, playerState } = usePlayer();
const { activePlayer, activeProvider, playerState, previousPlayerState } =
usePlayer();

if (!activePlayer) {
navigate({ to: "/" });
}

const image =
playerState && "album" in playerState?.item ?
playerState?.item?.album?.images[0]?.url
: "assets/images/no_song.png";

const title = playerState?.item?.name ?? "NowPlaying";
const artist =
playerState && "artists" in playerState?.item ?
playerState?.item?.artists?.map((artist) => artist.name).join(", ")
: "NowPlaying";
const album =
playerState && "album" in playerState?.item ?
playerState?.item?.album?.name
: "NowPlaying";

const isEpisode =
playerState &&
"type" in playerState?.item &&
playerState?.item?.type === "episode";

const isPaused = !playerState || playerState.is_playing === false;

const positionNow = playerState?.progress_ms ?? 0;
const positionTotal = playerState?.item?.duration_ms ?? 0;
const positionPercent = positionNow / positionTotal;

const shouldAnimateProgress =
Math.abs(
(playerState?.progress_ms ?? 0) - (previousPlayerState?.progress_ms ?? 0)
) < 5000;

return (
<main className="flex flex-col h-screen gap-12 px-6 py-4">
<h1 className="text-4xl font-bold">Playing with {activePlayer}</h1>
<main className="flex h-full w-full">
<div
id="background-image-div"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 translate-z-0 w-[max(115vh,115vw)] h-[max(115vh,115vw)]"
>
<div
className="bg-cover bg-center transition-[background] duration-[2s] ease-in-out z-[-10] h-full w-full blur-2xl transform-gpu"
style={{
backgroundImage: `url(${image})`,
}}
>
<div className="h-full w-full bg-black/30"></div>
</div>
</div>

<div
id="settings-div"
className="settings-div fadeInOut z-30 absolute top-6 left-0 right-0 flex items-center justify-center"
>
<div className="flex flex-row items-center gap-2 px-4 py-2 bg-white/10 border-2 border-white/40 text-white/80 rounded-full">
<svg
className="cursor-pointer"
width="28"
height="28"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.66675 6.66666H13.3334V9.33332H9.33341V13.3333H6.66675V6.66666ZM18.6667 6.66666H25.3334V13.3333H22.6667V9.33332H18.6667V6.66666ZM22.6667 18.6667H25.3334V25.3333H18.6667V22.6667H22.6667V18.6667ZM13.3334 22.6667V25.3333H6.66675V18.6667H9.33341V22.6667H13.3334Z"
fill="white"
/>
</svg>
</div>
</div>

<div className="h-full w-full flex align-center justify-center z-20">
<div className="flex flex-col landscape:flex-row lg:flex-row gap-6 lg:gap-12 justify-center items-center px-6 lg:px-12 xl:px-0 w-full xl:w-5/6">
<div className="relative w-[20rem] landscape:w-[20rem] landscape:lg:w-[30rem] md:w-[30rem] flex-shrink-0">
<img
src={image}
className="rounded-2xl h-auto w-full custom-img-shadow"
/>

{isPaused && (
<div className="absolute bottom-6 right-6 z-30 p-3 bg-black/20 border-2 border-white/60 text-white rounded-full backdrop-blur-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="h-10 w-10"
fill="currentColor"
>
<path d="M14,19H18V5H14M6,19H10V5H6V19Z" />
</svg>
</div>
)}
</div>

<div className="flex flex-col lg:gap-1 xl:gap-2 w-full text-white">
<h1
id="song-title"
className="text-4xl lg:text-7xl font-bold text-pretty"
>
{title}
</h1>
<h2
id="song-artist"
className="text-2xl lg:text-5xl font-bold text-pretty"
>
{artist}
</h2>
<h3
id="song-album"
className="text-xl lg:text-4xl font-semibold opacity-80 text-pretty"
>
{album}
</h3>

<div className="flex flex-col gap-2 lg:gap-3 mt-4 lg:mt-8 w-full">
<div
className="text-xl flex flex-row justify-between w-full font-semibold"
id="progress-time"
>
<span id="progress-time-now">{msToTime(positionNow)}</span>
<span id="progress-time-total">{msToTime(positionTotal)}</span>
</div>

<div className="h-3 w-full rounded-full overflow-hidden bg-white/30">
<div
id="progressbar"
className={twMerge(
"h-full bg-white",
shouldAnimateProgress &&
"transition-all duration-1000 ease-linear"
)}
style={{
width: `${positionPercent * 100}%`,
}}
/>
</div>

<div
className="flex flex-row gap-3 items-center"
id="player-controls"
>
<div>
<svg
width="42"
height="42"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15H36V30H12M36 33C36.7956 33 37.5587 32.6839 38.1213 32.1213C38.6839 31.5587 39 30.7956 39 30V15C39 14.2044 38.6839 13.4413 38.1213 12.8787C37.5587 12.3161 36.7956 12 36 12H12C10.335 12 9 13.335 9 15V30C9 30.7956 9.31607 31.5587 9.87868 32.1213C10.4413 32.6839 11.2044 33 12 33H6V36H42V33H36Z"
fill="white"
/>
</svg>
</div>

<span className="text-xl font-bold">
<span>{activeProvider?.meta.name ?? activePlayer}</span>
<span className="text-white/80 font-semibold"></span>
</span>

<pre>{JSON.stringify(playerState, null, 2)}</pre>
{isEpisode && (
<svg
className="lucide lucide-podcast ml-auto opacity-75"
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16.85 18.58a9 9 0 1 0-9.7 0" />
<path d="M8 14a5 5 0 1 1 8 0" />
<circle cx="12" cy="11" r="1" />
<path d="M13 17a1 1 0 1 0-2 0l.5 4.5a.5.5 0 1 0 1 0Z" />
</svg>
)}
</div>
</div>
</div>
</div>
</div>
</main>
);
}
15 changes: 15 additions & 0 deletions app/styles/playing.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.custom-img-shadow {
box-shadow:
0 5px 10px rgba(0, 0, 0, 0.12),
0 10px 20px rgba(0, 0, 0, 0.15),
0 15px 28px rgba(0, 0, 0, 0.18),
0 20px 38px rgba(0, 0, 0, 0.2);
}

body {
@apply h-screen w-screen overflow-hidden;
}

#root {
@apply h-screen w-screen;
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
"author": "busybox11",
"license": "GPL-3.0",
"dependencies": {
"@fontsource-variable/outfit": "^5.1.0",
"@spotify/web-api-ts-sdk": "^1.2.0",
"@tanstack/react-router": "^1.81.5",
"@tanstack/start": "^1.81.5",
"@uidotdev/usehooks": "^2.4.1",
"dotenv": "^16.4.5",
"jotai": "^2.10.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"tailwind-merge": "^2.5.4",
"usehooks-ts": "^3.1.0",
"vinxi": "^0.4.3",
"zod": "^3.23.8"
Expand Down
31 changes: 31 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
/** @type {import('tailwindcss').Config} */
import defaultTheme from "tailwindcss/defaultTheme";

export default {
content: ["./app/**/*.{html,js,ts,jsx,tsx}"],
theme: {
extend: {},
extend: {
fontFamily: {
sans: ["Outfit Variable", "Outfit", defaultTheme.fontFamily.sans],
},
},
},
plugins: [],
};

0 comments on commit 6c4ac89

Please sign in to comment.