WIP Working on player
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useNavigate, useLocation, matchPath } from "react-router-dom";
|
||||
import { usePlayerStore } from "../stores/playerStore";
|
||||
|
||||
// ── Icons (inline SVG) ──────────────────────────────────────────────────────
|
||||
function IconLibrary() {
|
||||
@@ -9,6 +10,14 @@ function IconLibrary() {
|
||||
);
|
||||
}
|
||||
|
||||
function IconPlay() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 14 14" fill="currentColor">
|
||||
<path d="M3 2l9 5-9 5V2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function IconSettings() {
|
||||
@@ -85,6 +94,10 @@ export function BottomNavBar() {
|
||||
const isLibrary = !!matchPath("/bands/:bandId", location.pathname) ||
|
||||
!!matchPath("/bands/:bandId/sessions/:sessionId", location.pathname);
|
||||
const isSettings = location.pathname.startsWith("/settings");
|
||||
|
||||
// Player state
|
||||
const { currentSongId, currentBandId: playerBandId, isPlaying } = usePlayerStore();
|
||||
const hasActiveSong = !!currentSongId && !!playerBandId;
|
||||
|
||||
return (
|
||||
<nav
|
||||
@@ -115,6 +128,18 @@ export function BottomNavBar() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<NavItem
|
||||
icon={<IconPlay />}
|
||||
label="Player"
|
||||
active={hasActiveSong && isPlaying}
|
||||
onClick={() => {
|
||||
if (hasActiveSong) {
|
||||
navigate(`/bands/${playerBandId}/songs/${currentSongId}`);
|
||||
}
|
||||
}}
|
||||
disabled={!hasActiveSong}
|
||||
/>
|
||||
|
||||
<NavItem
|
||||
icon={<IconMembers />}
|
||||
label="Members"
|
||||
|
||||
119
web/src/components/MiniPlayer.tsx
Normal file
119
web/src/components/MiniPlayer.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { usePlayerStore } from "../stores/playerStore";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function MiniPlayer() {
|
||||
const { currentSongId, currentBandId, isPlaying, currentTime, duration } = usePlayerStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!currentSongId || !currentBandId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
background: "#18181e",
|
||||
borderTop: "1px solid rgba(255,255,255,0.06)",
|
||||
padding: "8px 16px",
|
||||
zIndex: 999,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => navigate(`/bands/${currentBandId}/songs/${currentSongId}`)}
|
||||
style={
|
||||
{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: "white",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "4px 8px",
|
||||
borderRadius: 4,
|
||||
}
|
||||
}
|
||||
title="Go to song"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 14 14" fill="currentColor">
|
||||
<path d="M3 2l9 5-9 5V2z" />
|
||||
</svg>
|
||||
<span style={{ fontSize: 12, color: "rgba(255,255,255,0.8)" }}>
|
||||
Now Playing
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
style={
|
||||
{
|
||||
flex: 1,
|
||||
height: 4,
|
||||
background: "rgba(255,255,255,0.1)",
|
||||
borderRadius: 2,
|
||||
overflow: "hidden",
|
||||
cursor: "pointer",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
width: `${progress}%`,
|
||||
height: "100%",
|
||||
background: "#e8a22a",
|
||||
transition: "width 0.1s linear",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 11, color: "rgba(255,255,255,0.6)", minWidth: 60, textAlign: "right" }}>
|
||||
{formatTime(currentTime)} / {formatTime(duration)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
// This would need to be connected to actual play/pause functionality
|
||||
// For now, it's just a visual indicator
|
||||
}}
|
||||
style={
|
||||
{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: "white",
|
||||
cursor: "pointer",
|
||||
padding: "4px",
|
||||
}
|
||||
}
|
||||
title={isPlaying ? "Pause" : "Play"}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<svg width="16" height="16" viewBox="0 0 14 14" fill="currentColor">
|
||||
<path d="M4 2h2v10H4zm4 0h2v10h-2z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 14 14" fill="currentColor">
|
||||
<path d="M3 2l9 5-9 5V2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import { BottomNavBar } from "./BottomNavBar";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { TopBar } from "./TopBar";
|
||||
import { MiniPlayer } from "./MiniPlayer";
|
||||
|
||||
export function ResponsiveLayout({ children }: { children: React.ReactNode }) {
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
@@ -35,8 +36,12 @@ export function ResponsiveLayout({ children }: { children: React.ReactNode }) {
|
||||
{children}
|
||||
</div>
|
||||
<BottomNavBar />
|
||||
<MiniPlayer />
|
||||
</>
|
||||
) : (
|
||||
<Sidebar>{children}</Sidebar>
|
||||
<>
|
||||
<Sidebar>{children}</Sidebar>
|
||||
<MiniPlayer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { api } from "../api/client";
|
||||
import { logout } from "../api/auth";
|
||||
import { getInitials } from "../utils";
|
||||
import type { MemberRead } from "../api/auth";
|
||||
import { usePlayerStore } from "../stores/playerStore";
|
||||
|
||||
// ── Icons (inline SVG) ──────────────────────────────────────────────────────
|
||||
function IconWaveform() {
|
||||
@@ -168,6 +169,10 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
const isSettings = location.pathname.startsWith("/settings");
|
||||
const isBandSettings = !!matchPath("/bands/:bandId/settings/*", location.pathname);
|
||||
const bandSettingsPanel = matchPath("/bands/:bandId/settings/:panel", location.pathname)?.params?.panel ?? null;
|
||||
|
||||
// Player state
|
||||
const { currentSongId, currentBandId: playerBandId, isPlaying: isPlayerPlaying } = usePlayerStore();
|
||||
const hasActiveSong = !!currentSongId && !!playerBandId;
|
||||
|
||||
// Close dropdown on outside click
|
||||
useEffect(() => {
|
||||
@@ -429,9 +434,13 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
<NavItem
|
||||
icon={<IconPlay />}
|
||||
label="Player"
|
||||
active={isPlayer}
|
||||
onClick={() => {}}
|
||||
disabled={!isPlayer}
|
||||
active={hasActiveSong && (isPlayer || isPlayerPlaying)}
|
||||
onClick={() => {
|
||||
if (hasActiveSong) {
|
||||
navigate(`/bands/${playerBandId}/songs/${currentSongId}`);
|
||||
}
|
||||
}}
|
||||
disabled={!hasActiveSong}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user